server.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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. def __init__(self, request, client_address, server):
  14. self.logger = logging.getLogger('API')
  15. BaseHTTPRequestHandler.__init__(self, request, client_address, server)
  16. def parse_url_pathquery(self):
  17. url = re.match(r'^\/(?P<path>.+?)(\?(?P<query>.+))?$', self.path)
  18. if url is None:
  19. logging.warn('Failed to parse URL \'' + str(self.path) + '\'.')
  20. return ( None, None )
  21. path = url.group('path')
  22. query = {}
  23. if not url.group('query') is None:
  24. for m in re.finditer(r'(?P<key>.+?)=(?P<value>.+?)(&|$)', url.group('query')):
  25. query[m.group('key')] = m.group('value')
  26. return ( path, query )
  27. def do_GET(self):
  28. path, query = self.parse_url_pathquery()
  29. if path == '':
  30. self.respond_index(query)
  31. return
  32. if path == 'list':
  33. self.respond_list(query)
  34. return
  35. if path == 'vpn':
  36. self.respond_vpn(query)
  37. return
  38. m = re.match(r'node/([a-f0-9]{12})(?P<cmd>\.json|/[a-zA-Z0-9_\-]+)$', path)
  39. if m != None:
  40. cmd = m.group('cmd')
  41. if cmd == '.json':
  42. self.respond_node(m.group(1))
  43. else:
  44. self.respond_nodedetail(m.group(1), cmd[1:])
  45. return
  46. self.send_error(404, 'The URL \'{0}\' was not found here.'.format(path))
  47. def send_nocache_headers(self):
  48. self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
  49. self.send_header('Pragma', 'no-cache')
  50. self.send_header('Expires', '0')
  51. def send_headers(self, content_type='text/html; charset=utf-8', nocache=True):
  52. self.send_response(200)
  53. self.send_header('Content-Type', content_type)
  54. if nocache: self.send_nocache_headers()
  55. self.end_headers()
  56. def respond_index(self, query):
  57. storagy = self.server.storage
  58. self.send_headers()
  59. self.wfile.write('<!DOCTYPE html><html><head><title>BATCAVE</title></head>\n')
  60. self.wfile.write('<body>\n')
  61. self.wfile.write('<H1 title="Batman/Alfred Transmission Collection, Aggregation & Value Engine">BATCAVE</H1>\n')
  62. self.wfile.write('<p>Dies ist ein interner Hintergrund-Dienst. Er wird nur von anderen Diensten\n')
  63. self.wfile.write('angesprochen und sollte aus einer Mehrzahl von Gr&uuml;nden nicht &ouml;ffentlich\n')
  64. self.wfile.write('zug&auml;nglich sein.</p>\n')
  65. self.wfile.write('<H2>Status</H2>\n')
  66. self.wfile.write('Daten: <span id="datacount" class="value">')
  67. self.wfile.write(len(storage.data))
  68. self.wfile.write('</span>\n')
  69. self.wfile.write('<H2>API</H2>\n')
  70. 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>')
  71. self.wfile.write('<dl>\n')
  72. self.wfile.write('<dt><a href="/nodes.json">nodes.json</a></dt><dd>zur Verwendung mit ffmap (MACs anonymisiert)</dd>\n')
  73. 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')
  74. self.wfile.write('</dl>\n')
  75. self.wfile.write('</body></html>')
  76. def respond_list(self, query):
  77. storage = self.server.storage
  78. self.send_headers()
  79. self.wfile.write('<!DOCTYPE html><html><head><title>BATCAVE</title></head>\n')
  80. self.wfile.write('<body>\n')
  81. self.wfile.write('<H1>BATCAVE - LIST</H1>\n')
  82. self.wfile.write('<table>\n')
  83. self.wfile.write('<thead><tr><th>ID</th><th>Name</th></tr></thead>\n')
  84. self.wfile.write('<tbody>\n')
  85. data = storage.data
  86. if 'sort' in query:
  87. if query['sort'] == 'name':
  88. sorteddata = sorted(data, key=lambda x: data[x]['hostname'].lower())
  89. data = sorteddata
  90. elif query['sort'] == 'id':
  91. sorteddata = sorted(data)
  92. data = sorteddata
  93. for nodeid in data:
  94. nodename = storage.data[nodeid]['hostname'] if 'hostname' in storage.data[nodeid] else '&lt;?&gt;'
  95. self.wfile.write('<tr><td><a href="/node/' + nodeid + '.json">' + nodeid + '</a></td><td>' + nodename + '</td></tr>')
  96. self.wfile.write('</tbody>\n')
  97. self.wfile.write('</table>\n')
  98. def respond_node(self, nodeid):
  99. storage = self.server.storage
  100. if nodeid == 'ff00ff00ff00':
  101. self.send_headers('text/json')
  102. self.wfile.write(json.dumps({
  103. 'name': 'API-Example',
  104. 'nodeid': nodeid,
  105. 'META': 'Dies ist ein minimaler Beispiel-Datensatz. Herzlichen Glückwunsch, du hast das Prinzip der API kapiert.',
  106. }))
  107. return
  108. if not nodeid in storage.data:
  109. self.send_error(404, 'No node with id \'' + nodeid + '\' present.')
  110. return
  111. self.send_headers('text/json')
  112. self.wfile.write(json.dumps(storage.data[nodeid]))
  113. def respond_nodedetail(self, nodeid, field):
  114. storage = self.server.storage
  115. if not nodeid in storage.data:
  116. self.send_error(404, 'No node with id \'' + nodeid + '\' present.')
  117. return
  118. if not field in storage.data[nodeid]:
  119. self.send_error(404, 'The node \'' + nodeid + '\' does not have a field named \'' + str(field) + '\'.')
  120. return
  121. value = storage.data[nodeid][field]
  122. self.send_headers('text/plain' if isinstance(value, basestring) else 'text/json')
  123. self.wfile.write(value if isinstance(value, basestring) else json.dumps(value))
  124. def respond_vpn(self, query):
  125. storage = self.server.storage
  126. peername = query['peer'] if 'peer' in query else None
  127. key = query['key'] if 'key' in query else None
  128. action = query['action'] if 'action' in query else None
  129. remote = query['remote'] if 'remote' in query else None
  130. gw = query['gw'] if 'gw' in query else None
  131. if action == 'list':
  132. self.respond_vpnlist()
  133. return
  134. if action != 'establish' and action != 'disestablish':
  135. self.logger.error('VPN: unknown action \'{0}\''.format(action))
  136. self.send_error(400, 'Invalid action.')
  137. return
  138. for k,v in { 'peername': peername, 'key': key, 'remote': remote, 'gw': gw }.items():
  139. if v is None or len(v.strip()) == 0:
  140. self.logger.error('VPN {0}: no or empty {1}'.format(action, k))
  141. self.send_error(400, 'Missing value for ' + str(k))
  142. return
  143. if key is None or re.match(r'^[a-fA-F0-9]+$', key) is None:
  144. self.logger.error('VPN peer \'{0}\' {1}: bad key \'{2}\''.format(peername, action, key))
  145. self.send_error(400, 'Bad key.')
  146. return
  147. if not 'vpn' in storage.data: storage.data['vpn'] = {}
  148. if not key in storage.data['vpn']: storage.data['vpn'][key] = { 'active': {}, 'last': {} }
  149. item = storage.data['vpn'][key]
  150. if action == 'establish':
  151. item['active'][gw] = { 'establish': time.time(), 'peer': peername, 'remote': remote }
  152. elif action == 'disestablish':
  153. active = {}
  154. if gw in item['active']:
  155. active = item['active'][gw]
  156. del(item['active'][gw])
  157. active['disestablish'] = time.time()
  158. item['last'][gw] = active
  159. else:
  160. self.send_error(500, 'Unknown action not filtered (' + str(action) + ')')
  161. return
  162. self.send_headers('text/plain')
  163. self.wfile.write('OK')
  164. storage.save()
  165. def respond_vpnlist(self):
  166. storage = self.server.storage
  167. self.send_headers()
  168. self.wfile.write('<!DOCTYPE html>\n')
  169. self.wfile.write('<html><head><title>BATCAVE - VPN LIST</title></head>\n')
  170. self.wfile.write('<body>\n')
  171. self.wfile.write('<table>\n<thead><tr><th>key</th><th>active</th><th>last</th></tr></thead>\n')
  172. if 'vpn' in storage.data:
  173. for key in storage.data['vpn']:
  174. item = storage.data['vpn'][key]
  175. self.wfile.write('<tr><td>' + str(key) + '</td>')
  176. self.wfile.write('<td>' + json.dumps(item['active'] if 'active' in item else {}) + '</td>')
  177. self.wfile.write('<td>' + json.dumps(item['last'] if 'last' in item else {}) + '</td>')
  178. self.wfile.write('</tr>\n')
  179. self.wfile.write('</table>\n')
  180. self.wfile.write('</body>')
  181. self.wfile.write('</html>')
  182. class ApiServer(ThreadingMixIn, HTTPServer):
  183. def __init__(self, endpoint, storage):
  184. if ':' in endpoint[0]: self.address_family = socket.AF_INET6
  185. HTTPServer.__init__(self, endpoint, BatcaveHttpRequestHandler)
  186. self.storage = storage
  187. def __str__(self):
  188. return 'ApiServer on {0}'.format(self.server_address)
  189. if __name__ == '__main__':
  190. dummystorage = Storage()
  191. server = ApiServer(('0.0.0.0', 8888), dummystorage)
  192. print("Server:", str(server))
  193. server.serve_forever()