import asyncio import base64 import hashlib import hmac from http.cookies import SimpleCookie import json import urllib.request SALT = "datasette-auth-github" class BadSignature(Exception): pass class Signer: def __init__(self, secret): self.secret = secret def signature(self, value): return ( base64.urlsafe_b64encode(salted_hmac(SALT, value, self.secret).digest()) .strip(b"=") .decode() ) def sign(self, value): return "{}:{}".format(value, self.signature(value)) def unsign(self, signed_value): if ":" not in signed_value: raise BadSignature("No : found") value, signature = signed_value.rsplit(":", 1) if hmac.compare_digest(signature, self.signature(value)): return value raise BadSignature("Signature does not match") async def send_html(send, html, status=200, headers=None): headers = headers or [] if "content-type" not in [h.lower() for h, v in headers]: headers.append(["content-type", "text/html; charset=UTF-8"]) await send( { "type": "http.response.start", "status": status, "headers": [ [key.encode("utf8"), value.encode("utf8")] for key, value in headers ], } ) await send({"type": "http.response.body", "body": html.encode("utf8")}) async def http_request(url, body=None, headers=None): "Performs POST if body provided, GET otherwise." headers = headers or {} def _request(): message = urllib.request.urlopen(urllib.request.Request(url, body, headers)) return message.status, tuple(message.headers.raw_items()), message.read() loop = asyncio.get_event_loop() status_code, headers, body = await loop.run_in_executor(None, _request) return Response(status_code, headers, body) class Response: "Wrapper class making HTTP responses easier to work with" def __init__(self, status_code, headers, body): self.status_code = status_code self.headers = headers self.body = body def json(self): return json.loads(self.text) @property def text(self): # Should decode according to Content-Type, for the moment assumes utf8 return self.body.decode("utf-8") def ensure_bytes(s): if not isinstance(s, bytes): return s.encode("utf-8") else: return s def force_list(value): if isinstance(value, str): return [value] return value def salted_hmac(salt, value, secret): salt = ensure_bytes(salt) secret = ensure_bytes(secret) key = hashlib.sha1(salt + secret).digest() return hmac.new(key, msg=ensure_bytes(value), digestmod=hashlib.sha1) def cookies_from_scope(scope): cookie = dict(scope.get("headers") or {}).get(b"cookie") if not cookie: return {} simple_cookie = SimpleCookie() simple_cookie.load(cookie.decode("utf8")) return {key: morsel.value for key, morsel in simple_cookie.items()}