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