#!/usr/bin/python # -*- coding: utf-8 -*- from __future__ import print_function from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer from storage import Storage import json import logging import re import socket from SocketServer import ThreadingMixIn import time class BatcaveHttpRequestHandler(BaseHTTPRequestHandler): DATAKEY_VPN = '__VPN__' def __init__(self, request, client_address, server): self.logger = logging.getLogger('API') BaseHTTPRequestHandler.__init__(self, request, client_address, server) def parse_url_pathquery(self): """Extracts the query parameters from the request path.""" url = re.match(r'^/(?P.*?)(\?(?P.+))?$', self.path.strip()) if url is None: logging.warn('Failed to parse URL \'' + str(self.path) + '\'.') return ( None, None ) path = url.group('path') query = {} if not url.group('query') is None: for m in re.finditer(r'(?P.+?)=(?P.+?)(&|$)', url.group('query')): query[m.group('key')] = m.group('value') return ( path, query ) def do_GET(self): """Handles all HTTP GET requests.""" path, query = self.parse_url_pathquery() if path is None: self.send_error(400, 'Could not parse URL (' + str(self.path) + ')') return # / - index page, shows generic help if path == '': self.respond_index(query) return # /list - list stored nodes if path == 'list': self.respond_list(query) return # /vpn - notification endpoint for gateway's VPN connections if path == 'vpn': self.respond_vpn(query) return # /node/.json - node's data # /node//field - return specific field from node's data m = re.match(r'node/([a-f0-9]{12})(?P\.json|/[a-zA-Z0-9_\-]+)$', path) if m != None: cmd = m.group('cmd') if cmd == '.json': self.respond_node(m.group(1)) else: self.respond_nodedetail(m.group(1), cmd[1:]) return # no match -> 404 self.send_error(404, 'The URL \'{0}\' was not found here.'.format(path)) def send_nocache_headers(self): """Sets HTTP headers indicating that this response shall not be cached.""" self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate') self.send_header('Pragma', 'no-cache') self.send_header('Expires', '0') def send_headers(self, content_type='text/html; charset=utf-8', nocache=True): """Send HTTP 200 Response header with the given Content-Type. Optionally send no-caching headers, too.""" self.send_response(200) self.send_header('Content-Type', content_type) if nocache: self.send_nocache_headers() self.end_headers() def respond_index(self, query): """Display the index page.""" storage = self.server.storage self.send_headers() self.wfile.write('BATCAVE\n') self.wfile.write('\n') self.wfile.write('

BATCAVE

\n') self.wfile.write('

Dies ist ein interner Hintergrund-Dienst. Er wird nur von anderen Diensten\n') self.wfile.write('angesprochen und sollte aus einer Mehrzahl von Gründen nicht öffentlich\n') self.wfile.write('zugänglich sein.

\n') self.wfile.write('

Status

\n') self.wfile.write('Daten: ') self.wfile.write(len(storage.data)) self.wfile.write('\n') self.wfile.write('

API

\n') self.wfile.write('

Grundsätzlich ist das Antwort-Format JSON und alle Daten sind Live-Daten (kein Cache) die ggf. etwas Bearbeitungs-Zeit erfordern.

') self.wfile.write('
\n') self.wfile.write('
nodes.json
zur Verwendung mit ffmap (MACs anonymisiert)
\n') self.wfile.write('
/node/<id>.json
alle vorhandenen Information zu der gewünschten Node
\n') self.wfile.write('
\n') self.wfile.write('') def respond_list(self, query): """List stored data.""" storage = self.server.storage self.send_headers() self.wfile.write('BATCAVE\n') self.wfile.write('\n') self.wfile.write('

BATCAVE - LIST

