kopia lustrzana https://gitlab.com/marnanel/chapeau
Porównaj commity
No commits in common. "4787f7c09aa8fd9c146ee6f47df736020c1dfe75" and "0918382eae8a8d580762df8c55f2de77dfb316e3" have entirely different histories.
4787f7c09a
...
0918382eae
|
@ -1,46 +1,28 @@
|
||||||
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
|
||||||
|
@ -65,16 +47,5 @@ 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}'
|
|
||||||
|
|
134
kepi/fastcgi.py
134
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, normalise_url
|
from kepi.config import config, subcommand
|
||||||
|
|
||||||
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_RE = r'/users/([a-z0-9-]+)/?'
|
USER_PAGE_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,6 +28,12 @@ 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
|
||||||
|
|
||||||
|
@ -63,11 +69,15 @@ 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(env, match):
|
def despatch_user_page(env, match):
|
||||||
result = {
|
|
||||||
'mimetype': CONTENTTYPE_ACTIVITY,
|
result = f"""Content-Type: {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):
|
||||||
|
@ -76,7 +86,7 @@ def despatch_host_meta(env, match):
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'mimetype': CONTENTTYPE_HOST_META,
|
'mimetype': CONTENTTYPE_HOST_META,
|
||||||
'text': f"""<?xml version="1.0" encoding="UTF-8"?>
|
'text': """<?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}}"/>
|
||||||
|
@ -133,33 +143,35 @@ def despatch_nodeinfo_part_2(env, match):
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _despatch_webfinger_inner(env, match):
|
def despatch_webfinger(env, match):
|
||||||
|
|
||||||
username = urllib.parse.parse_qs(
|
user = urllib.parse.parse_qs(
|
||||||
qs = env['QUERY_STRING'],
|
qs = env['QUERY_STRING'],
|
||||||
).get('resource', '')
|
).get('resource', '')
|
||||||
|
|
||||||
if not username:
|
print("9100", user)
|
||||||
|
if not user:
|
||||||
return {
|
return {
|
||||||
'status': 400,
|
'status': 400,
|
||||||
'reason': 'No resource specified for webfinger',
|
'reason': 'No resource specified for webfinger',
|
||||||
}
|
}
|
||||||
username = username[0]
|
user = user[0]
|
||||||
|
|
||||||
# Generally, username resources should be prefaced with "acct:",
|
# Generally, user 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.
|
||||||
username = re.sub(r'^acct:', '', username)
|
user = re.sub(r'^acct:', '', user)
|
||||||
|
|
||||||
|
print("9110", user)
|
||||||
|
|
||||||
if '@' not in username:
|
if '@' not in user:
|
||||||
return {
|
return {
|
||||||
'status': 400,
|
'status': 400,
|
||||||
'reason': 'Absolute name required',
|
'reason': 'Absolute name required',
|
||||||
}
|
}
|
||||||
|
|
||||||
user_part, host_part = username.split('@', 2)
|
username, hostname = user.split('@', 2)
|
||||||
|
|
||||||
if host_part not in [
|
if hostname not in [
|
||||||
env['SERVER_NAME'],
|
env['SERVER_NAME'],
|
||||||
]:
|
]:
|
||||||
return {
|
return {
|
||||||
|
@ -167,75 +179,38 @@ def _despatch_webfinger_inner(env, match):
|
||||||
'reason': 'Not local',
|
'reason': 'Not local',
|
||||||
}
|
}
|
||||||
|
|
||||||
user = config.users[user_part]
|
return {
|
||||||
|
|
||||||
|
|
||||||
if not user.exists:
|
|
||||||
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" : f"acct:{user_part}@{host_part}",
|
"subject" : "acct:{}@{}".format(username, hostname),
|
||||||
"aliases" : [
|
"aliases" : [
|
||||||
user_url,
|
actor_url,
|
||||||
],
|
],
|
||||||
|
|
||||||
"links":[
|
"links":[
|
||||||
{
|
{
|
||||||
'rel': 'http://webfinger.net/rel/profile-page',
|
'rel': 'http://webfinger.net/rel/profile-page',
|
||||||
'type': 'text/html',
|
'type': 'text/html',
|
||||||
'href': user_url,
|
'href': actor_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': user_url,
|
'href': actor_url,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'rel': 'http://ostatus.org/schema/1.0/subscribe',
|
'rel': 'http://ostatus.org/schema/1.0/subscribe',
|
||||||
'template': normalise_url(
|
'template': configured_url('AUTHORIZE_FOLLOW_LINK'),
|
||||||
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(
|
||||||
|
@ -244,12 +219,11 @@ 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(json, indent=2, sort_keys=True)
|
body = dumps(text, indent=2, sort_keys=True)
|
||||||
elif text is not None:
|
elif text is not None:
|
||||||
body = text
|
body = text
|
||||||
else:
|
else:
|
||||||
|
@ -262,13 +236,9 @@ 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
|
||||||
|
@ -278,12 +248,23 @@ 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_RE, despatch_user),
|
(USER_PAGE_RE, despatch_user_page),
|
||||||
(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),
|
||||||
|
@ -291,6 +272,7 @@ 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
|
||||||
|
|
||||||
|
@ -303,9 +285,7 @@ def despatch(env):
|
||||||
result = format_message_for_http(**result)
|
result = format_message_for_http(**result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
return format_message_for_http(
|
return ERROR_404
|
||||||
status = 404,
|
|
||||||
)
|
|
||||||
|
|
||||||
class KepiHandler(FcgiHandler):
|
class KepiHandler(FcgiHandler):
|
||||||
|
|
||||||
|
|
|
@ -19,10 +19,6 @@ 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,12 +1,11 @@
|
||||||
# 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')
|
||||||
|
|
||||||
|
@ -26,8 +25,6 @@ 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)
|
||||||
|
@ -40,6 +37,7 @@ 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():
|
||||||
|
|
||||||
|
@ -102,18 +100,25 @@ def test_fastcgi_nodeinfo_part_2():
|
||||||
assert 'activitypub' in response['protocols']
|
assert 'activitypub' in response['protocols']
|
||||||
|
|
||||||
"""
|
"""
|
||||||
class _TestUsers:
|
From the original: we need to do this before the tests which get 200
|
||||||
|
def setUp():
|
||||||
|
keys = json.load(open('kepi/bowler_pub/tests/keys/keys-0001.json', 'r'))
|
||||||
|
|
||||||
def __enter__(self):
|
create_local_person(
|
||||||
self.tempd = tempfile.TemporaryDirectory()
|
name='alice',
|
||||||
print("9444", "================", self.tempd)
|
publicKey=keys['public'],
|
||||||
config.users_dir = self.tempd.name
|
privateKey=keys['private'],
|
||||||
print("9445", "================", config.settings)
|
)
|
||||||
return self.tempd.__enter__()
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, exc_tb):
|
self._alice_keys = keys
|
||||||
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():
|
||||||
|
|
||||||
|
@ -136,16 +141,6 @@ 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,
|
||||||
|
@ -167,7 +162,7 @@ def test_fastcgi_webfinger():
|
||||||
)
|
)
|
||||||
|
|
||||||
response = webfinger(
|
response = webfinger(
|
||||||
who = 'sheila@wombles.example.org',
|
who = 'alice@wombles.example.org',
|
||||||
expected_status = 200,
|
expected_status = 200,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -177,23 +172,12 @@ def test_fastcgi_webfinger():
|
||||||
'ACAO is *, per RFC'
|
'ACAO is *, per RFC'
|
||||||
)
|
)
|
||||||
|
|
||||||
parsed = json.loads(response.get_payload())
|
parsed = json.loads(response.content)
|
||||||
|
|
||||||
assert parsed['subject']=='acct:sheila@wombles.example.org'
|
assert parsed['subject']=='acct:alice@testserver'
|
||||||
assert 'https://wombles.example.org/users/sheila' in parsed['aliases']
|
assert 'https://testserver/users/alice' in parsed['aliases']
|
||||||
assert {
|
assert {
|
||||||
'rel': 'self',
|
'rel': 'self',
|
||||||
'type': 'application/activity+json',
|
'type': 'application/activity+json',
|
||||||
'href': 'https://wombles.example.org/users/sheila',
|
'href': 'https://testserver/users/alice',
|
||||||
} 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