From 60bc8247c335805482502c3018139655af4a3174 Mon Sep 17 00:00:00 2001 From: Micke Nordin Date: Tue, 24 Jun 2025 11:06:10 +0200 Subject: [PATCH] first commit Signed-off-by: Micke Nordin --- .gitignore | 2 ++ README.md | 0 app/__init__.py | 0 app/accept_invite.py | 47 ++++++++++++++++++++++++++++++++++ app/crypto.py | 54 ++++++++++++++++++++++++++++++++++++++++ app/datatx.py | 10 ++++++++ app/disco.py | 31 +++++++++++++++++++++++ app/main.py | 34 +++++++++++++++++++++++++ app/root.py | 10 ++++++++ app/token.py | 36 +++++++++++++++++++++++++++ app/wayf.py | 14 +++++++++++ server.conf | 3 +++ templates/discovery.yaml | 26 +++++++++++++++++++ templates/error.html | 2 ++ templates/index.html | 1 + templates/wayf.html | 17 +++++++++++++ 16 files changed, 287 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/accept_invite.py create mode 100644 app/crypto.py create mode 100644 app/datatx.py create mode 100644 app/disco.py create mode 100644 app/main.py create mode 100644 app/root.py create mode 100644 app/token.py create mode 100644 app/wayf.py create mode 100644 server.conf create mode 100644 templates/discovery.yaml create mode 100644 templates/error.html create mode 100644 templates/index.html create mode 100644 templates/wayf.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a380dcd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +ed25519_private_key.pem +ed25519_public_key.pem diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/accept_invite.py b/app/accept_invite.py new file mode 100644 index 0000000..23078a5 --- /dev/null +++ b/app/accept_invite.py @@ -0,0 +1,47 @@ +import cherrypy +import requests +from jinja2 import Environment, FileSystemLoader + +env = Environment(loader=FileSystemLoader('templates')) + + +def get_working_url(primary, fallback, timeout=1): + try: + response = requests.get(primary, timeout=timeout) + if response.status_code == 200: + return primary + except requests.RequestException: + pass + return fallback + + +class AcceptInvite(object): + + @cherrypy.expose + def index(self, remote=None, token=None): + if not remote: + raise cherrypy.HTTPError(400, "Missing remote") + if not token: + raise cherrypy.HTTPError(400, "Missing token") + disco_url = get_working_url(f'{remote}/.well-known/ocm', + f'{remote}/ocm-provider') + try: + r = requests.get(disco_url) + except requests.exceptions.ConnectionError: + raise cherrypy.HTTPError(500, "Unable to reach OCM") + if r.status_code != 200: + raise cherrypy.HTTPError(500, + f"Unable to reach OCM: {r.status_code}") + + provider_data = r.json() + + if ('capabilities' not in provider_data) or ( + 'inviteAcceptDialog' not in provider_data) or ( + 'capabilities' in provider_data + and 'invite' not in provider_data['capabilities']): + raise cherrypy.HTTPError(500, + "Remote does not support OCM invites") + redirect_url = f'{provider_data['inviteAcceptDialog']}' + redirect_url += f'?token={token}&providerDomain={cherrypy.request.base}' + + raise cherrypy.HTTPRedirect(redirect_url) diff --git a/app/crypto.py b/app/crypto.py new file mode 100644 index 0000000..ca4918d --- /dev/null +++ b/app/crypto.py @@ -0,0 +1,54 @@ +from cryptography.hazmat.primitives.asymmetric import ed25519 +from cryptography.hazmat.primitives import serialization + +PRIVATE_KEY_PATH = "ed25519_private_key.pem" +PUBLIC_KEY_PATH = "ed25519_public_key.pem" + +def generate_and_save_keypair(private_key_path, public_key_path): + private_key = ed25519.Ed25519PrivateKey.generate() + public_key = private_key.public_key() + + # Serialize the private key to PEM format without encryption + pem_private = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + # Serialize the public key to PEM format + pem_public = public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + for key in [{ + 'path': private_key_path, + 'pem': pem_private + }, { + 'path': public_key_path, + 'pem': pem_public + }]: + with open(key["path"], "wb") as f: + f.write(key["pem"]) + + +def load_keypair(private_key_path, public_key_path): + # Load private key from PEM file + with open(private_key_path, "rb") as f: + private_key = serialization.load_pem_private_key( + f.read(), + password=None, + ) + + # Load public key from PEM file + with open(public_key_path, "rb") as f: + public_key = serialization.load_pem_public_key(f.read()) + + # Type check (optional but good for Ed25519-specific code) + if not isinstance(private_key, ed25519.Ed25519PrivateKey): + raise TypeError("The private key is not an Ed25519 key") + if not isinstance(public_key, ed25519.Ed25519PublicKey): + raise TypeError("The public key is not an Ed25519 key") + + return private_key, public_key + diff --git a/app/datatx.py b/app/datatx.py new file mode 100644 index 0000000..6e4e68d --- /dev/null +++ b/app/datatx.py @@ -0,0 +1,10 @@ +import cherrypy +from jinja2 import Environment, FileSystemLoader +env = Environment(loader=FileSystemLoader('templates')) + +class DataTx(object): + + @cherrypy.expose + def index(self): + tmpl = env.get_template('index.html') + return tmpl.render(salutation='Hello', target='World') diff --git a/app/disco.py b/app/disco.py new file mode 100644 index 0000000..1e13a93 --- /dev/null +++ b/app/disco.py @@ -0,0 +1,31 @@ +import cherrypy +import yaml +import os +from jinja2 import Environment, FileSystemLoader +from cryptography.hazmat.primitives import serialization +from .crypto import generate_and_save_keypair, load_keypair, PRIVATE_KEY_PATH, PUBLIC_KEY_PATH + +env = Environment(loader=FileSystemLoader('templates')) + + +class Disco(object): + + @cherrypy.expose + @cherrypy.tools.json_out() + def index(self): + if not os.path.exists(PRIVATE_KEY_PATH): + generate_and_save_keypair(PRIVATE_KEY_PATH, PUBLIC_KEY_PATH) + _, public_key = load_keypair(PRIVATE_KEY_PATH, PUBLIC_KEY_PATH) + pem_bytes = public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + pem_string = pem_bytes.decode('utf-8') + baseurl = cherrypy.request.base + tmpl = env.get_template('discovery.yaml') + yamldoc = yaml.safe_load( + tmpl.render(endpoint=baseurl + '/ocm', + public_key_id=baseurl + '/ocm#signature', + public_key_pem=pem_string)) + return yamldoc diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..d4747d0 --- /dev/null +++ b/app/main.py @@ -0,0 +1,34 @@ +import cherrypy + +from .root import Root +from .disco import Disco +from .token import Token +from .wayf import Wayf +from .accept_invite import AcceptInvite + + +def main(): + + cherrypy.config.update("server.conf") + + cherrypy.tree.mount(Disco(), '/.well-known/ocm') + cherrypy.tree.mount(Disco(), '/ocm-provider') + cherrypy.tree.mount(Root(), '/') + cherrypy.tree.mount(Token(), '/ocm/token') + cherrypy.tree.mount(Wayf(), '/wayf') + # cherrypy.tree.mount(AcceptInvite(), '/accept-invite') + cherrypy.tree.mount( + AcceptInvite(), + '/accept-invite', + config={ + '/': { + 'tools.trailing_slash.on': False + } + }) + + cherrypy.engine.start() + cherrypy.engine.block() + + +if __name__ == '__main__': + main() diff --git a/app/root.py b/app/root.py new file mode 100644 index 0000000..a78d624 --- /dev/null +++ b/app/root.py @@ -0,0 +1,10 @@ +import cherrypy +from jinja2 import Environment, FileSystemLoader +env = Environment(loader=FileSystemLoader('templates')) + +class Root(object): + + @cherrypy.expose + def index(self): + tmpl = env.get_template('index.html') + return tmpl.render(salutation='Hello', target='World') diff --git a/app/token.py b/app/token.py new file mode 100644 index 0000000..3be741f --- /dev/null +++ b/app/token.py @@ -0,0 +1,36 @@ +import cherrypy +import json + + +class Token(object): + + @cherrypy.expose + @cherrypy.tools.json_out() + def index(self): + raw_body = cherrypy.request.body.read() + data = json.loads(raw_body) + + client_id = data['client_id'] + code = data['code'] + grant_type = data['grant_type'] + + if grant_type == 'ocm_authorization_code': + return self.get_token(client_id, code) + else: + return {'error': 'unsupported grant_type'} + + def get_token(self, client_id, code) -> dict: + if self.validate_token(client_id, code): + token = { + "access_token": "asdfgh", + "token_type": "bearer", + "expires_in": 3600, + "refresh_token": "qwertyuiop" + } + return token + else: + error = {'error': 'invalid_grant'} + return error + + def validate_token(self, client_id, token) -> bool: + return True diff --git a/app/wayf.py b/app/wayf.py new file mode 100644 index 0000000..de2594a --- /dev/null +++ b/app/wayf.py @@ -0,0 +1,14 @@ +import cherrypy +from jinja2 import Environment, FileSystemLoader +env = Environment(loader=FileSystemLoader('templates')) + +class Wayf(object): + + @cherrypy.expose + def index(self,token=None): + if token is None: + tmpl = env.get_template('error.html') + return tmpl.render(message="Sorry you need a token to access this page") + else: + tmpl = env.get_template('wayf.html') + return tmpl.render(token=token) diff --git a/server.conf b/server.conf new file mode 100644 index 0000000..ec5e523 --- /dev/null +++ b/server.conf @@ -0,0 +1,3 @@ +[global] +server.socket_port: 9090 + diff --git a/templates/discovery.yaml b/templates/discovery.yaml new file mode 100644 index 0000000..86c6769 --- /dev/null +++ b/templates/discovery.yaml @@ -0,0 +1,26 @@ +enabled: true +apiVersion: "1.3.0" +endPoint: {{ endpoint }} +provider: mlog +resoureTypes: + - name: file + shareTypes: + - user + protocols: + datatx: /datatx + webdav: /webdav +capabilities: + - enforce-mfa + - invites + - protocol-object + - recieve-code + - webdav-uri +criteria: + - http-request-signatures + - recieve-code + - denylist + - allowlist + - invite +publicKey: + keyId: {{ public_key_id }} + publicKeyPem: '{{ public_key_pem }}' diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000..9ab7e02 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,2 @@ +

Oops!

+

{{ message }}

diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..c367f79 --- /dev/null +++ b/templates/index.html @@ -0,0 +1 @@ +

{{ salutation }} {{ target }}

diff --git a/templates/wayf.html b/templates/wayf.html new file mode 100644 index 0000000..9142228 --- /dev/null +++ b/templates/wayf.html @@ -0,0 +1,17 @@ +

Where are you from?

+
+
+ + + +
+
+ +
+