#! /usr/bin/env python3 import os import sys from wsgiref.simple_server import make_server import falcon import json from db import DictDB from base64 import b64decode import authn class CollectorResource(): def __init__(self, db, users): self._db = db self._users = users def parse_error(data): return "I want valid JSON but got this:\n{}\n".format(data) def user_auth(self, auth_header, authfun): if not auth_header: return None, None # Fail. BAlit, b64 = auth_header.split() if BAlit != "Basic": return None, None # Fail userbytes, pwbytes = b64decode(b64).split(b':') try: user = userbytes.decode('utf-8') pw = pwbytes.decode('utf-8') except Exception: return None, None # Fail return authfun(user, pw) class EPGet(CollectorResource): def on_get(self, req, resp): resp.status = falcon.HTTP_200 resp.content_type = falcon.MEDIA_JSON out = [] orgs = self.user_auth(req.auth, self._users.read_perms) if not orgs: resp.status = falcon.HTTP_401 resp.text = 'Invalid username or password\n' return # We really should rely on req.params in its pure form since # it might contain garbage. selectors = req.params for org in orgs: selectors['domain'] = org out.append(self._db.search(**selectors)) resp.text = json.dumps(out) + '\n' class EPAdd(CollectorResource): def on_post(self, req, resp): resp.status = falcon.HTTP_200 resp.content_type = falcon.MEDIA_TEXT self._indata = [] orgs = self.user_auth(req.auth, self._users.write_perms) if not orgs: resp.status = falcon.HTTP_401 resp.text = 'Invalid user or password\n' return # NOTE: Allowing writing to _any_ org! # TODO: Allow only input where input.domain in orgs == True. # TODO: can we do json.load(req.bounded_stream, # cls=customDecoder) where our decoder calls JSONDecoder after # decoding UTF-8? # NOTE: Reading the whole body in one go instead of streaming # it nicely. rawin = req.bounded_stream.read() try: decodedin = rawin.decode('UTF-8') except Exception: resp.status = falcon.HTTP_400 resp.text = 'Need UTF-8\n' return try: json_data = json.loads(decodedin) except TypeError: print('DEBUG: type error') resp.status = falcon.HTTP_400 resp.text = CollectorResource.parse_error(decodedin) return except json.decoder.JSONDecodeError: print('DEBUG: json decode error') resp.status = falcon.HTTP_400 resp.text = CollectorResource.parse_error(decodedin) return keys = self._db.add(json_data) resp.text = repr(keys) + '\n' def init(url_res_map, addr='', port=8000): app = falcon.App(cors_enable=True) for url, res in url_res_map: app.add_route(url, res) return make_server(addr, port, app) def main(): # Simple demo. Run it from the demo directory where a sample user # database can be found: # # $ cd demo && ../src/wsgi.py # Serving on port 8000... # # 1. Try adding some observations, basic auth user:pw from # wsgi_demo_users.yaml, including {"domain": "sunet.se"} in at # least one of them: # # $ echo '[{"ip": "192.168.0.1", "port": 80, "domain": "sunet.se"}]' | curl -s -u user3:pw3 --data-binary @- http://localhost:8000/sc/v0/add # # 2. Try retreiving all observations for a user with read access # to 'sunet.se': # # $ curl -s -u user1:pw1 http://localhost:8000/sc/v0/get | json_pp -json_opt utf8,pretty try: database = os.environ['DB_NAME'] hostname = os.environ['DB_HOSTNAME'] username = os.environ['DB_USERNAME'] password = os.environ['DB_PASSWORD'] except KeyError: print('The environment variables DB_NAME, DB_HOSTNAME, DB_USERNAME ' + 'and DB_PASSWORD must be set.') sys.exit(-1) db = DictDB(database, hostname, username, password) users = authn.UserDB('wsgi_demo_users.yaml') httpd = init([('/sc/v0/add', EPAdd(db, users)), ('/sc/v0/get', EPGet(db, users))]) print('Serving on port 8000...') httpd.serve_forever() if __name__ == '__main__': sys.exit(main())