Refactor the Micropub error responses into a non-view module, have them produce an immediately raise-able exception

This commit is contained in:
Danielle McLean 2018-07-03 10:03:35 +10:00
parent 065619772e
commit d68dda85ad
Signed by: 00dani
GPG Key ID: 8EB789DDF3ABD240
9 changed files with 69 additions and 73 deletions

View File

@ -3,8 +3,7 @@ from urllib.parse import urlparse
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.http import HttpResponse from django.http import HttpResponse
from django.urls import resolve, Resolver404 from django.urls import resolve, Resolver404
from micropub.views import error from micropub import error
from lemoncurry.middleware import ResponseException
from .models import Entry from .models import Entry
@ -12,24 +11,24 @@ from .models import Entry
def from_url(url: str) -> Entry: def from_url(url: str) -> Entry:
domain = Site.objects.get_current().domain domain = Site.objects.get_current().domain
if not url: if not url:
raise ResponseException(error.bad_req('url parameter required')) raise error.bad_req('url parameter required')
if '//' not in url: if '//' not in url:
url = '//' + url url = '//' + url
parts = urlparse(url, scheme='https') parts = urlparse(url, scheme='https')
if parts.scheme not in ('http', 'https') or parts.netloc != domain: if parts.scheme not in ('http', 'https') or parts.netloc != domain:
raise ResponseException(error.bad_req('url does not point to this site')) raise error.bad_req('url does not point to this site')
try: try:
match = resolve(parts.path) match = resolve(parts.path)
except Resolver404: except Resolver404:
raise ResponseException(error.bad_req('url does not point to a valid page on this site')) raise error.bad_req('url does not point to a valid page on this site')
if match.view_name != 'entries:entry': if match.view_name != 'entries:entry':
raise ResponseException(error.bad_req('url does not point to an entry on this site')) raise error.bad_req('url does not point to an entry on this site')
try: try:
entry = Entry.objects.get(pk=match.kwargs['id']) entry = Entry.objects.get(pk=match.kwargs['id'])
except Entry.DoesNotExist: except Entry.DoesNotExist:
raise ResponseException(error.bad_req('url does not point to an existing entry')) raise error.bad_req('url does not point to an existing entry')
return entry return entry

View File

@ -1,31 +1,26 @@
from lemoncurry.middleware import ResponseException from micropub import error
from micropub.views import error
from .models import IndieAuthCode, Token from .models import IndieAuthCode, Token
def auth(request): def auth(request) -> Token:
if 'HTTP_AUTHORIZATION' in request.META: if 'HTTP_AUTHORIZATION' in request.META:
auth = request.META.get('HTTP_AUTHORIZATION').split(' ') auth = request.META.get('HTTP_AUTHORIZATION').split(' ')
if auth[0] != 'Bearer': if auth[0] != 'Bearer':
raise ResponseException(error.bad_req( raise error.bad_req('auth type {0} not supported'.format(auth[0]))
'auth type {0} not supported'.format(auth[0])
))
if len(auth) != 2: if len(auth) != 2:
raise ResponseException(error.bad_req( raise error.bad_req('invalid Bearer auth format, must be Bearer <token>')
'invalid Bearer auth format, must be Bearer <token>'
))
token = auth[1] token = auth[1]
elif 'access_token' in request.POST: elif 'access_token' in request.POST:
token = request.POST.get('access_token') token = request.POST.get('access_token')
elif 'access_token' in request.GET: elif 'access_token' in request.GET:
token = request.GET.get('access_token') token = request.GET.get('access_token')
else: else:
raise ResponseException(error.unauthorized()) raise error.unauthorized()
try: try:
token = Token.objects.get(pk=token) token = Token.objects.get(pk=token)
except Token.DoesNotExist: except Token.DoesNotExist:
raise ResponseException(error.forbidden()) raise error.forbidden()
return token return token

36
micropub/error.py Normal file
View File

@ -0,0 +1,36 @@
from django.http import JsonResponse
from lemoncurry.middleware import ResponseException
from typing import Optional
def forbidden() -> ResponseException:
return res('forbidden', 403)
def unauthorized() -> ResponseException:
return res('unauthorized', 401)
def bad_req(msg: str) -> ResponseException:
return res('invalid_request', msg=msg)
def bad_type(type: str) -> ResponseException:
msg = 'unsupported request type {0}'.format(type)
return res('invalid_request', 415, msg)
def bad_scope(scope: str) -> ResponseException:
return res('insufficient_scope', 401, scope=scope)
def res(error: str,
status: Optional[int]=400,
msg: Optional[str]=None,
scope: Optional[str]=None):
content = {'error': error}
if msg is not None:
content['error_description'] = msg
if scope:
content['scope'] = scope
return ResponseException(JsonResponse(content, status=status))

View File

@ -26,6 +26,6 @@ def micropub(request):
if request.content_type == 'application/json': if request.content_type == 'application/json':
request.json = json.load(request) request.json = json.load(request)
action = request.json.get('action', 'create') action = request.json.get('action', 'create')
if action in actions: if action not in actions:
return actions[action](request) raise error.bad_req('unknown action: {}'.format(action))
return error.bad_req('unknown action: {}'.format(action)) return actions[action](request)

View File

