#!/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 import os import sys import importlib.machinery import importlib.util def import_filename(modulename, filename): # look in the same dir as this script pathname = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename) loader = importlib.machinery.SourceFileLoader(modulename, pathname) spec = importlib.util.spec_from_loader(modulename, loader) module = importlib.util.module_from_spec(spec) try: loader.exec_module(module) except FileNotFoundError: print("Script {} not found".format(pathname), file=sys.stderr) sys.exit(1) return module # We share the implementation of the RPC class with the n2n-ctl script. We # cannot just import the module as 'n2n-ctl' has a dash in its name :-( JsonUDP = import_filename('n2nctl', 'n2n-ctl').JsonUDP 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) { if (!response.ok) { throw new Error('Fetch got ' + response.status) } 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) { if (!response.ok) { throw new Error('Fetch got ' + response.status) } 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 except socket.timeout as e: self._simplereply(HTTPStatus.REQUEST_TIMEOUT, str(e)) 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 if hasattr(signal, 'SIGPIPE'): signal.signal(signal.SIGPIPE, signal.SIG_DFL) socketserver.TCPServer.allow_reuse_address = True handler = functools.partial(SimpleHandler, rpc, snrpc) httpd = socketserver.TCPServer(("", args.port), handler) try: print( f'Serving HTTP at port {args.port} ' f'(http://localhost:{args.port}/) ...' ) httpd.serve_forever() except KeyboardInterrupt: return if __name__ == '__main__': main()