basic oauth support

This commit is contained in:
Izalia Mae 2020-01-15 08:56:27 -05:00
parent 257fd8900e
commit 46489746c5
9 changed files with 229 additions and 94 deletions

View file

@ -46,7 +46,7 @@ rewrite {
rewrite { rewrite {
if_op or if_op or
if {path} starts_with /@ if {path} starts_with /@
if {path} starts_with /authorize if {path} starts_with /paws
to {path} /auth/{path} to {path} /auth/{path}
} }

View file

@ -9,7 +9,7 @@ from envbash import load_envbash
from .functions import bool_check from .functions import bool_check
VERSION = '0.1' VERSION = '0.2-beta'
full_path = abspath(sys.executable) if getattr(sys, 'frozen', False) else abspath(__file__) full_path = abspath(sys.executable) if getattr(sys, 'frozen', False) else abspath(__file__)
script_path = getattr(sys, '_MEIPASS', dirname(abspath(__file__))) script_path = getattr(sys, '_MEIPASS', dirname(abspath(__file__)))

View file

@ -2,7 +2,7 @@ import sys
from DBUtils.PooledPg import PooledPg as DB from DBUtils.PooledPg import PooledPg as DB
from datetime import datetime from datetime import datetime
from tinydb import TinyDB, Query from tinydb import TinyDB, Query, where
from tinydb_smartcache import SmartCacheTable from tinydb_smartcache import SmartCacheTable
from tinyrecord import transaction as trans from tinyrecord import transaction as trans
from tldextract import extract from tldextract import extract
@ -23,14 +23,12 @@ def jsondb():
db.table_class = SmartCacheTable db.table_class = SmartCacheTable
tables = { class table:
'bans': db.table('bans'), bans = db.table('bans')
'follows': db.table('follows'), follows = db.table('follows')
'users': db.table('users'), users = db.table('users')
'domains': db.table('domains')
}
return tables return table
def pgdb(): def pgdb():

View file

@ -3,6 +3,9 @@ import re
import json import json
import logging import logging
from colour import Color
from aiohttp_jinja2 import render_template as render
error_codes = { error_codes = {
400: 'BadRequest', 400: 'BadRequest',
@ -26,7 +29,7 @@ def bool_check(value):
return value return value
def json_error(code, error): def error(code, error):
error_body = json.dumps({'error': error}) error_body = json.dumps({'error': error})
cont_type = 'application/json' cont_type = 'application/json'
@ -56,3 +59,54 @@ def user_check(path):
return False return False
class color:
def __init__(self):
self.check = lambda color: Color(f'#{str(color)}' if re.search(r'^(?:[0-9a-fA-F]{3}){1,2}$', color) else color)
def multi(self, multiplier):
if multiplier >= 1:
return 1
elif multiplier <= 0:
return 0
return multiplier
def lighten(self, color, multiplier):
col = self.check(color)
col.luminance += ((1 - col.luminance) * self.multi(multiplier))
return col.hex_l
def darken(self, color, multiplier):
col = self.check(color)
col.luminance -= (col.luminance * self.multi(multiplier))
return col.hex_l
def saturate(self, color, multiplier):
col = self.check(color)
col.saturation += ((1 - col.saturation) * self.multi(multiplier))
return col.hex_l
def desaturate(self, color, multiplier):
col = self.check(color)
col.saturation -= (col.saturation * self.multi(multiplier))
return col.hex_l
def rgba(self, color, transparency):
col = self.check(color)
red = col.red*255
green = col.green*255
blue = col.blue*255
trans = self.multi(transparency)
return f'rgba({red:0.2f}, {green:0.2f}, {blue:0.2f}, {trans:0.2f})'

View file

@ -6,14 +6,14 @@ import binascii
import base64 import base64
import traceback import traceback
from urllib.parse import urlparse from urllib.parse import urlparse, quote_plus
from aiohttp.http_exceptions import * from aiohttp.http_exceptions import *
from aiohttp.client_exceptions import * from aiohttp.client_exceptions import *
from .signature import validate, pass_hash from .signature import validate, pass_hash
from .functions import json_error, user_check from .functions import error, user_check
from .config import MASTOCONFIG, PAWSCONFIG, script_path from .config import MASTOCONFIG, PAWSCONFIG, VERSION, script_path
from . import database as db from .database import pawsdb, query, trans, ban_check
# I'm a little teapot :3 # I'm a little teapot :3
@ -99,40 +99,41 @@ async def passthrough(path, headers, post=None, query=None):
if resp.status not in [200, 202]: if resp.status not in [200, 202]:
print(data) print(data)
logging.warning(f'Recieved error {resp.status} from Mastodon') logging.debug(f'Recieved error {resp.status} from Mastodon')
json_error(resp.status, f'Failed to forward request. Recieved error {resp.status} from Mastodon') error(resp.status, f'Failed to forward request. Recieved error {resp.status} from Mastodon')
raise aiohttp.web.HTTPOk(body=data, content_type=resp.content_type) raise aiohttp.web.HTTPOk(body=data, content_type=resp.content_type)
except ClientConnectorError: except ClientConnectorError:
traceback.print_exc() traceback.print_exc()
return json_error(504, f'Failed to connect to Mastodon') return error(504, f'Failed to connect to Mastodon')
async def http_redirect(app, handler): async def http_redirect(app, handler):
async def redirect_handler(request): async def redirect_handler(request):
querydata = request.query if not request.path.startswith('/paws'):
querydata = request.query
rawquery = '?' rawquery = '?'
if len(querydata) > 0: if len(querydata) > 0:
for var in querydata: for var in querydata:
if rawquery == '?': if rawquery == '?':
rawquery += f'{var}={querydata[var]}' rawquery += f'{var}={querydata[var]}'
else: else:
rawquery += f'&{var}={querydata[var]}' rawquery += f'&{var}={querydata[var]}'
query = rawquery if rawquery != '' else None query = rawquery if rawquery != '' else None
try: try:
data = await request.json() data = await request.json()
except Exception as e: except Exception as e:
#logging.warning(f'failed to grab data: {e}') #logging.warning(f'failed to grab data: {e}')
data = None data = None
await passthrough(request.path, request.headers, post=data, query=query) await passthrough(request.path, request.headers, post=data, query=query)
return (await handler(request)) return (await handler(request))
return redirect_handler return redirect_handler
@ -152,40 +153,19 @@ async def http_signatures(app, handler):
if not signature: if not signature:
logging.warning('missing signature') logging.warning('missing signature')
raise json_error(401, 'Missing signature') raise error(401, 'Missing signature')
if not (await validate(actor, request)): if not (await validate(actor, request)):
logging.info(f'Signature validation failed for: {actor}') logging.info(f'Signature validation failed for: {actor}')
raise json_error(401, 'signature check failed, signature did not match key') raise error(401, 'signature check failed, signature did not match key')
else: elif not request.path.startswith('/paws/'):
request['reqtype'] = 'html' request['reqtype'] = 'html'
auth_username = PAWSCONFIG['user'] token = request.cookies.get('paws_token')
auth_password = PAWSCONFIG['pass']
auth_realm = 'Nope'
auth_header = request.headers.get(aiohttp.hdrs.AUTHORIZATION) if not token or not pawsdb.users.get(query.token == token):
return aiohttp.web.HTTPFound(f'/paws/login?redir={quote_plus(request.path)}')
if auth_header == None or not auth_header.startswith('Basic '):
return await raise_auth_error(request, auth_realm)
try:
secret = auth_header[6:].encode('utf-8')
auth_decoded = base64.decodebytes(secret).decode('utf-8')
except (UnicodeDecodeError, UnicodeEncodeError, binascii.Error):
await raise_auth_error(request)
credentials = auth_decoded.split(':')
if len(credentials) != 2:
await raise_auth_error(request, auth_realm)
username, password = credentials
if username != auth_username or password != auth_password:
await raise_auth_error(request, auth_realm)
return (await handler(request)) return (await handler(request))
return http_signatures_handler return http_signatures_handler
@ -203,15 +183,15 @@ async def http_filter(app, handler):
domain = ua_domain if not sig_domain else sig_domain domain = ua_domain if not sig_domain else sig_domain
if not domain: if not domain:
raise json_error(401, 'Can\'t find instance domain') raise error(401, 'Can\'t find instance domain')
if [agent for agent in blocked_agents if agent in ua]: if [agent for agent in blocked_agents if agent in ua]:
logging.info(f'Blocked garbage: {domain}') logging.info(f'Blocked garbage: {domain}')
raise HTTPTeapot(body='418 This teapot kills fascists', content_type='text/plain') raise HTTPTeapot(body='418 This teapot kills fascists', content_type='text/plain')
if db.ban_check(domain): if ban_check(domain):
logging.info(f'Blocked instance: {domain}') logging.info(f'Blocked instance: {domain}')
raise json_error(403, 'Forbidden') raise error(403, 'Forbidden')
return (await handler(request)) return (await handler(request))
return http_filter_handler return http_filter_handler
@ -227,4 +207,9 @@ async def http_trailing_slash(app, handler):
return http_trailing_slash_handler return http_trailing_slash_handler
__all__ = ['http_signatures_middleware', 'http_auth_middleware', 'http_filter_middleware', 'http_trailing_slash'] async def http_server_header(request, response):
response.headers['SErver'] = f'PAWS/{VERSION}'
response.headers['trans_rights'] = 'are human rights'
__all__ = ['http_signatures_middleware', 'http_auth_middleware', 'http_filter_middleware', 'http_trailing_slash', 'http_server_header']

View file

@ -2,11 +2,14 @@ import os
import sys import sys
import asyncio import asyncio
import aiohttp import aiohttp
import aiohttp_jinja2
import jinja2
from ipaddress import ip_address as address from ipaddress import ip_address as address
from urllib.parse import urlparse from urllib.parse import urlparse
from .config import PAWSCONFIG, VERSION, script_path, logging from .config import PAWSCONFIG, MASTOCONFIG, VERSION, script_path, logging
from .functions import color
from . import middleware from . import middleware
@ -19,10 +22,40 @@ def webserver():
middleware.http_redirect middleware.http_redirect
]) ])
web.on_response_prepare.append(middleware.http_server_header)
web.add_routes([ web.add_routes([
aiohttp.web.route('GET', '/authorize', views.authorize), aiohttp.web.route('GET', '/paws/login', views.get_login),
aiohttp.web.route('POST', '/paws/login', views.post_login),
aiohttp.web.route('GET', '/paws/logout', views.get_logout),
aiohttp.web.route('GET', '/paws/auth', views.get_auth),
aiohttp.web.route('GET', '/paws/style.css', views.get_style)
]) ])
async def global_vars(request):
return {
'VERSION': VERSION,
'len': len,
'request': request,
'domain': MASTOCONFIG['domain'],
'urlparse': urlparse,
'lighten': color().lighten,
'darken': color().darken,
'saturate': color().saturate,
'desaturate': color().desaturate,
'rgba': color().rgba
}
aiohttp_jinja2.setup(
web,
loader=jinja2.FileSystemLoader(f'{script_path}/templates'),
autoescape=jinja2.select_autoescape(['html', 'css']),
context_processors=[global_vars],
lstrip_blocks=True,
trim_blocks=True
)
return web return web

