move common.error() to webutil.flask_util

corresponds to snarfed/webutil@10c088cebd
pull/79/head
Ryan Barrett 2021-08-06 10:29:25 -07:00
rodzic a27ebc4697
commit 32d9e2bf6c
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
11 zmienionych plików z 49 dodań i 51 usunięć

Wyświetl plik

@ -11,13 +11,14 @@ from google.cloud import ndb
from granary import as2, microformats2 from granary import as2, microformats2
import mf2util import mf2util
from oauth_dropins.webutil import util from oauth_dropins.webutil import util
from oauth_dropins.webutil.flask_util import error
from oauth_dropins.webutil.handlers import cache_response from oauth_dropins.webutil.handlers import cache_response
from oauth_dropins.webutil.util import json_dumps, json_loads from oauth_dropins.webutil.util import json_dumps, json_loads
import webapp2 import webapp2
from app import app, cache from app import app, cache
import common import common
from common import error, redirect_unwrap, redirect_wrap from common import redirect_unwrap, redirect_wrap
from models import Follower, MagicKey from models import Follower, MagicKey
from httpsig.requests_auth import HTTPSignatureAuth from httpsig.requests_auth import HTTPSignatureAuth
@ -85,7 +86,7 @@ def actor(domain):
"""Serves /[DOMAIN], fetches its mf2, converts to AS Actor, and serves it.""" """Serves /[DOMAIN], fetches its mf2, converts to AS Actor, and serves it."""
tld = domain.split('.')[-1] tld = domain.split('.')[-1]
if tld in common.TLD_BLOCKLIST: if tld in common.TLD_BLOCKLIST:
return error('', status=404) error('', status=404)
mf2 = util.fetch_mf2('http://%s/' % domain, gateway=True, mf2 = util.fetch_mf2('http://%s/' % domain, gateway=True,
headers=common.HEADERS) headers=common.HEADERS)
@ -94,7 +95,7 @@ def actor(domain):
hcard = mf2util.representative_hcard(mf2, mf2['url']) hcard = mf2util.representative_hcard(mf2, mf2['url'])
logging.info('Representative h-card: %s', json_dumps(hcard, indent=2)) logging.info('Representative h-card: %s', json_dumps(hcard, indent=2))
if not hcard: if not hcard:
return error("""\ error("""\
Coul find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on %s""" % mf2['url']) Coul find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on %s""" % mf2['url'])
key = MagicKey.get_or_create(domain) key = MagicKey.get_or_create(domain)
@ -125,7 +126,7 @@ def inbox(domain):
activity = request.json activity = request.json
assert activity assert activity
except (TypeError, ValueError, AssertionError): except (TypeError, ValueError, AssertionError):
return error("Couldn't parse body as JSON", exc_info=True) error("Couldn't parse body as JSON", exc_info=True)
obj = activity.get('object') or {} obj = activity.get('object') or {}
if isinstance(obj, str): if isinstance(obj, str):
@ -137,7 +138,7 @@ def inbox(domain):
if type == 'Create': if type == 'Create':
type = obj.get('type') type = obj.get('type')
elif type not in SUPPORTED_TYPES: elif type not in SUPPORTED_TYPES:
return error('Sorry, %s activities are not supported yet.' % type, error('Sorry, %s activities are not supported yet.' % type,
status=501) status=501)
# TODO: verify signature if there is one # TODO: verify signature if there is one
@ -196,12 +197,12 @@ def accept_follow(follow, follow_unwrapped):
followee_unwrapped = follow_unwrapped.get('object') followee_unwrapped = follow_unwrapped.get('object')
follower = follow.get('actor') follower = follow.get('actor')
if not followee or not followee_unwrapped or not follower: if not followee or not followee_unwrapped or not follower:
return error('Follow activity requires object and actor. Got: %s' % follow) error('Follow activity requires object and actor. Got: %s' % follow)
inbox = follower.get('inbox') inbox = follower.get('inbox')
follower_id = follower.get('id') follower_id = follower.get('id')
if not inbox or not follower_id: if not inbox or not follower_id:
return error('Follow actor requires id and inbox. Got: %s', follower) error('Follow actor requires id and inbox. Got: %s', follower)
# store Follower # store Follower
user_domain = util.domain_from_link(followee_unwrapped) user_domain = util.domain_from_link(followee_unwrapped)
@ -242,7 +243,7 @@ def undo_follow(undo_unwrapped):
follower = follow.get('actor') follower = follow.get('actor')
followee = follow.get('object') followee = follow.get('object')
if not follower or not followee: if not follower or not followee:
return error('Undo of Follow requires object with actor and object. Got: %s' % follow) error('Undo of Follow requires object with actor and object. Got: %s' % follow)
# deactivate Follower # deactivate Follower
user_domain = util.domain_from_link(followee) user_domain = util.domain_from_link(followee)

