commit 4dd92c0bdcfd8552f13e586a08237767dd4effb0 Author: Micke Nordin Date: Tue Jun 24 11:06:10 2025 +0200 first commit Signed-off-by: Micke Nordin 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/__pycache__/__init__.cpython-313.pyc b/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..8371bec Binary files /dev/null and b/app/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/__pycache__/accept_invite.cpython-313.pyc b/app/__pycache__/accept_invite.cpython-313.pyc new file mode 100644 index 0000000..4d94806 Binary files /dev/null and b/app/__pycache__/accept_invite.cpython-313.pyc differ diff --git a/app/__pycache__/crypto.cpython-313.pyc b/app/__pycache__/crypto.cpython-313.pyc new file mode 100644 index 0000000..e81bfbf Binary files /dev/null and b/app/__pycache__/crypto.cpython-313.pyc differ diff --git a/app/__pycache__/disco.cpython-313.pyc b/app/__pycache__/disco.cpython-313.pyc new file mode 100644 index 0000000..1a9afb5 Binary files /dev/null and b/app/__pycache__/disco.cpython-313.pyc differ diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..23b6560 Binary files /dev/null and b/app/__pycache__/main.cpython-313.pyc differ diff --git a/app/__pycache__/root.cpython-313.pyc b/app/__pycache__/root.cpython-313.pyc new file mode 100644 index 0000000..b3f0e92 Binary files /dev/null and b/app/__pycache__/root.cpython-313.pyc differ diff --git a/app/__pycache__/token.cpython-313.pyc b/app/__pycache__/token.cpython-313.pyc new file mode 100644 index 0000000..4549d61 Binary files /dev/null and b/app/__pycache__/token.cpython-313.pyc differ diff --git a/app/__pycache__/wayf.cpython-313.pyc b/app/__pycache__/wayf.cpython-313.pyc new file mode 100644 index 0000000..1482223 Binary files /dev/null and b/app/__pycache__/wayf.cpython-313.pyc differ diff --git a/app/accept_invite.py b/app/accept_invite.py new file mode 100644 index 0000000..ca48863 --- /dev/null +++ b/app/accept_invite.py @@ -0,0 +1,48 @@ +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: + tmpl = env.get_template('error.html') + return tmpl.render(message='Unable to reach OCM provider') + if r.status_code != 200: + tmpl = env.get_template('error.html') + return tmpl.render(message=f'Unable to reach OCM provider: {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']): + tmpl = env.get_template('error.html') + return tmpl.render(message='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..bd753e5 --- /dev/null +++ b/app/main.py @@ -0,0 +1,43 @@ +import cherrypy +import os + +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.tree.mount(None, + "/favicon.ico", + config={ + "/": { + "tools.staticfile.on": + True, + "tools.staticfile.filename": + os.path.abspath("static/favicon.ico") + } + }) + + 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..afa2707 --- /dev/null +++ b/app/root.py @@ -0,0 +1,11 @@ +import cherrypy +from jinja2 import Environment, FileSystemLoader +env = Environment(loader=FileSystemLoader('templates')) + +class Root(object): + + @cherrypy.expose + def index(self): + user = 'Anonymous' + tmpl = env.get_template('index.html') + return tmpl.render(user=user) 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/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..4246ff9 Binary files /dev/null and b/static/favicon.ico differ 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..3b5787c --- /dev/null +++ b/templates/index.html @@ -0,0 +1,24 @@ + + + Amity + + + + +

Hello {{ user }}

+ 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?

+
+
+ + + +
+
+ +
+