#!/usr/bin/env python3 # Licensed under GPLv3 # # Simple http server to allow user control of n2n edge nodes # # Currently only for demonstration # - needs nicer looking html written # - needs more json interfaces in edge # # Try it out with # http://localhost:8080/ # http://localhost:8080/edge/edges # http://localhost:8080/edge/supernodes import argparse import socket import json import socketserver import http.server import signal import functools import base64 from http import HTTPStatus class JsonUDP(): """encapsulate communication with the edge""" 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) def _next_tag(self): tagstr = str(self.tag) self.tag = (self.tag + 1) % 1000 return tagstr def _cmdstr(self, msgtype, cmdline): """Create the full command string to send""" tagstr = self._next_tag() options = [tagstr] if self.key is not None: options += ['1'] # Flags set for auth key field options += [self.key] optionsstr = ':'.join(options) return tagstr, ' '.join((msgtype, optionsstr, cmdline)) def _rx(self, tagstr): """Wait for rx packets""" # 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'] == 'error': raise ValueError('Error: {}'.format(data['error'])) assert(data['_type'] == 'begin') # 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) result = list() error = None while True: data, _ = self.sock.recvfrom(1024) data = json.loads(data.decode('utf8')) if data['_tag'] != tagstr: # this packet is not for us, ignore it continue if data['_type'] == 'error': # we still expect an end packet, so save the error error = ValueError('Error: {}'.format(data['error'])) continue if data['_type'] == 'end': if error: raise error 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) pages = { "/script.js": { "content_type": "text/javascript", "content": """ var verbose=-1; function rows2verbose(id, unused, data) { row0 = data[0] verbose = row0['traceLevel'] let div = document.getElementById(id); div.innerHTML=verbose } function rows2keyvalue(id, keys, data) { let s = "" data.forEach((row) => { keys.forEach((key) => { if (key in row) { s += "
" + key + "" + row[key]; } }); }); s += "
" let div = document.getElementById(id); div.innerHTML=s } function rows2keyvalueall(id, unused, data) { let s = "" data.forEach((row) => { Object.keys(row).forEach((key) => { s += "
" + key + "" + row[key]; }); }); s += "
" let div = document.getElementById(id); div.innerHTML=s } function rows2table(id, columns, data) { let s = "" s += "" columns.forEach((col) => { s += "" columns.forEach((col) => { val = row[col] if (typeof val === "undefined") { val = '' } s += "
" + col }); data.forEach((row) => { s += "
" + val }); }); s += "
" let div = document.getElementById(id); div.innerHTML=s } function do_get(url, id, handler, handler_param) { fetch(url) .then(function (response) { return response.json(); }) .then(function (data) { handler(id,handler_param,data); // update the timestamp on success let now = Math.round(new Date().getTime() / 1000); let time = document.getElementById('time'); time.innerHTML=now; }) .catch(function (err) { console.log('error: ' + err); }); } function do_post(url, body, id, handler, handler_param) { fetch(url, {method:'POST', body: body}) .then(function (response) { return response.json(); }) .then(function (data) { handler(id,handler_param,data); }) .catch(function (err) { console.log('error: ' + err); }); } function do_stop(tracelevel) { // FIXME: uses global in script library fetch(nodetype + '/stop', {method:'POST'}) } function setverbose(tracelevel) { if (tracelevel < 0) { tracelevel = 0; } // FIXME: uses global in script library do_post( nodetype + '/verbose', tracelevel, 'verbose', rows2verbose, null ); } function refresh_setup(interval) { var timer = setInterval(refresh_job, interval); } """, }, "/": { "content_type": "text/html; charset=utf-8", "content": """ n2n edge management
Last Updated:
Logging Verbosity:


Edges/Peers:

Supernodes:


""", }, "/supernode.html": { "content_type": "text/html; charset=utf-8", "content": """ n2n supernode management
Last Updated:
Logging Verbosity:

Communities:

Edges/Peers:


""", }, } class SimpleHandler(http.server.BaseHTTPRequestHandler): def __init__(self, rpc, snrpc, *args, **kwargs): self.rpc = rpc self.snrpc = snrpc 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 _replyjson(self, data): self.send_response(HTTPStatus.OK) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(json.dumps(data).encode('utf8')) def _replyunauth(self): self.send_response(HTTPStatus.UNAUTHORIZED) self.send_header('WWW-Authenticate', 'Basic realm="n2n"') self.end_headers() def _extractauth(self, rpc): # Avoid caching the key inside the object for all clients rpc.key = None header = self.headers.get('Authorization') if header is not None: authtype, encoded = header.split(' ') if authtype == 'Basic': user, key = base64.b64decode(encoded).decode('utf8').split(':') rpc.key = key if rpc.key is None: rpc.key = rpc.defaultkey def _rpc(self, method, cmdline): try: data = method(cmdline) except ValueError as e: if str(e) == "Error: badauth": self._replyunauth() return self._simplereply(HTTPStatus.BAD_REQUEST, 'Bad Command') return self._replyjson(data) return def _rpc_read(self, rpc): self._extractauth(rpc) tail = self.path.split('/') cmd = tail[2] # if reads ever need args, could use more of the tail self._rpc(rpc.read, cmd) def _rpc_write(self, rpc): self._extractauth(rpc) content_length = int(self.headers['Content-Length']) post_data = self.rfile.read(content_length).decode('utf8') tail = self.path.split('/') cmd = tail[2] cmdline = cmd + ' ' + post_data self._rpc(rpc.write, cmdline) def do_GET(self): if self.path.startswith("/edge/"): self._rpc_read(self.rpc) return if self.path.startswith("/supernode/"): self._rpc_read(self.snrpc) return if self.path in pages: page = pages[self.path] self.send_response(HTTPStatus.OK) self.send_header('Content-type', page['content_type']) self.end_headers() self.wfile.write(page['content'].encode('utf8')) return self._simplereply(HTTPStatus.NOT_FOUND, 'Not Found') return def do_POST(self): if self.path.startswith("/edge/"): self._rpc_write(self.rpc) return if self.path.startswith("/supernode/"): self._rpc_write(self.snrpc) return def main(): ap = argparse.ArgumentParser( description='Control the running local n2n edge via http') ap.add_argument('-t', '--mgmtport', action='store', default=5644, help='Management Port (default=5644)', type=int) ap.add_argument('--snmgmtport', action='store', default=5645, help='Supernode Management Port (default=5645)', type=int) 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() rpc = JsonUDP(args.mgmtport) rpc.debug = args.debug rpc.defaultkey = args.key snrpc = JsonUDP(args.snmgmtport) snrpc.debug = args.debug snrpc.defaultkey = args.key signal.signal(signal.SIGPIPE, signal.SIG_DFL) socketserver.TCPServer.allow_reuse_address = True handler = functools.partial(SimpleHandler, rpc, snrpc) with socketserver.TCPServer(("", args.port), handler) as httpd: try: httpd.serve_forever() except KeyboardInterrupt: return if __name__ == '__main__': main()