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
This commit is contained in:
Hamish Coleman 2021-10-17 21:16:42 +01:00 committed by GitHub
parent 966b6b9394
commit e6fcf1c55b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 629 additions and 125 deletions

175
doc/ManagementAPI.md Normal file
View File

@ -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.

41
doc/Scripts.md Normal file
View File

@ -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`

View File

@ -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 */

View File

@ -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_ */

View File

@ -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)

View File

@ -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__':

View File

@ -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<enter> | 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;
}