kopia lustrzana https://gitlab.com/marnanel/chapeau
Porównaj commity
3 Commity
0918382eae
...
4787f7c09a
Autor | SHA1 | Data |
---|---|---|
Marnanel Thurman | 4787f7c09a | |
Marnanel Thurman | fb9fe9aaa7 | |
Marnanel Thurman | de8ca26303 |
|
@ -1,28 +1,46 @@
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
import kepi.users
|
||||||
|
|
||||||
DEFAULT_CONFIG_FILENAME = '/etc/kepi/kepi.conf'
|
DEFAULT_CONFIG_FILENAME = '/etc/kepi/kepi.conf'
|
||||||
CONFIG_SECTION = 'kepi'
|
CONFIG_SECTION = 'kepi'
|
||||||
|
|
||||||
|
class ConfigUsers:
|
||||||
|
def __init__(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def __getitem__(self, who):
|
||||||
|
|
||||||
|
assert '/' not in who
|
||||||
|
assert not who.startswith('.')
|
||||||
|
|
||||||
|
filename = os.path.join(
|
||||||
|
config.users_dir,
|
||||||
|
who,
|
||||||
|
f'{who}.json',
|
||||||
|
)
|
||||||
|
return kepi.users.User(filename)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|
||||||
settings = {}
|
settings = {}
|
||||||
subcommands = []
|
subcommands = []
|
||||||
argparser = None
|
argparser = None
|
||||||
|
|
||||||
|
users = ConfigUsers.__new__(ConfigUsers)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def users(self):
|
|
||||||
raise ValueError()
|
|
||||||
|
|
||||||
def __getattr__(self, field):
|
def __getattr__(self, field):
|
||||||
if field in self.settings:
|
if field in self.settings:
|
||||||
print("9199", self.settings, field)
|
|
||||||
return self.settings[field]
|
return self.settings[field]
|
||||||
else:
|
else:
|
||||||
raise AttributeError(field)
|
raise AttributeError(field)
|
||||||
|
|
||||||
|
def __setattr__(self, field, value):
|
||||||
|
self.settings[field] = value
|
||||||
|
|
||||||
def parse_args(self, argparser):
|
def parse_args(self, argparser):
|
||||||
|
|
||||||
assert self.subcommands is not None
|
assert self.subcommands is not None
|
||||||
|
@ -47,5 +65,16 @@ class Config:
|
||||||
config = Config.__new__(Config)
|
config = Config.__new__(Config)
|
||||||
|
|
||||||
def subcommand(fn):
|
def subcommand(fn):
|
||||||
|
"""
|
||||||
|
Decorator
|
||||||
|
"""
|
||||||
config.subcommands.append(fn)
|
config.subcommands.append(fn)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def normalise_url(
|
||||||
|
address,
|
||||||
|
env = {},
|
||||||
|
):
|
||||||
|
|
||||||
|
# For now, a naive implementation
|
||||||
|
return f'https://{env["SERVER_NAME"]}{address}'
|
||||||
|
|
132
kepi/fastcgi.py
132
kepi/fastcgi.py
|
@ -5,7 +5,7 @@ from json import dumps
|
||||||
import logging
|
import logging
|
||||||
import kepi
|
import kepi
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from kepi.config import config, subcommand
|
from kepi.config import config, subcommand, normalise_url
|
||||||
|
|
||||||
logger = logging.getLogger('kepi.fastcgi')
|
logger = logging.getLogger('kepi.fastcgi')
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ CONTENTTYPE_NODEINFO = (
|
||||||
'profile=http://nodeinfo.diaspora.software/ns/schema/2.0#'
|
'profile=http://nodeinfo.diaspora.software/ns/schema/2.0#'
|
||||||
)
|
)
|
||||||
|
|
||||||
USER_PAGE_RE = r'users/([a-z0-9-]+)/?'
|
USER_RE = r'/users/([a-z0-9-]+)/?'
|
||||||
HOST_META_URI = '/.well-known/host-meta'
|
HOST_META_URI = '/.well-known/host-meta'
|
||||||
NODEINFO_PART_1_URI = '/.well-known/nodeinfo'
|
NODEINFO_PART_1_URI = '/.well-known/nodeinfo'
|
||||||
NODEINFO_PART_2_URI = '/nodeinfo.json'
|
NODEINFO_PART_2_URI = '/nodeinfo.json'
|
||||||
|
@ -28,12 +28,6 @@ NODEINFO_PART_2_URI = '/nodeinfo.json'
|
||||||
WEBFINGER_URI = '/.well-known/webfinger'
|
WEBFINGER_URI = '/.well-known/webfinger'
|
||||||
WEBFINGER_MIMETYPE = 'application/jrd+json; charset=utf-8'
|
WEBFINGER_MIMETYPE = 'application/jrd+json; charset=utf-8'
|
||||||
|
|
||||||
ERROR_404 = """Content-Type: text/html
|
|
||||||
Status: 404 Not found
|
|
||||||
|
|
||||||
That resource does not exist here.
|
|
||||||
"""
|
|
||||||
|
|
||||||
DEFAULT_HOST = 'localhost'
|
DEFAULT_HOST = 'localhost'
|
||||||
DEFAULT_PORT = 17177
|
DEFAULT_PORT = 17177
|
||||||
|
|
||||||
|
@ -69,15 +63,11 @@ def fastcgi_command(subparsers):
|
||||||
def _encode(s):
|
def _encode(s):
|
||||||
return s.replace('\n','\r\n').encode('UTF-8')
|
return s.replace('\n','\r\n').encode('UTF-8')
|
||||||
|
|
||||||
def despatch_user_page(env, match):
|
def despatch_user(env, match):
|
||||||
|
result = {
|
||||||
result = f"""Content-Type: {CONTENTTYPE_ACTIVITY}
|
'mimetype': CONTENTTYPE_ACTIVITY,
|
||||||
Status: 200 OK
|
}
|
||||||
|
raise NotImplementedError()
|
||||||
Hello world.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def despatch_host_meta(env, match):
|
def despatch_host_meta(env, match):
|
||||||
|
@ -86,7 +76,7 @@ def despatch_host_meta(env, match):
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'mimetype': CONTENTTYPE_HOST_META,
|
'mimetype': CONTENTTYPE_HOST_META,
|
||||||
'text': """<?xml version="1.0" encoding="UTF-8"?>
|
'text': f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||||
<Link rel="lrdd" type="application/xrd+xml"
|
<Link rel="lrdd" type="application/xrd+xml"
|
||||||
template="https://{env['SERVER_NAME']}/.well-known/webfinger?resource={{uri}}"/>
|
template="https://{env['SERVER_NAME']}/.well-known/webfinger?resource={{uri}}"/>
|
||||||
|
@ -143,35 +133,33 @@ def despatch_nodeinfo_part_2(env, match):
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def despatch_webfinger(env, match):
|
def _despatch_webfinger_inner(env, match):
|
||||||
|
|
||||||
user = urllib.parse.parse_qs(
|
username = urllib.parse.parse_qs(
|
||||||
qs = env['QUERY_STRING'],
|
qs = env['QUERY_STRING'],
|
||||||
).get('resource', '')
|
).get('resource', '')
|
||||||
|
|
||||||
print("9100", user)
|
if not username:
|
||||||
if not user:
|
|
||||||
return {
|
return {
|
||||||
'status': 400,
|
'status': 400,
|
||||||
'reason': 'No resource specified for webfinger',
|
'reason': 'No resource specified for webfinger',
|
||||||
}
|
}
|
||||||
user = user[0]
|
username = username[0]
|
||||||
|
|
||||||
# Generally, user resources should be prefaced with "acct:",
|
# Generally, username resources should be prefaced with "acct:",
|
||||||
# per RFC7565. We support this, but we don't enforce it.
|
# per RFC7565. We support this, but we don't enforce it.
|
||||||
user = re.sub(r'^acct:', '', user)
|
username = re.sub(r'^acct:', '', username)
|
||||||
|
|
||||||
print("9110", user)
|
|
||||||
|
|
||||||
if '@' not in user:
|
if '@' not in username:
|
||||||
return {
|
return {
|
||||||
'status': 400,
|
'status': 400,
|
||||||
'reason': 'Absolute name required',
|
'reason': 'Absolute name required',
|
||||||
}
|
}
|
||||||
|
|
||||||
username, hostname = user.split('@', 2)
|
user_part, host_part = username.split('@', 2)
|
||||||
|
|
||||||
if hostname not in [
|
if host_part not in [
|
||||||
env['SERVER_NAME'],
|
env['SERVER_NAME'],
|
||||||
]:
|
]:
|
||||||
return {
|
return {
|
||||||
|
@ -179,38 +167,75 @@ def despatch_webfinger(env, match):
|
||||||
'reason': 'Not local',
|
'reason': 'Not local',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user = config.users[user_part]
|
||||||
|
|
||||||
|
|
||||||
|
if not user.exists:
|
||||||
return {
|
return {
|
||||||
|
'status': 404,
|
||||||
|
'reason': 'Not known',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
user_url = normalise_url(
|
||||||
|
address = config.user_uri % {'username':user_part},
|
||||||
|
env = env,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
atom_uri = config.atom_uri
|
||||||
|
except AttributeError:
|
||||||
|
atom_uri = None
|
||||||
|
|
||||||
|
result = {
|
||||||
'mimetype': WEBFINGER_MIMETYPE,
|
'mimetype': WEBFINGER_MIMETYPE,
|
||||||
'json': {
|
'json': {
|
||||||
"subject" : "acct:{}@{}".format(username, hostname),
|
"subject" : f"acct:{user_part}@{host_part}",
|
||||||
"aliases" : [
|
"aliases" : [
|
||||||
actor_url,
|
user_url,
|
||||||
],
|
],
|
||||||
|
|
||||||
"links":[
|
"links":[
|
||||||
{
|
{
|
||||||
'rel': 'http://webfinger.net/rel/profile-page',
|
'rel': 'http://webfinger.net/rel/profile-page',
|
||||||
'type': 'text/html',
|
'type': 'text/html',
|
||||||
'href': actor_url,
|
'href': user_url,
|
||||||
},
|
},
|
||||||
# TODO
|
|
||||||
#{
|
|
||||||
# 'rel': 'http://schemas.google.com/g/2010#updates-from',
|
|
||||||
# 'type': 'application/atom+xml',
|
|
||||||
# 'href': 'FIXME',
|
|
||||||
#},
|
|
||||||
{
|
{
|
||||||
'rel': 'self',
|
'rel': 'self',
|
||||||
'type': 'application/activity+json',
|
'type': 'application/activity+json',
|
||||||
'href': actor_url,
|
'href': user_url,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'rel': 'http://ostatus.org/schema/1.0/subscribe',
|
'rel': 'http://ostatus.org/schema/1.0/subscribe',
|
||||||
'template': configured_url('AUTHORIZE_FOLLOW_LINK'),
|
'template': normalise_url(
|
||||||
|
address = config.authorise_follow_uri,
|
||||||
|
env=env,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
]},
|
]},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if atom_uri is not None:
|
||||||
|
result['json']['links'].append(
|
||||||
|
{
|
||||||
|
'rel': 'http://schemas.google.com/g/2010#updates-from',
|
||||||
|
'type': 'application/atom+xml',
|
||||||
|
'href': normalise_url(
|
||||||
|
address = atom_uri,
|
||||||
|
env = env,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def despatch_webfinger(env, match):
|
||||||
|
result = _despatch_webfinger_inner(env, match)
|
||||||
|
result['extra_headers'] = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def format_message_for_http(
|
def format_message_for_http(
|
||||||
|
@ -219,11 +244,12 @@ def format_message_for_http(
|
||||||
reason = 'OK',
|
reason = 'OK',
|
||||||
text = None,
|
text = None,
|
||||||
json = None,
|
json = None,
|
||||||
|
extra_headers = {},
|
||||||
):
|
):
|
||||||
|
|
||||||
if json is not None:
|
if json is not None:
|
||||||
assert text is None
|
assert text is None
|
||||||
body = dumps(text, indent=2, sort_keys=True)
|
body = dumps(json, indent=2, sort_keys=True)
|
||||||
elif text is not None:
|
elif text is not None:
|
||||||
body = text
|
body = text
|
||||||
else:
|
else:
|
||||||
|
@ -236,9 +262,13 @@ def format_message_for_http(
|
||||||
f"Content-Type: {mimetype}\r\n"
|
f"Content-Type: {mimetype}\r\n"
|
||||||
f"Status: {status} {reason}\r\n"
|
f"Status: {status} {reason}\r\n"
|
||||||
f"Content-Length: {len(body)}\r\n"
|
f"Content-Length: {len(body)}\r\n"
|
||||||
"\r\n"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for f,v in extra_headers.items():
|
||||||
|
headers += f'{f}: {v}\r\n'
|
||||||
|
|
||||||
|
headers += "\r\n"
|
||||||
|
|
||||||
headers = headers.encode('UTF-8')
|
headers = headers.encode('UTF-8')
|
||||||
|
|
||||||
return headers+body
|
return headers+body
|
||||||
|
@ -248,23 +278,12 @@ def despatch(env):
|
||||||
# XXX and DOCUMENT_URI and possibly QUERY_STRING
|
# XXX and DOCUMENT_URI and possibly QUERY_STRING
|
||||||
# XXX and despatch as appropriate
|
# XXX and despatch as appropriate
|
||||||
|
|
||||||
"""
|
|
||||||
response_headers = ''
|
|
||||||
|
|
||||||
response_headers += f'Content-Type: {CONTENTTYPE_ACTIVITY}\r\n'
|
|
||||||
response_headers += '\r\n'
|
|
||||||
|
|
||||||
response_body = response_body.encode('UTF-8')
|
|
||||||
response_headers = response_headers.encode('UTF-8')
|
|
||||||
"""
|
|
||||||
|
|
||||||
logger.debug('query: %s', env)
|
logger.debug('query: %s', env)
|
||||||
|
|
||||||
uri = env['DOCUMENT_URI']
|
uri = env['DOCUMENT_URI']
|
||||||
print(env)
|
|
||||||
|
|
||||||
for regex, handler in [
|
for regex, handler in [
|
||||||
(USER_PAGE_RE, despatch_user_page),
|
(USER_RE, despatch_user),
|
||||||
(HOST_META_URI, despatch_host_meta),
|
(HOST_META_URI, despatch_host_meta),
|
||||||
(NODEINFO_PART_1_URI, despatch_nodeinfo_part_1),
|
(NODEINFO_PART_1_URI, despatch_nodeinfo_part_1),
|
||||||
(NODEINFO_PART_2_URI, despatch_nodeinfo_part_2),
|
(NODEINFO_PART_2_URI, despatch_nodeinfo_part_2),
|
||||||
|
@ -272,7 +291,6 @@ def despatch(env):
|
||||||
]:
|
]:
|
||||||
|
|
||||||
match = re.match(regex, uri)
|
match = re.match(regex, uri)
|
||||||
print(regex, uri, match)
|
|
||||||
if match is None:
|
if match is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -285,7 +303,9 @@ def despatch(env):
|
||||||
result = format_message_for_http(**result)
|
result = format_message_for_http(**result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
return ERROR_404
|
return format_message_for_http(
|
||||||
|
status = 404,
|
||||||
|
)
|
||||||
|
|
||||||
class KepiHandler(FcgiHandler):
|
class KepiHandler(FcgiHandler):
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,10 @@ class User:
|
||||||
def name(self):
|
def name(self):
|
||||||
return os.path.splitext(os.path.basename(self.filename))[0]
|
return os.path.splitext(os.path.basename(self.filename))[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exists(self):
|
||||||
|
return os.path.exists(self.filename)
|
||||||
|
|
||||||
def _load_details(self):
|
def _load_details(self):
|
||||||
if self.details is not None:
|
if self.details is not None:
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
# Many of these tests are based on tests from busby, and we should
|
# Many of these tests are based on tests from busby, and we should
|
||||||
# refactor those when the dust settles.
|
# refactor those when the dust settles.
|
||||||
|
|
||||||
from kepi.fastcgi import despatch
|
|
||||||
from test import *
|
from test import *
|
||||||
import logging
|
import logging
|
||||||
import email
|
import email
|
||||||
import json
|
import json
|
||||||
|
from kepi.fastcgi import despatch
|
||||||
|
from kepi.config import config
|
||||||
|
|
||||||
logger = logging.getLogger(name='kepi')
|
logger = logging.getLogger(name='kepi')
|
||||||
|
|
||||||
|
@ -25,6 +26,8 @@ WEBFINGER_BASE_URI = '/.well-known/webfinger'
|
||||||
WEBFINGER_URI = WEBFINGER_BASE_URI + '?resource={}'
|
WEBFINGER_URI = WEBFINGER_BASE_URI + '?resource={}'
|
||||||
WEBFINGER_MIMETYPE = 'application/jrd+json; charset=utf-8'
|
WEBFINGER_MIMETYPE = 'application/jrd+json; charset=utf-8'
|
||||||
|
|
||||||
|
ACTIVITY_MIMETYPE = 'application/activity+json'
|
||||||
|
|
||||||
def call_despatch_and_parse_result(*args, **kwargs):
|
def call_despatch_and_parse_result(*args, **kwargs):
|
||||||
found = despatch(*args, **kwargs).decode('UTF-8')
|
found = despatch(*args, **kwargs).decode('UTF-8')
|
||||||
result = email.message_from_string(found)
|
result = email.message_from_string(found)
|
||||||
|
@ -37,7 +40,6 @@ def test_fastcgi_simple():
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert found['Status'].startswith('404 ')
|
assert found['Status'].startswith('404 ')
|
||||||
assert found['Content-Type']==HTML_MIMETYPE
|
|
||||||
|
|
||||||
def test_fastcgi_host_meta():
|
def test_fastcgi_host_meta():
|
||||||
|
|
||||||
|
@ -100,24 +102,17 @@ def test_fastcgi_nodeinfo_part_2():
|
||||||
assert 'activitypub' in response['protocols']
|
assert 'activitypub' in response['protocols']
|
||||||
|
|
||||||
"""
|
"""
|
||||||
From the original: we need to do this before the tests which get 200
|
class _TestUsers:
|
||||||
def setUp():
|
|
||||||
keys = json.load(open('kepi/bowler_pub/tests/keys/keys-0001.json', 'r'))
|
|
||||||
|
|
||||||
create_local_person(
|
def __enter__(self):
|
||||||
name='alice',
|
self.tempd = tempfile.TemporaryDirectory()
|
||||||
publicKey=keys['public'],
|
print("9444", "================", self.tempd)
|
||||||
privateKey=keys['private'],
|
config.users_dir = self.tempd.name
|
||||||
)
|
print("9445", "================", config.settings)
|
||||||
|
return self.tempd.__enter__()
|
||||||
|
|
||||||
self._alice_keys = keys
|
def __exit__(self, exc_type, exc_value, exc_tb):
|
||||||
|
return self.tempd.__exit__(exc_type, exc_value, exc_tb)
|
||||||
settings.ALLOWED_HOSTS = [
|
|
||||||
'altair.example.com',
|
|
||||||
'testserver',
|
|
||||||
]
|
|
||||||
|
|
||||||
settings.KEPI['LOCAL_OBJECT_HOSTNAME'] = 'testserver'
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def test_fastcgi_webfinger():
|
def test_fastcgi_webfinger():
|
||||||
|
@ -141,6 +136,16 @@ def test_fastcgi_webfinger():
|
||||||
assert found['Status'].startswith(
|
assert found['Status'].startswith(
|
||||||
f'{expected_status} '), 'Status is correct'
|
f'{expected_status} '), 'Status is correct'
|
||||||
|
|
||||||
|
return found
|
||||||
|
|
||||||
|
config.authorise_follow_uri = '/authorise-follow'
|
||||||
|
config.user_uri = '/users/%(username)s'
|
||||||
|
|
||||||
|
config.users_dir = os.path.join(
|
||||||
|
os.path.dirname(__file__),
|
||||||
|
'example-users',
|
||||||
|
)
|
||||||
|
|
||||||
response = webfinger(
|
response = webfinger(
|
||||||
who = None,
|
who = None,
|
||||||
expected_status = 400,
|
expected_status = 400,
|
||||||
|
@ -162,7 +167,7 @@ def test_fastcgi_webfinger():
|
||||||
)
|
)
|
||||||
|
|
||||||
response = webfinger(
|
response = webfinger(
|
||||||
who = 'alice@wombles.example.org',
|
who = 'sheila@wombles.example.org',
|
||||||
expected_status = 200,
|
expected_status = 200,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -172,12 +177,23 @@ def test_fastcgi_webfinger():
|
||||||
'ACAO is *, per RFC'
|
'ACAO is *, per RFC'
|
||||||
)
|
)
|
||||||
|
|
||||||
parsed = json.loads(response.content)
|
parsed = json.loads(response.get_payload())
|
||||||
|
|
||||||
assert parsed['subject']=='acct:alice@testserver'
|
assert parsed['subject']=='acct:sheila@wombles.example.org'
|
||||||
assert 'https://testserver/users/alice' in parsed['aliases']
|
assert 'https://wombles.example.org/users/sheila' in parsed['aliases']
|
||||||
assert {
|
assert {
|
||||||
'rel': 'self',
|
'rel': 'self',
|
||||||
'type': 'application/activity+json',
|
'type': 'application/activity+json',
|
||||||
'href': 'https://testserver/users/alice',
|
'href': 'https://wombles.example.org/users/sheila',
|
||||||
} in parsed['links']
|
} in parsed['links']
|
||||||
|
|
||||||
|
def test_fastcgi_user():
|
||||||
|
found = call_despatch_and_parse_result(
|
||||||
|
env = {
|
||||||
|
'DOCUMENT_URI': '/users/alice',
|
||||||
|
'ACCEPT': ACTIVITY_MIMETYPE,
|
||||||
|
'SERVER_NAME': 'wombles.example.org',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert found['Status'].startswith('200 ')
|
||||||
|
assert found == 'womle'
|
||||||
|
|
Ładowanie…
Reference in New Issue