|
|
|
#!/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/peer
|
|
|
|
# http://localhost:8080/edge/super
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import socket
|
|
|
|
import json
|
|
|
|
import socketserver
|
|
|
|
import http.server
|
|
|
|
import signal
|
|
|
|
import functools
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
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':
|
|
|
|
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 = """
|
|
|
|
<html>
|
|
|
|
<head>
|
|
|
|
<title>n2n management</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div id="time"></div>
|
|
|
|
<br>
|
|
|
|
Supernodes:
|
|
|
|
<div id="super"></div>
|
|
|
|
<br>
|
|
|
|
Peers:
|
|
|
|
<div id="peer"></div>
|
|
|
|
|
|
|
|
<script>
|
|
|
|
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) => {
|
|
|
|
s += "<td>" + row[col]
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
s += "</table>"
|
|
|
|
let div = document.getElementById(id);
|
|
|
|
div.innerHTML=s
|
|
|
|
}
|
|
|
|
|
|
|
|
function fetch_table(url, id, columns) {
|
|
|
|
fetch(url)
|
|
|
|
.then(function (response) {
|
|
|
|
return response.json();
|
|
|
|
})
|
|
|
|
.then(function (data) {
|
|
|
|
rows2table(id,columns,data);
|
|
|
|
})
|
|
|
|
.catch(function (err) {
|
|
|
|
console.log('error: ' + err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function refresh_job() {
|
|
|
|
let now = new Date().getTime();
|
|
|
|
|
|
|
|
let time = document.getElementById('time');
|
|
|
|
time.innerHTML="last updated: " + now;
|
|
|
|
|
|
|
|
fetch_table(
|
|
|
|
'edge/super',
|
|
|
|
'super',
|
|
|
|
['version','current','macaddr','sockaddr','uptime']
|
|
|
|
);
|
|
|
|
fetch_table(
|
|
|
|
'edge/peer',
|
|
|
|
'peer',
|
|
|
|
['mode','ip4addr','macaddr','sockaddr','desc']
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function refresh_setup(interval) {
|
|
|
|
var timer = setInterval(refresh_job, interval);
|
|
|
|
}
|
|
|
|
|
|
|
|
refresh_setup(5000);
|
|
|
|
refresh_job();
|
|
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
if url_tail == "/":
|
|
|
|
self.send_response(HTTPStatus.OK)
|
|
|
|
self.send_header('Content-type', 'text/html; charset=utf-8')
|
|
|
|
self.end_headers()
|
|
|
|
self.wfile.write(indexhtml.encode('utf8'))
|
|
|
|
return
|
|
|
|
|
|
|
|
if url_tail.startswith("/edge/"):
|
|
|
|
tail = url_tail.split('/')
|
|
|
|
cmd = tail[2]
|
|
|
|
# if commands ever need args, use more of the path components
|
|
|
|
|
|
|
|
try:
|
|
|
|
data = self.rpc.read(cmd)
|
|
|
|
except ValueError:
|
|
|
|
self._simplereply(HTTPStatus.BAD_REQUEST, 'Bad Command')
|
|
|
|
return
|
|
|
|
|
|
|
|
self.send_response(HTTPStatus.OK)
|
|
|
|
self.send_header('Content-type', 'application/json')
|
|
|
|
self.end_headers()
|
|
|
|
self.wfile.write(json.dumps(data).encode('utf8'))
|
|
|
|
return
|
|
|
|
|
|
|
|
self._simplereply(HTTPStatus.NOT_FOUND, 'Not Found')
|
|
|
|
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)')
|
|
|
|
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.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__':
|
|
|
|
main()
|