\n') self.wfile.write('\n') self.wfile.write('\n') self.wfile.write('\n') data = storage.data if 'sort' in query: if query['sort'] == 'name': sorteddata = sorted(data, key=lambda x: data[x]['hostname'].lower()) data = sorteddata elif query['sort'] == 'id': sorteddata = sorted(data) data = sorteddata for nodeid in data: if nodeid.startswith('__'): continue nodename = storage.data[nodeid]['hostname'] if 'hostname' in storage.data[nodeid] else '<?>' self.wfile.write('') self.wfile.write('\n') self.wfile.write('
IDName
' + nodeid + '' + nodename + '
\n') def respond_node(self, nodeid): """Display node data.""" storage = self.server.storage if nodeid == 'ff00ff00ff00': self.send_headers('text/json') self.wfile.write(json.dumps({ 'name': 'API-Example', 'nodeid': nodeid, 'META': 'Dies ist ein minimaler Beispiel-Datensatz. Herzlichen Glückwunsch, du hast das Prinzip der API kapiert.', })) return # handle unknown nodes if not nodeid in storage.data: self.send_error(404, 'No node with id \'' + nodeid + '\' present.') return # dump node data as JSON self.send_headers('text/json') self.wfile.write(json.dumps(storage.data[nodeid])) def respond_nodedetail(self, nodeid, field): storage = self.server.storage if not nodeid in storage.data: self.send_error(404, 'No node with id \'' + nodeid + '\' present.') return if not field in storage.data[nodeid]: self.send_error(404, 'The node \'' + nodeid + '\' does not have a field named \'' + str(field) + '\'.') return value = storage.data[nodeid][field] self.send_headers('text/plain' if isinstance(value, basestring) else 'text/json') self.wfile.write(value if isinstance(value, basestring) else json.dumps(value)) def respond_vpn(self, query): storage = self.server.storage peername = query['peer'] if 'peer' in query else None key = query['key'] if 'key' in query else None action = query['action'] if 'action' in query else None remote = query['remote'] if 'remote' in query else None gw = query['gw'] if 'gw' in query else None if action == 'list': self.respond_vpnlist() return if action != 'establish' and action != 'disestablish': self.logger.error('VPN: unknown action \'{0}\''.format(action)) self.send_error(400, 'Invalid action.') return for k,v in { 'peername': peername, 'key': key, 'remote': remote, 'gw': gw }.items(): if v is None or len(v.strip()) == 0: self.logger.error('VPN {0}: no or empty {1}'.format(action, k)) self.send_error(400, 'Missing value for ' + str(k)) return if key is None or re.match(r'^[a-fA-F0-9]+$', key) is None: self.logger.error('VPN peer \'{0}\' {1}: bad key \'{2}\''.format(peername, action, key)) self.send_error(400, 'Bad key.') return if not self.DATAKEY_VPN in storage.data: storage.data[self.DATAKEY_VPN] = {} if not key in storage.data[self.DATAKEY_VPN]: storage.data[self.DATAKEY_VPN][key] = { 'active': {}, 'last': {} } item = storage.data[self.DATAKEY_VPN][key] if action == 'establish': item['active'][gw] = { 'establish': time.time(), 'peer': peername, 'remote': remote } elif action == 'disestablish': active = {} if gw in item['active']: active = item['active'][gw] del(item['active'][gw]) active['disestablish'] = time.time() item['last'][gw] = active else: self.send_error(500, 'Unknown action not filtered (' + str(action) + ')') return self.send_headers('text/plain') self.wfile.write('OK') storage.save() def respond_vpnlist(self): storage = self.server.storage self.send_headers() self.wfile.write('\n') self.wfile.write('BATCAVE - VPN LIST\n') self.wfile.write('\n') self.wfile.write('\n\n') if self.DATAKEY_VPN in storage.data: for key in storage.data[self.DATAKEY_VPN]: item = storage.data[self.DATAKEY_VPN][key] self.wfile.write('') self.wfile.write('') self.wfile.write('') self.wfile.write('\n') self.wfile.write('
keyactivelast
' + str(key) + '' + json.dumps(item['active'] if 'active' in item else {}) + '' + json.dumps(item['last'] if 'last' in item else {}) + '
\n') self.wfile.write('') self.wfile.write('') class ApiServer(ThreadingMixIn, HTTPServer): def __init__(self, endpoint, storage): if ':' in endpoint[0]: self.address_family = socket.AF_INET6 HTTPServer.__init__(self, endpoint, BatcaveHttpRequestHandler) self.storage = storage def __str__(self): return 'ApiServer on {0}'.format(self.server_address) if __name__ == '__main__': dummystorage = Storage() server = ApiServer(('0.0.0.0', 8888), dummystorage) print("Server:", str(server)) server.serve_forever()