From e6fcf1c55bc0d779f3b3409e61bd2695964a1c65 Mon Sep 17 00:00:00 2001 From: Hamish Coleman Date: Sun, 17 Oct 2021 21:16:42 +0100 Subject: [PATCH] Cleanup and Documentation for JSON management API (#856) * Reimplement JSON mgmt with clear separation of read/write actions * Reduce boilerplate by using a table driven command definition for json mgmt commands * Port tools to use new json api * Add a basic authentication for json mgmt commands * If a auth key is given, it must match * Add auth key to management scripts * Add a flag bitfield to clearly turn the tag param into a options list * Allow simple pass-through of any command from n2nctl * Convert the n2nctl to use an object oriented interface * Handle sigpipe in the n2nhttpd - this happens if the remote client disconnects unexpectely * Remove some repetition from the server * Use the correct options to allow reuseaddr * Dont generate a scary message on ctrl-c * Convert n2nhttpd to use object based RPC * Use the same longopt for both tools * Pass any extra args through to the RPC * Add some documentation for the scripts in the repository * Spelling fix * Add documentation for the JSON reply mangement API --- doc/ManagementAPI.md | 175 ++++++++++++++++++++++++++++++++++++ doc/Scripts.md | 41 +++++++++ include/n2n_define.h | 5 ++ include/n2n_typedefs.h | 6 ++ scripts/n2nctl | 170 ++++++++++++++++++++++++++--------- scripts/n2nhttpd | 157 ++++++++++++++++++++++---------- src/edge_utils.c | 200 ++++++++++++++++++++++++++++++++++------- 7 files changed, 629 insertions(+), 125 deletions(-) create mode 100644 doc/ManagementAPI.md create mode 100644 doc/Scripts.md diff --git a/doc/ManagementAPI.md b/doc/ManagementAPI.md new file mode 100644 index 0000000..2291dcd --- /dev/null +++ b/doc/ManagementAPI.md @@ -0,0 +1,175 @@ +# Management API + +This document is focused on the machine readable API interfaces. + +Both the edge and the supernode provide a management interface UDP port. +These interfaces have some documentation on their non machine readable +commands in the respective daemon man pages. + +Default Ports: +- UDP/5644 - edge +- UDP/5645 - supernode + +## JSON Query interface + +As part of the management interface, A machine readable API exists for the +edge daemon. It takes a simple text request and replies with JSON formatted +data. + +The request is in simple text so that the daemon does not need to include any +complex parser. + +The replies are all in JSON so that the data is fully machine readable and +the schema can be updated as needed - the burden is entirely on the client +to handle different replies with different fields. It is expected that +any client software will be written in higher level programming languages +where this flexibility is easy to provide. + +Since the API is over UDP, the replies are structured so that each part of +the reply is clearly tagged as belonging to one request and there is a +clear begin and end marker for the reply. This is expected to support the +future possibilities of pipelined and overlapping transactions as well as +pub/sub asynchronous event channels. + +The replies will also handle some small amount of re-ordering of the +packets, but that is not an specific goal of the protocol. + +With a small amount of effort, the API is intended to be human readable, +but this is intended for debugging. + +## Request API + +The request is a single UDP packet containing one line of text with at least +three space separated fields. Any text after the third field is available for +the API method to use for additional parameters + +Fields: +- Message Type +- Options +- Method +- Optional Additional Parameters + +The maximum length of the entire line of text is 80 octets. + +### Message Type + +This is a single octet that is either "r" for a read (or query) method +call or "w" for a write (or change) method call. + +To simplify the interface, the reply from both read and write calls to the +same method is expected to contain the same data. In the case of a write +call, the reply will contain the new state after making the requested change. + +### Options + +The options field is a colon separated set of options for this request. Only +the first subfield (the "tag") is mandatory. The second subfield is a set +of flags that describe which optional subfields are present. +If there are no additional subfields then the flags can be omitted. + +SubFields: +- Message Tag +- Optional Message Flags (defaults to 0) +- Optional Authentication Key + +#### Message Tag + +Each request provides a tag value. Any non error reply associated with this +request will include this tag value, allowing all related messages to be +collected within the client. + +Where possible, the error replies will also include this tag, however some +errors occur before the tag is parsed. + +The tag is not interpreted by the daemon, it is simply echoed back in all +the replies. It is expected to be a short string that the client chooses +to be unique amongst all recent or still outstanding requests. + +One possible client implementation is a number between 0 and 999, incremented +for each request and wrapping around to zero when it is greater than 999. + +#### Message Flags + +This subfield is a set of bit flags that are hex-encoded and describe any +remaining optional subfields. + +Currently, only one flag is defined. The presence of that flag indicates +that an authentication key subfield is also present. + +Values: +- 0 - No additional subfields are present +- 1 - One additional field, containing the authentication key + +#### Authentication Key + +A simple string password that is provided by the client to authenticate +this request. See the Authentication section below for more discussion. + +#### Example Options value + +e.g: + `102:1:PassWord` + +### Example Request string + +e.g: + `r 103:1:PassWord peer` + +## Reply API + +Each UDP packet in the reply is a complete and valid JSON dictionary +containing a fragment of information related to the entire reply. + +### Common metadata + +There are two keys in each dictionary containing metadata. First +is the `_tag`, containing the Message Tag from the original request. +Second is the `_type` whic identifies the expected contents of this +packet. + +### `_type: error` + +If an error condition occurs, a packet with a `error` key describing +the error will be sent. This usually also indicates that there will +be no more substantial data arriving related to this request. + +e.g: + `{"_tag":"107","_type":"error","error":"badauth"}` + +### `_type: begin` + +Before the start of any substantial data packets, a `begin` packet is +sent. For consistency checking, the method in the request is echoed +back in the `error` key. + +e.g: + `{"_tag":"108","_type":"begin","cmd":"peer"}` + +For simplicity in decoding, if a `begin` packet is sent, all attempts +are made to ensure that a final `end` packet is also sent. + +### `_type: end` + +After the last substantial data packet, a final `end` packet is sent +to signal to the client that this reply is finished. + +e.g: + `{"_tag":"108","_type":"end"}` + +### `_type: row` + +The substantial bulk of the data in the reply is contained within one or +more `row` packets. The non metadata contents of each `row` packet is +defined entirely by the method called and may change from version to version. + +e.g: + `{"_tag":"108","_type":"row","mode":"p2p","ip4addr":"10.135.98.84","macaddr":"86:56:21:E4:AA:39","sockaddr":"192.168.7.191:41701","desc":"client4","lastseen":1584682200}` + +## Authentication + +Some API requests will make global changes to the running daemon and may +affect the availability of the n2n networking. Therefore the machine +readable API include an authentication component. + +Currently, the only authentication is a simple password that the client +must provide. diff --git a/doc/Scripts.md b/doc/Scripts.md new file mode 100644 index 0000000..e928b95 --- /dev/null +++ b/doc/Scripts.md @@ -0,0 +1,41 @@ +# Scripts + +There are a number of useful scripts included with the distribution. +Some of these scripts are only useful during build and development, but +other scripts are intended for end users to be able to use. These scripts +may be installed with n2n as part of your operating system package. + +Short descriptions of these scripts are below. + +## `scripts/hack_fakeautoconf` + +This shell script is used during development to help build on Windows +systems. An example of how to use it is shown in +the [Building document](Building.md) + +## `tools/test_harness` + +This shell script is used to run automated tests during development. + +## `scripts/n2nctl` + +This python script provides an easy command line interface to the running +edge. It uses UDP communications to talk to the Management API. + +Example: +- `scripts/n2nctl --help` +- `scripts/n2nctl help` + +## `scripts/n2nhttpd` + +This python script is a simple http gateway to the running edge. It provides +a proxy for REST-like HTTP requests to talk to the Management API. + +By default it runs on port 8080. + +It also provides a simple HTML page showing some information, which when +run with default settings can be seen at http://localhost:8080/ + +Example: +- `scripts/n2nhttpd --help` +- `scripts/n2nhttpd 8087` diff --git a/include/n2n_define.h b/include/n2n_define.h index ddba86a..22443e9 100644 --- a/include/n2n_define.h +++ b/include/n2n_define.h @@ -121,6 +121,11 @@ enum sn_purge{SN_PURGEABLE = 0, SN_UNPURGEABLE = 1}; #define N2N_EDGE_MGMT_PORT 5644 #define N2N_SN_MGMT_PORT 5645 +enum n2n_mgmt_type { + N2N_MGMT_READ = 0, + N2N_MGMT_WRITE = 1, +}; + #define N2N_TCP_BACKLOG_QUEUE_SIZE 3 /* number of concurrently pending connections to be accepted */ /* NOT the number of max. TCP connections */ diff --git a/include/n2n_typedefs.h b/include/n2n_typedefs.h index fc9c8cb..532c063 100644 --- a/include/n2n_typedefs.h +++ b/include/n2n_typedefs.h @@ -831,6 +831,12 @@ typedef struct n2n_sn { } n2n_sn_t; +/* *************************************************** */ +typedef struct n2n_mgmt_handler { + char *cmd; + char *help; + void (*func)(n2n_edge_t *eee, char *udp_buf, struct sockaddr_in sender_sock, enum n2n_mgmt_type type, char *tag, char *argv0, char *argv); +} n2n_mgmt_handler_t; #endif /* _N2N_TYPEDEFS_H_ */ diff --git a/scripts/n2nctl b/scripts/n2nctl index 03fe664..0598202 100755 --- a/scripts/n2nctl +++ b/scripts/n2nctl @@ -8,54 +8,96 @@ import socket import json import collections -next_tag = 0 +class JsonUDP(): + """encapsulate communication with the edge""" -def send_cmd(port, debug, cmd): - global next_tag + def __init__(self, port): + self.address = "127.0.0.1" + self.port = port + self.tag = 0 + self.key = None + self.debug = False + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + def _next_tag(self): + tagstr = str(self.tag) + self.tag = (self.tag + 1) % 1000 + return tagstr - tagstr = str(next_tag) - next_tag = (next_tag + 1) % 1000 + def _cmdstr(self, msgtype, cmdline): + """Create the full command string to send""" + tagstr = self._next_tag() - message = "{} {}".format(cmd, tagstr).encode('utf8') - sock.sendto(message, ("127.0.0.1", 5644)) + options = [tagstr] + if self.key is not None: + options += ['1'] # Flags set for auth key field + options += [self.key] + optionsstr = ':'.join(options) - # FIXME: - # - there is no timeout for any of the socket handling + return tagstr, ' '.join((msgtype, optionsstr, cmdline)) - begin, _ = sock.recvfrom(1024) - begin = json.loads(begin.decode('utf8')) - assert(begin['_tag'] == tagstr) - assert(begin['_type'] == 'begin') - assert(begin['_cmd'] == cmd) + def _rx(self, tagstr): + """Wait for rx packets""" - result = list() - - while True: - data, _ = sock.recvfrom(1024) + # TODO: there are no timeouts with any of the recv calls + data, _ = self.sock.recvfrom(1024) data = json.loads(data.decode('utf8')) + + # TODO: We assume the first packet we get will be tagged for us + # and be either an "error" or a "begin" assert(data['_tag'] == tagstr) - if data['_type'] == 'unknowncmd': - raise ValueError('Unknown command {}'.format(cmd)) + if data['_type'] == 'error': + raise ValueError('Error: {}'.format(data['error'])) - if data['_type'] == 'end': - return result + assert(data['_type'] == 'begin') - if data['_type'] != 'row': - raise ValueError('Unknown data type {} from ' - 'edge'.format(data['_type'])) + # Ideally, we would confirm that this is our "begin", but that + # would need the cmd passed into this method, and that would + # probably require parsing the cmdline passed to us :-( + # assert(data['cmd'] == cmd) - # remove our boring metadata - del data['_tag'] - del data['_type'] + result = list() - if debug: - print(data) + while True: + data, _ = self.sock.recvfrom(1024) + data = json.loads(data.decode('utf8')) - result.append(data) + if data['_tag'] != tagstr: + # this packet is not for us, ignore it + continue + + if data['_type'] == 'error': + raise ValueError('Error: {}'.format(data['error'])) + + if data['_type'] == 'end': + return result + + if data['_type'] != 'row': + raise ValueError('Unknown data type {} from ' + 'edge'.format(data['_type'])) + + # remove our boring metadata + del data['_tag'] + del data['_type'] + + if self.debug: + print(data) + + result.append(data) + + def _call(self, msgtype, cmdline): + """Perform a rpc call""" + tagstr, msgstr = self._cmdstr(msgtype, cmdline) + self.sock.sendto(msgstr.encode('utf8'), (self.address, self.port)) + return self._rx(tagstr) + + def read(self, cmdline): + return self._call('r', cmdline) + + def write(self, cmdline): + return self._call('w', cmdline) def str_table(rows, columns): @@ -85,8 +127,8 @@ def str_table(rows, columns): return ''.join(result) -def subcmd_show_supernodes(args): - rows = send_cmd(args.port, args.debug, 'j.super') +def subcmd_show_supernodes(rpc, args): + rows = rpc.read('super') columns = [ 'version', 'current', @@ -98,8 +140,8 @@ def subcmd_show_supernodes(args): return str_table(rows, columns) -def subcmd_show_peers(args): - rows = send_cmd(args.port, args.debug, 'j.peer') +def subcmd_show_peers(rpc, args): + rows = rpc.read('peer') columns = [ 'mode', 'ip4addr', @@ -111,7 +153,24 @@ def subcmd_show_peers(args): return str_table(rows, columns) +def subcmd_show_help(rpc, args): + result = 'Commands with pretty-printed output:\n\n' + for name, cmd in subcmds.items(): + result += "{:12} {}\n".format(name, cmd['help']) + + result += "\n" + result += "Possble remote commands:\n" + result += "(those without pretty-printer, will pass-through)\n\n" + rows = rpc.read('help') + result += json.dumps(rows, sort_keys=True, indent=4) + return result + + subcmds = { + 'help': { + 'func': subcmd_show_help, + 'help': 'Show available commands', + }, 'supernodes': { 'func': subcmd_show_supernodes, 'help': 'Show the list of supernodes', @@ -123,24 +182,49 @@ subcmds = { } +def subcmd_default(rpc, args): + """Just pass command through to edge""" + cmdline = ' '.join([args.cmd] + args.args) + if args.write: + rows = rpc.write(cmdline) + else: + rows = rpc.read(cmdline) + return json.dumps(rows, sort_keys=True, indent=4) + + def main(): ap = argparse.ArgumentParser( description='Query the running local n2n edge') - ap.add_argument('-t', '--port', action='store', default=5644, + ap.add_argument('-t', '--mgmtport', action='store', default=5644, help='Management Port (default=5644)') + ap.add_argument('-k', '--key', action='store', + help='Password for mgmt commands') ap.add_argument('-d', '--debug', action='store_true', help='Also show raw internal data') - subcmd = ap.add_subparsers(help='Subcommand', dest='cmd') - subcmd.required = True + group = ap.add_mutually_exclusive_group() + group.add_argument('--read', action='store_true', + help='Make a read request (default)') + group.add_argument('--write', action='store_true', + help='Make a write request') - for key, value in subcmds.items(): - value['parser'] = subcmd.add_parser(key, help=value['help']) - value['parser'].set_defaults(func=value['func']) + ap.add_argument('cmd', action='store', + help='Command to run (try "help" for list)') + ap.add_argument('args', action='store', nargs="*", + help='Optional args for the command') args = ap.parse_args() - result = args.func(args) + if args.cmd not in subcmds: + func = subcmd_default + else: + func = subcmds[args.cmd]['func'] + + rpc = JsonUDP(args.mgmtport) + rpc.debug = args.debug + rpc.key = args.key + + result = func(rpc, args) print(result) diff --git a/scripts/n2nhttpd b/scripts/n2nhttpd index f6202cc..253d46d 100755 --- a/scripts/n2nhttpd +++ b/scripts/n2nhttpd @@ -17,58 +17,101 @@ import socket import json import socketserver import http.server +import signal +import functools from http import HTTPStatus -next_tag = 0 +class JsonUDP(): + """encapsulate communication with the edge""" -def send_cmd(port, debug, cmd): - """Send a text command to the edge and process the JSON reply packets""" - global next_tag + def __init__(self, port): + self.address = "127.0.0.1" + self.port = port + self.tag = 0 + self.key = None + self.debug = False + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + def _next_tag(self): + tagstr = str(self.tag) + self.tag = (self.tag + 1) % 1000 + return tagstr - tagstr = str(next_tag) - next_tag = (next_tag + 1) % 1000 + def _cmdstr(self, msgtype, cmdline): + """Create the full command string to send""" + tagstr = self._next_tag() - message = "{} {}".format(cmd, tagstr).encode('utf8') - sock.sendto(message, ("127.0.0.1", 5644)) + options = [tagstr] + if self.key is not None: + options += ['1'] # Flags set for auth key field + options += [self.key] + optionsstr = ':'.join(options) - # FIXME: - # - there is no timeout for any of the socket handling + return tagstr, ' '.join((msgtype, optionsstr, cmdline)) - begin, _ = sock.recvfrom(1024) - begin = json.loads(begin.decode('utf8')) - assert(begin['_tag'] == tagstr) - assert(begin['_type'] == 'begin') - assert(begin['_cmd'] == cmd) + def _rx(self, tagstr): + """Wait for rx packets""" - result = list() - - while True: - data, _ = sock.recvfrom(1024) + # TODO: there are no timeouts with any of the recv calls + data, _ = self.sock.recvfrom(1024) data = json.loads(data.decode('utf8')) + + # TODO: We assume the first packet we get will be tagged for us + # and be either an "error" or a "begin" assert(data['_tag'] == tagstr) - if data['_type'] == 'unknowncmd': - raise ValueError('Unknown command {}'.format(cmd)) + if data['_type'] == 'error': + raise ValueError('Error: {}'.format(data['error'])) - if data['_type'] == 'end': - return result + assert(data['_type'] == 'begin') - if data['_type'] != 'row': - raise ValueError('Unknown data type {} from ' - 'edge'.format(data['_type'])) + # Ideally, we would confirm that this is our "begin", but that + # would need the cmd passed into this method, and that would + # probably require parsing the cmdline passed to us :-( + # assert(data['cmd'] == cmd) - # remove our boring metadata - del data['_tag'] - del data['_type'] + result = list() - if debug: - print(data) + while True: + data, _ = self.sock.recvfrom(1024) + data = json.loads(data.decode('utf8')) - result.append(data) + if data['_tag'] != tagstr: + # this packet is not for us, ignore it + continue + + if data['_type'] == 'error': + raise ValueError('Error: {}'.format(data['error'])) + + if data['_type'] == 'end': + return result + + if data['_type'] != 'row': + raise ValueError('Unknown data type {} from ' + 'edge'.format(data['_type'])) + + # remove our boring metadata + del data['_tag'] + del data['_type'] + + if self.debug: + print(data) + + result.append(data) + + def _call(self, msgtype, cmdline): + """Perform a rpc call""" + tagstr, msgstr = self._cmdstr(msgtype, cmdline) + self.sock.sendto(msgstr.encode('utf8'), (self.address, self.port)) + return self._rx(tagstr) + + def read(self, cmdline): + return self._call('r', cmdline) + + def write(self, cmdline): + return self._call('w', cmdline) indexhtml = """ @@ -149,10 +192,19 @@ refresh_job(); class SimpleHandler(http.server.BaseHTTPRequestHandler): + def __init__(self, rpc, *args, **kwargs): + self.rpc = rpc + super().__init__(*args, **kwargs) + def log_request(self, code='-', size='-'): # Dont spam the output pass + def _simplereply(self, number, message): + self.send_response(number) + self.end_headers() + self.wfile.write(message.encode('utf8')) + def do_GET(self): url_tail = self.path @@ -165,15 +217,13 @@ class SimpleHandler(http.server.BaseHTTPRequestHandler): if url_tail.startswith("/edge/"): tail = url_tail.split('/') - cmd = 'j.' + tail[2] + cmd = tail[2] # if commands ever need args, use more of the path components try: - data = send_cmd(5644, False, cmd) + data = self.rpc.read(cmd) except ValueError: - self.send_response(HTTPStatus.BAD_REQUEST) - self.end_headers() - self.wfile.write(b'Bad Command') + self._simplereply(HTTPStatus.BAD_REQUEST, 'Bad Command') return self.send_response(HTTPStatus.OK) @@ -182,29 +232,38 @@ class SimpleHandler(http.server.BaseHTTPRequestHandler): self.wfile.write(json.dumps(data).encode('utf8')) return - self.send_response(HTTPStatus.NOT_FOUND) - self.end_headers() - self.wfile.write(b'Not Found') + self._simplereply(HTTPStatus.NOT_FOUND, 'Not Found') return def main(): ap = argparse.ArgumentParser( description='Control the running local n2n edge via http') - # TODO - this needs to pass into the handler object - # ap.add_argument('-t', '--mgmtport', action='store', default=5644, - # help='Management Port (default=5644)') - # ap.add_argument('-d', '--debug', action='store_true', - # help='Also show raw internal data') + ap.add_argument('-t', '--mgmtport', action='store', default=5644, + help='Management Port (default=5644)') + ap.add_argument('-k', '--key', action='store', + help='Password for mgmt commands') + ap.add_argument('-d', '--debug', action='store_true', + help='Also show raw internal data') ap.add_argument('port', action='store', default=8080, type=int, nargs='?', help='Serve requests on TCP port (default 8080)') args = ap.parse_args() - with socketserver.TCPServer(("", args.port), SimpleHandler) as httpd: - httpd.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - httpd.serve_forever() + rpc = JsonUDP(args.mgmtport) + rpc.debug = args.debug + rpc.key = args.key + + signal.signal(signal.SIGPIPE, signal.SIG_DFL) + + socketserver.TCPServer.allow_reuse_address = True + handler = functools.partial(SimpleHandler, rpc) + with socketserver.TCPServer(("", args.port), handler) as httpd: + try: + httpd.serve_forever() + except KeyboardInterrupt: + return if __name__ == '__main__': diff --git a/src/edge_utils.c b/src/edge_utils.c index d4332c8..f1c59b6 100644 --- a/src/edge_utils.c +++ b/src/edge_utils.c @@ -1806,14 +1806,30 @@ static char *get_ip_from_arp (dec_ip_str_t buf, const n2n_mac_t req_mac) { #endif #endif -static void handleMgmtJson_super (n2n_edge_t *eee, char *tag, char *udp_buf, struct sockaddr_in sender_sock) { +static void handleMgmtJson_error (n2n_edge_t *eee, char *udp_buf, struct sockaddr_in sender_sock, char *tag, char *msg) { + size_t msg_len; + msg_len = snprintf(udp_buf, N2N_PKT_BUF_SIZE, + "{" + "\"_tag\":\"%s\"," + "\"_type\":\"error\"," + "\"error\":\"%s\"}\n", + tag, + msg); + sendto(eee->udp_mgmt_sock, udp_buf, msg_len, 0, + (struct sockaddr *) &sender_sock, sizeof(struct sockaddr_in)); +} + +static void handleMgmtJson_super (n2n_edge_t *eee, char *udp_buf, struct sockaddr_in sender_sock, enum n2n_mgmt_type type, char *tag, char *argv0, char *argv) { size_t msg_len; struct peer_info *peer, *tmpPeer; macstr_t mac_buf; n2n_sock_str_t sockbuf; selection_criterion_str_t sel_buf; - traceEvent(TRACE_DEBUG, "mgmt j.super"); + if(type!=N2N_MGMT_READ) { + handleMgmtJson_error(eee, udp_buf, sender_sock, tag, "readonly"); + return; + } HASH_ITER(hh, eee->conf.supernodes, peer, tmpPeer) { @@ -1851,14 +1867,17 @@ static void handleMgmtJson_super (n2n_edge_t *eee, char *tag, char *udp_buf, str } } -static void handleMgmtJson_peer (n2n_edge_t *eee, char *tag, char *udp_buf, struct sockaddr_in sender_sock) { +static void handleMgmtJson_peer (n2n_edge_t *eee, char *udp_buf, struct sockaddr_in sender_sock, enum n2n_mgmt_type type, char *tag, char *argv0, char *argv) { size_t msg_len; struct peer_info *peer, *tmpPeer; macstr_t mac_buf; n2n_sock_str_t sockbuf; in_addr_t net; - traceEvent(TRACE_DEBUG, "mgmt j.peer"); + if(type!=N2N_MGMT_READ) { + handleMgmtJson_error(eee, udp_buf, sender_sock, tag, "readonly"); + return; + } /* FIXME: * dont repeat yourself - the body of these two loops is identical @@ -1912,24 +1931,149 @@ static void handleMgmtJson_peer (n2n_edge_t *eee, char *tag, char *udp_buf, stru } } -static void handleMgmtJson (n2n_edge_t *eee, char *cmdp, char *udp_buf, struct sockaddr_in sender_sock) { +static void handleMgmtJson_help (n2n_edge_t *eee, char *udp_buf, struct sockaddr_in sender_sock, enum n2n_mgmt_type type, char *tag, char *argv0, char *argv); + +n2n_mgmt_handler_t mgmt_handlers[] = { + { .cmd = "peer", .help = "List current peers", .func = handleMgmtJson_peer}, + { .cmd = "super", .help = "List current supernodes", .func = handleMgmtJson_super}, + { .cmd = "help", .help = "Show JSON commands", .func = handleMgmtJson_help}, + { .cmd = NULL }, +}; + +static void handleMgmtJson_help (n2n_edge_t *eee, char *udp_buf, struct sockaddr_in sender_sock, enum n2n_mgmt_type type, char *tag, char *argv0, char *argv) { + size_t msg_len; + n2n_mgmt_handler_t *handler; + + /* + * Even though this command is readonly, we deliberately do not check + * the type - allowing help replys to both read and write requests + */ + + for( handler=mgmt_handlers; handler->cmd; handler++ ) { + msg_len = snprintf(udp_buf, N2N_PKT_BUF_SIZE, + "{" + "\"_tag\":\"%s\"," + "\"_type\":\"row\"," + "\"cmd\":\"%s\"," + "\"help\":\"%s\"}\n", + tag, + handler->cmd, + handler->help); + + sendto(eee->udp_mgmt_sock, udp_buf, msg_len, 0, + (struct sockaddr *) &sender_sock, sizeof(struct sockaddr_in)); + } +} + +/* + * Check if the user is authorised for this command. + * - this should be more configurable! + * - for the moment we use some simple heuristics: + * Reads are not dangerous, so they are simply allowed + * Writes are possibly dangerous, so they need a fake password + */ +int handleMgmtJson_auth(struct sockaddr_in sender_sock, enum n2n_mgmt_type type, char *auth, char *argv0, char *argv) { + if(auth) { + /* If we have an auth key, it must match */ + if(0 == strcmp(auth,"CHANGEME")) { + return 1; + } + return 0; + } + /* if we dont have an auth key, we can still read */ + if(type==N2N_MGMT_READ) { + return 1; + } + return 0; +} + +static void handleMgmtJson (n2n_edge_t *eee, char *udp_buf, struct sockaddr_in sender_sock) { + + char cmdlinebuf[80]; + enum n2n_mgmt_type type; + char *typechar; + char *options; + char *argv0; + char *argv; + char *tag; + char *flagstr; + int flags; + char *auth; + n2n_mgmt_handler_t *handler; size_t msg_len; - char cmd[10]; - char tag[10]; + /* save a copy of the commandline before we reuse the udp_buf */ + strncpy(cmdlinebuf, udp_buf, sizeof(cmdlinebuf)-1); + cmdlinebuf[sizeof(cmdlinebuf)-1] = 0; - /* save the command name before we reuse the buffer */ - strncpy(cmd, cmdp, sizeof(cmd)-1); - cmd[sizeof(cmd)-1] = 0; + traceEvent(TRACE_DEBUG, "mgmt json %s", cmdlinebuf); + + typechar = strtok(cmdlinebuf, " \r\n"); + if(!typechar) { + /* should not happen */ + handleMgmtJson_error(eee, udp_buf, sender_sock, "-1", "notype"); + return; + } + if(*typechar == 'r') { + type=N2N_MGMT_READ; + } else if(*typechar == 'w') { + type=N2N_MGMT_WRITE; + } else { + /* dunno how we got here */ + handleMgmtJson_error(eee, udp_buf, sender_sock, "-1", "badtype"); + return; + } /* Extract the tag to use in all reply packets */ - char *tagp = strtok(NULL, " \r\n"); - if(tagp) { - strncpy(tag, tagp, sizeof(tag)-1); - tag[sizeof(tag)-1] = 0; + options = strtok(NULL, " \r\n"); + if(!options) { + handleMgmtJson_error(eee, udp_buf, sender_sock, "-1", "nooptions"); + return; + } + + argv0 = strtok(NULL, " \r\n"); + if(!argv0) { + handleMgmtJson_error(eee, udp_buf, sender_sock, "-1", "nocmd"); + return; + } + + /* + * The entire rest of the line is the argv. We apply no processing + * or arg separation so that the cmd can use it however it needs. + */ + argv = strtok(NULL, "\r\n"); + + /* + * There might be an auth token mixed in with the tag + */ + tag = strtok(options, ":"); + flagstr = strtok(NULL, ":"); + if (flagstr) { + flags = strtoul(flagstr, NULL, 16); } else { - tag[0] = '0'; - tag[1] = 0; + flags = 0; + } + + /* Only 1 flag bit defined at the moment - "auth option present" */ + if (flags & 1) { + auth = strtok(NULL, ":"); + } else { + auth = NULL; + } + + if(!handleMgmtJson_auth(sender_sock, type, auth, argv0, argv)) { + handleMgmtJson_error(eee, udp_buf, sender_sock, tag, "badauth"); + return; + } + + for( handler=mgmt_handlers; handler->cmd; handler++ ) { + if(0 == strcmp(handler->cmd, argv0)) { + break; + } + } + if(!handler->cmd) { + handleMgmtJson_error(eee, udp_buf, sender_sock, tag, "unknowncmd"); + return; } /* @@ -1939,20 +2083,11 @@ static void handleMgmtJson (n2n_edge_t *eee, char *cmdp, char *udp_buf, struct s * - do we care? */ msg_len = snprintf(udp_buf, N2N_PKT_BUF_SIZE, - "{\"_tag\":\"%s\",\"_type\":\"begin\",\"_cmd\":\"%s\"}\n", tag, cmd); + "{\"_tag\":\"%s\",\"_type\":\"begin\",\"cmd\":\"%s\"}\n", tag, argv0); sendto(eee->udp_mgmt_sock, udp_buf, msg_len, 0, (struct sockaddr *) &sender_sock, sizeof(struct sockaddr_in)); - if(0 == strcmp(cmd, "j.super")) { - handleMgmtJson_super(eee, tag, udp_buf, sender_sock); - } else if(0 == strcmp(cmd, "j.peer")) { - handleMgmtJson_peer(eee, tag, udp_buf, sender_sock); - } else { - msg_len = snprintf(udp_buf, N2N_PKT_BUF_SIZE, - "{\"_tag\":\"%s\",\"_type\":\"unknowncmd\"}\n", tag); - sendto(eee->udp_mgmt_sock, udp_buf, msg_len, 0, - (struct sockaddr *) &sender_sock, sizeof(struct sockaddr_in)); - } + handler->func(eee, udp_buf, sender_sock, type, tag, argv0, argv); msg_len = snprintf(udp_buf, N2N_PKT_BUF_SIZE, "{\"_tag\":\"%s\",\"_type\":\"end\"}\n", tag); @@ -2008,8 +2143,8 @@ static void readFromMgmtSocket (n2n_edge_t *eee, int *keep_running) { "\thelp | This help message\n" "\t+verb | Increase verbosity of logging\n" "\t-verb | Decrease verbosity of logging\n" - "\tj.super | JSON supernode info\n" - "\tj.peer | JSON peer info\n" + "\tr ... | start query with JSON reply\n" + "\tw ... | start update with JSON reply\n" "\t | Display statistics\n\n"); sendto(eee->udp_mgmt_sock, udp_buf, msg_len, 0/*flags*/, @@ -2057,10 +2192,9 @@ static void readFromMgmtSocket (n2n_edge_t *eee, int *keep_running) { return; } - char * cmdp = strtok( (char *)udp_buf, " \r\n"); - if(cmdp && (0 == memcmp(cmdp, "j.", 2))) { - /* We think this is a JSON request */ - handleMgmtJson(eee, cmdp, (char *)udp_buf, sender_sock); + if((udp_buf[0] == 'r' || udp_buf[0] == 'w') && (udp_buf[1] == ' ')) { + /* this is a JSON request */ + handleMgmtJson(eee, (char *)udp_buf, sender_sock); return; }