mirror of
https://github.com/ntop/n2n.git
synced 2024-09-19 16:41:11 +02:00
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:
parent
966b6b9394
commit
e6fcf1c55b
175
doc/ManagementAPI.md
Normal file
175
doc/ManagementAPI.md
Normal 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
41
doc/Scripts.md
Normal 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`
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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_ */
|
||||
|
|
170
scripts/n2nctl
170
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)
|
||||
|
||||
|
||||
|
|
157
scripts/n2nhttpd
157
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__':
|
||||
|
|
200
src/edge_utils.c
200
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<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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user