Wyświetl plik

@ -6,11 +6,11 @@ import urllib.parse
import flask import flask
from flask import request from flask import request
from oauth_dropins.webutil import flask_util from oauth_dropins.webutil import flask_util
from oauth_dropins.webutil.flask_util import error
import requests import requests
from app import app, cache from app import app, cache
import common import common
from common import error
LINK_HEADER = '<%s>; rel="webmention"' LINK_HEADER = '<%s>; rel="webmention"'
CACHE_TIME = datetime.timedelta(seconds=15) CACHE_TIME = datetime.timedelta(seconds=15)
@ -23,14 +23,14 @@ def add_wm(url=None):
"""Proxies HTTP requests and adds Link header to our webmention endpoint.""" """Proxies HTTP requests and adds Link header to our webmention endpoint."""
url = urllib.parse.unquote(url) url = urllib.parse.unquote(url)
if not url.startswith('http://') and not url.startswith('https://'): if not url.startswith('http://') and not url.startswith('https://'):
return error('URL must start with http:// or https://') error('URL must start with http:// or https://')
try: try:
got = common.requests_get(url) got = common.requests_get(url)
except requests.exceptions.Timeout as e: except requests.exceptions.Timeout as e:
return error(str(e), status=504, exc_info=True) error(str(e), status=504, exc_info=True)
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
return error(str(e), status=502, exc_info=True) error(str(e), status=502, exc_info=True)
resp = flask.make_response(got.content, got.status_code, dict(got.headers)) resp = flask.make_response(got.content, got.status_code, dict(got.headers))
resp.headers.add('Link', LINK_HEADER % (request.args.get('endpoint') or resp.headers.add('Link', LINK_HEADER % (request.args.get('endpoint') or

Wyświetl plik

@ -23,8 +23,6 @@ HEADERS = {
'User-Agent': 'Bridgy Fed (https://fed.brid.gy/)', 'User-Agent': 'Bridgy Fed (https://fed.brid.gy/)',
} }
XML_UTF8 = "<?xml version='1.0' encoding='UTF-8'?>\n" XML_UTF8 = "<?xml version='1.0' encoding='UTF-8'?>\n"
# USERNAME = 'me'
# USERNAME_EMOJI = '🌎' # globe
LINK_HEADER_RE = re.compile(r""" *< *([^ >]+) *> *; *rel=['"]([^'"]+)['"] *""") LINK_HEADER_RE = re.compile(r""" *< *([^ >]+) *> *; *rel=['"]([^'"]+)['"] *""")
AS2_PUBLIC_AUDIENCE = 'https://www.w3.org/ns/activitystreams#Public' AS2_PUBLIC_AUDIENCE = 'https://www.w3.org/ns/activitystreams#Public'
@ -147,13 +145,6 @@ def content_type(resp):
return type.split(';')[0] return type.split(';')[0]
def error(msg, status=None, exc_info=False):
if not status:
status = 400
logging.info('Returning %s: %s' % (status, msg), exc_info=exc_info)
return (msg, status)
def send_webmentions(activity_wrapped, proxy=None, **response_props): def send_webmentions(activity_wrapped, proxy=None, **response_props):
"""Sends webmentions for an incoming Salmon slap or ActivityPub inbox delivery. """Sends webmentions for an incoming Salmon slap or ActivityPub inbox delivery.
Args: Args:
@ -164,7 +155,7 @@ def send_webmentions(activity_wrapped, proxy=None, **response_props):
verb = activity.get('verb') verb = activity.get('verb')
if verb and verb not in SUPPORTED_VERBS: if verb and verb not in SUPPORTED_VERBS:
return error('%s activities are not supported yet.' % verb) error('%s activities are not supported yet.' % verb)
# extract source and targets # extract source and targets
source = activity.get('url') or activity.get('id') source = activity.get('url') or activity.get('id')
@ -192,9 +183,9 @@ def send_webmentions(activity_wrapped, proxy=None, **response_props):
targets = util.dedupe_urls(util.get_url(t) for t in targets) targets = util.dedupe_urls(util.get_url(t) for t in targets)
if not source: if not source:
return error("Couldn't find original post URL") error("Couldn't find original post URL")
if not targets: if not targets:
return error("Couldn't find any target URLs in inReplyTo, object, or mention tags") error("Couldn't find any target URLs in inReplyTo, object, or mention tags")
# send webmentions and store Responses # send webmentions and store Responses
errors = [] errors = []
@ -226,7 +217,7 @@ def send_webmentions(activity_wrapped, proxy=None, **response_props):
if errors: if errors:
msg = 'Errors:\n' + '\n'.join(str(e) for e in errors) msg = 'Errors:\n' + '\n'.join(str(e) for e in errors)
return error(msg, status=getattr(errors[0], 'http_status', None)) error(msg, status=getattr(errors[0], 'http_status', None))
def postprocess_as2(activity, target=None, key=None): def postprocess_as2(activity, target=None, key=None):

Wyświetl plik

@ -15,9 +15,9 @@ import humanize
from oauth_dropins.webutil import flask_util, util from oauth_dropins.webutil import flask_util, util
from oauth_dropins.webutil.appengine_info import APP_ID from oauth_dropins.webutil.appengine_info import APP_ID
import webapp2 import webapp2
from oauth_dropins.webutil.flask_util import error
from app import app, cache from app import app, cache
from common import error
from models import Response from models import Response
LEVELS = { LEVELS = {

Wyświetl plik

@ -17,12 +17,12 @@ from flask import redirect, request
from granary import as2, microformats2 from granary import as2, microformats2
import mf2util import mf2util
from oauth_dropins.webutil import flask_util, util from oauth_dropins.webutil import flask_util, util
from oauth_dropins.webutil.flask_util import error
from oauth_dropins.webutil.util import json_dumps from oauth_dropins.webutil.util import json_dumps
from werkzeug.exceptions import abort from werkzeug.exceptions import abort
from app import app, cache from app import app, cache
import common import common
from common import error
from models import MagicKey from models import MagicKey
CACHE_TIME = datetime.timedelta(seconds=15) CACHE_TIME = datetime.timedelta(seconds=15)
@ -43,7 +43,7 @@ def redir(to):
to = re.sub(r'^(https?:/)([^/])', r'\1/\2', to) to = re.sub(r'^(https?:/)([^/])', r'\1/\2', to)
if not to.startswith('http://') and not to.startswith('https://'): if not to.startswith('http://') and not to.startswith('https://'):
return error(f'Expected fully qualified URL; got {to}') error(f'Expected fully qualified URL; got {to}')
# check that we've seen this domain before so we're not an open redirect # check that we've seen this domain before so we're not an open redirect
domains = set((util.domain_from_link(to), domains = set((util.domain_from_link(to),

Wyświetl plik

@ -5,11 +5,11 @@ import datetime
from flask import request from flask import request
from granary import as2, atom, microformats2 from granary import as2, atom, microformats2
from oauth_dropins.webutil import flask_util from oauth_dropins.webutil import flask_util
from oauth_dropins.webutil.flask_util import error
from oauth_dropins.webutil.util import json_loads from oauth_dropins.webutil.util import json_loads
from app import app, cache from app import app, cache
import common import common
from common import error
from models import Response from models import Response
CACHE_TIME = datetime.timedelta(minutes=15) CACHE_TIME = datetime.timedelta(minutes=15)
@ -26,7 +26,7 @@ def render():
id = f'{source} {target}' id = f'{source} {target}'
resp = Response.get_by_id(id) resp = Response.get_by_id(id)
if not resp: if not resp:
return error(f'No stored response for {id}', status=404) error(f'No stored response for {id}', status=404)
if resp.source_mf2: if resp.source_mf2:
as1 = microformats2.json_to_object(json_loads(resp.source_mf2)) as1 = microformats2.json_to_object(json_loads(resp.source_mf2))
@ -35,7 +35,7 @@ def render():
elif resp.source_atom: elif resp.source_atom:
as1 = atom.atom_to_activity(resp.source_atom) as1 = atom.atom_to_activity(resp.source_atom)
else: else:
return error(f'Stored response for {id} has no data', status=404) error(f'Stored response for {id} has no data', status=404)
# add HTML meta redirect to source page. should trigger for end users in # add HTML meta redirect to source page. should trigger for end users in
# browsers but not for webmention receivers (hopefully). # browsers but not for webmention receivers (hopefully).

Wyświetl plik

@ -11,10 +11,10 @@ from django_salmon import magicsigs, utils
from flask import request from flask import request
from granary import atom from granary import atom
from oauth_dropins.webutil import util from oauth_dropins.webutil import util
from oauth_dropins.webutil.flask_util import error
from app import app from app import app
import common import common
from common import error
# from django_salmon.feeds # from django_salmon.feeds
ATOM_NS = 'http://www.w3.org/2005/Atom' ATOM_NS = 'http://www.w3.org/2005/Atom'
@ -41,7 +41,7 @@ def slap(acct):
try: try:
parsed = utils.parse_magic_envelope(body) parsed = utils.parse_magic_envelope(body)
except ParseError as e: except ParseError as e:
return error('Could not parse POST body as XML', exc_info=True) error('Could not parse POST body as XML', exc_info=True)
data = parsed['data'] data = parsed['data']
logging.info(f'Decoded: {data}') logging.info(f'Decoded: {data}')
@ -49,22 +49,22 @@ def slap(acct):
try: try:
activity = atom.atom_to_activity(data) activity = atom.atom_to_activity(data)
except ParseError as e: except ParseError as e:
return error('Could not parse envelope data as XML', exc_info=True) error('Could not parse envelope data as XML', exc_info=True)
verb = activity.get('verb') verb = activity.get('verb')
if verb and verb not in SUPPORTED_VERBS: if verb and verb not in SUPPORTED_VERBS:
return error(f'Sorry, {verb} activities are not supported yet.', status=501) error(f'Sorry, {verb} activities are not supported yet.', status=501)
# verify author and signature # verify author and signature
author = util.get_url(activity.get('actor')) author = util.get_url(activity.get('actor'))
if ':' not in author: if ':' not in author:
author = f'acct:{author}' author = f'acct:{author}'
elif not author.startswith('acct:'): elif not author.startswith('acct:'):
return error(f'Author URI {author} has unsupported scheme; expected acct:') error(f'Author URI {author} has unsupported scheme; expected acct:')
logging.info(f'Fetching Salmon key for {author}') logging.info(f'Fetching Salmon key for {author}')
if not magicsigs.verify(data, parsed['sig'], author_uri=author): if not magicsigs.verify(data, parsed['sig'], author_uri=author):
return error('Could not verify magic signature.') error('Could not verify magic signature.')
logging.info('Verified magic signature.') logging.info('Verified magic signature.')
# Verify that the timestamp is recent. Required by spec. # Verify that the timestamp is recent. Required by spec.
@ -73,7 +73,7 @@ def slap(acct):
# #
# updated = utils.parse_updated_from_atom(data) # updated = utils.parse_updated_from_atom(data)
# if not utils.verify_timestamp(updated): # if not utils.verify_timestamp(updated):
# return error('Timestamp is more than 1h old.') # error('Timestamp is more than 1h old.')
# send webmentions to each target # send webmentions to each target
activity = atom.atom_to_activity(data) activity = atom.atom_to_activity(data)

Wyświetl plik

@ -5,6 +5,7 @@ to test:
* user URL that redirects * user URL that redirects
* error handling * error handling
""" """
import html
from unittest import mock from unittest import mock
import urllib.parse import urllib.parse
@ -179,7 +180,8 @@ class WebfingerTest(testutil.TestCase):
def test_user_handler_bad_tld(self): def test_user_handler_bad_tld(self):
got = client.get('/acct:foo.json') got = client.get('/acct:foo.json')
self.assertEqual(404, got.status_code) self.assertEqual(404, got.status_code)
self.assertIn("doesn't look like a domain", got.get_data(as_text=True)) self.assertIn("doesn't look like a domain",
html.unescape(got.get_data(as_text=True)))
@mock.patch('requests.get') @mock.patch('requests.get')
def test_webfinger_handler(self, mock_get): def test_webfinger_handler(self, mock_get):

Wyświetl plik

@ -857,7 +857,11 @@ class WebmentionTest(testutil.TestCase):
self.assertEqual(400, got.status_code) self.assertEqual(400, got.status_code)
self.assertIn('Target post http://orig/url has no Atom link', self.assertIn('Target post http://orig/url has no Atom link',
got.get_data(as_text=True)) got.get_data(as_text=True))
self.assertEqual(0, Response.query().count())
resp = Response.get_by_id('http://a/reply http://orig/url')
self.assertEqual('out', resp.direction)
self.assertEqual('ostatus', resp.protocol)
self.assertEqual('error', resp.status)
def test_salmon_relative_atom_href(self, mock_get, mock_post): def test_salmon_relative_atom_href(self, mock_get, mock_post):
orig_relative = requests_response("""\ orig_relative = requests_response("""\

Wyświetl plik

@ -12,12 +12,12 @@ from flask import render_template, request
from granary.microformats2 import get_text from granary.microformats2 import get_text
import mf2util import mf2util
from oauth_dropins.webutil import flask_util, handlers, util from oauth_dropins.webutil import flask_util, handlers, util
from oauth_dropins.webutil.flask_util import error
from oauth_dropins.webutil.util import json_dumps from oauth_dropins.webutil.util import json_dumps
import webapp2 import webapp2
from app import app, cache from app import app, cache
import common import common
from common import error
import models import models
CACHE_TIME = datetime.timedelta(seconds=15) CACHE_TIME = datetime.timedelta(seconds=15)
@ -38,7 +38,7 @@ class User(flask_util.XrdOrJrd):
logging.debug(f'Headers: {list(request.headers.items())}') logging.debug(f'Headers: {list(request.headers.items())}')
if domain.split('.')[-1] in NON_TLDS: if domain.split('.')[-1] in NON_TLDS:
return error(f"{domain} doesn't look like a domain", status=404) error(f"{domain} doesn't look like a domain", status=404)
# find representative h-card. try url, then url's home page, then domain # find representative h-card. try url, then url's home page, then domain
urls = [f'http://{domain}/'] urls = [f'http://{domain}/']
@ -55,7 +55,7 @@ class User(flask_util.XrdOrJrd):
logging.info(f'Representative h-card: {json_dumps(hcard, indent=2)}') logging.info(f'Representative h-card: {json_dumps(hcard, indent=2)}')
break break
else: else:
return error(f"didn't find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on {resp.url}") error(f"didn't find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on {resp.url}")
logging.info(f'Generating WebFinger data for {domain}') logging.info(f'Generating WebFinger data for {domain}')
key = models.MagicKey.get_or_create(domain) key = models.MagicKey.get_or_create(domain)

Wyświetl plik

@ -17,6 +17,7 @@ from google.cloud.ndb import Key
from granary import as2, atom, microformats2, source from granary import as2, atom, microformats2, source
import mf2util import mf2util
from oauth_dropins.webutil import flask_util, util from oauth_dropins.webutil import flask_util, util
from oauth_dropins.webutil.flask_util import error
from oauth_dropins.webutil.util import json_dumps, json_loads from oauth_dropins.webutil.util import json_dumps, json_loads
import requests import requests
import webapp2 import webapp2
@ -25,7 +26,6 @@ from webob import exc
import activitypub import activitypub
from app import app from app import app
import common import common
from common import error
from models import Follower, MagicKey, Response from models import Follower, MagicKey, Response
SKIP_EMAIL_DOMAINS = frozenset(('localhost', 'snarfed.org')) SKIP_EMAIL_DOMAINS = frozenset(('localhost', 'snarfed.org'))
@ -55,12 +55,12 @@ class Webmention(View):
# source's intent to federate to mastodon) # source's intent to federate to mastodon)
if (request.host_url not in source_resp.text and if (request.host_url not in source_resp.text and
urllib.parse.quote(request.host_url, safe='') not in source_resp.text): urllib.parse.quote(request.host_url, safe='') not in source_resp.text):
return error("Couldn't find link to {request.host_url}") error("Couldn't find link to {request.host_url}")
# convert source page to ActivityStreams # convert source page to ActivityStreams
entry = mf2util.find_first_entry(self.source_mf2, ['h-entry']) entry = mf2util.find_first_entry(self.source_mf2, ['h-entry'])
if not entry: if not entry:
return error('No microformats2 found on {self.source_url}') error('No microformats2 found on {self.source_url}')
logging.info(f'First entry: {json_dumps(entry, indent=2)}') logging.info(f'First entry: {json_dumps(entry, indent=2)}')
# make sure it has url, since we use that for AS2 id, which is required # make sure it has url, since we use that for AS2 id, which is required
@ -191,9 +191,9 @@ class Webmention(View):
inbox_url = actor.get('inbox') inbox_url = actor.get('inbox')
actor = actor.get('url') or actor.get('id') actor = actor.get('url') or actor.get('id')
if not inbox_url and not actor: if not inbox_url and not actor:
return error('Target object has no actor or attributedTo with URL or id.') error('Target object has no actor or attributedTo with URL or id.')
elif not isinstance(actor, str): elif not isinstance(actor, str):
return error(f'Target actor or attributedTo has unexpected url or id object: {actor}') error(f'Target actor or attributedTo has unexpected url or id object: {actor}')
if not inbox_url: if not inbox_url:
# fetch actor as AS object # fetch actor as AS object
@ -203,7 +203,7 @@ class Webmention(View):
if not inbox_url: if not inbox_url:
# TODO: probably need a way to save errors like this so that we can # TODO: probably need a way to save errors like this so that we can
# return them if ostatus fails too. # return them if ostatus fails too.
# return error('Target actor has no inbox') # error('Target actor has no inbox')
continue continue
inbox_url = urllib.parse.urljoin(target_url, inbox_url) inbox_url = urllib.parse.urljoin(target_url, inbox_url)
@ -255,7 +255,7 @@ class Webmention(View):
parsed = util.parse_html(self.target_resp) parsed = util.parse_html(self.target_resp)
atom_url = parsed.find('link', rel='alternate', type=common.CONTENT_TYPE_ATOM) atom_url = parsed.find('link', rel='alternate', type=common.CONTENT_TYPE_ATOM)
if not atom_url or not atom_url.get('href'): if not atom_url or not atom_url.get('href'):
return error(f'Target post {target} has no Atom link') error(f'Target post {target} has no Atom link')
# fetch Atom target post, extract and inject id into source object # fetch Atom target post, extract and inject id into source object
base_url = '' base_url = ''
@ -311,7 +311,7 @@ class Webmention(View):
pass pass
if not endpoint: if not endpoint:
return error('No salmon endpoint found!') error('No salmon endpoint found!')
logging.info(f'Discovered Salmon endpoint {endpoint}') logging.info(f'Discovered Salmon endpoint {endpoint}')
# construct reply Atom object # construct reply Atom object