server.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. from __future__ import print_function
  4. from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
  5. from storage import Storage
  6. import json
  7. import logging
  8. import re
  9. import socket
  10. from SocketServer import ThreadingMixIn
  11. import time
  12. class BatcaveHttpRequestHandler(BaseHTTPRequestHandler):
  13. DATAKEY_VPN = '__VPN__'
  14. def __init__(self, request, client_address, server):
  15. self.logger = logging.getLogger('API')
  16. BaseHTTPRequestHandler.__init__(self, request, client_address, server)
  17. def parse_url_pathquery(self):
  18. """Extracts the query parameters from the request path."""
  19. url = re.match(r'^/(?P<path>.*?)(\?(?P<query>.+))?$', self.path.strip())
  20. if url is None:
  21. logging.warn('Failed to parse URL \'' + str(self.path) + '\'.')
  22. return ( None, None )
  23. path = url.group('path')
  24. query = {}
  25. if not url.group('query') is None:
  26. for m in re.finditer(r'(?P<key>.+?)=(?P<value>.+?)(&|$)', url.group('query')):
  27. query[m.group('key')] = m.group('value')
  28. return ( path, query )
  29. def do_GET(self):
  30. """Handles all HTTP GET requests."""
  31. path, query = self.parse_url_pathquery()
  32. if path is None:
  33. self.send_error(400, 'Could not parse URL (' + str(self.path) + ')')
  34. return
  35. # / - index page, shows generic help
  36. if path == '':
  37. self.respond_index(query)
  38. return
  39. # /list - list stored nodes
  40. if path == 'list':
  41. self.respond_list(query)
  42. return
  43. # /vpn - notification endpoint for gateway's VPN connections
  44. if path == 'vpn':
  45. self.respond_vpn(query)
  46. return
  47. # /node/<id>.json - node's data
  48. # /node/<id>/field - return specific field from node's data
  49. m = re.match(r'node/([a-f0-9]{12})(?P<cmd>\.json|/[a-zA-Z0-9_\-]+)$', path)
  50. if m != None:
  51. cmd = m.group('cmd')
  52. if cmd == '.json':
  53. self.respond_node(m.group(1))
  54. else:
  55. self.respond_nodedetail(m.group(1), cmd[1:])
  56. return
  57. # no match -> 404
  58. self.send_error(404, 'The URL \'{0}\' was not found here.'.format(path))
  59. def send_nocache_headers(self):
  60. """Sets HTTP headers indicating that this response shall not be cached."""
  61. self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
  62. self.send_header('Pragma', 'no-cache')
  63. self.send_header('Expires', '0')
  64. def send_headers(self, content_type='text/html; charset=utf-8', nocache=True):
  65. """Send HTTP 200 Response header with the given Content-Type.
  66. Optionally send no-caching headers, too."""
  67. self.send_response(200)
  68. self.send_header('Content-Type', content_type)
  69. if nocache: self.send_nocache_headers()
  70. self.end_headers()
  71. def respond_index(self, query):
  72. """Display the index page."""
  73. storage = self.server.storage
  74. self.send_headers()
  75. self.wfile.write('<!DOCTYPE html><html><head><title>BATCAVE</title></head>\n')
  76. self.wfile.write('<body>\n')
  77. self.wfile.write('<H1 title="Batman/Alfred Transmission Collection, Aggregation & Value Engine">BATCAVE</H1>\n')
  78. self.wfile.write('<p>Dies ist ein interner Hintergrund-Dienst. Er wird nur von anderen Diensten\n')
  79. self.wfile.write('angesprochen und sollte aus einer Mehrzahl von Gr&uuml;nden nicht &ouml;ffentlich\n')
  80. self.wfile.write('zug&auml;nglich sein.</p>\n')
  81. self.wfile.write('<H2>Status</H2>\n')
  82. self.wfile.write('Daten: <span id="datacount" class="value">')
  83. self.wfile.write(len(storage.data))
  84. self.wfile.write('</span>\n')
  85. self.wfile.write('<H2>API</H2>\n')
  86. self.wfile.write('<p>Grundsätzlich ist das Antwort-Format JSON und alle Daten sind Live-Daten (kein Cache) die ggf. etwas Bearbeitungs-Zeit erfordern.</p>')
  87. self.wfile.write('<dl>\n')
  88. self.wfile.write('<dt><a href="/nodes.json">nodes.json</a></dt><dd>zur Verwendung mit ffmap (MACs anonymisiert)</dd>\n')
  89. self.wfile.write('<dt><a href="/node/ff00ff00ff00.json">/node/&lt;id&gt;.json</a></dt><dd><u>alle</u> vorhandenen Information zu der gewünschten Node</dd>\n')
  90. self.wfile.write('</dl>\n')
  91. self.wfile.write('</body></html>')
  92. def respond_list(self, query):
  93. """List stored data."""
  94. storage = self.server.storage
  95. self.send_headers()
  96. self.wfile.write('<!DOCTYPE html><html><head><title>BATCAVE</title></head>\n')
  97. self.wfile.write('<body>\n')
  98. self.wfile.write('<H1>BATCAVE - LIST</H1>\n')
  99. self.wfile.write('<table>\n')
  100. self.wfile.write('<thead><tr><th>ID</th><th>Name</th></tr></thead>\n')
  101. self.wfile.write('<tbody>\n')
  102. data = storage.data
  103. if 'sort' in query:
  104. if query['sort'] == 'name':
  105. sorteddata = sorted(data, key=lambda x: data[x]['hostname'].lower())
  106. data = sorteddata
  107. elif query['sort'] == 'id':
  108. sorteddata = sorted(data)
  109. data = sorteddata
  110. for nodeid in data:
  111. if nodeid.startswith('__'): continue
  112. nodename = storage.data[nodeid]['hostname'] if 'hostname' in storage.data[nodeid] else '&lt;?&gt;'
  113. self.wfile.write('<tr><td><a href="/node/' + nodeid + '.json">' + nodeid + '</a></td><td>' + nodename + '</td></tr>')
  114. self.wfile.write('</tbody>\n')
  115. self.wfile.write('</table>\n')
  116. def find_node(self, rawid):
  117. """Fetch node data from storage by given id, if necessary looking thorugh node aliases."""
  118. storage = self.server.storage
  119. # if we have a direct hit, return it immediately
  120. if rawid in storage.data:
  121. return storage.data[rawid]
  122. # no direct hit -> search via aliases
  123. nodeid = rawid
  124. for n in storage.data:
  125. if 'aliases' in storage.data[n] and rawid in storage.data[n]['aliases']:
  126. nodeid = n
  127. # return found node
  128. return storage.data[nodeid] if nodeid in storage.data else None
  129. def respond_node(self, rawid):
  130. """Display node data."""
  131. # handle API example linked on index page
  132. if rawid == 'ff00ff00ff00':
  133. self.send_headers('text/json')
  134. self.wfile.write(json.dumps({
  135. 'name': 'API-Example',
  136. 'nodeid': rawid,
  137. 'META': 'Dies ist ein minimaler Beispiel-Datensatz. Herzlichen Glückwunsch, du hast das Prinzip der API kapiert.',
  138. }))
  139. return
  140. # search node by the given id
  141. node = self.find_node(rawid)
  142. # handle unknown nodes
  143. if node is None:
  144. self.send_error(404, 'No node with id \'' + rawid + '\' present.')
  145. return
  146. # dump node data as JSON
  147. self.send_headers('text/json')
  148. self.wfile.write(json.dumps(node))
  149. def respond_nodedetail(self, nodeid, field):
  150. """Return a field from the given node - a string is returned as text, all other as JSON."""
  151. node = self.find_node(nodeid)
  152. if node is None:
  153. self.send_error(404, 'No node with id \'' + nodeid + '\' present.')
  154. return
  155. if not field in node:
  156. self.send_error(404, 'The node \'' + nodeid + '\' does not have a field named \'' + str(field) + '\'.')
  157. return
  158. value = node[field]
  159. self.send_headers('text/plain' if isinstance(value, basestring) else 'text/json')
  160. self.wfile.write(value if isinstance(value, basestring) else json.dumps(value))
  161. def respond_vpn(self, query):
  162. storage = self.server.storage
  163. peername = query['peer'] if 'peer' in query else None
  164. key = query['key'] if 'key' in query else None
  165. action = query['action'] if 'action' in query else None
  166. remote = query['remote'] if 'remote' in query else None
  167. gw = query['gw'] if 'gw' in query else None
  168. if action == 'list':
  169. self.respond_vpnlist()
  170. return
  171. if action != 'establish' and action != 'disestablish':
  172. self.logger.error('VPN: unknown action \'{0}\''.format(action))
  173. self.send_error(400, 'Invalid action.')
  174. return
  175. for k,v in { 'peername': peername, 'key': key, 'remote': remote, 'gw': gw }.items():
  176. if v is None or len(v.strip()) == 0:
  177. self.logger.error('VPN {0}: no or empty {1}'.format(action, k))
  178. self.send_error(400, 'Missing value for ' + str(k))
  179. return
  180. if key is None or re.match(r'^[a-fA-F0-9]+$', key) is None:
  181. self.logger.error('VPN peer \'{0}\' {1}: bad key \'{2}\''.format(peername, action, key))
  182. self.send_error(400, 'Bad key.')
  183. return
  184. if not self.DATAKEY_VPN in storage.data: storage.data[self.DATAKEY_VPN] = {}
  185. if not key in storage.data[self.DATAKEY_VPN]: storage.data[self.DATAKEY_VPN][key] = { 'active': {}, 'last': {} }
  186. item = storage.data[self.DATAKEY_VPN][key]
  187. if action == 'establish':
  188. item['active'][gw] = { 'establish': time.time(), 'peer': peername, 'remote': remote }
  189. elif action == 'disestablish':
  190. active = {}
  191. if gw in item['active']:
  192. active = item['active'][gw]
  193. del(item['active'][gw])
  194. active['disestablish'] = time.time()
  195. item['last'][gw] = active
  196. else:
  197. self.send_error(500, 'Unknown action not filtered (' + str(action) + ')')
  198. return
  199. self.send_headers('text/plain')
  200. self.wfile.write('OK')
  201. storage.save()
  202. def respond_vpnlist(self):
  203. storage = self.server.storage
  204. gateways = ['gw01','gw02','gw03','gw04']
  205. self.send_headers()
  206. self.wfile.write('<!DOCTYPE html>\n')
  207. self.wfile.write('<html><head><title>BATCAVE - VPN LIST</title></head>\n')
  208. self.wfile.write('<body>\n')
  209. self.wfile.write('<style type="text/css">\n')
  210. self.wfile.write('table { border: 2px solid #999; border-collapse: collapse; }\n')
  211. self.wfile.write('th, td { border: 1px solid #CCC; }\n')
  212. self.wfile.write('table tbody tr.online { background-color: #CFC; }\n')
  213. self.wfile.write('table tbody tr.offline { background-color: #FCC; }\n')
  214. self.wfile.write('</style>\n')
  215. self.wfile.write('<table>\n<thead>\n')
  216. self.wfile.write('<tr><th rowspan="2">names (key)</th><th colspan="' + str(len(gateways)) + '">active</th><th colspan="' + str(len(gateways)) + '">last</th></tr>\n')
  217. self.wfile.write('<tr><th>' + '</th><th>'.join(gateways) + '</th><th>' + '</th><th>'.join(gateways) + '</th></tr>\n')
  218. self.wfile.write('</thead>\n')
  219. if self.DATAKEY_VPN in storage.data:
  220. for key in storage.data[self.DATAKEY_VPN]:
  221. item = storage.data[self.DATAKEY_VPN][key]
  222. names = set()
  223. count = {}
  224. for t in [ 'active', 'last' ]:
  225. count[t] = 0
  226. if t in item:
  227. for gw in item[t]:
  228. if 'remote' in item[t][gw] and len(item[t][gw]['remote']) > 0:
  229. count[t] += 1
  230. if 'peer' in item[t][gw]:
  231. names.add(item[t][gw]['peer'])
  232. self.wfile.write('<tr class="online">' if count['active'] > 0 else '<tr class="offline">')
  233. self.wfile.write('<td title="' + str(key) + '">' + (' / '.join(names) if len(names) > 0 else '?') + '</td>')
  234. for t in [ 'active', 'last' ]:
  235. for gw in gateways:
  236. ip = ''
  237. details = ''
  238. if t in item and gw in item[t]:
  239. ip = item[t][gw]['remote'] if 'remote' in item[t][gw] else ''
  240. self.wfile.write('<td>' + ip + '</td>')
  241. self.wfile.write('</tr>\n')
  242. self.wfile.write('</table>\n')
  243. self.wfile.write('</body>')
  244. self.wfile.write('</html>')
  245. class ApiServer(ThreadingMixIn, HTTPServer):
  246. def __init__(self, endpoint, storage):
  247. if ':' in endpoint[0]: self.address_family = socket.AF_INET6
  248. HTTPServer.__init__(self, endpoint, BatcaveHttpRequestHandler)
  249. self.storage = storage
  250. def __str__(self):
  251. return 'ApiServer on {0}'.format(self.server_address)
  252. if __name__ == '__main__':
  253. dummystorage = Storage()
  254. server = ApiServer(('0.0.0.0', 8888), dummystorage)
  255. print("Server:", str(server))
  256. server.serve_forever()