a lot of changes

This commit is contained in:
Izalia Mae 2020-10-10 23:47:08 -04:00
parent 62e045fb66
commit de081b0463
15 changed files with 272 additions and 229 deletions

View file

@ -2,3 +2,5 @@
Uncia Relay by Zoey Mae
https://git.barkshark.xyz/izaliamae/uncia
'''
import sys, os
sys.path.append(f'{os.getcwd()}/modules')

View file

@ -72,7 +72,7 @@ except:
dbconfig = {
'host': env.get('DBHOST'),
'port': env.get('DBPORT', 54321),
'port': int(env.get('DBPORT', 5432)),
'user': env.get('DBUSER', env.get('USER')),
'pass': env.get('DBPASS'),
'name': env.get('DBNAME', 'uncia'),

View file

@ -101,7 +101,7 @@ def setup(db=None):
'whitelist': False,
'block_relays': True,
'require_approval': True,
'notifications': False,
'notification': False,
'log_level': 'INFO',
'development': False,
'setup': False

View file

@ -4,7 +4,8 @@ from Crypto.PublicKey import RSA
from . import *
from . import bool_check as bcheck, dbcache
from ..log import logging
from ..Lib.IzzyLib import logging
from ..functions import DotDict
Hash = HashContext()
Hash.setsalt()
@ -35,17 +36,20 @@ def rsa_key(actor, db=None, cached=True):
actor_key = db.insert('keys', new_key)
db.end()
actor_key = DotDict(actor_key)
actor_key.update({
'PRIVKEY': RSA.importKey(actor_key['privkey']),
'PUBKEY': RSA.importKey(actor_key['pubkey'])
})
actor_key.size = actor_key.PRIVKEY.size_in_bytes()/2
dbcache.key.store(actor, actor_key)
return actor_key
@connection
def config(data, cache=True, db=None):
def config(data, default=None, cache=True, db=None):
if len(dbcache.config.keys()) < 1 and cache:
update_config()
@ -91,7 +95,7 @@ def config(data, cache=True, db=None):
if row:
value = bcheck(row['value'])
dbcache.config[data] = value
return value
return value if value != None else default
@connection
@ -239,4 +243,4 @@ else:
# Set log level from config
log_level = config('log_level')
logging.setLevel(log_level)
logging.setLevel(log_level if log_level else 'INFO')

View file

@ -1,5 +1,4 @@
import pg
import ujson as json
import pg, json
from datetime import datetime
@ -108,10 +107,15 @@ def add_retry(msgid, inbox, data, headers, db=None):
if row:
return True
domain_retries = get.retries({'inbox': inbox})
if len(domain_retries) >= 500:
return
data = {
'inbox': inbox,
'data': json.dumps(data, escape_forward_slashes=False),
'headers': json.dumps(headers, escape_forward_slashes=False),
'data': json.dumps(data),
'headers': json.dumps(headers),
'msgid': msgid,
'timestamp': datetime.now().timestamp()
}

View file

@ -3,6 +3,7 @@ import traceback
from sanic import response
from .log import logging
from .templates import error
def logstr(request, status, e=False):
@ -13,24 +14,20 @@ def logstr(request, status, e=False):
def not_found(request, exception):
from .templates import error
return error(request, f'Not found: {request.path}', 404)
def method_not_supported(request, exception):
from .templates import error
return error(request, f'Invalid method: {request.method}', 405)
def server_error(request, exception):
from .templates import error
logstr(request, 500, e=exception)
msg = 'OOPSIE WOOPSIE!! Uwu We made a fucky wucky!! A wittle fucko boingo! The code monkeys at our headquarters are working VEWY HAWD to fix this!'
return error(request, msg, 500)
def no_template(request, exception):
from .templates import error
logstr(request, 500, e=exception)
msg = 'I\'m a dumbass and forgot to create a template for this page'
return error(request, msg, 500)

View file

@ -194,7 +194,7 @@
%tr
%td{'class': 'col1 instance'}
-if user.domain != 'any'
%a{'href': 'https://{{user.domain}}/user/{{user.user}}', 'target': '_new'}
%a{'href': 'https://{{user.domain}}/users/{{user.user}}', 'target': '_new'}
{{user.user}}@{{user.domain}}
-else

View file

@ -20,9 +20,67 @@ from .config import script_path, stor_path, version, pyv
httpclient = urllib3.PoolManager(num_pools=100, timeout=urllib3.Timeout(connect=15, read=15))
def format_urls(urls):
from .messages import fetch
def defhead():
from .database import get
host = get.config('host')
data = {
'User-Agent': f'python/{pyv[0]}.{pyv[1]}.{pyv[2]} (UnciaRelay/{version}; +https://{host})',
}
return data
def fetch(url, cached=True, signed=False, new_headers={}):
from .signatures import SignHeaders
from .database import get
cached_data = cache.url.fetch(url)
host = get.config('host')
if cached and cached_data:
logging.debug(f'Returning cached data for {url}')
return cached_data
headers = defhead()
headers.update(new_headers)
headers.update({'Accept': 'application/json'})
if signed:
headers = SignHeaders(headers, 'default', f'https://{host}/actor#main-key', url, 'get')
try:
logging.debug(f'Fetching new data for {url}')
response = httpclient.request('GET', url, headers=headers)
except Exception as e:
logging.debug(f'Failed to fetch {url}')
logging.debug(e)
return
if response.data == b'':
logging.debug(f'Received blank data while fetching url: {url}')
return
try:
data = json.loads(response.data)
if cached:
logging.debug(f'Caching {url}')
cache.url.store(url, data)
if data.get('error'):
return
return data
except Exception as e:
logging.debug(f'Failed to load data: {response.data}')
logging.debug(e)
return
def format_urls(urls):
actor = urls.get('actor')
inbox = urls.get('inbox')
domain = urls.get('domain')
@ -67,7 +125,6 @@ def get_id(data):
def get_user(user):
from .messages import fetch
username, domain = user.split('@')
webfinger = fetch(f'https://{domain}/.well-known/webfinger?resource=acct:{user}')
@ -87,7 +144,6 @@ def get_user(user):
def get_post_user(data):
from .messages import fetch
if data['type'] in ['Follow', 'Undo']:
return
@ -121,7 +177,7 @@ def get_post_user(data):
def format_date(timestamp):
def format_date(timestamp=None):
if timestamp:
date = datetime.fromtimestamp(timestamp)
@ -222,6 +278,49 @@ class LRUCache(OrderedDict):
return None
class DotDict(dict):
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__
def __init__(self, value=None, **kwargs):
super().__init__()
if value.__class__ == str:
self.FromJson(value)
elif value.__class__ in [dict, DotDict]:
self.update(value)
elif value:
raise TypeError('The value must be a JSON string, dict, or another DotDict object, not', value.__class__)
if kwargs:
self.update(kwargs)
def __getattr__(self, value, default=None):
val = self.get(value, default) if default else self[value]
return DotDict(val) if type(val) == dict else val
def ToJson(self, **kwargs):
return self.__str__(**kwargs)
def FromJson(self, string):
data = json.loads(string)
self.update(data)
def AsDict(self):
return {k: v for k, v in self.items() if not k.startswith('__')}
def __parse_item(self, data):
return DotDict(data) if type(data) == dict else data
cache_size = 2048
class cache:

View file

@ -1,10 +1,10 @@
import sys
import logging as logger
import logging.config as logconf
from os import environ as env
from datetime import datetime
from .Lib.IzzyLib import logging
# Custom logger
class Log():
@ -131,6 +131,6 @@ LOG = dict(
},
)
logconf.dictConfig(LOG)
#logconf.dictConfig(LOG)
logging = Log()
#logging = Log()

View file

@ -1,11 +1,12 @@
import asyncio, threading, uuid, ujson as json
import urllib3
import asyncio, threading, uuid, traceback, json, base64
from urllib.parse import urlparse
from .log import logging
from .functions import format_date, get_id, cache, httpclient, get_user, get_inbox
from .signatures import sign_headers
import urllib3
from .Lib.IzzyLib import logging
from .functions import format_date, get_id, cache, httpclient, get_user, get_inbox, fetch, defhead
from .signatures import SignHeaders, SignHeaders, SignBody
from .database import get, put
from .config import version, pyv
@ -13,15 +14,6 @@ from .config import version, pyv
host = get.config('host')
def defhead():
host = get.config('host')
data = {
'User-Agent': f'python/{pyv[0]}.{pyv[1]}.{pyv[2]} (UnciaRelay/{version}; +https://{host})',
}
return data
def accept(followid, urls):
actor_url = urls['actor']
inbox = urls['inbox']
@ -44,15 +36,11 @@ def accept(followid, urls):
'id': f'https://{host}/activities/{UUID}',
}
headers = {
'date': format_date(None)
}
if push(inbox, body, headers):
if push(inbox, body):
put.inbox('add', urls)
if not get.config('require_approval'):
thread = threading.Thread(target=notification, args=(domain))
if not get.config('require_approval') and get.config('notification'):
thread = threading.Thread(target=notification, args=[domain])
thread.start()
return True
@ -70,19 +58,11 @@ def announce(obj_id, inbox):
'id': activity_id
}
headers = {
'date': format_date(None)
}
push_inboxes(message, headers, origin=inbox)
push_inboxes(message, origin=inbox)
def forward(data, inbox):
headers = {
'date': format_date(None)
}
push_inboxes(data, headers, origin=inbox)
push_inboxes(data, origin=inbox)
def paws(url):
@ -95,14 +75,10 @@ def paws(url):
"object": f"https://{host}/paws/lorge"
}
headers = {
'date': format_date(None)
}
push(url, data, headers)
push(url, data)
def notification(domain, *args):
def notification(domain):
acct = get.config('admin')
admin_user, admin_domain = acct.split('@')
admin_uuid = uuid.uuid4()
@ -145,7 +121,7 @@ def notification(domain, *args):
if not get.config('require_approval'):
admin_message['object']['content'] = f'<p><a href=\"https://{domain}/about\">{domain}</a> has joined the relay</p>'
return True if push(inbox, admin_message, None) else False
return True if push(inbox, admin_message) else False
### End of messages
@ -199,7 +175,7 @@ def run_retries(inbox=None, msgid=None):
thread.start()
def push_inboxes(data, headers, origin=None):
def push_inboxes(data, headers={}, origin=None):
object_id = get_id(data)
object_domain = urlparse(object_id).netloc
threads = []
@ -209,40 +185,40 @@ def push_inboxes(data, headers, origin=None):
domain = inbox['domain']
if domain != object_domain and inbox_url != origin:
threads.append(threading.Thread(target=push, args=(inbox_url, data, headers, 'retry')))
threads.append(threading.Thread(target=push, args=(inbox_url, data, headers, True)))
for thread in threads:
thread.start()
def push(inbox, data, headers, *args):
def push(inbox, data, headers={}, retry=False):
logging.debug(f'Sending message to {inbox}')
body = json.dumps(data, escape_forward_slashes=False).encode('utf-8')
body = json.dumps(data)
url = get_id(data)
orig_head = {} if not headers else headers.copy()
if not headers:
headers = {}
posthost = urlparse(inbox).netloc
orig_head = headers.copy()
headers.update(defhead())
if 'Content-Type' not in headers:
headers.update({'Content-Type': 'application/activity+json'})
headers['content-type'] = 'application/activity+json'
headers['digest'] = f'SHA-256={SignBody(body)}'
if headers.get('signature'):
del headers['signature']
headers = sign_headers(headers, 'default', f'https://{host}/actor#main-key', f'get {urlparse(inbox).path}')
headers = SignHeaders(headers, 'default', f'https://{host}/actor#main-key', inbox, 'post')
try:
response = httpclient.request('POST', inbox, body=body, headers=headers)
respdata = response.data.decode()
if response.status not in [200, 202]:
logging.verbose(f'Failed to push to {inbox}: Error {response.status}')
logging.debug(f'Response: {response.data.decode()}')
if response.status not in [401, 403]:
if len(respdata) < 200:
logging.debug(f'Response from {posthost}: {respdata}')
if response.status not in [403]:
put.add_retry(url, inbox, data, orig_head)
else:
@ -253,49 +229,3 @@ def push(inbox, data, headers, *args):
except Exception as e:
logging.verbose(f'Connection error when pushing to {inbox}: {e}')
put.add_retry(url, inbox, data, orig_head)
def fetch(url, cached=True, signed=False):
from .signatures import sign_headers
cached_data = cache.url.fetch(url)
host = get.config('host')
if cached and cached_data:
logging.debug(f'Returning cached data for {url}')
return cached_data
headers = defhead()
headers.update({'Accept': 'application/json'})
headers = sign_headers(headers, 'default', f'https://{host}/actor#main-key', f'get {urlparse(url).path}') if signed else headers
try:
logging.debug(f'Fetching new data for {url}')
response = httpclient.request('GET', url, headers=headers)
except Exception as e:
logging.debug(f'Failed to fetch {url}')
logging.debug(e)
return
if response.data == b'':
logging.debug(f'Received blank data while fetching url: {url}')
return
try:
data = json.loads(response.data)
if cached:
logging.debug(f'Caching {url}')
cache.url.store(url, data)
if data.get('error'):
return
return data
except Exception as e:
logging.debug(f'Failed to load data: {response.data}')
logging.debug(e)
return

View file

@ -4,7 +4,7 @@ from sanic import response
from .log import logging
from .templates import error
from .signatures import validate
from .signatures import ValidateRequest
from .views import Login
from .database import get, put
from .config import version
@ -14,24 +14,25 @@ async def access_log(request, response):
response.headers['Server'] = f'Uncia/{version}'
response.headers['Trans'] = 'Rights'
addr = request.headers.get('x-forwarded-for', request.remote_addr)
uagent = request.headers.get('user-agent')
logging.info(f'{request.remote_addr} {request.method} {request.path} {response.status} "{uagent}"')
logging.info(f'{addr} {request.method} {request.path} {response.status} "{uagent}"')
async def query_post_dict(request):
request['query'] = {}
request['form'] = {}
request.ctx.query = {}
request.ctx.form = {}
for k, v in request.query_args:
request['query'].update({k: v})
request.ctx.query.update({k: v})
for k, v in request.form.items():
request['form'].update({k: v[0]})
request.ctx.form.update({k: v[0]})
async def authentication(request):
if request.path == '/inbox':
valid = validate(request)
valid = ValidateRequest(request)
data = request.json
if valid == False and data.get('type') == 'Delete':

View file

@ -13,6 +13,7 @@ from .functions import get_inbox, cache, get_id, get_post_user, format_urls
def relay_announce(data, actor, urls):
object_id = get_id(data)
inbox = urls['inbox']
instance = urls['domain']
if not object_id:
logging.debug(f'Can\'t find object id')
@ -26,8 +27,11 @@ def relay_announce(data, actor, urls):
if username:
user, domain = username
if get.domainban(domain):
logging.info(f'Rejected post from banned instance: {domain} from {instance}')
if get.userban(user, domain):
logging.info(f'Rejected banned user: {user}@{domain}')
logging.info(f'Rejected post from banned user: {user}@{domain} from {instance}')
return
announce(object_id, inbox)
@ -62,8 +66,9 @@ def relay_follow(data, actor, urls):
if get.config('require_approval'):
put.request('add', urls, followid=followid)
thread = threading.Thread(target=notification, args=[domain])
thread.start()
if get.config('notification'):
thread = threading.Thread(target=notification, args=[domain])
thread.start()
return

View file

@ -16,7 +16,7 @@ from .templates import build_templates
from . import errors, views, middleware as mw
app = Sanic(log_config=LOG)
app = Sanic()
app.config.FORWARDED_SECRET = fwsecret
# Register middlewares

View file

@ -1,13 +1,17 @@
import json
from base64 import b64decode, b64encode
from urllib.parse import urlparse
from datetime import datetime
import httpsig
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA, SHA256, SHA512
from Crypto.Signature import PKCS1_v1_5
from .log import logging
from .functions import cache, format_date
from .functions import cache, format_date, fetch
from .database import get
@ -18,23 +22,27 @@ HASHES = {
}
def parse_sig(sig):
if not sig:
logging.warning('Missing signature header')
def ParseSig(headers):
sig_header = headers.get('signature')
if not sig_header:
logging.verbose('Missing signature header')
return
parts = {'headers': 'date'}
split_sig = sig_header.split(',')
signature = {}
for part in sig.strip().split(','):
k, v = part.replace('"', '').split('=', maxsplit=1)
for part in split_sig:
key, value = part.split('=', 1)
signature[key.lower()] = value.replace('"', '')
if k == 'headers':
parts['headers'] = v.split()
if not signature.get('headers'):
logging.verbose('Missing headers section in signature')
return
else:
parts[k] = v
signature['headers'] = signature['headers'].split()
return parts
return signature
def build_sigstring(request, used_headers, target=None):
@ -61,87 +69,81 @@ def build_sigstring(request, used_headers, target=None):
return string
def sign_sigstring(sigstring, key, hashalg='SHA256'):
cached_data = cache.sig.fetch(sigstring)
def SignBody(body):
bodyhash = cache.sig.fetch(body)
if cached_data:
logging.info('Returning cache sigstring')
return cached_data
if not bodyhash:
h = SHA256.new(body.encode('utf-8'))
bodyhash = b64encode(h.digest()).decode('utf-8')
cache.sig[body] = bodyhash
sign_key = get.rsa_key(key)
return bodyhash
if not sign_key:
def ValidateSignature(headers, method, path):
headers = {k.lower(): v for k,v in headers.items()}
signature = ParseSig(headers)
actor_data = fetch(signature['keyid'])
logging.debug(actor_data)
try:
pubkey = actor_data['publicKey']['publicKeyPem']
except Exception as e:
logging.verbose(f'Failed to get public key for actor {signature["keyid"]}')
return
pkcs = PKCS1_v1_5.new(sign_key['PRIVKEY'])
h = HASHES[hashalg.lower()].new()
h.update(sigstring.encode('ascii'))
valid = httpsig.HeaderVerifier(headers, pubkey, signature['headers'], method, path, sign_header='signature').verify()
sigdata = b64encode(pkcs.sign(h)).decode('ascii')
cache.sig.store(sigstring, sigdata)
if not valid:
if not isinstance(valid, tuple):
logging.verbose('Signature validation failed for unknown actor')
logging.verbose(valid)
return sigdata
else:
logging.verbose(f'Signature validation failed for actor: {valid[1]}')
def sign_headers(headers, key, key_id, target):
headers = {k.lower(): v for k, v in headers.items()}
headers['date'] = format_date(None)
used_headers = headers.keys()
sigstring = build_sigstring(headers, used_headers, target=target)
signed_sigstring = sign_sigstring(sigstring, key)
sig = {
'keyId': key_id,
'algorithm': 'rsa-sha256',
'headers': ' '.join(used_headers),
'signature': signed_sigstring
}
sig_header = ['{}="{}"'.format(k, v) for k, v in sig.items()]
headers['signature'] = ','.join(sig_header)
return headers
def validate(request):
from .messages import fetch
data = request.json
actor = data.get('actor')
sig = parse_sig(request.headers.get('signature'))
if not actor:
logging.debug('Missing actor')
return
if not sig:
logging.debug('Missing signature')
else:
return True
def ValidateRequest(request):
'''
Validates the headers in a Sanic or Aiohttp request (other frameworks may be supported)
See ValidateSignature for 'client' and 'agent' usage
'''
return ValidateSignature(request.headers, request.method, request.path)
def SignHeaders(headers, key, keyid, url, method='get'):
if headers.get('date'):
del headers['date']
actor_key = get.rsa_key(key)
if not actor_key:
logging.error('Could not find signing key:', key)
return
actor_data = fetch(actor)
privkey = actor_key['privkey']
RSAkey = RSA.import_key(privkey)
key_size = int(RSAkey.size_in_bytes()/2)
logging.debug('Signing key size:', key_size)
if not actor_data:
logging.debug('Missing actor data')
return False
parsed_url = urlparse(url)
if not actor_data.get('publicKey') or not actor_data['publicKey'].get('publicKeyPem'):
logging.debug(f'Missing pubkey')
return
raw_headers = {'date': format_date(), 'host': parsed_url.netloc, '(request-target)': ' '.join([method.lower(), parsed_url.path])}
raw_headers.update(dict(headers))
header_keys = raw_headers.keys()
actor_key = actor_data['publicKey']['publicKeyPem']
pkcs = PKCS1_v1_5.new(RSA.importKey(actor_data['publicKey']['publicKeyPem']))
signer = httpsig.HeaderSigner(keyid, privkey, f'rsa-sha{key_size}', headers=header_keys, sign_header='signature')
new_headers = signer.sign(raw_headers, parsed_url.netloc, method, parsed_url.path)
logging.debug('Signed headers:', new_headers)
sigstring = build_sigstring(request, sig['headers'])
logging.debug(f'Signing string: {sigstring}')
del new_headers['(request-target)']
signalg, hashalg = sig['algorithm'].split('-')
sigdata = b64decode(sig['signature'])
h = HASHES[hashalg].new()
h.update(sigstring.encode('ascii'))
result = pkcs.verify(h, sigdata)
logging.debug(f'Sig verification result: {result}')
return result
return new_headers

View file

@ -54,7 +54,6 @@ class Inbox(HTTPMethodView):
return response.text('Failed to fetch actor', status=400)
domain = urlparse(actor_url).netloc
urls = {
'inbox': inbox,
'actor': actor_url,
@ -187,7 +186,7 @@ class WellknownNodeinfo(HTTPMethodView):
class WellknowWebfinger(HTTPMethodView):
async def get(self, request):
res = request['query'].get('resource')
res = request.ctx.query.get('resource')
if not res or res != f'acct:relay@{host}':
data = {}
@ -229,7 +228,7 @@ class Faq(HTTPMethodView):
class Admin(HTTPMethodView):
async def get(self, request, *args, action=None, msg=None, **kwargs):
page = kwargs.get('page', request['query'].get('page', 'instances'))
page = kwargs.get('page', request.ctx.query.get('page', 'instances'))
if action:
return error(request, f'Not found: {request.path}', 404)
@ -252,8 +251,8 @@ class Admin(HTTPMethodView):
async def post(self, request, action=''):
action = re.sub(r'[^a-z]+', '', action.lower())
data = request['form']
page = request['form'].get('page', 'instances')
data = request.ctx.form
page = data.get('page', 'instances')
msg = admin.run(action, data)
@ -280,7 +279,7 @@ class Account(HTTPMethodView):
async def post(self, request, action=''):
action = re.sub(r'[^a-z]+', '', action.lower())
password = request['form'].get('password')
password = request.ctx.form.get('password')
token = request.cookies.get('token')
token_data = get.token(token)
user = get.user(token_data['userid'])
@ -300,8 +299,8 @@ class Account(HTTPMethodView):
return resp
if action == 'password':
pass1 = request['form'].get('newpass1')
pass2 = request['form'].get('newpass2')
pass1 = request.ctx.form.get('newpass1')
pass2 = request.ctx.form.get('newpass2')
if pass1 != pass2:
return await self.get(request, msg='New passwords do not match')
@ -315,7 +314,7 @@ class Account(HTTPMethodView):
return await self.get(request, msg='Updated password')
if action == 'name':
dispname = request['form'].get('displayname')
dispname = request.ctx.form.get('displayname')
if not dispname:
return await self.get(request, msg='Missing new display name')
@ -327,7 +326,7 @@ class Account(HTTPMethodView):
return await self.get(request, msg='Failed to update display name')
if action == 'token':
form_token = request['form'].get('token')
form_token = request.ctx.form.get('token')
if not form_token:
return await self.get(request, msg='Failed to provide token to delete')
@ -359,14 +358,14 @@ class Login(HTTPMethodView):
async def get(self, request, msg=None):
data = {
'msg': msg,
'code': request['query'].get('code')
'code': request.ctx.query.get('code')
}
return render('login.html', request, data)
async def post(self, request):
username = request['form'].get('username')
password = request['form'].get('password')
username = request.ctx.form.get('username')
password = request.ctx.form.get('password')
if None in [username, password]:
return await self.get(request, msg='Missing username or password')
@ -405,13 +404,13 @@ class Register(HTTPMethodView):
async def get(self, request, msg=None):
data = {
'msg': msg,
'code': request['query'].get('code')
'code': request.ctx.query.get('code')
}
return render('register.html', request, data)
async def post(self, request):
data = request['form']
data = request.ctx.form
keys = ['username', 'password', 'password2', 'code']
for key in keys:
@ -465,12 +464,12 @@ class Robots(HTTPMethodView):
class Setup(HTTPMethodView):
async def get(self, request, *args, msg=None, **kwargs):
data = {'code': request['query'].get('code'), 'msg': msg}
data = {'code': request.ctx.query.get('code'), 'msg': msg}
return render('setup.html', request, data)
async def post(self, request, action=''):
data = request['form']
data = request.ctx.form
if data.get('code') != get.auth_code:
return await self.get(request, msg='Invalid auth code')