Skip to content
Snippets Groups Projects
Commit 7bb15a3a authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Merge branch 'federation-inbox' into 'develop'

Federation inbox

See merge request funkwhale/funkwhale!121
parents bfe8f454 76c1abe9
No related branches found
No related tags found
No related merge requests found
Showing
with 783 additions and 86 deletions
API_AUTHENTICATION_REQUIRED=True
RAVEN_ENABLED=false
RAVEN_DSN=https://44332e9fdd3d42879c7d35bf8562c6a4:0062dc16a22b41679cd5765e5342f716@sentry.eliotberriot.com/5
DJANGO_ALLOWED_HOSTS=localhost,nginx
DJANGO_SETTINGS_MODULE=config.settings.local
DJANGO_SECRET_KEY=dev
C_FORCE_ROOT=true
FUNKWHALE_URL=http://localhost
PYTHONDONTWRITEBYTECODE=true
......@@ -13,6 +13,7 @@ stages:
test_api:
services:
- postgres:9.4
- redis:3
stage: test
image: funkwhale/funkwhale:latest
cache:
......@@ -24,6 +25,7 @@ test_api:
DATABASE_URL: "postgresql://postgres@postgres/postgres"
FUNKWHALE_URL: "https://funkwhale.ci"
CACHEOPS_ENABLED: "false"
DJANGO_SETTINGS_MODULE: config.settings.local
before_script:
- cd api
......
#!/bin/bash
set -e
if [ $1 = "pytest" ]; then
# let pytest.ini handle it
unset DJANGO_SETTINGS_MODULE
fi
exec "$@"
......@@ -344,7 +344,12 @@ REST_FRAMEWORK = {
),
'DEFAULT_PAGINATION_CLASS': 'funkwhale_api.common.pagination.FunkwhalePagination',
'PAGE_SIZE': 25,
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.FormParser',
'rest_framework.parsers.MultiPartParser',
'funkwhale_api.federation.parsers.ActivityParser',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS',
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
......@@ -396,6 +401,9 @@ PLAYLISTS_MAX_TRACKS = env.int('PLAYLISTS_MAX_TRACKS', default=250)
ACCOUNT_USERNAME_BLACKLIST = [
'funkwhale',
'library',
'test',
'status',
'root',
'admin',
'owner',
......
......@@ -72,6 +72,10 @@ LOGGING = {
'handlers':['console'],
'propagate': True,
'level':'DEBUG',
}
},
'': {
'level': 'DEBUG',
'handlers': ['console'],
},
},
}
from .common import * # noqa
SECRET_KEY = env("DJANGO_SECRET_KEY", default='test')
# Mail settings
# ------------------------------------------------------------------------------
EMAIL_HOST = 'localhost'
EMAIL_PORT = 1025
EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND',
default='django.core.mail.backends.console.EmailBackend')
# CACHING
# ------------------------------------------------------------------------------
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': ''
}
}
CELERY_BROKER_URL = 'memory://'
########## CELERY
# In development, all tasks will be executed locally by blocking until the task returns
CELERY_TASK_ALWAYS_EAGER = True
########## END CELERY
# Your local stuff: Below this line define 3rd party library settings
API_AUTHENTICATION_REQUIRED = False
CACHEOPS_ENABLED = False
import logging
import json
import requests
import requests_http_signature
from . import signing
logger = logging.getLogger(__name__)
ACTIVITY_TYPES = [
'Accept',
'Add',
'Announce',
'Arrive',
'Block',
'Create',
'Delete',
'Dislike',
'Flag',
'Follow',
'Ignore',
'Invite',
'Join',
'Leave',
'Like',
'Listen',
'Move',
'Offer',
'Question',
'Reject',
'Read',
'Remove',
'TentativeReject',
'TentativeAccept',
'Travel',
'Undo',
'Update',
'View',
]
OBJECT_TYPES = [
'Article',
'Audio',
'Document',
'Event',
'Image',
'Note',
'Page',
'Place',
'Profile',
'Relationship',
'Tombstone',
'Video',
]
def deliver(activity, on_behalf_of, to=[]):
from . import actors
logger.info('Preparing activity delivery to %s', to)
auth = requests_http_signature.HTTPSignatureAuth(
use_auth_header=False,
headers=[
'(request-target)',
'user-agent',
'host',
'date',
'content-type',],
algorithm='rsa-sha256',
key=on_behalf_of.private_key.encode('utf-8'),
key_id=on_behalf_of.private_key_id,
)
for url in to:
recipient_actor = actors.get_actor(url)
logger.debug('delivering to %s', recipient_actor.inbox_url)
logger.debug('activity content: %s', json.dumps(activity))
response = requests.post(
auth=auth,
json=activity,
url=recipient_actor.inbox_url,
headers={
'Content-Type': 'application/activity+json'
}
)
response.raise_for_status()
logger.debug('Remote answered with %s', response.status_code)
import logging
import requests
import xml
from django.conf import settings
from django.urls import reverse
from django.utils import timezone
from rest_framework.exceptions import PermissionDenied
from dynamic_preferences.registries import global_preferences_registry
from . import activity
from . import factories
from . import models
from . import serializers
from . import utils
logger = logging.getLogger(__name__)
def remove_tags(text):
logger.debug('Removing tags from %s', text)
return ''.join(xml.etree.ElementTree.fromstring('<div>{}</div>'.format(text)).itertext())
def get_actor_data(actor_url):
response = requests.get(
actor_url,
headers={
'Accept': 'application/activity+json',
}
)
response.raise_for_status()
try:
return response.json()
except:
raise ValueError(
'Invalid actor payload: {}'.format(response.text))
def get_actor(actor_url):
data = get_actor_data(actor_url)
serializer = serializers.ActorSerializer(data=data)
serializer.is_valid(raise_exception=True)
return serializer.build()
class SystemActor(object):
additional_attributes = {}
def get_actor_instance(self):
a = models.Actor(
**self.get_instance_argument(
self.id,
name=self.name,
summary=self.summary,
**self.additional_attributes
)
)
a.pk = self.id
return a
def get_instance_argument(self, id, name, summary, **kwargs):
preferences = global_preferences_registry.manager()
p = {
'preferred_username': id,
'domain': settings.FEDERATION_HOSTNAME,
'type': 'Person',
'name': name.format(host=settings.FEDERATION_HOSTNAME),
'manually_approves_followers': True,
'url': utils.full_url(
reverse(
'federation:instance-actors-detail',
kwargs={'actor': id})),
'shared_inbox_url': utils.full_url(
reverse(
'federation:instance-actors-inbox',
kwargs={'actor': id})),
'inbox_url': utils.full_url(
reverse(
'federation:instance-actors-inbox',
kwargs={'actor': id})),
'outbox_url': utils.full_url(
reverse(
'federation:instance-actors-outbox',
kwargs={'actor': id})),
'public_key': preferences['federation__public_key'],
'private_key': preferences['federation__private_key'],
'summary': summary.format(host=settings.FEDERATION_HOSTNAME)
}
p.update(kwargs)
return p
def get_inbox(self, data, actor=None):
raise NotImplementedError
def post_inbox(self, data, actor=None):
raise NotImplementedError
def get_outbox(self, data, actor=None):
raise NotImplementedError
def post_outbox(self, data, actor=None):
raise NotImplementedError
class LibraryActor(SystemActor):
id = 'library'
name = '{host}\'s library'
summary = 'Bot account to federate with {host}\'s library'
additional_attributes = {
'manually_approves_followers': True
}
class TestActor(SystemActor):
id = 'test'
name = '{host}\'s test account'
summary = (
'Bot account to test federation with {host}. '
'Send me /ping and I\'ll answer you.'
)
additional_attributes = {
'manually_approves_followers': False
}
def get_outbox(self, data, actor=None):
return {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{}
],
"id": utils.full_url(
reverse(
'federation:instance-actors-outbox',
kwargs={'actor': self.id})),
"type": "OrderedCollection",
"totalItems": 0,
"orderedItems": []
}
def post_inbox(self, data, actor=None):
if actor is None:
raise PermissionDenied('Actor not authenticated')
serializer = serializers.ActivitySerializer(
data=data, context={'actor': actor})
serializer.is_valid(raise_exception=True)
ac = serializer.validated_data
logger.info('Received activity on %s inbox', self.id)
if ac['type'] == 'Create' and ac['object']['type'] == 'Note':
# we received a toot \o/
command = self.parse_command(ac['object']['content'])
logger.debug('Parsed command: %s', command)
if command == 'ping':
self.handle_ping(ac, actor)
def parse_command(self, message):
"""
Remove any links or fancy markup to extract /command from
a note message.
"""
raw = remove_tags(message)
try:
return raw.split('/')[1]
except IndexError:
return
def handle_ping(self, ac, sender):
now = timezone.now()
test_actor = self.get_actor_instance()
reply_url = 'https://{}/activities/note/{}'.format(
settings.FEDERATION_HOSTNAME, now.timestamp()
)
reply_content = '{} Pong!'.format(
sender.mention_username
)
reply_activity = {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{}
],
'type': 'Create',
'actor': test_actor.url,
'id': '{}/activity'.format(reply_url),
'published': now.isoformat(),
'to': ac['actor'],
'cc': [],
'object': factories.NoteFactory(
content='Pong!',
summary=None,
published=now.isoformat(),
id=reply_url,
inReplyTo=ac['object']['id'],
sensitive=False,
url=reply_url,
to=[ac['actor']],
attributedTo=test_actor.url,
cc=[],
attachment=[],
tag=[
{
"type": "Mention",
"href": ac['actor'],
"name": sender.mention_username
}
]
)
}
activity.deliver(
reply_activity,
to=[ac['actor']],
on_behalf_of=test_actor)
SYSTEM_ACTORS = {
'library': LibraryActor(),
'test': TestActor(),
}
import cryptography
from django.contrib.auth.models import AnonymousUser
from rest_framework import authentication
from rest_framework import exceptions
from . import actors
from . import keys
from . import serializers
from . import signing
from . import utils
class SignatureAuthentication(authentication.BaseAuthentication):
def authenticate_actor(self, request):
headers = utils.clean_wsgi_headers(request.META)
try:
signature = headers['Signature']
key_id = keys.get_key_id_from_signature_header(signature)
except KeyError:
return
except ValueError as e:
raise exceptions.AuthenticationFailed(str(e))
try:
actor_data = actors.get_actor_data(key_id)
except Exception as e:
raise exceptions.AuthenticationFailed(str(e))
try:
public_key = actor_data['publicKey']['publicKeyPem']
except KeyError:
raise exceptions.AuthenticationFailed('No public key found')
serializer = serializers.ActorSerializer(data=actor_data)
if not serializer.is_valid():
raise exceptions.AuthenticationFailed('Invalid actor payload: {}'.format(serializer.errors))
try:
signing.verify_django(request, public_key.encode('utf-8'))
except cryptography.exceptions.InvalidSignature:
raise exceptions.AuthenticationFailed('Invalid signature')
return serializer.build()
def authenticate(self, request):
setattr(request, 'actor', None)
actor = self.authenticate_actor(request)
user = AnonymousUser()
setattr(request, 'actor', actor)
return (user, None)
......@@ -2,3 +2,7 @@
class MalformedPayload(ValueError):
pass
class MissingSignature(KeyError):
pass
......@@ -2,9 +2,12 @@ import factory
import requests
import requests_http_signature
from django.utils import timezone
from funkwhale_api.factories import registry
from . import keys
from . import models
registry.register(keys.get_key_pair, name='federation.KeyPair')
......@@ -15,7 +18,13 @@ class SignatureAuthFactory(factory.Factory):
algorithm = 'rsa-sha256'
key = factory.LazyFunction(lambda: keys.get_key_pair()[0])
key_id = factory.Faker('url')
use_auth_header = False
headers = [
'(request-target)',
'user-agent',
'host',
'date',
'content-type',]
class Meta:
model = requests_http_signature.HTTPSignatureAuth
......@@ -28,3 +37,55 @@ class SignedRequestFactory(factory.Factory):
class Meta:
model = requests.Request
@factory.post_generation
def headers(self, create, extracted, **kwargs):
default_headers = {
'User-Agent': 'Test',
'Host': 'test.host',
'Date': 'Right now',
'Content-Type': 'application/activity+json'
}
if extracted:
default_headers.update(extracted)
self.headers.update(default_headers)
@registry.register
class ActorFactory(factory.DjangoModelFactory):
public_key = None
private_key = None
preferred_username = factory.Faker('user_name')
summary = factory.Faker('paragraph')
domain = factory.Faker('domain_name')
url = factory.LazyAttribute(lambda o: 'https://{}/users/{}'.format(o.domain, o.preferred_username))
inbox_url = factory.LazyAttribute(lambda o: 'https://{}/users/{}/inbox'.format(o.domain, o.preferred_username))
outbox_url = factory.LazyAttribute(lambda o: 'https://{}/users/{}/outbox'.format(o.domain, o.preferred_username))
class Meta:
model = models.Actor
@classmethod
def _generate(cls, create, attrs):
has_public = attrs.get('public_key') is not None
has_private = attrs.get('private_key') is not None
if not has_public and not has_private:
private, public = keys.get_key_pair()
attrs['private_key'] = private.decode('utf-8')
attrs['public_key'] = public.decode('utf-8')
return super()._generate(create, attrs)
@registry.register(name='federation.Note')
class NoteFactory(factory.Factory):
type = 'Note'
id = factory.Faker('url')
published = factory.LazyFunction(
lambda: timezone.now().isoformat()
)
inReplyTo = None
content = factory.Faker('sentence')
class Meta:
model = dict
......@@ -2,10 +2,14 @@ from cryptography.hazmat.primitives import serialization as crypto_serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend as crypto_default_backend
import re
import requests
import urllib.parse
from . import exceptions
KEY_ID_REGEX = re.compile(r'keyId=\"(?P<id>.*)\"')
def get_key_pair(size=2048):
key = rsa.generate_private_key(
......@@ -25,19 +29,21 @@ def get_key_pair(size=2048):
return private_key, public_key
def get_public_key(actor_url):
"""
Given an actor_url, request it and extract publicKey data from
the response payload.
"""
response = requests.get(actor_url)
response.raise_for_status()
payload = response.json()
def get_key_id_from_signature_header(header_string):
parts = header_string.split(',')
try:
return {
'public_key_pem': payload['publicKey']['publicKeyPem'],
'id': payload['publicKey']['id'],
'owner': payload['publicKey']['owner'],
}
except KeyError:
raise exceptions.MalformedPayload(str(payload))
raw_key_id = [p for p in parts if p.startswith('keyId="')][0]
except IndexError:
raise ValueError('Missing key id')
match = KEY_ID_REGEX.match(raw_key_id)
if not match:
raise ValueError('Invalid key id')
key_id = match.groups()[0]
url = urllib.parse.urlparse(key_id)
if not url.scheme or not url.netloc:
raise ValueError('Invalid url')
if url.scheme not in ['http', 'https']:
raise ValueError('Invalid shceme')
return key_id
# Generated by Django 2.0.3 on 2018-03-31 13:43
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Actor',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('url', models.URLField(db_index=True, max_length=500, unique=True)),
('outbox_url', models.URLField(max_length=500)),
('inbox_url', models.URLField(max_length=500)),
('following_url', models.URLField(blank=True, max_length=500, null=True)),
('followers_url', models.URLField(blank=True, max_length=500, null=True)),
('shared_inbox_url', models.URLField(blank=True, max_length=500, null=True)),
('type', models.CharField(choices=[('Person', 'Person'), ('Application', 'Application'), ('Group', 'Group'), ('Organization', 'Organization'), ('Service', 'Service')], default='Person', max_length=25)),
('name', models.CharField(blank=True, max_length=200, null=True)),
('domain', models.CharField(max_length=1000)),
('summary', models.CharField(blank=True, max_length=500, null=True)),
('preferred_username', models.CharField(blank=True, max_length=200, null=True)),
('public_key', models.CharField(blank=True, max_length=5000, null=True)),
('private_key', models.CharField(blank=True, max_length=5000, null=True)),
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
('last_fetch_date', models.DateTimeField(default=django.utils.timezone.now)),
('manually_approves_followers', models.NullBooleanField(default=None)),
],
),
]
from django.conf import settings
from django.db import models
from django.utils import timezone
TYPE_CHOICES = [
('Person', 'Person'),
('Application', 'Application'),
('Group', 'Group'),
('Organization', 'Organization'),
('Service', 'Service'),
]
class Actor(models.Model):
url = models.URLField(unique=True, max_length=500, db_index=True)
outbox_url = models.URLField(max_length=500)
inbox_url = models.URLField(max_length=500)
following_url = models.URLField(max_length=500, null=True, blank=True)
followers_url = models.URLField(max_length=500, null=True, blank=True)
shared_inbox_url = models.URLField(max_length=500, null=True, blank=True)
type = models.CharField(
choices=TYPE_CHOICES, default='Person', max_length=25)
name = models.CharField(max_length=200, null=True, blank=True)
domain = models.CharField(max_length=1000)
summary = models.CharField(max_length=500, null=True, blank=True)
preferred_username = models.CharField(
max_length=200, null=True, blank=True)
public_key = models.CharField(max_length=5000, null=True, blank=True)
private_key = models.CharField(max_length=5000, null=True, blank=True)
creation_date = models.DateTimeField(default=timezone.now)
last_fetch_date = models.DateTimeField(
default=timezone.now)
manually_approves_followers = models.NullBooleanField(default=None)
@property
def webfinger_subject(self):
return '{}@{}'.format(
self.preferred_username,
settings.FEDERATION_HOSTNAME,
)
@property
def private_key_id(self):
return '{}#main-key'.format(self.url)
@property
def mention_username(self):
return '@{}@{}'.format(self.preferred_username, self.domain)
def save(self, **kwargs):
lowercase_fields = [
'domain',
]
for field in lowercase_fields:
v = getattr(self, field, None)
if v:
setattr(self, field, v.lower())
super().save(**kwargs)
from rest_framework import parsers
class ActivityParser(parsers.JSONParser):
media_type = 'application/activity+json'
import urllib.parse
from django.urls import reverse
from django.conf import settings
from rest_framework import serializers
from dynamic_preferences.registries import global_preferences_registry
from . import activity
from . import models
from . import utils
def repr_instance_actor():
"""
We do not use a serializer here, since it's pretty static
"""
actor_url = utils.full_url(reverse('federation:instance-actor'))
preferences = global_preferences_registry.manager()
public_key = preferences['federation__public_key']
class ActorSerializer(serializers.ModelSerializer):
# left maps to activitypub fields, right to our internal models
id = serializers.URLField(source='url')
outbox = serializers.URLField(source='outbox_url')
inbox = serializers.URLField(source='inbox_url')
following = serializers.URLField(source='following_url', required=False)
followers = serializers.URLField(source='followers_url', required=False)
preferredUsername = serializers.CharField(
source='preferred_username', required=False)
publicKey = serializers.JSONField(source='public_key', required=False)
manuallyApprovesFollowers = serializers.NullBooleanField(
source='manually_approves_followers', required=False)
summary = serializers.CharField(max_length=None, required=False)
class Meta:
model = models.Actor
fields = [
'id',
'type',
'name',
'summary',
'preferredUsername',
'publicKey',
'inbox',
'outbox',
'following',
'followers',
'manuallyApprovesFollowers',
]
return {
'@context': [
def to_representation(self, instance):
ret = super().to_representation(instance)
ret['@context'] = [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
{},
],
'id': utils.full_url(reverse('federation:instance-actor')),
'type': 'Person',
'inbox': utils.full_url(reverse('federation:instance-inbox')),
'outbox': utils.full_url(reverse('federation:instance-outbox')),
'preferredUsername': 'service',
'name': 'Service Bot - {}'.format(settings.FEDERATION_HOSTNAME),
'summary': 'Bot account for federating with {}'.format(
settings.FEDERATION_HOSTNAME
),
'publicKey': {
'id': '{}#main-key'.format(actor_url),
'owner': actor_url,
'publicKeyPem': public_key
},
}
]
if instance.public_key:
ret['publicKey'] = {
'owner': instance.url,
'publicKeyPem': instance.public_key,
'id': '{}#main-key'.format(instance.url)
}
ret['endpoints'] = {}
if instance.shared_inbox_url:
ret['endpoints']['sharedInbox'] = instance.shared_inbox_url
return ret
def prepare_missing_fields(self):
kwargs = {}
domain = urllib.parse.urlparse(self.validated_data['url']).netloc
kwargs['domain'] = domain
for endpoint, url in self.initial_data.get('endpoints', {}).items():
if endpoint == 'sharedInbox':
kwargs['shared_inbox_url'] = url
break
try:
kwargs['public_key'] = self.initial_data['publicKey']['publicKeyPem']
except KeyError:
pass
return kwargs
def build(self):
d = self.validated_data.copy()
d.update(self.prepare_missing_fields())
return self.Meta.model(**d)
def save(self, **kwargs):
kwargs.update(self.prepare_missing_fields())
return super().save(**kwargs)
def validate_summary(self, value):
if value:
return value[:500]
class ActorWebfingerSerializer(serializers.ModelSerializer):
class Meta:
model = models.Actor
fields = ['url']
def to_representation(self, instance):
data = {}
data['subject'] = 'acct:{}'.format(instance.webfinger_subject)
data['links'] = [
{
'rel': 'self',
'href': instance.url,
'type': 'application/activity+json'
}
]
data['aliases'] = [
instance.url
]
return data
class ActivitySerializer(serializers.Serializer):
actor = serializers.URLField()
id = serializers.URLField()
type = serializers.ChoiceField(
choices=[(c, c) for c in activity.ACTIVITY_TYPES])
object = serializers.JSONField()
def validate_object(self, value):
try:
type = value['type']
except KeyError:
raise serializers.ValidationError('Missing object type')
try:
object_serializer = OBJECT_SERIALIZERS[type]
except KeyError:
raise serializers.ValidationError(
'Unsupported type {}'.format(type))
serializer = object_serializer(data=value)
serializer.is_valid(raise_exception=True)
return serializer.data
def validate_actor(self, value):
request_actor = self.context.get('actor')
if request_actor and request_actor.url != value:
raise serializers.ValidationError(
'The actor making the request do not match'
' the activity actor'
)
return value
class ObjectSerializer(serializers.Serializer):
id = serializers.URLField()
url = serializers.URLField(required=False, allow_null=True)
type = serializers.ChoiceField(
choices=[(c, c) for c in activity.OBJECT_TYPES])
content = serializers.CharField(
required=False, allow_null=True)
summary = serializers.CharField(
required=False, allow_null=True)
name = serializers.CharField(
required=False, allow_null=True)
published = serializers.DateTimeField(
required=False, allow_null=True)
updated = serializers.DateTimeField(
required=False, allow_null=True)
to = serializers.ListField(
child=serializers.URLField(),
required=False, allow_null=True)
cc = serializers.ListField(
child=serializers.URLField(),
required=False, allow_null=True)
bto = serializers.ListField(
child=serializers.URLField(),
required=False, allow_null=True)
bcc = serializers.ListField(
child=serializers.URLField(),
required=False, allow_null=True)
OBJECT_SERIALIZERS = {
t: ObjectSerializer
for t in activity.OBJECT_TYPES
}
import logging
import requests
import requests_http_signature
from . import exceptions
from . import utils
logger = logging.getLogger(__name__)
def verify(request, public_key):
return requests_http_signature.HTTPSignatureAuth.verify(
request,
key_resolver=lambda **kwargs: public_key
key_resolver=lambda **kwargs: public_key,
use_auth_header=False,
)
......@@ -14,21 +21,35 @@ def verify_django(django_request, public_key):
Given a django WSGI request, create an underlying requests.PreparedRequest
instance we can verify
"""
headers = django_request.META.get('headers', {}).copy()
headers = utils.clean_wsgi_headers(django_request.META)
for h, v in list(headers.items()):
# we include lower-cased version of the headers for compatibility
# with requests_http_signature
headers[h.lower()] = v
try:
signature = headers['authorization']
signature = headers['Signature']
except KeyError:
raise exceptions.MissingSignature
url = 'http://noop{}'.format(django_request.path)
query = django_request.META['QUERY_STRING']
if query:
url += '?{}'.format(query)
signature_headers = signature.split('headers="')[1].split('",')[0]
expected = signature_headers.split(' ')
logger.debug('Signature expected headers: %s', expected)
for header in expected:
try:
headers[header]
except KeyError:
logger.debug('Missing header: %s', header)
request = requests.Request(
method=django_request.method,
url='http://noop',
url=url,
data=django_request.body,
headers=headers)
for h in request.headers.keys():
v = request.headers[h]
if v:
request.headers[h] = str(v)
prepared_request = request.prepare()
return verify(request, public_key)
......@@ -4,9 +4,9 @@ from . import views
router = routers.SimpleRouter(trailing_slash=False)
router.register(
r'federation/instance',
views.InstanceViewSet,
'instance')
r'federation/instance/actors',
views.InstanceActorViewSet,
'instance-actors')
router.register(
r'.well-known',
views.WellKnownViewSet,
......
......@@ -12,3 +12,24 @@ def full_url(path):
return root + '/' + path
else:
return root + path
def clean_wsgi_headers(raw_headers):
"""
Convert WSGI headers from CONTENT_TYPE to Content-Type notation
"""
cleaned = {}
non_prefixed = [
'content_type',
'content_length',
]
for raw_header, value in raw_headers.items():
h = raw_header.lower()
if not h.startswith('http_') and h not in non_prefixed:
continue
words = h.replace('http_', '', 1).split('_')
cleaned_header = '-'.join([w.capitalize() for w in words])
cleaned[cleaned_header] = value
return cleaned
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment