basic oauth support
This commit is contained in:
parent
257fd8900e
commit
46489746c5
|
@ -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}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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__)))
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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})'
|
||||||
|
|
||||||
|
|
|
@ -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']
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -9,4 +9,6 @@ pycryptodome
|
||||||
tldextract
|
tldextract
|
||||||
envbash
|
envbash
|
||||||
ipaddress
|
ipaddress
|
||||||
|
mastodon.py
|
||||||
|
aiohttp_jinja2
|
||||||
|
colour
|
||||||
|
|
Loading…
Reference in a new issue