mirror of https://github.com/ntop/n2n.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
477 lines
12 KiB
477 lines
12 KiB
#!/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 = "<table border=1 cellspacing=0>"
|
|
data.forEach((row) => {
|
|
keys.forEach((key) => {
|
|
if (key in row) {
|
|
s += "<tr><th>" + key + "<td>" + row[key];
|
|
}
|
|
});
|
|
});
|
|
|
|
s += "</table>"
|
|
let div = document.getElementById(id);
|
|
div.innerHTML=s
|
|
}
|
|
|
|
function rows2keyvalueall(id, unused, data) {
|
|
let s = "<table border=1 cellspacing=0>"
|
|
data.forEach((row) => {
|
|
Object.keys(row).forEach((key) => {
|
|
s += "<tr><th>" + key + "<td>" + row[key];
|
|
});
|
|
});
|
|
|
|
s += "</table>"
|
|
let div = document.getElementById(id);
|
|
div.innerHTML=s
|
|
}
|
|
|
|
function rows2table(id, columns, data) {
|
|
let s = "<table border=1 cellspacing=0>"
|
|
s += "<tr>"
|
|
columns.forEach((col) => {
|
|
s += "<th>" + col
|
|
});
|
|
data.forEach((row) => {
|
|
s += "<tr>"
|
|
columns.forEach((col) => {
|
|
val = row[col]
|
|
if (typeof val === "undefined") {
|
|
val = ''
|
|
}
|
|
s += "<td>" + val
|
|
});
|
|
});
|
|
|
|
s += "</table>"
|
|
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": """
|
|
<html>
|
|
<head>
|
|
<title>n2n edge management</title>
|
|
</head>
|
|
<body>
|
|
<table>
|
|
<tr>
|
|
<td>Last Updated:
|
|
<td><div id="time"></div>
|
|
<td><button onclick=refresh_job()>update</button>
|
|
<td><button onclick=do_stop()>stop edge</button>
|
|
<tr>
|
|
<td>Logging Verbosity:
|
|
<td>
|
|
<div id="verbose"></div>
|
|
<td>
|
|
<button onclick=setverbose(verbose+1)>+</button>
|
|
<button onclick=setverbose(verbose-1)>-</button>
|
|
</table>
|
|
<br>
|
|
<div id="communities"></div>
|
|
<br>
|
|
Edges/Peers:
|
|
<div id="edges"></div>
|
|
<br>
|
|
Supernodes:
|
|
<div id="supernodes"></div>
|
|
<br>
|
|
<div id="timestamps"></div>
|
|
<br>
|
|
<div id="packetstats"></div>
|
|
|
|
<script src="script.js"></script>
|
|
<script>
|
|
// FIXME: hacky global
|
|
var nodetype="edge";
|
|
|
|
function refresh_job() {
|
|
do_get(
|
|
nodetype + '/verbose', 'verbose',
|
|
rows2verbose, null
|
|
);
|
|
do_get(
|
|
nodetype + '/communities', 'communities',
|
|
rows2keyvalue, ['community']
|
|
);
|
|
do_get(
|
|
nodetype + '/supernodes', 'supernodes',
|
|
rows2table, ['version','current','macaddr','sockaddr','uptime']
|
|
);
|
|
do_get(
|
|
nodetype + '/edges', 'edges',
|
|
rows2table, ['mode','ip4addr','macaddr','sockaddr','desc']
|
|
);
|
|
do_get(
|
|
nodetype + '/timestamps', 'timestamps',
|
|
rows2keyvalueall, null
|
|
);
|
|
do_get(
|
|
nodetype + '/packetstats', 'packetstats',
|
|
rows2table, ['type','tx_pkt','rx_pkt']
|
|
);
|
|
}
|
|
|
|
refresh_setup(10000);
|
|
refresh_job();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
""",
|
|
},
|
|
"/supernode.html": {
|
|
"content_type": "text/html; charset=utf-8",
|
|
"content": """
|
|
<html>
|
|
<head>
|
|
<title>n2n supernode management</title>
|
|
</head>
|
|
<body>
|
|
<table>
|
|
<tr>
|
|
<td>Last Updated:
|
|
<td><div id="time"></div>
|
|
<td><button onclick=refresh_job()>update</button>
|
|
<td><button onclick=do_stop()>stop supernode</button>
|
|
<tr>
|
|
<td>Logging Verbosity:
|
|
<td>
|
|
<div id="verbose"></div>
|
|
<td>
|
|
<button onclick=setverbose(verbose+1)>+</button>
|
|
<button onclick=setverbose(verbose-1)>-</button>
|
|
<td><button onclick=do_reload()>reload communities</button>
|
|
</table>
|
|
<br>
|
|
Communities:
|
|
<div id="communities"></div>
|
|
<br>
|
|
Edges/Peers:
|
|
<div id="edges"></div>
|
|
<br>
|
|
<div id="timestamps"></div>
|
|
<br>
|
|
<div id="packetstats"></div>
|
|
|
|
<script src="script.js"></script>
|
|
<script>
|
|
// FIXME: hacky global
|
|
var nodetype="supernode";
|
|
|
|
function do_reload() {
|
|
fetch(nodetype + '/reload_communities', {method:'POST'})
|
|
}
|
|
|
|
function refresh_job() {
|
|
do_get(
|
|
nodetype + '/verbose', 'verbose',
|
|
rows2verbose, null
|
|
);
|
|
do_get(
|
|
nodetype + '/communities', 'communities',
|
|
rows2table, ['community','ip4addr','is_federation','purgeable']
|
|
);
|
|
do_get(
|
|
nodetype + '/edges', 'edges',
|
|
rows2table,
|
|
['community','ip4addr','macaddr','sockaddr','proto','desc']
|
|
);
|
|
do_get(
|
|
nodetype + '/timestamps', 'timestamps',
|
|
rows2keyvalueall, null
|
|
);
|
|
do_get(
|
|
nodetype + '/packetstats', 'packetstats',
|
|
rows2table, ['type','tx_pkt','rx_pkt']
|
|
);
|
|
}
|
|
|
|
refresh_setup(10000);
|
|
refresh_job();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
""",
|
|
},
|
|
}
|
|
|
|
|
|
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()
|
|
|