first commit
Signed-off-by: Micke Nordin <kano@sunet.se>
This commit is contained in:
commit
4dd92c0bdc
25 changed files with 321 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ed25519_private_key.pem
|
||||||
|
ed25519_public_key.pem
|
0
README.md
Normal file
0
README.md
Normal file
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
BIN
app/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/accept_invite.cpython-313.pyc
Normal file
BIN
app/__pycache__/accept_invite.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/crypto.cpython-313.pyc
Normal file
BIN
app/__pycache__/crypto.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/disco.cpython-313.pyc
Normal file
BIN
app/__pycache__/disco.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/main.cpython-313.pyc
Normal file
BIN
app/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/root.cpython-313.pyc
Normal file
BIN
app/__pycache__/root.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/token.cpython-313.pyc
Normal file
BIN
app/__pycache__/token.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/wayf.cpython-313.pyc
Normal file
BIN
app/__pycache__/wayf.cpython-313.pyc
Normal file
Binary file not shown.
48
app/accept_invite.py
Normal file
48
app/accept_invite.py
Normal file
|
@ -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)
|
54
app/crypto.py
Normal file
54
app/crypto.py
Normal file
|
@ -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
|
||||||
|
|
10
app/datatx.py
Normal file
10
app/datatx.py
Normal file
|
@ -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')
|
31
app/disco.py
Normal file
31
app/disco.py
Normal file
|
@ -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
|
43
app/main.py
Normal file
43
app/main.py
Normal file
|
@ -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()
|
11
app/root.py
Normal file
11
app/root.py
Normal file
|
@ -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)
|
36
app/token.py
Normal file
36
app/token.py
Normal file
|
@ -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
|
14
app/wayf.py
Normal file
14
app/wayf.py
Normal file
|
@ -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)
|
3
server.conf
Normal file
3
server.conf
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[global]
|
||||||
|
server.socket_port: 9090
|
||||||
|
|
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
26
templates/discovery.yaml
Normal file
26
templates/discovery.yaml
Normal file
|
@ -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 }}'
|
2
templates/error.html
Normal file
2
templates/error.html
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<h1>Oops!</h1>
|
||||||
|
<p>{{ message }}</p>
|
24
templates/index.html
Normal file
24
templates/index.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css" />
|
||||||
|
<title>Amity</title>
|
||||||
|
<style>
|
||||||
|
nav ul {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
nav ul li {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li><a aria-current="files" href="/files">Files</a></li>
|
||||||
|
<li><a aria-current="invites" href="/friends">Friends</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<h1>Hello {{ user }}</h1>
|
||||||
|
</body>
|
17
templates/wayf.html
Normal file
17
templates/wayf.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<h1>Where are you from?</h1>
|
||||||
|
<form action="/accept-invite" method="POST">
|
||||||
|
<div>
|
||||||
|
<label for="remote">Home server:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="remote"
|
||||||
|
name="remote"
|
||||||
|
placeholder="https://cernbox.cern.ch"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input type="hidden" name="token" value="{{ token }}" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button>Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
Loading…
Add table
Reference in a new issue