summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorvuko <vuko@hackerspace.pl>2020-06-20 22:41:10 +0200
committervuko <vuko@hackerspace.pl>2020-06-20 22:41:10 +0200
commit71a224a830626e9a1c04b9a64b9f1c2b98f02a3e (patch)
treed289b8eee3820e157725e810bf318f918d83c67a
parent884368a262509aee4a06fc63b829fe8b2291cd02 (diff)
downloadcheckinator-71a224a830626e9a1c04b9a64b9f1c2b98f02a3e.tar.gz
checkinator-71a224a830626e9a1c04b9a64b9f1c2b98f02a3e.tar.bz2
checkinator-71a224a830626e9a1c04b9a64b9f1c2b98f02a3e.zip
adding grpc api
-rw-r--r--.gitignore4
-rw-r--r--at/cmd.py14
-rw-r--r--at/tracker.proto19
-rw-r--r--at/tracker.py82
-rw-r--r--at/web.py49
-rw-r--r--run.py63
-rw-r--r--setup.py19
-rw-r--r--tracker-config.dist.yaml13
-rw-r--r--web-config.dist.yaml50
9 files changed, 289 insertions, 24 deletions
diff --git a/.gitignore b/.gitignore
index 2f2e0f0..e655d75 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,7 @@ at.cfg
at.db
*.egg-info
venv
+**/*_pb2*.py
+*-config.yaml
+result
+cert-*
diff --git a/at/cmd.py b/at/cmd.py
index d2a0bb0..4680d61 100644
--- a/at/cmd.py
+++ b/at/cmd.py
@@ -16,3 +16,17 @@ def list():
print("Found devices:")
for device in devices._devices.values():
print(device._replace(atime = time() - device.atime))
+
+import grpc
+from .tracker_pb2 import ClientsRequest
+from .tracker_pb2_grpc import DhcpTrackerStub
+
+def format_mac(raw: bytes) -> str:
+ return ':'.join(f'{b:02x}' for b in raw)
+
+def tracker_list():
+ with grpc.insecure_channel('unix:///tmp/checkinator.sock') as channel:
+ stub = DhcpTrackerStub(channel)
+ response = stub.GetClients(ClientsRequest())
+ for client in response.clients:
+ print(format_mac(client.hw_address), client.last_seen )
diff --git a/at/tracker.proto b/at/tracker.proto
new file mode 100644
index 0000000..52e07df
--- /dev/null
+++ b/at/tracker.proto
@@ -0,0 +1,19 @@
+syntax = "proto3";
+
+service DhcpTracker {
+ rpc GetClients (ClientsRequest) returns (DhcpClients) {}
+}
+
+message ClientsRequest {
+}
+
+message DhcpClient {
+ bytes hw_address = 1;
+ string last_seen = 2;
+ string client_hostname = 3;
+ string ip_address = 4;
+}
+
+message DhcpClients {
+ repeated DhcpClient clients = 1;
+}
diff --git a/at/tracker.py b/at/tracker.py
new file mode 100644
index 0000000..8c4bb77
--- /dev/null
+++ b/at/tracker.py
@@ -0,0 +1,82 @@
+from at.dhcp import DhcpdUpdater, DhcpLease
+from pathlib import Path
+import yaml
+import grpc
+from concurrent import futures
+from datetime import datetime
+
+from .tracker_pb2 import DhcpClient, DhcpClients
+from .tracker_pb2_grpc import DhcpTrackerServicer, add_DhcpTrackerServicer_to_server
+
+import argparse
+parser = argparse.ArgumentParser()
+parser.add_argument("--verbose", help="output more info", action="store_true")
+parser.add_argument("config", type=Path, help="input file")
+
+def lease_to_client(lease: DhcpLease) -> DhcpClient:
+ return DhcpClient(
+ hw_address = bytes.fromhex(lease.hwaddr.replace(':', '')),
+ last_seen = datetime.fromtimestamp(lease.atime).isoformat(),
+ )
+
+class DhcpTrackerServicer(DhcpTrackerServicer):
+ def __init__(self, tracker: DhcpdUpdater, *args, **kwargs):
+ self._tracker = tracker
+ super().__init__(*args, **kwargs)
+
+ def GetClients(self, request, context):
+ auth = context.auth_context()
+ ctype = auth.get('transport_security_type', 'local')
+ if ctype == 'ssl':
+ if b'at.hackerspace.pl' not in context.peer_identities():
+ context.abort(
+ grpc.StatusCode.PERMISSION_DENIED,
+ (
+ "Only at.hackespace.pl is allowed to access raw "
+ "clients addresses"
+ )
+ )
+ elif ctype == 'local':
+ # connection from local unix socket is trusted by default
+ pass
+ else:
+ context.abort(
+ grpc.StatusCode.PERMISSION_DENIED,
+ f"Unknown transport type: {ctype}"
+ )
+
+ clients = [
+ lease_to_client(c) for c in self._tracker.get_active_devices().values()]
+ return DhcpClients(clients = clients)
+
+def server():
+ args = parser.parse_args()
+
+ config = yaml.safe_load(args.config.read_text())
+ tracker = DhcpdUpdater(config['LEASE_FILE'], config['TIMEOUT'])
+ tracker.start()
+
+ server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
+ add_DhcpTrackerServicer_to_server(DhcpTrackerServicer(tracker), server)
+
+
+ cert_dir = Path(config.get('GRPC_TLS_CERT_DIR', 'cert'))
+ ca_cert = Path(config.get('GRPC_TLS_CA_CERT', 'ca.pem')).read_bytes()
+
+ server_credentials = grpc.ssl_server_credentials(
+ private_key_certificate_chain_pairs = ((
+ cert_dir.joinpath('key.pem').read_bytes(),
+ cert_dir.joinpath('cert.pem').read_bytes()
+ ),),
+ root_certificates = ca_cert,
+ require_client_auth = True
+ )
+
+ server.add_secure_port(config.get('GRPC_TLS_ADDRESS', '[::]:2847'), server_credentials)
+ unix_socket = config.get('GRPC_UNIX_SOCKET', False)
+ if unix_socket:
+ server.add_insecure_port(unix_socket)
+
+ print('starting grpc server ...')
+ server.start()
+ server.wait_for_termination()
diff --git a/at/web.py b/at/web.py
index fc82b9f..4a3d298 100644
--- a/at/web.py
+++ b/at/web.py
@@ -2,7 +2,7 @@ import json
import sqlite3
from pathlib import Path
from datetime import datetime
-from collections import namedtuple
+from typing import NamedTuple, Iterable, Iterator
from functools import wraps
from flask import Flask, render_template, abort, g, \
redirect, request, url_for, make_response
@@ -10,7 +10,12 @@ from flask import Flask, render_template, abort, g, \
from spaceauth import SpaceAuth, login_required, current_user, cap_required
-DeviceInfo = namedtuple('DeviceInfo', ['hwaddr', 'name', 'owner', 'ignored'])
+# device infomation stored in database
+class DeviceInfo(NamedTuple):
+ hwaddr: str
+ name: str
+ owner: str
+ ignored: bool
def v4addr():
if request.headers.getlist("X-Forwarded-For"):
@@ -25,11 +30,11 @@ def v4addr():
def req_to_ctx():
return dict(iter(request.form.items()))
-def get_device_info(conn, hwaddr):
+def get_device_info(conn: sqlite3.Connection, hwaddr: str) -> DeviceInfo:
return list(get_device_infos(conn, (hwaddr,)))[0]
-def get_device_infos(conn, hwaddrs):
+def get_device_infos(conn: sqlite3.Connection, hwaddrs: Iterable[str]) -> Iterator[DeviceInfo]:
in_clause = '({})'.format(', '.join(['?'] * len(hwaddrs)))
stmt = '''select hwaddr, name, ignored, owner from
devices where devices.hwaddr in ''' + in_clause
@@ -72,7 +77,7 @@ def app(instance_path, devices_api, config):
@app.template_filter('wikiurl')
def wikiurl(user):
- return app.config['WIKI_URL'] % {'login': user}
+ return app.config['WIKI_URL'].replace("${login}", user)
@app.before_request
@@ -112,29 +117,35 @@ def app(instance_path, devices_api, config):
res = make_response(json.dumps(result), 200)
res.headers['Access-Control-Allow-Origin'] = '*'
return res
-
-
+
def now_at():
result = dict()
devices = app.updater.get_active_devices()
- device_infos = list(get_device_infos(g.db, list(devices.keys())))
- device_infos.sort(key=lambda di: devices[di.hwaddr])
- unknown = set(devices.keys()) - set(d.hwaddr for d in device_infos)
+ macs = list(devices.keys())
+
+ identified_devices = get_device_infos(g.db, macs)
+ unknown = set(macs) - set(d.hwaddr for d in identified_devices)
+
# das kektop sorting maschine
- for name, prefixes in list(app.config['SPECIAL_DEVICES'].items()):
+ # identify special devices
+ for name, prefixes in app.config['SPECIAL_DEVICES'].items():
result[name] = set()
- for u in unknown.copy():
- if u.startswith(prefixes):
- result[name].add(u)
- unknown.discard(u)
+ prefixes = tuple(prefixes) # startswith accepts tuple as argument
+ for hwaddr in unknown:
+ if hwaddr.startswith(prefixes):
+ result[name].add(hwaddr)
+ unknown.discard(hwaddr)
result['unknown'] = unknown
users = {}
- for info in device_infos:
- if info.owner not in users and not info.ignored:
- users[info.owner] = devices[info.hwaddr][0]
- result['users'] = sorted(list(users.items()), key=lambda u_a: u_a[1], reverse=True)
+ for info in identified_devices:
+ # append device to user
+ owner_devices = users.get(info.owner, [])
+ owner_devices.append(devices[info.hwaddr][0])
+ users.set(info.owner, owner_devices)
+
+ result['users'] = sorted(users.items(), key=lambda u_a: u_a[1].last_seen, reverse=True)
return result
diff --git a/run.py b/run.py
index 2b037c4..2da0e7b 100644
--- a/run.py
+++ b/run.py
@@ -5,9 +5,64 @@ from at.dhcp import DhcpdUpdater
from pathlib import Path
import yaml
-config = yaml.safe_load(Path('config.yaml').read_text())
+import grpc
+from at.tracker_pb2 import ClientsRequest
+from at.tracker_pb2_grpc import DhcpTrackerStub
+from at.dhcp import DhcpLease
+from datetime import datetime
-updater = DhcpdUpdater(config['LEASE_FILE'], config['TIMEOUT'])
+def format_mac(raw: bytes) -> str:
+ return ':'.join(f'{b:02x}' for b in raw)
-updater.start()
-app = at.web.app(Path(__file__).parent, updater, config)
+class DevicesApi:
+ def __init__(self, grpc_channel):
+ self._api = DhcpTrackerStub(grpc_channel)
+
+
+ def get_active_devices(self) -> DhcpLease:
+ devices = self._api.GetClients(ClientsRequest())
+ return {
+ format_mac(d.hw_address): DhcpLease(
+ hwaddr=format_mac(d.hw_address),
+ atime=datetime.fromisoformat(d.last_seen).timestamp(),
+ ip=d.ip_address,
+ name=d.client_hostname
+ ) for d in devices.clients
+ }
+
+ def get_device(self, ip: str) -> Tuple[str, name]:
+ devices = self._api.GetClients(ClientsRequest)
+ for device in devices:
+ if device.ip_address == ip:
+ return (
+ format_mac(device.hw_address), device.client_hostname
+ )
+ return None, None
+
+
+config = yaml.safe_load(Path('web-config.yaml').read_text())
+
+#updater = DhcpdUpdater(config['LEASE_FILE'], config['TIMEOUT'])
+#updater.start()
+
+
+if 'GRPC_TLS_ADDRESS' in config:
+ print("using secure channel")
+ ca_cert = Path(config.get('GRPC_TLS_CA_CERT')).read_bytes()
+ cert_dir = Path(config.get('GRPC_TLS_CERT_DIR'))
+
+ channel_credential = grpc.ssl_channel_credentials(
+ root_certificates = ca_cert,
+ private_key = cert_dir.joinpath('key.pem').read_bytes(),
+ certificate_chain = cert_dir.joinpath('cert.pem').read_bytes(),
+ )
+
+ options = [
+ ('grpc.ssl_target_name_override', 'at.customs.hackerspace.pl')
+ ]
+ channel = grpc.secure_channel(config.get('GRPC_TLS_ADDRESS'), channel_credential, options=options)
+else:
+ print("using insecure channel")
+ channel = grpc.insecure_channel('unix:///tmp/checkinator.sock')
+
+app = at.web.app(Path(__file__).parent, DevicesApi(channel), config)
diff --git a/setup.py b/setup.py
index f96170a..e9a1d9c 100644
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,19 @@
from setuptools import setup
+import grpc_tools.protoc
+import pkg_resources
+from pathlib import Path
+
+setupdir = Path(__file__).parent
+
+proto_include = pkg_resources.resource_filename('grpc_tools', '_proto')
+
+grpc_tools.protoc.main([
+ 'grpc_tools.protoc',
+ f'-I{setupdir!s}',
+ '--python_out=./',
+ '--grpc_python_out=./',
+ 'at/tracker.proto'
+])
setup(
name='hswaw-at',
@@ -8,10 +23,12 @@ setup(
packages=['at'],
package_data={"at": ["templates/*"]},
python_requires='>=3.6,',
- install_requires=['Flask', 'requests', 'flask-spaceauth', 'pyyaml'],
+ install_requires=['Flask', 'requests', 'flask-spaceauth', 'pyyaml', 'grpcio', 'protobuf'],
entry_points={
'console_scripts': [
'checkinator-list=at.cmd:list',
+ 'checkinator-tracker=at.tracker:server',
+ 'checkinator-tracker-list=at.cmd:tracker_list',
],
},
)
diff --git a/tracker-config.dist.yaml b/tracker-config.dist.yaml
new file mode 100644
index 0000000..681d3d0
--- /dev/null
+++ b/tracker-config.dist.yaml
@@ -0,0 +1,13 @@
+# path to dhcpd lease file
+LEASE_FILE: './dhcpd.leases'
+
+# timeout for old leases
+TIMEOUT: 1500
+
+# optional - local trusted socket
+GRPC_UNIX_SOCKET: "unix://tmp/checkinator.sock"
+
+# optional - remote authenticated (TLS cert) socket
+GRPC_TLS_CERT_DIR: "./cert-tracker"
+GRPC_TLS_CA_CERT: "./ca.pem"
+GRPC_TLS_ADDRESS: "[::1]:2847"
diff --git a/web-config.dist.yaml b/web-config.dist.yaml
new file mode 100644
index 0000000..ad481e8
--- /dev/null
+++ b/web-config.dist.yaml
@@ -0,0 +1,50 @@
+# local sqlite db for storing user and MAC
+DB: 'at.db'
+
+# debug option interpreted by flask app
+DEBUG: true
+
+# url to member wiki page
+# "${login}" string is replaced by member login (uid)
+WIKI_URL: 'https://wiki.hackerspace.pl/people:%(login)s:start'
+
+CLAIMABLE_PREFIX: '10.8.0.'
+CLAIMABLE_EXCLUDE: [ ]
+
+SECRET_KEY: 'CHANGEME'
+
+SPACEAUTH_CONSUMER_KEY: 'checkinator'
+SPACEAUTH_CONSUMER_SECRET: 'CHANGEME'
+
+SPECIAL_DEVICES:
+ 'kektops':
+ - '90:e6:ba:84'
+ 'esps':
+ - 'ec:fa:bc'
+ - 'dc:4f:22'
+ - 'd8:a0:1d'
+ - 'b4:e6:2d'
+ - 'ac:d0:74'
+ - 'a4:7b:9d'
+ - 'a0:20:a6'
+ - '90:97:d5'
+ - '68:c6:3a'
+ - '60:01:94'
+ - '5c:cf:7f'
+ - '54:5a:a6'
+ - '30:ae:a4'
+ - '2c:3a:e8'
+ - '24:b2:de'
+ - '24:0a:c4'
+ - '18:fe:34'
+ - '38:2b:78'
+ - 'bc:dd:c2'
+ - 'cc:50:e3'
+ 'vms':
+ - '52:54:00' # craptrap VMs
+
+PROXY_FIX: true
+
+GRPC_TLS_CERT_DIR: "./cert-webapp"
+GRPC_TLS_CA_CERT: "./ca.pem"
+GRPC_TLS_ADDRESS: '[::1]:2847'