n2n/scripts/n2nhttpd
Hamish Coleman e6fcf1c55b
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
2021-10-18 02:01:42 +05:45

271 lines
7.0 KiB
Python
Executable File

#!/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()