@ -7,7 +7,7 @@ from entries.models import Cat, Entry
from entries.kinds import Article, Note, Reply, Like, Repost from entries.kinds import Article, Note, Reply, Like, Repost
from lemoncurry import utils from lemoncurry import utils
from . import error from .. import error
def form_to_mf2(request): def form_to_mf2(request):
@ -33,14 +33,14 @@ def create(request):
'application/x-www-form-urlencoded': form_to_mf2, 'application/x-www-form-urlencoded': form_to_mf2,
} }
if 'create' not in request.token: if 'create' not in request.token:
return error.bad_scope('create') raise error.bad_scope('create')
if request.content_type not in normalise: if request.content_type not in normalise:
return error.unsupported_type(request.content_type) raise error.unsupported_type(request.content_type)
body = normalise[request.content_type](request) body = normalise[request.content_type](request)
if 'type' not in body: if 'type' not in body:
return error.bad_req('mf2 object type required') raise error.bad_req('mf2 object type required')
if body['type'] != ['h-entry']: if body['type'] != ['h-entry']:
return error.bad_req('only h-entry supported') raise error.bad_req('only h-entry supported')
entry = Entry(author=request.token.user) entry = Entry(author=request.token.user)
props = body.get('properties', {}) props = body.get('properties', {})

View File

@ -4,7 +4,7 @@ from ronkyuu import webmention
from entries.from_url import from_url from entries.from_url import from_url
from entries.jobs import ping_hub, send_mentions from entries.jobs import ping_hub, send_mentions
from . import error from .. import error
def delete(request): def delete(request):
normalise = { normalise = {
@ -12,14 +12,14 @@ def delete(request):
'application/x-www-form-urlencoded': lambda r: r.POST.get('url'), 'application/x-www-form-urlencoded': lambda r: r.POST.get('url'),
} }
if 'delete' not in request.token: if 'delete' not in request.token:
return error.bad_scope('delete') raise error.bad_scope('delete')
if request.content_type not in normalise: if request.content_type not in normalise:
return error.unsupported_type(request.content_type) raise error.unsupported_type(request.content_type)
url = normalise[request.content_type](request) url = normalise[request.content_type](request)
entry = from_url(url) entry = from_url(url)
if entry.author != request.token.user: if entry.author != request.token.user:
return error.forbid('entry belongs to another user') raise error.forbid('entry belongs to another user')
perma = entry.absolute_url perma = entry.absolute_url
pings = entry.affected_urls pings = entry.affected_urls

View File

@ -1,31 +0,0 @@
from django.http import JsonResponse
def forbidden():
return res('forbidden', 403)
def unauthorized():
return res('unauthorized', 401)
def bad_req(msg):
return res('invalid_request', msg=msg)
def bad_type(type):
msg = 'unsupported request type {0}'.format(type)
return res('invalid_request', 415, msg)
def bad_scope(scope):
return res('insufficient_scope', 401, scope=scope)
def res(error, status=400, msg=None, scope=None):
content = {'error': error}
if msg:
content['error_description'] = msg
if scope:
content['scope'] = scope
return JsonResponse(content, status=status)

View File

@ -8,7 +8,7 @@ import magic
from lemonauth import tokens from lemonauth import tokens
from lemoncurry.utils import absolute_url from lemoncurry.utils import absolute_url
from . import error from .. import error
ACCEPTED_MEDIA_TYPES = ( ACCEPTED_MEDIA_TYPES = (
'image/gif', 'image/gif',
@ -22,12 +22,12 @@ ACCEPTED_MEDIA_TYPES = (
def media(request): def media(request):
token = tokens.auth(request) token = tokens.auth(request)
if 'file' not in request.FILES: if 'file' not in request.FILES:
return error.bad_req( raise error.bad_req(
"a file named 'file' must be provided to the media endpoint" "a file named 'file' must be provided to the media endpoint"
) )
file = request.FILES['file'] file = request.FILES['file']
if file.content_type not in ACCEPTED_MEDIA_TYPES: if file.content_type not in ACCEPTED_MEDIA_TYPES:
return error.bad_req( raise error.bad_req(
'unacceptable file type {0}'.format(file.content_type) 'unacceptable file type {0}'.format(file.content_type)
) )
@ -39,7 +39,7 @@ def media(request):
sha.update(chunk) sha.update(chunk)
if mime != file.content_type: if mime != file.content_type:
return error.bad_req( raise error.bad_req(
'detected file type {0} did not match specified file type {1}' 'detected file type {0} did not match specified file type {1}'
.format(mime, file.content_type) .format(mime, file.content_type)
) )

View File

@ -1,9 +1,8 @@
from django.http import JsonResponse from django.http import JsonResponse
from django.urls import reverse from django.urls import reverse
from lemoncurry import requests from lemoncurry import requests
from lemoncurry.middleware import ResponseException
from lemoncurry.utils import absolute_url from lemoncurry.utils import absolute_url
from . import error from .. import error
def config(request): def config(request):
@ -15,12 +14,10 @@ def config(request):
def source(request): def source(request):
get = request.GET get = request.GET
if 'url' not in get: if 'url' not in get:
raise ResponseException(error.bad_req( raise error.bad_req('must specify url parameter for source query')
'must specify url parameter for source query'
))
mf2 = requests.mf2(get['url']).to_dict(filter_by_type='h-entry') mf2 = requests.mf2(get['url']).to_dict(filter_by_type='h-entry')
if not mf2: if not mf2:
raise ResponseException(error.bad_req('no h-entry at the requested url')) raise error.bad_req('no h-entry at the requested url')
entry = mf2[0] entry = mf2[0]
keys = get.getlist('properties', []) + get.getlist('properties[]', []) keys = get.getlist('properties', []) + get.getlist('properties[]', [])
if not keys: if not keys:
@ -43,9 +40,9 @@ queries = {
def query(request): def query(request):
if 'q' not in request.GET: if 'q' not in request.GET:
return error.bad_req('must specify q parameter') raise error.bad_req('must specify q parameter')
q = request.GET['q'] q = request.GET['q']
if q not in queries: if q not in queries:
return error.bad_req('unsupported query {0}'.format(q)) raise error.bad_req('unsupported query {0}'.format(q))
res = queries[q](request) res = queries[q](request)
return JsonResponse(res) return JsonResponse(res)