|
@@ -1,14 +1,12 @@
|
|
|
#!/usr/bin/python
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
-from __future__ import print_function
|
|
|
+from __future__ import print_function, unicode_literals
|
|
|
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
|
|
|
|
|
import cgi
|
|
|
-from storage import Storage
|
|
|
import json
|
|
|
import logging
|
|
|
-import pygeoip
|
|
|
import re
|
|
|
import socket
|
|
|
from SocketServer import ThreadingMixIn
|
|
@@ -17,8 +15,6 @@ import time
|
|
|
import ffstatus
|
|
|
|
|
|
class BatcaveHttpRequestHandler(BaseHTTPRequestHandler):
|
|
|
- DATAKEY_VPN = '__VPN__'
|
|
|
- FIELDKEY_UPDATED = '__UPDATED__'
|
|
|
|
|
|
def __init__(self, request, client_address, server):
|
|
|
self.logger = logging.getLogger('API')
|
|
@@ -162,7 +158,6 @@ class BatcaveHttpRequestHandler(BaseHTTPRequestHandler):
|
|
|
def respond_list(self, query):
|
|
|
"""List stored data."""
|
|
|
|
|
|
- storage = self.server.storage
|
|
|
self.send_headers()
|
|
|
|
|
|
self.wfile.write('<!DOCTYPE html><html>\n')
|
|
@@ -174,67 +169,17 @@ class BatcaveHttpRequestHandler(BaseHTTPRequestHandler):
|
|
|
self.wfile.write('<thead><tr><th>ID</th><th>Name</th></tr></thead>\n')
|
|
|
self.wfile.write('<tbody>\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 '<?>'
|
|
|
+ sortkey = query['sort'] if 'sort' in query else None
|
|
|
+ data = self.server.storage.get_nodes(sortby=sortkey)
|
|
|
+ for node in data:
|
|
|
+ nodeid = node['node_id']
|
|
|
+ nodename = node['hostname'] if 'hostname' in node else '<?>'
|
|
|
+
|
|
|
self.wfile.write('<tr><td><a href="/node/' + nodeid + '.json">' + nodeid + '</a></td><td>' + nodename + '</td></tr>')
|
|
|
|
|
|
self.wfile.write('</tbody>\n')
|
|
|
self.wfile.write('</table>\n')
|
|
|
|
|
|
- def find_node(self, rawid):
|
|
|
- """Fetch node data from storage by given id, if necessary looking thorugh node aliases."""
|
|
|
-
|
|
|
- storage = self.server.storage
|
|
|
-
|
|
|
- # if we have a direct hit, return it immediately
|
|
|
- if rawid in storage.data:
|
|
|
- return storage.data[rawid]
|
|
|
-
|
|
|
- # no direct hit -> search via aliases
|
|
|
- nodeid = rawid
|
|
|
- for nid in storage.data:
|
|
|
- if 'aliases' in storage.data[nid] and rawid in storage.data[nid]['aliases']:
|
|
|
- nodeid = nid
|
|
|
-
|
|
|
- # return found node
|
|
|
- return storage.data[nodeid] if nodeid in storage.data else None
|
|
|
-
|
|
|
- def find_node_by_mac(self, mac):
|
|
|
- """Fetch node data from storage by given MAC address."""
|
|
|
-
|
|
|
- storage = self.server.storage
|
|
|
- needle = mac.lower()
|
|
|
-
|
|
|
- # iterate over all nodes
|
|
|
- for nodeid in storage.data:
|
|
|
- if nodeid.startswith('__'):
|
|
|
- continue
|
|
|
- node = storage.data[nodeid]
|
|
|
-
|
|
|
- # check node's primary MAC
|
|
|
- if 'mac' in node and needle == node['mac'].lower():
|
|
|
- return node
|
|
|
-
|
|
|
- # check alias MACs
|
|
|
- if 'macs' in node:
|
|
|
- haystack = [x.lower() for x in node['macs']]
|
|
|
- if mac in haystack:
|
|
|
- return node
|
|
|
-
|
|
|
- # MAC address not found
|
|
|
- return None
|
|
|
-
|
|
|
def respond_node(self, rawid):
|
|
|
"""Display node data."""
|
|
|
|
|
@@ -249,50 +194,21 @@ class BatcaveHttpRequestHandler(BaseHTTPRequestHandler):
|
|
|
return
|
|
|
|
|
|
# search node by the given id
|
|
|
- node = self.find_node(rawid)
|
|
|
+ node = self.server.storage.find_node(rawid)
|
|
|
|
|
|
# handle unknown nodes
|
|
|
if node is None:
|
|
|
self.send_error(404, 'No node with id \'' + rawid + '\' present.')
|
|
|
return
|
|
|
|
|
|
- # remove fields from output: __RAW__
|
|
|
- export = ffstatus.dict_merge({}, node)
|
|
|
- if '__RAW__' in export:
|
|
|
- del export['__RAW__']
|
|
|
-
|
|
|
# dump node data as JSON
|
|
|
self.send_headers('text/json')
|
|
|
- self.wfile.write(json.dumps(export))
|
|
|
-
|
|
|
- def get_nodestatus(self, rawid):
|
|
|
- """Determine node's status."""
|
|
|
-
|
|
|
- # search node by the given id
|
|
|
- node = self.find_node(rawid)
|
|
|
-
|
|
|
- # handle unknown nodes
|
|
|
- if node is None:
|
|
|
- return None
|
|
|
-
|
|
|
- # check that the last batadv update is noted in the data
|
|
|
- updated = node[self.FIELDKEY_UPDATED] if self.FIELDKEY_UPDATED in node else None
|
|
|
- if updated is None or not 'batadv' in updated:
|
|
|
- return 'unknown'
|
|
|
-
|
|
|
- # make decision based on time of last batadv update
|
|
|
- diff = time.time() - updated['batadv']
|
|
|
- if diff < 150:
|
|
|
- return 'active'
|
|
|
- elif diff < 300:
|
|
|
- return 'stale'
|
|
|
- else:
|
|
|
- return 'offline'
|
|
|
+ self.wfile.write(json.dumps(node))
|
|
|
|
|
|
def respond_nodestatus(self, rawid):
|
|
|
"""Display node status."""
|
|
|
|
|
|
- status = self.get_nodestatus(rawid)
|
|
|
+ status = self.server.storage.get_nodestatus(rawid)
|
|
|
|
|
|
if status is None:
|
|
|
self.send_error(404, 'No node with id \'' + rawid + '\' present.')
|
|
@@ -305,14 +221,14 @@ class BatcaveHttpRequestHandler(BaseHTTPRequestHandler):
|
|
|
|
|
|
self.send_headers('text/plain')
|
|
|
for nodeid in ids:
|
|
|
- node = self.find_node(nodeid) if not ':' in nodeid else self.find_node_by_mac(nodeid)
|
|
|
+ node = self.server.storage.find_node(nodeid) if not ':' in nodeid else self.server.storage.find_node_by_mac(nodeid)
|
|
|
nodename = node['hostname'] if (not node is None) and 'hostname' in node else nodeid
|
|
|
self.wfile.write('{0}={1}\n'.format(nodeid, nodename))
|
|
|
|
|
|
def respond_nodedetail(self, nodeid, field):
|
|
|
"""Return a field from the given node - a string is returned as text, all other as JSON."""
|
|
|
|
|
|
- node = self.find_node(nodeid)
|
|
|
+ node = self.server.storage.find_node(nodeid)
|
|
|
if node is None:
|
|
|
self.send_error(404, 'No node with id \'' + nodeid + '\' present.')
|
|
|
return
|
|
@@ -358,44 +274,25 @@ class BatcaveHttpRequestHandler(BaseHTTPRequestHandler):
|
|
|
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
|
|
|
+ try:
|
|
|
+ if action == 'establish':
|
|
|
+ self.server.storage.log_vpn_connect(
|
|
|
+ key, peername, remote, gw, ts)
|
|
|
|
|
|
- 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]
|
|
|
-
|
|
|
- # resolve remote addr to its netblock
|
|
|
- remote_raw = remote
|
|
|
- remote_resolved = None
|
|
|
- if not remote is None:
|
|
|
- remote_resolved = ffstatus.resolve_ipblock(remote)
|
|
|
- if not remote_resolved is None:
|
|
|
- self.logger.debug('Resolved IP \'{0}\' to block \'{1}\'.'.format(remote, remote_resolved['name']))
|
|
|
- remote = remote_resolved
|
|
|
-
|
|
|
- if action == 'establish':
|
|
|
- item['active'][gw] = {
|
|
|
- 'establish': ts,
|
|
|
- 'peer': peername,
|
|
|
- 'remote': remote,
|
|
|
- 'remote_raw': remote_raw,
|
|
|
- }
|
|
|
-
|
|
|
- elif action == 'disestablish':
|
|
|
- active = {}
|
|
|
- if gw in item['active']:
|
|
|
- active = item['active'][gw]
|
|
|
- del item['active'][gw]
|
|
|
- active['disestablish'] = ts
|
|
|
- item['last'][gw] = active
|
|
|
+ elif action == 'disestablish':
|
|
|
+ self.server.storage.log_vpn_connect(key, gw, ts)
|
|
|
|
|
|
- else:
|
|
|
- self.send_error(500, 'Unknown action not filtered: ' + str(action))
|
|
|
+ else:
|
|
|
+ self.logger.error('Unknown VPN action \'%s\' not filtered.',
|
|
|
+ action)
|
|
|
+ self.send_error(500)
|
|
|
+ return
|
|
|
+
|
|
|
+ except ffstatus.exceptions.VpnKeyFormatError:
|
|
|
+ self.logger.error('VPN peer \'{0}\' {1}: bad key \'{2}\''.format(
|
|
|
+ peername, action, key,
|
|
|
+ ))
|
|
|
+ self.send_error(400, 'Bad key.')
|
|
|
return
|
|
|
|
|
|
self.send_headers('text/plain')
|
|
@@ -404,10 +301,6 @@ class BatcaveHttpRequestHandler(BaseHTTPRequestHandler):
|
|
|
storage.save()
|
|
|
|
|
|
def respond_vpnlist(self):
|
|
|
- storage = self.server.storage
|
|
|
-
|
|
|
- gateways = ['gw01', 'gw02', 'gw03', 'gw04', 'gw05', 'gw06']
|
|
|
-
|
|
|
self.send_headers()
|
|
|
self.wfile.write('<!DOCTYPE html>\n')
|
|
|
self.wfile.write('<html><head><title>BATCAVE - VPN LIST</title></head>\n')
|
|
@@ -419,39 +312,30 @@ class BatcaveHttpRequestHandler(BaseHTTPRequestHandler):
|
|
|
self.wfile.write('table tbody tr.offline { background-color: #FCC; }\n')
|
|
|
self.wfile.write('</style>\n')
|
|
|
self.wfile.write('<table>\n<thead>\n')
|
|
|
+ gateways = self.server.storage.get_vpn_gateways()
|
|
|
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')
|
|
|
self.wfile.write('<tr><th>' + '</th><th>'.join(gateways) + '</th><th>' + '</th><th>'.join(gateways) + '</th></tr>\n')
|
|
|
self.wfile.write('</thead>\n')
|
|
|
|
|
|
- if self.DATAKEY_VPN in storage.data:
|
|
|
- for key in storage.data[self.DATAKEY_VPN]:
|
|
|
- item = storage.data[self.DATAKEY_VPN][key]
|
|
|
- if not isinstance(item, dict):
|
|
|
- continue
|
|
|
-
|
|
|
- names = set()
|
|
|
- count = {}
|
|
|
- for t in [ 'active', 'last' ]:
|
|
|
- count[t] = 0
|
|
|
- if t in item:
|
|
|
- for gw in item[t]:
|
|
|
- if 'remote' in item[t][gw] and len(item[t][gw]['remote']) > 0:
|
|
|
- count[t] += 1
|
|
|
- if 'peer' in item[t][gw]:
|
|
|
- names.add(item[t][gw]['peer'])
|
|
|
-
|
|
|
- self.wfile.write('<tr class="online">' if count['active'] > 0 else '<tr class="offline">')
|
|
|
- self.wfile.write('<td title="' + str(key) + '">' + (' / '.join(names) if len(names) > 0 else '?') + '</td>')
|
|
|
- for t in [ 'active', 'last' ]:
|
|
|
- for gw in gateways:
|
|
|
- ip = ''
|
|
|
- if t in item and gw in item[t]:
|
|
|
- ip = item[t][gw]['remote'] if 'remote' in item[t][gw] else ''
|
|
|
- if isinstance(ip, dict):
|
|
|
- ip = ip['name']
|
|
|
- self.wfile.write('<td title="' + ip + '">' + ('✓' if len(ip) > 0 else '×') + '</td>')
|
|
|
-
|
|
|
- self.wfile.write('</tr>\n')
|
|
|
+ for item in self.server.storage.get_vpn_connections():
|
|
|
+ self.wfile.write('<tr class="{0}">'.format('online' if item['online'] else 'offline'))
|
|
|
+ self.wfile.write('<td title="{0}">{1}</td>'.format(
|
|
|
+ item['key'],
|
|
|
+ ' / '.join(item['names']) if len(item['names']) > 0 else '?',
|
|
|
+ ))
|
|
|
+
|
|
|
+ for conntype in ['active', 'last']:
|
|
|
+ for gateway in gateways:
|
|
|
+ remote = ''
|
|
|
+ if conntype in item['remote'] and gateway in item['remote'][conntype]:
|
|
|
+ remote = item['remote'][conntype][gateway]
|
|
|
+ if isinstance(remote, dict):
|
|
|
+ remote = remote['name']
|
|
|
+ symbol = '✓' if len(remote) > 0 else '×'
|
|
|
+ self.wfile.write('<td title="{0}">{1}</td>'.format(
|
|
|
+ remote, symbol))
|
|
|
+
|
|
|
+ self.wfile.write('</tr>\n')
|
|
|
|
|
|
self.wfile.write('</table>\n')
|
|
|
self.wfile.write('</body>')
|
|
@@ -460,49 +344,32 @@ class BatcaveHttpRequestHandler(BaseHTTPRequestHandler):
|
|
|
def respond_providers(self, query):
|
|
|
"""Return a summary of providers."""
|
|
|
|
|
|
- vpn = self.server.storage.data[self.DATAKEY_VPN]
|
|
|
outputformat = query['format'].lower() if 'format' in query else 'html'
|
|
|
|
|
|
isps = {}
|
|
|
ispblocks = {}
|
|
|
- vpnstorage_updated = False
|
|
|
- vpnstorage_update_allowed = 'update' in query and query['update'] == 'allowed'
|
|
|
- for key in vpn:
|
|
|
- if key is None:
|
|
|
- continue
|
|
|
- item = vpn[key]
|
|
|
- if not isinstance(item, dict):
|
|
|
- continue
|
|
|
- if not 'active' in item:
|
|
|
+ for item in self.server.storage.get_vpn_connections():
|
|
|
+ if item['count']['active'] == 0:
|
|
|
continue
|
|
|
|
|
|
- ips = []
|
|
|
- for gw in item['active']:
|
|
|
- if 'remote' in item['active'][gw]:
|
|
|
- ip = item['active'][gw]['remote']
|
|
|
- if vpnstorage_update_allowed and not isinstance(ip, dict):
|
|
|
- # try to resolve ip now
|
|
|
- resolved = ffstatus.resolve_ipblock(ip)
|
|
|
- if not resolved is None:
|
|
|
- self.logger.debug('Resolved IP \'{0}\' to block \'{1}\'.'.format(ip, resolved))
|
|
|
- item['active'][gw]['remote'] = resolved
|
|
|
- vpnstorage_updated = True
|
|
|
- ip = resolved
|
|
|
- else:
|
|
|
- self.logger.debug('Failed to resolve IP \'{0}\'.'.format(ip))
|
|
|
- ips.append(ip)
|
|
|
-
|
|
|
- if len(ips) == 0:
|
|
|
- # no active dialins -> no need to process this key any further
|
|
|
+ remotes = []
|
|
|
+ for gateway in item['remote']['active']:
|
|
|
+ remote = item['remote']['active'][gateway]
|
|
|
+ remotes.append(remote)
|
|
|
+
|
|
|
+ if len(remotes) == 0:
|
|
|
+ self.logger.warn(
|
|
|
+ 'VPN key \'%s\' is marked with active remotes but 0 found?',
|
|
|
+ item['key'])
|
|
|
continue
|
|
|
|
|
|
item_isps = set()
|
|
|
- for ip in ips:
|
|
|
+ for remote in remotes:
|
|
|
isp = "UNKNOWN"
|
|
|
- ispblock = ip
|
|
|
- if isinstance(ip, dict):
|
|
|
- ispblock = ip['name']
|
|
|
- desc_lines = ip['description'].split('\n')
|
|
|
+ ispblock = remote
|
|
|
+ if isinstance(remote, dict):
|
|
|
+ ispblock = remote['name']
|
|
|
+ desc_lines = remote['description'].split('\n')
|
|
|
isp = desc_lines[0].strip()
|
|
|
|
|
|
# normalize name: strip company indication
|
|
@@ -538,9 +405,6 @@ class BatcaveHttpRequestHandler(BaseHTTPRequestHandler):
|
|
|
|
|
|
isps_sum = sum([isps[x] for x in isps])
|
|
|
|
|
|
- if vpnstorage_updated:
|
|
|
- self.server.storage.save()
|
|
|
-
|
|
|
if outputformat == 'csv':
|
|
|
self.send_headers('text/csv')
|
|
|
|
|
@@ -586,6 +450,7 @@ class BatcaveHttpRequestHandler(BaseHTTPRequestHandler):
|
|
|
else:
|
|
|
self.send_error(400, 'Unknown output format.')
|
|
|
|
|
|
+
|
|
|
class ApiServer(ThreadingMixIn, HTTPServer):
|
|
|
def __init__(self, endpoint, storage):
|
|
|
if ':' in endpoint[0]:
|
|
@@ -593,34 +458,6 @@ class ApiServer(ThreadingMixIn, HTTPServer):
|
|
|
HTTPServer.__init__(self, endpoint, BatcaveHttpRequestHandler)
|
|
|
self.storage = storage
|
|
|
|
|
|
- # check all entries for a proper 'remote' entry
|
|
|
- vpn = storage.data[BatcaveHttpRequestHandler.DATAKEY_VPN] if BatcaveHttpRequestHandler.DATAKEY_VPN in storage.data else {}
|
|
|
- init_vpn_cache = {}
|
|
|
- for key in vpn:
|
|
|
- if not isinstance(vpn[key], dict):
|
|
|
- continue
|
|
|
- for mode in vpn[key]:
|
|
|
- if not isinstance(vpn[key][mode], dict):
|
|
|
- continue
|
|
|
- for gw in vpn[key][mode]:
|
|
|
- if not isinstance(vpn[key][mode][gw], dict):
|
|
|
- continue
|
|
|
- item = vpn[key][mode][gw]
|
|
|
- if 'remote' in item and not 'remote_raw' in item:
|
|
|
- item['remote_raw'] = item['remote']
|
|
|
- resolved = None
|
|
|
- if item['remote'] in init_vpn_cache:
|
|
|
- resolved = init_vpn_cache[item['remote']]
|
|
|
- else:
|
|
|
- resolved = ffstatus.resolve_ipblock(item['remote'])
|
|
|
- init_vpn_cache[item['remote']] = resolved
|
|
|
- if not resolved is None:
|
|
|
- logging.info('Startup: resolved VPN entry \'{0}\' to net \'{1}\'.'.format(item['remote'], resolved['name']))
|
|
|
- if not resolved is None:
|
|
|
- item['remote'] = resolved
|
|
|
- storage.save()
|
|
|
-
|
|
|
-
|
|
|
def __str__(self):
|
|
|
return 'ApiServer on {0}'.format(self.server_address)
|
|
|
|