start on instance approval mode
This commit is contained in:
parent
9d1445c080
commit
67efd787c7
|
@ -36,6 +36,10 @@ if not isfile(f'{stor_path}/production.env'):
|
|||
#PAWS_DOMAIN=bappypaws.example.com
|
||||
#PAWS_NORSS=true
|
||||
|
||||
### Require approval for unknown instances. Set to true for all AP servers or a comma-separated list for specific server software
|
||||
### Ex: mastodon,pleroma,miskey,activityrelay,unciarelay,other,unidentified
|
||||
#PAWS_REQ_APPROVAL=false
|
||||
|
||||
#MASTOPATH=/home/mastodon/glitch-soc
|
||||
#MASTOHOST=localhost:3000
|
||||
'''
|
||||
|
@ -48,13 +52,21 @@ if not isfile(f'{stor_path}/production.env'):
|
|||
else:
|
||||
load_envbash(f'{stor_path}/production.env')
|
||||
|
||||
|
||||
req_approval = boolean(env.get('PAWS_REQ_APPROVAL', False), False)
|
||||
|
||||
if not isinstance(req_approval, bool):
|
||||
req_approval = [sw.strip() for sw in req_approval.split(',')]
|
||||
|
||||
|
||||
PAWSCONFIG = {
|
||||
'host': env.get('PAWS_HOST', '127.0.0.1'),
|
||||
'port': int(env.get('PAWS_PORT', 3001)),
|
||||
'domain': env.get('PAWS_DOMAIN', 'bappypaws.example.com'),
|
||||
'disable_rss': boolean(env.get('PAWS_DISABLE_RSS', True)),
|
||||
'req_approval': req_approval,
|
||||
'mastopath': env.get('MASTOPATH', os.getcwd()),
|
||||
'mastohost': env.get('MASTOHOST', 'localhost:3000')
|
||||
'mastohost': env.get('MASTOHOST', 'localhost:3000'),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -15,9 +15,10 @@ from tinyrecord import transaction as trans
|
|||
from tldextract import extract
|
||||
from Crypto.PublicKey import RSA
|
||||
from mastodon import Mastodon
|
||||
from mastodon.Mastodon import MastodonUnauthorizedError, MastodonBadGatewayError
|
||||
from mastodon.Mastodon import MastodonUnauthorizedError, MastodonBadGatewayError, MastodonNetworkError
|
||||
|
||||
from .config import stor_path, MASTOCONFIG as mdb
|
||||
from .functions import fetch, get_nodeinfo
|
||||
|
||||
|
||||
def jsondb():
|
||||
|
@ -32,8 +33,7 @@ def jsondb():
|
|||
|
||||
class table:
|
||||
keys = db.table('keys')
|
||||
follows = db.table('follows')
|
||||
accept = db.table('accept')
|
||||
instances = db.table('instances')
|
||||
users = db.table('users')
|
||||
whitelist = db.table('whitelist')
|
||||
|
||||
|
@ -221,6 +221,55 @@ def whitelist(action, instance):
|
|||
return 'InvalidAction'
|
||||
|
||||
|
||||
def get_instances(domain=None, state=None):
|
||||
if domain:
|
||||
rows = table.instances.get(query.domain == domain)
|
||||
|
||||
elif state:
|
||||
rows = table.instances.search(query.state == state)
|
||||
|
||||
else:
|
||||
rows = table.instances.all()
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
def instances(action, instance, state=None):
|
||||
domain = urlparse(instance).netloc if instance.startswith('http') else instance
|
||||
req_data = table.requests.get(query.domain == domain)
|
||||
|
||||
if action == 'add':
|
||||
if not state:
|
||||
logging.debug('Instance state not specified')
|
||||
return
|
||||
|
||||
if req_data:
|
||||
logging.debug(f'Domain already in request list: {domain}')
|
||||
return
|
||||
|
||||
ni_software = get_nodeinfo(domain)
|
||||
|
||||
if not ni_software:
|
||||
logging.debug(f'Failed to get nodeinfo data from instance: {domain}')
|
||||
|
||||
data = {
|
||||
'instance': domain,
|
||||
'software': ni_software['name'],
|
||||
'state': state,
|
||||
'timestamp': datetime.timestamp(datetime.now())
|
||||
}
|
||||
|
||||
with trans(table.instances) as tr:
|
||||
tr.insert(data)
|
||||
|
||||
elif action == 'remove':
|
||||
if not req_data:
|
||||
logging.debug(f'Domain not in request list: {domain}')
|
||||
|
||||
with trans(table.instances) as tr:
|
||||
tr.remove(doc_ids=[req_data.doc_id])
|
||||
|
||||
|
||||
def cleanup_users(invalid_check=None):
|
||||
timestamp = datetime.timestamp(datetime.now())
|
||||
invalid_offset = 60 * 15
|
||||
|
@ -253,7 +302,7 @@ def cleanup_users(invalid_check=None):
|
|||
logging.debug(f'invalid token for {user["handle"]}@{user["domain"]}')
|
||||
tr.remove(doc_ids=[user.doc_id])
|
||||
|
||||
except (urllib3.exceptions.NewConnectionError, MastodonBadGatewayError):
|
||||
except (urllib3.exceptions.NewConnectionError, MastodonBadGatewayError, MastodonNetworkError):
|
||||
logging.debug(f'failed to connect to domain for {user["handle"]}@{user["domain"]}')
|
||||
|
||||
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
import json, socket, os, re
|
||||
import aiohttp, validators
|
||||
import aiohttp, validators, urllib3
|
||||
|
||||
from http import client as http
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from IzzyLib import logging
|
||||
from IzzyLib.template import aiohttpTemplate
|
||||
from IzzyLib.cache import LRUCache
|
||||
|
||||
from .config import VERSION
|
||||
|
||||
|
||||
httpclient = urllib3.PoolManager(num_pools=100, timeout=urllib3.Timeout(connect=15, read=15))
|
||||
|
||||
|
||||
class cache:
|
||||
url = LRUCache()
|
||||
|
||||
|
||||
def error(request, code, msg, isjson=False):
|
||||
|
@ -24,30 +33,55 @@ def json_error(code, error):
|
|||
|
||||
def http_error(request, code, msg):
|
||||
data = {'msg': msg, 'code': str(code)}
|
||||
return aiohttpTemplate('pages/error.html', data, request, status=code)
|
||||
return aiohttpTemplate('error.html', data, request, status=code)
|
||||
|
||||
|
||||
def fed_domain(user, domain):
|
||||
headers = {'Accept': 'application/json'}
|
||||
conn = http.HTTPSConnection(domain)
|
||||
conn.request('GET', f'/.well-known/webfinger?resource=acct:{user}@{domain}', headers=headers)
|
||||
response = conn.getresponse()
|
||||
data = fetch(f'https://{domain}/.well-known/webfinger?resource=acct:{user}@{domain}')
|
||||
|
||||
if response.status != 200:
|
||||
if not data:
|
||||
logging.error(f'User doesn\'t exist: {user}@{domain}')
|
||||
return
|
||||
|
||||
try:
|
||||
data = json.loads(response.read())
|
||||
|
||||
except:
|
||||
logging.debug(f'Invalid user: {user}@{domain}')
|
||||
|
||||
wf_data = data.get('subject')
|
||||
user_data = wf_data.replace('acct:', '').split('@')
|
||||
|
||||
return user_data[1]
|
||||
|
||||
|
||||
def get_nodeinfo(instance):
|
||||
domain = urlparse(instance).netloc if instance.startswith('http') else instance
|
||||
|
||||
nodeinfo = fetch(f'https://{domain}/nodeinfo/2.0.json')
|
||||
|
||||
if not nodeinfo:
|
||||
logging.debug('Wrong nodeinfo url. Finding correct one...')
|
||||
wk_url = f'https://{domain}/.well-known/nodeinfo'
|
||||
well_known = fetch(wk_url)
|
||||
|
||||
if not well_known:
|
||||
logging.debug(f'failed to fetch {wk_url}')
|
||||
return
|
||||
|
||||
ni_url = None
|
||||
|
||||
try:
|
||||
for link in well_known['links']:
|
||||
if link['rel'].startswith('http://nodeinfo.diaspora.software/ns/schema/2'):
|
||||
ni_url = link['href']
|
||||
|
||||
except Exception as e:
|
||||
logging.debug(f'Invalid nodeinfo data, Error: {e}')
|
||||
return
|
||||
|
||||
nodeinfo = fetch(ni_url)
|
||||
|
||||
if not nodeinfo:
|
||||
logging.debug(f'Failed to fetch nodeinfo for {domain}')
|
||||
|
||||
return nodeinfo['software'] if nodeinfo else None
|
||||
|
||||
|
||||
def domain_check(domain):
|
||||
stripped = domain.replace(' ', '')
|
||||
return stripped if validators.domain(stripped) else None
|
||||
|
@ -126,3 +160,50 @@ def css_ts():
|
|||
layout = css_check('layout')
|
||||
|
||||
return color + layout
|
||||
|
||||
|
||||
def fetch(url, cached=True):
|
||||
if not url:
|
||||
logging.debug(f'Fetch was not fed a url')
|
||||
return
|
||||
|
||||
cached_data = cache.url.fetch(url)
|
||||
|
||||
if cached and cached_data:
|
||||
logging.debug(f'Returning cached data for {url}')
|
||||
return cached_data
|
||||
|
||||
headers = {
|
||||
'User-Agent': f'PAWS/{VERSION}',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -16,9 +16,9 @@ from aiohttp.client_exceptions import *
|
|||
from Crypto.PublicKey import RSA
|
||||
|
||||
from .signature import validate, pass_hash, sign_headers
|
||||
from .functions import error, user_check, domain_check, parse_sig, parse_ua, dig, distill_query
|
||||
from .functions import error, user_check, domain_check, parse_sig, parse_ua, dig, distill_query, get_nodeinfo, httpclient
|
||||
from .config import MASTOCONFIG, PAWSCONFIG, VERSION, script_path
|
||||
from .database import pawsdb, query, trans, ban_check, user_ban_check, banned_user_check, wl_check, keys, user_domain_ban_check
|
||||
from .database import pawsdb, query, trans, ban_check, user_ban_check, banned_user_check, wl_check, keys, user_domain_ban_check, admin_check
|
||||
|
||||
|
||||
paws_host = PAWSCONFIG['domain']
|
||||
|
@ -51,25 +51,34 @@ auth_paths = [
|
|||
|
||||
async def passthrough(request, headers):
|
||||
mastohost = PAWSCONFIG['mastohost']
|
||||
data = await request.read() if request.body_exists else None
|
||||
req_data = await request.read() if request.body_exists else None
|
||||
is_json = request.get('jsonreq', False)
|
||||
|
||||
try:
|
||||
async with aiohttp.request(request.method, f'http://{mastohost}{request.path}?{request.query_string}', headers=headers, data=data) as resp:
|
||||
data = await resp.read()
|
||||
resp = httpclient.request(request.method, f'http://{mastohost}{request.path}?{request.query_string}', body=req_data, headers=headers, redirect=False)
|
||||
#async with aiohttp.request(request.method, f'http://{mastohost}{request.path}?{request.query_string}', headers=headers, data=data) as resp:
|
||||
data = resp.data
|
||||
|
||||
if resp.status not in [200, 202]:
|
||||
if data in [b'Request not signed', 'Request not signed']:
|
||||
err_msg = 'Missing signature'
|
||||
logging.debug(err_msg)
|
||||
if resp.status not in [200, 202, 301]:
|
||||
if data in [b'Request not signed', 'Request not signed']:
|
||||
err_msg = 'Missing signature'
|
||||
logging.debug(err_msg)
|
||||
|
||||
else:
|
||||
err_msg = f'Recieved error {resp.status} from Mastodon'
|
||||
logging.debug(err_msg)
|
||||
else:
|
||||
err_msg = f'Recieved error {resp.status} from Mastodon'
|
||||
logging.debug(err_msg)
|
||||
|
||||
return error(request, resp.status, f'Failed to forward request')
|
||||
return error(request, resp.status, f'Failed to forward request')
|
||||
|
||||
return aiohttp.web.HTTPOk(body=data, content_type=resp.content_type)
|
||||
resp_headers = dict(resp.headers)
|
||||
|
||||
if resp_headers.get('Content-Encoding'):
|
||||
del resp_headers['Content-Encoding']
|
||||
|
||||
if resp_headers.get('Transfer-Encoding'):
|
||||
del resp_headers['Transfer-Encoding']
|
||||
|
||||
return aiohttp.web.Response(body=data, headers=resp_headers, status=resp.status)
|
||||
|
||||
except ClientConnectorError:
|
||||
traceback.print_exc()
|
||||
|
@ -89,6 +98,9 @@ async def http_filter(app, handler):
|
|||
real_ip = request.headers.get('X-Real-Ip', request.remote)
|
||||
ua_ip = dig(ua_domain)
|
||||
|
||||
nodeinfo = get_nodeinfo(domain)
|
||||
software = nodeinfo['name'] if nodeinfo else None
|
||||
|
||||
request['jsonreq'] = True if 'json' in request.headers.get('Accept', '') or request.path.endswith('.json') else False
|
||||
|
||||
# Disable rss feeds
|
||||
|
@ -97,6 +109,7 @@ async def http_filter(app, handler):
|
|||
|
||||
# add logged in user data to the request for the frontend
|
||||
request['user'] = user_data
|
||||
request['admin'] = admin_check(user_data['handle']) if user_data else None
|
||||
|
||||
if request.path in ['/paws/actor', '/paws/inbox', '/.well-known/webfinger'] and request.host != paws_host:
|
||||
return aiohttp.web.HTTPFound('/paws')
|
||||
|
|
|
@ -90,7 +90,7 @@ def login(user, code):
|
|||
|
||||
except:
|
||||
msg = 'Failed to fetch token'
|
||||
logging.info(msg)
|
||||
logging.error(msg)
|
||||
return ('error', msg)
|
||||
|
||||
try:
|
||||
|
@ -99,7 +99,7 @@ def login(user, code):
|
|||
|
||||
except:
|
||||
msg = 'Failed to get user info'
|
||||
logging.info(msg)
|
||||
logging.error(msg)
|
||||
return ('error', msg)
|
||||
|
||||
return (token, fetch_user)
|
||||
|
|
|
@ -8,7 +8,7 @@ from mastodon import Mastodon
|
|||
|
||||
from .config import PAWSCONFIG, MASTOCONFIG, VERSION, script_path
|
||||
from .functions import css_ts
|
||||
from .database import cleanup_users
|
||||
from .database import cleanup_users, admin_check
|
||||
from . import middleware, views, stor_path
|
||||
|
||||
|
||||
|
@ -44,7 +44,8 @@ def webserver():
|
|||
'domain': MASTOCONFIG['domain'],
|
||||
'VERSION': VERSION,
|
||||
'len': len,
|
||||
'urlparse': urlparse
|
||||
'urlparse': urlparse,
|
||||
'admin_check': admin_check
|
||||
})
|
||||
|
||||
return web
|
||||
|
|
|
@ -106,4 +106,12 @@ tr:not(.header):hover td a {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
/* Dropdown menus */
|
||||
.menu {
|
||||
background-color: var(--bg-color-dark);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
|
||||
{% include 'layout.css' %}
|
||||
|
|
|
@ -175,6 +175,39 @@ input[type=text]:focus {
|
|||
}
|
||||
|
||||
|
||||
/* Dropdown menu */
|
||||
.menu {
|
||||
position: fixed;
|
||||
display: block;
|
||||
right: 0;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
border-radius: 0 0 0 5px;
|
||||
}
|
||||
|
||||
.menu a {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.menu .title {
|
||||
font-weight: bold;
|
||||
font-size: 14pt;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.menu .item {
|
||||
padding: 5px 0;
|
||||
text-transform: uppercase;
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
.menu-right details[open] summary ~ * {
|
||||
animation: sweep-left .5s ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
/* mobile/small screen */
|
||||
@media (max-width : 1000px) {
|
||||
#content {
|
||||
|
|
|
@ -13,10 +13,34 @@
|
|||
%meta{'name': 'viewport', 'content': 'width=device-width, initial-scale=1, shrink-to-fit=no'}
|
||||
|
||||
%body
|
||||
%div{'class': 'menu menu-right'}
|
||||
%details
|
||||
%summary{'class': "title"}
|
||||
%a
|
||||
-if request.user:
|
||||
{{request.user.handle}}
|
||||
-else
|
||||
Guest
|
||||
|
||||
.item
|
||||
%a{'href': '/about', 'target': '_self'} Masto Home
|
||||
.item
|
||||
%a{'href': '/paws', 'target': '_self'} Paws Home
|
||||
-if request.user
|
||||
-if request.admin
|
||||
.item
|
||||
%a{'href': '/paws/whitelist', 'target': '_self'} Whitelist
|
||||
.item
|
||||
%a{'href': '/paws/instances', 'target': '_self'} Instances
|
||||
.item
|
||||
%a{'href': '/paws/logout', 'target': '_self'} Logout
|
||||
-else
|
||||
.item
|
||||
%a{'href': '/paws/login', 'target': '_self'} Login
|
||||
|
||||
#content
|
||||
#header
|
||||
%h1{'id': 'name'}<
|
||||
{{logo_svg}}
|
||||
%h1{'id': 'name'}= logo_svg
|
||||
|
||||
#body
|
||||
-block content
|
||||
|
|
|
@ -23,26 +23,20 @@ async def get_home(request):
|
|||
|
||||
|
||||
async def get_paws(request):
|
||||
token = request.cookies.get('paws_token')
|
||||
user_data = pawsdb.users.get(query.token == token)
|
||||
|
||||
admin = admin_check(user_data['handle']) if user_data else None
|
||||
|
||||
if admin:
|
||||
if request['admin']:
|
||||
whitelist = [line['domain'] for line in pawsdb.whitelist.all()]
|
||||
whitelist.sort()
|
||||
|
||||
else:
|
||||
whitelist = None
|
||||
|
||||
data = {'admin': admin, 'whitelist': whitelist}
|
||||
data = {'whitelist': whitelist}
|
||||
return aiohttpTemplate('panel.html', data, request)
|
||||
|
||||
|
||||
async def post_paws(request):
|
||||
data = await request.post()
|
||||
token = request.cookies.get('paws_token')
|
||||
user_data = pawsdb.users.get(query.token == token)
|
||||
user_data = request['user']
|
||||
domain = data.get('name')
|
||||
action = request.match_info['action']
|
||||
|
||||
|
@ -68,10 +62,9 @@ async def post_paws(request):
|
|||
async def get_login(request):
|
||||
parms = request.rel_url.query
|
||||
redir = parms.get('redir')
|
||||
token = request.cookies.get('paws_token')
|
||||
numid = random.randint(1*1000000, 10*1000000-1)
|
||||
|
||||
if token and pawsdb.users.get(query.token == token):
|
||||
if request['user']:
|
||||
return aiohttp.web.HTTPFound('/paws')
|
||||
|
||||
data = {'redir': redir, 'numid': numid}
|
||||
|
@ -104,7 +97,7 @@ async def post_login(request):
|
|||
with trans(pawsdb.users) as tr:
|
||||
tr.insert({
|
||||
'handle': data['numid'],
|
||||
'domain': data['domain'],
|
||||
'domain': data['domain'].lower(),
|
||||
'appid': appid,
|
||||
'appsecret': appsecret,
|
||||
'token': None,
|
||||
|
@ -146,9 +139,9 @@ async def get_auth(request):
|
|||
{
|
||||
'handle': userinfo['username'],
|
||||
'instance': instance,
|
||||
'token': token,
|
||||
'appid': None,
|
||||
'appsecret': None
|
||||
'appsecret': None,
|
||||
'token': token
|
||||
},
|
||||
where('handle') == numid
|
||||
)
|
||||
|
@ -161,10 +154,13 @@ async def get_auth(request):
|
|||
|
||||
|
||||
async def get_logout(request):
|
||||
token = request.cookies.get('paws_token')
|
||||
if not request['user']:
|
||||
return aiohttp.web.HTTPFound('/paws/login')
|
||||
|
||||
token_id = request['user'].doc_id
|
||||
|
||||
with trans(pawsdb.users) as tr:
|
||||
tr.remove(where('token') == token)
|
||||
tr.remove(doc_ids=[token_id])
|
||||
|
||||
response = aiohttp.web.HTTPFound('/paws/login')
|
||||
response.del_cookie('token')
|
||||
|
|
Loading…
Reference in a new issue