View file

@ -1,24 +0,0 @@
<html>
<head>
<title>Nope.mov</title>
<style>
body {
background-color: #111;
color: #ddd;
}
.center {
width: 300px;
height: 300px;
position: absolute;
left: 50%;
top: 50%;
margin-left: -150px;
margin-top: -150px;
}
</style>
</head>
<body>
<img src="https://static.barkshark.xyz/main/img/YouDidntSayTheMagicWord.gif" class="center" />
</body>
</html>

View file

@ -1,5 +1,92 @@
import aiohttp import aiohttp
import random
from aiohttp_jinja2 import render_template as render
from urllib.parse import quote_plus, unquote_plus
from .database import pawsdb, trans, query, where
from .functions import error
from .oauth import create_app, login
async def get_login(request):
parms = request.rel_url.query
redir = parms.get('redir')
numid = random.randint(1*1000000, 10*1000000-1)
return render('pages/login.html', request, {'redir': redir, 'numid': numid})
async def post_login(request):
data = await request.post()
domain = data.get('domain')
redir = data.get('redir')
numid = data.get('numid')
if domain in ['', None]:
return render('pages/login.html', request, {'msg': 'Missing domain'})
appid, appsecret, redir_url = create_app(domain)
with trans(pawsdb.users) as tr:
tr.insert({
'handle': data['numid'],
'domain': data['domain'],
'icon': None,
'appid': appid,
'appsecret': appsecret,
'token': None
})
response = aiohttp.web.HTTPFound(redir_url)
response.set_cookie('paws_numid', numid, max_age=60*60*24*14, path='/paws')
if redir not in ['', None]:
response.set_cookie('paws_redir', redir, max_age=60*60*24*14, path='/paws')
return response
async def get_auth(request):
parms = request.rel_url.query
cookies = request.cookies
redir = cookies.get('paws_redir')
numid = cookies.get('paws_numid')
code = parms.get('code')
if None in [numid, code]:
response = render('pages/error.html', request, {'msg': 'Missing temporary userid or auth code', 'code': 500}, status=500)
user = pawsdb.users.get(query.handle == str(numid))
token, userinfo = login(user, code)
with trans(pawsdb.users) as tr:
tr.update({'handle': userinfo['username'], 'icon': userinfo['avatar_static'], 'token': token}, where('handle') == numid)
print(user)
response = aiohttp.web.HTTPFound(redir)
response.set_cookie('paws_token', token, max_age=60*60*24*14)
response.del_cookie('paws_redir', path='/paws')
response.del_cookie('paws_numid', path='/paws')
return response
async def get_logout(request):
token = request.cookies.get('paws_token')
with trans(pawsdb.users) as tr:
tr.remove(where('token') == token)
response = render('pages/login.html', request, {'msg': 'Logged out'})
response.del_cookie('token')
return response
async def get_style(request):
response = render('color.css', request, {})
response.headers['Content-Type'] = 'text/css'
return response
async def authorize(request):
data = {['heck']}
return aiohttp.web.json_response(data)

View file

@ -9,4 +9,6 @@ pycryptodome
tldextract tldextract
envbash envbash
ipaddress ipaddress
mastodon.py
aiohttp_jinja2
colour