server.py 8.4 KB

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