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 {
if_op or
if {path} starts_with /@
if {path} starts_with /authorize
if {path} starts_with /paws
to {path} /auth/{path}
}

View file

@ -9,7 +9,7 @@ from envbash import load_envbash
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__)
script_path = getattr(sys, '_MEIPASS', dirname(abspath(__file__)))

View file

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

View file

@ -3,6 +3,9 @@ import re
import json
import logging
from colour import Color
from aiohttp_jinja2 import render_template as render
error_codes = {
400: 'BadRequest',
@ -26,7 +29,7 @@ def bool_check(value):
return value
def json_error(code, error):
def error(code, error):
error_body = json.dumps({'error': error})
cont_type = 'application/json'
@ -56,3 +59,54 @@ def user_check(path):
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 traceback
from urllib.parse import urlparse
from urllib.parse import urlparse, quote_plus
from aiohttp.http_exceptions import *
from aiohttp.client_exceptions import *
from .signature import validate, pass_hash
from .functions import json_error, user_check
from .config import MASTOCONFIG, PAWSCONFIG, script_path
from . import database as db
from .functions import error, user_check
from .config import MASTOCONFIG, PAWSCONFIG, VERSION, script_path
from .database import pawsdb, query, trans, ban_check
# 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]:
print(data)
logging.warning(f'Recieved error {resp.status} from Mastodon')
json_error(resp.status, f'Failed to forward request. Recieved error {resp.status} from Mastodon')
logging.debug(f'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)
except ClientConnectorError:
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 redirect_handler(request):
querydata = request.query
if not request.path.startswith('/paws'):
querydata = request.query
rawquery = '?'
rawquery = '?'
if len(querydata) > 0:
for var in querydata:
if rawquery == '?':
rawquery += f'{var}={querydata[var]}'
if len(querydata) > 0:
for var in querydata:
if rawquery == '?':
rawquery += f'{var}={querydata[var]}'
else:
rawquery += f'&{var}={querydata[var]}'
else:
rawquery += f'&{var}={querydata[var]}'
query = rawquery if rawquery != '' else None
query = rawquery if rawquery != '' else None
try:
data = await request.json()
try:
data = await request.json()
except Exception as e:
#logging.warning(f'failed to grab data: {e}')
data = None
except Exception as e:
#logging.warning(f'failed to grab data: {e}')
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 redirect_handler
@ -152,40 +153,19 @@ async def http_signatures(app, handler):
if not signature:
logging.warning('missing signature')
raise json_error(401, 'Missing signature')
raise error(401, 'Missing signature')
if not (await validate(actor, request)):
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'
auth_username = PAWSCONFIG['user']
auth_password = PAWSCONFIG['pass']
auth_realm = 'Nope'
token = request.cookies.get('paws_token')
auth_header = request.headers.get(aiohttp.hdrs.AUTHORIZATION)
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)
if not token or not pawsdb.users.get(query.token == token):
return aiohttp.web.HTTPFound(f'/paws/login?redir={quote_plus(request.path)}')
return (await handler(request))
return http_signatures_handler
@ -203,15 +183,15 @@ async def http_filter(app, handler):
domain = ua_domain if not sig_domain else sig_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]:
logging.info(f'Blocked garbage: {domain}')
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}')
raise json_error(403, 'Forbidden')
raise error(403, 'Forbidden')
return (await handler(request))
return http_filter_handler
@ -227,4 +207,9 @@ async def http_trailing_slash(app, 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 asyncio
import aiohttp
import aiohttp_jinja2
import jinja2
from ipaddress import ip_address as address
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
@ -19,10 +22,40 @@ def webserver():
middleware.http_redirect
])
web.on_response_prepare.append(middleware.http_server_header)
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

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 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
envbash
ipaddress
mastodon.py
aiohttp_jinja2
colour