server.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. from __future__ import print_function
  4. from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
  5. import cgi
  6. from storage import Storage
  7. import json
  8. import logging
  9. import pygeoip
  10. import re
  11. import socket
  12. from SocketServer import ThreadingMixIn
  13. import time
  14. import ffstatus
  15. class BatcaveHttpRequestHandler(BaseHTTPRequestHandler):
  16. DATAKEY_VPN = '__VPN__'
  17. FIELDKEY_UPDATED = '__UPDATED__'
  18. def __init__(self, request, client_address, server):
  19. self.logger = logging.getLogger('API')
  20. BaseHTTPRequestHandler.__init__(self, request, client_address, server)
  21. def parse_url_pathquery(self):
  22. """Extracts the query parameters from the request path."""
  23. url = re.match(r'^/(?P<path>.*?)(\?(?P<query>.+))?$', self.path.strip())
  24. if url is None:
  25. logging.warn('Failed to parse URL \'' + str(self.path) + '\'.')
  26. return ( None, None )
  27. path = url.group('path')
  28. query = {}
  29. if not url.group('query') is None:
  30. for m in re.finditer(r'(?P<key>.+?)=(?P<value>.+?)(&|$)', url.group('query')):
  31. query[m.group('key')] = m.group('value')
  32. return ( path, query )
  33. def do_GET(self):
  34. """Handles all HTTP GET requests."""
  35. path, query = self.parse_url_pathquery()
  36. if path is None:
  37. self.send_error(400, 'Could not parse URL (' + str(self.path) + ')')
  38. return
  39. # / - index page, shows generic help
  40. if path == '':
  41. self.respond_index(query)
  42. return
  43. # /list - list stored nodes
  44. if path == 'list':
  45. self.respond_list(query)
  46. return
  47. # /vpn - notification endpoint for gateway's VPN connections
  48. if path == 'vpn':
  49. self.respond_vpn(query)
  50. return
  51. # /providers
  52. if path == 'providers':
  53. self.respond_providers(query)
  54. return
  55. # /node/<id>.json - node's data
  56. # /node/<id>/field - return specific field from node's data
  57. m = re.match(r'node/(?P<id>[a-fA-F0-9]{12})(?P<cmd>\.json|/[a-zA-Z0-9_\-\.]+)$', path)
  58. if m != None:
  59. cmd = m.group('cmd')
  60. nodeid = m.group('id').lower()
  61. if cmd == '.json':
  62. self.respond_node(nodeid)
  63. else:
  64. self.respond_nodedetail(nodeid, cmd[1:])
  65. return
  66. # /status/<id> - node's status
  67. m = re.match(r'status/([a-f0-9]{12})$', path)
  68. if m != None:
  69. self.respond_nodestatus(m.group(1))
  70. return
  71. # no match -> 404
  72. self.send_error(404, 'The URL \'{0}\' was not found here.'.format(path))
  73. def do_POST(self):
  74. """Handles all HTTP POST requests."""
  75. path, query = self.parse_url_pathquery()
  76. if path is None:
  77. self.send_error(400, 'Could not parse URL (' + str(self.path) + ')')
  78. return
  79. params = self.parse_post_params()
  80. # node id/mac to name mapping
  81. if path == 'idmac2name':
  82. self.respond_nodeidmac2name(params)
  83. return
  84. # no match -> 404
  85. self.send_error(404, 'The URL \'{0}\' was not found here.'.format(path))
  86. def send_nocache_headers(self):
  87. """Sets HTTP headers indicating that this response shall not be cached."""
  88. self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
  89. self.send_header('Pragma', 'no-cache')
  90. self.send_header('Expires', '0')
  91. def send_headers(self, content_type='text/html; charset=utf-8', nocache=True):
  92. """Send HTTP 200 Response header with the given Content-Type.
  93. Optionally send no-caching headers, too."""
  94. self.send_response(200)
  95. self.send_header('Content-Type', content_type)
  96. if nocache:
  97. self.send_nocache_headers()
  98. self.end_headers()
  99. def parse_post_params(self):
  100. ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
  101. if ctype == 'multipart/form-data':
  102. postvars = cgi.parse_multipart(self.rfile, pdict)
  103. elif ctype == 'application/x-www-form-urlencoded':
  104. length = int(self.headers.getheader('content-length'))
  105. postvars = cgi.parse_qs(self.rfile.read(length), keep_blank_values=1)
  106. else:
  107. postvars = {}
  108. return postvars
  109. def respond_index(self, query):
  110. """Display the index page."""
  111. storage = self.server.storage
  112. self.send_headers()
  113. self.wfile.write('<!DOCTYPE html><html><head><title>BATCAVE</title></head>\n')
  114. self.wfile.write('<body>\n')
  115. self.wfile.write('<H1 title="Batman/Alfred Transmission Collection, Aggregation & Value Engine">BATCAVE</H1>\n')
  116. self.wfile.write('<p>Dies ist ein interner Hintergrund-Dienst. Er wird nur von anderen Diensten\n')
  117. self.wfile.write('angesprochen und sollte aus einer Mehrzahl von Gr&uuml;nden nicht &ouml;ffentlich\n')
  118. self.wfile.write('zug&auml;nglich sein.</p>\n')
  119. self.wfile.write('<H2>Status</H2>\n')
  120. self.wfile.write('Daten: <span id="datacount" class="value">')
  121. self.wfile.write(len(storage.data))
  122. self.wfile.write('</span>\n')
  123. self.wfile.write('<H2>API</H2>\n')
  124. 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>')
  125. self.wfile.write('<dl>\n')
  126. self.wfile.write('<dt><a href="/nodes.json">nodes.json</a></dt><dd>zur Verwendung mit ffmap (MACs anonymisiert)</dd>\n')
  127. 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')
  128. self.wfile.write('</dl>\n')
  129. self.wfile.write('</body></html>')
  130. def respond_list(self, query):
  131. """List stored data."""
  132. storage = self.server.storage
  133. self.send_headers()
  134. self.wfile.write('<!DOCTYPE html><html>\n')
  135. self.wfile.write('<head><title>BATCAVE</title></head>\n')
  136. self.wfile.write('<body>\n')
  137. self.wfile.write('<H1>BATCAVE - LIST</H1>\n')
  138. self.wfile.write('<table>\n')
  139. self.wfile.write('<thead><tr><th>ID</th><th>Name</th></tr></thead>\n')
  140. self.wfile.write('<tbody>\n')
  141. data = storage.data
  142. if 'sort' in query:
  143. if query['sort'] == 'name':
  144. sorteddata = sorted(data, key=lambda x: data[x]['hostname'].lower())
  145. data = sorteddata
  146. elif query['sort'] == 'id':
  147. sorteddata = sorted(data)
  148. data = sorteddata
  149. for nodeid in data:
  150. if nodeid.startswith('__'):
  151. continue
  152. nodename = storage.data[nodeid]['hostname'] if 'hostname' in storage.data[nodeid] else '&lt;?&gt;'
  153. self.wfile.write('<tr><td><a href="/node/' + nodeid + '.json">' + nodeid + '</a></td><td>' + nodename + '</td></tr>')
  154. self.wfile.write('</tbody>\n')
  155. self.wfile.write('</table>\n')
  156. def find_node(self, rawid):
  157. """Fetch node data from storage by given id, if necessary looking thorugh node aliases."""
  158. storage = self.server.storage
  159. # if we have a direct hit, return it immediately
  160. if rawid in storage.data:
  161. return storage.data[rawid]
  162. # no direct hit -> search via aliases
  163. nodeid = rawid
  164. for nid in storage.data:
  165. if 'aliases' in storage.data[nid] and rawid in storage.data[nid]['aliases']:
  166. nodeid = nid
  167. # return found node
  168. return storage.data[nodeid] if nodeid in storage.data else None
  169. def find_node_by_mac(self, mac):
  170. """Fetch node data from storage by given MAC address."""
  171. storage = self.server.storage
  172. needle = mac.lower()
  173. # iterate over all nodes
  174. for nodeid in storage.data:
  175. if nodeid.startswith('__'):
  176. continue
  177. node = storage.data[nodeid]
  178. # check node's primary MAC
  179. if 'mac' in node and needle == node['mac'].lower():
  180. return node
  181. # check alias MACs
  182. if 'macs' in node:
  183. haystack = [x.lower() for x in node['macs']]
  184. if mac in haystack:
  185. return node
  186. # MAC address not found
  187. return None
  188. def respond_node(self, rawid):
  189. """Display node data."""
  190. # handle API example linked on index page
  191. if rawid == 'ff00ff00ff00':
  192. self.send_headers('text/json')
  193. self.wfile.write(json.dumps({
  194. 'name': 'API-Example',
  195. 'nodeid': rawid,
  196. 'META': 'Dies ist ein minimaler Beispiel-Datensatz. Herzlichen Glückwunsch, du hast das Prinzip der API kapiert.',
  197. }))
  198. return
  199. # search node by the given id
  200. node = self.find_node(rawid)
  201. # handle unknown nodes
  202. if node is None:
  203. self.send_error(404, 'No node with id \'' + rawid + '\' present.')
  204. return
  205. # remove fields from output: __RAW__
  206. export = ffstatus.dict_merge({}, node)
  207. if '__RAW__' in export:
  208. del export['__RAW__']
  209. # dump node data as JSON
  210. self.send_headers('text/json')
  211. self.wfile.write(json.dumps(export))
  212. def get_nodestatus(self, rawid):
  213. """Determine node's status."""
  214. # search node by the given id
  215. node = self.find_node(rawid)
  216. # handle unknown nodes
  217. if node is None:
  218. return None
  219. # check that the last batadv update is noted in the data
  220. updated = node[self.FIELDKEY_UPDATED] if self.FIELDKEY_UPDATED in node else None
  221. if updated is None or not 'batadv' in updated:
  222. return 'unknown'
  223. # make decision based on time of last batadv update
  224. diff = time.time() - updated['batadv']
  225. if diff < 150:
  226. return 'active'
  227. elif diff < 300:
  228. return 'stale'
  229. else:
  230. return 'offline'
  231. def respond_nodestatus(self, rawid):
  232. """Display node status."""
  233. status = self.get_nodestatus(rawid)
  234. if status is None:
  235. self.send_error(404, 'No node with id \'' + rawid + '\' present.')
  236. self.send_headers('text/plain')
  237. self.wfile.write(status)
  238. def respond_nodeidmac2name(self, ids):
  239. """Return a mapping of the given IDs (or MACs) into their hostname."""
  240. self.send_headers('text/plain')
  241. for nodeid in ids:
  242. node = self.find_node(nodeid) if not ':' in nodeid else self.find_node_by_mac(nodeid)
  243. nodename = node['hostname'] if (not node is None) and 'hostname' in node else nodeid
  244. self.wfile.write('{0}={1}\n'.format(nodeid, nodename))
  245. def respond_nodedetail(self, nodeid, field):
  246. """Return a field from the given node - a string is returned as text, all other as JSON."""
  247. node = self.find_node(nodeid)
  248. if node is None:
  249. self.send_error(404, 'No node with id \'' + nodeid + '\' present.')
  250. return
  251. return_count = False
  252. if field.endswith('.count'):
  253. return_count = True
  254. field = field[0:-6]
  255. if not field in node:
  256. self.send_error(404, 'The node \'' + nodeid + '\' does not have a field named \'' + str(field) + '\'.')
  257. return
  258. value = node[field]
  259. if return_count:
  260. value = len(value)
  261. self.send_headers('text/plain' if isinstance(value, basestring) or isinstance(value, int) else 'text/json')
  262. self.wfile.write(value if isinstance(value, basestring) else json.dumps(value))
  263. def respond_vpn(self, query):
  264. storage = self.server.storage
  265. peername = query['peer'] if 'peer' in query else None
  266. key = query['key'] if 'key' in query else None
  267. action = query['action'] if 'action' in query else None
  268. remote = query['remote'] if 'remote' in query else None
  269. gw = query['gw'] if 'gw' in query else None
  270. ts = query['ts'] if 'ts' in query else time.time()
  271. if action == 'list':
  272. self.respond_vpnlist()
  273. return
  274. if action != 'establish' and action != 'disestablish':
  275. self.logger.error('VPN: unknown action \'{0}\''.format(action))
  276. self.send_error(400, 'Invalid action.')
  277. return
  278. check = {'peername': peername, 'key': key, 'remote': remote, 'gw': gw}
  279. for k, val in check.items():
  280. if val is None or len(val.strip()) == 0:
  281. self.logger.error('VPN {0}: no or empty {1}'.format(action, k))
  282. self.send_error(400, 'Missing value for ' + str(k))
  283. return
  284. if key is None or re.match(r'^[a-fA-F0-9]+$', key) is None:
  285. self.logger.error('VPN peer \'{0}\' {1}: bad key \'{2}\''.format(peername, action, key))
  286. self.send_error(400, 'Bad key.')
  287. return
  288. if not self.DATAKEY_VPN in storage.data:
  289. storage.data[self.DATAKEY_VPN] = {}
  290. if not key in storage.data[self.DATAKEY_VPN]:
  291. storage.data[self.DATAKEY_VPN][key] = {'active': {}, 'last': {}}
  292. item = storage.data[self.DATAKEY_VPN][key]
  293. # resolve remote addr to its netblock
  294. remote_raw = remote
  295. remote_resolved = None
  296. if not remote is None:
  297. remote_resolved = ffstatus.resolve_ipblock(remote)
  298. if not remote_resolved is None:
  299. self.logger.debug('Resolved IP \'{0}\' to block \'{1}\'.'.format(remote, remote_resolved['name']))
  300. remote = remote_resolved
  301. if action == 'establish':
  302. item['active'][gw] = {
  303. 'establish': ts,
  304. 'peer': peername,
  305. 'remote': remote,
  306. 'remote_raw': remote_raw,
  307. }
  308. elif action == 'disestablish':
  309. active = {}
  310. if gw in item['active']:
  311. active = item['active'][gw]
  312. del item['active'][gw]
  313. active['disestablish'] = ts
  314. item['last'][gw] = active
  315. else:
  316. self.send_error(500, 'Unknown action not filtered: ' + str(action))
  317. return
  318. self.send_headers('text/plain')
  319. self.wfile.write('OK')
  320. storage.save()
  321. def respond_vpnlist(self):
  322. storage = self.server.storage
  323. gateways = ['gw01', 'gw02', 'gw03', 'gw04', 'gw05', 'gw06']
  324. self.send_headers()
  325. self.wfile.write('<!DOCTYPE html>\n')
  326. self.wfile.write('<html><head><title>BATCAVE - VPN LIST</title></head>\n')
  327. self.wfile.write('<body>\n')
  328. self.wfile.write('<style type="text/css">\n')
  329. self.wfile.write('table { border: 2px solid #999; border-collapse: collapse; }\n')
  330. self.wfile.write('th, td { border: 1px solid #CCC; }\n')
  331. self.wfile.write('table tbody tr.online { background-color: #CFC; }\n')
  332. self.wfile.write('table tbody tr.offline { background-color: #FCC; }\n')
  333. self.wfile.write('</style>\n')
  334. self.wfile.write('<table>\n<thead>\n')
  335. 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')
  336. self.wfile.write('<tr><th>' + '</th><th>'.join(gateways) + '</th><th>' + '</th><th>'.join(gateways) + '</th></tr>\n')
  337. self.wfile.write('</thead>\n')
  338. if self.DATAKEY_VPN in storage.data:
  339. for key in storage.data[self.DATAKEY_VPN]:
  340. item = storage.data[self.DATAKEY_VPN][key]
  341. if not isinstance(item, dict):
  342. continue
  343. names = set()
  344. count = {}
  345. for t in [ 'active', 'last' ]:
  346. count[t] = 0
  347. if t in item:
  348. for gw in item[t]:
  349. if 'remote' in item[t][gw] and len(item[t][gw]['remote']) > 0:
  350. count[t] += 1
  351. if 'peer' in item[t][gw]:
  352. names.add(item[t][gw]['peer'])
  353. self.wfile.write('<tr class="online">' if count['active'] > 0 else '<tr class="offline">')
  354. self.wfile.write('<td title="' + str(key) + '">' + (' / '.join(names) if len(names) > 0 else '?') + '</td>')
  355. for t in [ 'active', 'last' ]:
  356. for gw in gateways:
  357. ip = ''
  358. if t in item and gw in item[t]:
  359. ip = item[t][gw]['remote'] if 'remote' in item[t][gw] else ''
  360. if isinstance(ip, dict):
  361. ip = ip['name']
  362. self.wfile.write('<td title="' + ip + '">' + ('&check;' if len(ip) > 0 else '&times;') + '</td>')
  363. self.wfile.write('</tr>\n')
  364. self.wfile.write('</table>\n')
  365. self.wfile.write('</body>')
  366. self.wfile.write('</html>')
  367. def respond_providers(self, query):
  368. """Return a summary of providers."""
  369. vpn = self.server.storage.data[self.DATAKEY_VPN]
  370. outputformat = query['format'].lower() if 'format' in query else 'html'
  371. isps = {}
  372. ispblocks = {}
  373. vpnstorage_updated = False
  374. vpnstorage_update_allowed = 'update' in query and query['update'] == 'allowed'
  375. for key in vpn:
  376. if key is None:
  377. continue
  378. item = vpn[key]
  379. if not isinstance(item, dict):
  380. continue
  381. if not 'active' in item:
  382. continue
  383. ips = []
  384. for gw in item['active']:
  385. if 'remote' in item['active'][gw]:
  386. ip = item['active'][gw]['remote']
  387. if vpnstorage_update_allowed and not isinstance(ip, dict):
  388. # try to resolve ip now
  389. resolved = ffstatus.resolve_ipblock(ip)
  390. if not resolved is None:
  391. self.logger.debug('Resolved IP \'{0}\' to block \'{1}\'.'.format(ip, resolved))
  392. item['active'][gw]['remote'] = resolved
  393. vpnstorage_updated = True
  394. ip = resolved
  395. else:
  396. self.logger.debug('Failed to resolve IP \'{0}\'.'.format(ip))
  397. ips.append(ip)
  398. if len(ips) == 0:
  399. # no active dialins -> no need to process this key any further
  400. continue
  401. item_isps = set()
  402. for ip in ips:
  403. isp = "UNKNOWN"
  404. ispblock = ip
  405. if isinstance(ip, dict):
  406. ispblock = ip['name']
  407. desc_lines = ip['description'].split('\n')
  408. isp = desc_lines[0].strip()
  409. # normalize name: strip company indication
  410. isp = re.sub(r'(AG|UG|G?mbH( ?& ?Co\.? ?(OH|K)G)?)$', '', isp, flags=re.IGNORECASE).strip()
  411. # normalize name: strip "pool" suffixes
  412. isp = re.sub(r'(dynamic )?(customer |subscriber )?(ip )?(pool|(address )?range|addresses)$', '', isp, flags=re.IGNORECASE).strip()
  413. # normalize name: strip "B2B" and aggregation suffixes
  414. isp = re.sub(r'(aggregate|aggregation)?$', '', isp, flags=re.IGNORECASE).strip()
  415. isp = re.sub(r'(B2B)?$', '', isp, flags=re.IGNORECASE).strip()
  416. # normalize name: strip country suffixes (in Germany)
  417. isp = re.sub(r'(DE|Deutschland|Germany|Nordrhein[- ]Westfalen|NRW|Baden[- ]Wuerttemburg|BW|Hessen|Niedersachsen|Rheinland[- ]Pfalz|RLP)$', '', isp, flags=re.IGNORECASE).strip()
  418. isp = str(isp)
  419. if not isp in ispblocks:
  420. ispblocks[isp] = set()
  421. ispblocks[isp].add(ispblock)
  422. item_isps.add(isp)
  423. if len(item_isps) == 0:
  424. item_isps.add('unknown')
  425. elif len(item_isps) > 1:
  426. self.logger.warn('VPN key \'{0}\' has {1} active IPs which resolved to {2} ISPs: \'{3}\''.format(key, len(ips), len(item_isps), '\', \''.join(item_isps)))
  427. for isp in item_isps:
  428. if not isp in isps:
  429. isps[isp] = 0
  430. isps[isp] += 1.0 / len(item_isps)
  431. isps_sum = sum([isps[x] for x in isps])
  432. if vpnstorage_updated:
  433. self.server.storage.save()
  434. if outputformat == 'csv':
  435. self.send_headers('text/csv')
  436. self.wfile.write('Count;Name\n')
  437. for isp in isps:
  438. self.wfile.write('{0};"{1}"\n'.format(isps[isp], isp))
  439. elif outputformat == 'json':
  440. self.send_headers('text/json')
  441. data = [
  442. {
  443. 'name': isp,
  444. 'count': isps[isp],
  445. 'percentage': isps[isp]*100.0/isps_sum,
  446. 'blocks': [block for block in ispblocks[isp]],
  447. } for isp in isps
  448. ]
  449. self.wfile.write(json.dumps(data))
  450. elif outputformat == 'html':
  451. self.send_headers()
  452. self.wfile.write('<!DOCTYPE html><html>\n')
  453. self.wfile.write('<head><title>BATCAVE - PROVIDERS</title></head>\n')
  454. self.wfile.write('<body>\n')
  455. self.wfile.write('<table border="2">\n')
  456. self.wfile.write('<thead><tr><th>Count</th><th>Percentage</th><th>Name</th><th>Blocks</th></tr></thead>\n')
  457. self.wfile.write('<tbody>\n')
  458. for isp in sorted(isps, key=lambda x: isps[x], reverse=True):
  459. self.wfile.write('<tr><td>{0}</td><td>{1:.1f}%</td><td>{2}</td><td>{3}</td></tr>\n'.format(
  460. isps[isp],
  461. isps[isp]*100.0/isps_sum,
  462. isp,
  463. ', '.join(sorted(ispblocks[isp])) if isp in ispblocks else '?',
  464. ))
  465. self.wfile.write('</tbody></table>\n')
  466. self.wfile.write('<p>Totals: {0} ISPs, {1} connections</p>\n'.format(len(isps), isps_sum))
  467. self.wfile.write('</body></html>')
  468. else:
  469. self.send_error(400, 'Unknown output format.')
  470. class ApiServer(ThreadingMixIn, HTTPServer):
  471. def __init__(self, endpoint, storage):
  472. if ':' in endpoint[0]:
  473. self.address_family = socket.AF_INET6
  474. HTTPServer.__init__(self, endpoint, BatcaveHttpRequestHandler)
  475. self.storage = storage
  476. # check all entries for a proper 'remote' entry
  477. vpn = storage.data[BatcaveHttpRequestHandler.DATAKEY_VPN] if BatcaveHttpRequestHandler.DATAKEY_VPN in storage.data else {}
  478. init_vpn_cache = {}
  479. for key in vpn:
  480. if not isinstance(vpn[key], dict):
  481. continue
  482. for mode in vpn[key]:
  483. if not isinstance(vpn[key][mode], dict):
  484. continue
  485. for gw in vpn[key][mode]:
  486. if not isinstance(vpn[key][mode][gw], dict):
  487. continue
  488. item = vpn[key][mode][gw]
  489. if 'remote' in item and not 'remote_raw' in item:
  490. item['remote_raw'] = item['remote']
  491. resolved = None
  492. if item['remote'] in init_vpn_cache:
  493. resolved = init_vpn_cache[item['remote']]
  494. else:
  495. resolved = ffstatus.resolve_ipblock(item['remote'])
  496. init_vpn_cache[item['remote']] = resolved
  497. if not resolved is None:
  498. logging.info('Startup: resolved VPN entry \'{0}\' to net \'{1}\'.'.format(item['remote'], resolved['name']))
  499. if not resolved is None:
  500. item['remote'] = resolved
  501. storage.save()
  502. def __str__(self):
  503. return 'ApiServer on {0}'.format(self.server_address)
  504. if __name__ == '__main__':
  505. dummystorage = Storage()
  506. server = ApiServer(('0.0.0.0', 8888), dummystorage)
  507. print("Server:", str(server))
  508. server.serve_forever()