fix db connections, logging, token deletion, etc

This commit is contained in:
Izalia Mae 2020-02-26 13:54:06 -05:00
parent b7d400d774
commit e1756fe901
14 changed files with 289 additions and 215 deletions

View file

@ -118,6 +118,7 @@ def settings(data):
if k == 'port':
try:
new_data.update({k: int(v)})
except ValueError:
logging.warning(f'{v} is not a valid value for \'port\'')
@ -126,8 +127,7 @@ def settings(data):
put.config(new_data)
#logging.level = eval(f'logger.{data["log_level"]}')
#logging.debug('heck')
logging.setLevel(data["log_level"])
def ban(data):
domain = data['name']

View file

@ -54,6 +54,9 @@ else:
# Web forward config
SECRET={ranchars}
# Development mode
#UNCIA_DEV=False
'''
with open(envfile, 'w') as newenvfile:
@ -75,3 +78,4 @@ dbconfig = {
}
fwsecret = env.get('SECRET')
development = env.get('UNCIA_DEV')

View file

@ -6,17 +6,16 @@ from DBUtils.PooledPg import PooledPg
from pg import DatabaseError
from passlib.context import CryptContext
from ..config import dbconfig, script_path, stor_path
from ..log import logging
from ..config import dbconfig, script_path, stor_path
from ..functions import LRUCache
CONFIG = {}
class cache_dicts():
config = {}
key = LRUCache()
def baguette_path(path):
return path.replace('🥖', '/')
dbsql = baguette_path(f'{script_path}🥖database🥖database.sql')
dbcache = cache_dicts()
def dbconn(database, pooled=True):
@ -34,7 +33,7 @@ def dbconn(database, pooled=True):
if pooled:
cached = 5 if dbconfig['connum'] >= 5 else 0
return PooledPg(maxconnections=dbconfig['connum'], mincached=cached, maxusage=2, **options)
return PooledPg(maxconnections=dbconfig['connum'], mincached=cached, maxusage=1, **options)
else:
return pg.DB(**options)
@ -64,54 +63,34 @@ def db_check():
if 'skipdbcheck' not in sys.argv:
db_check()
newcon = dbconn(dbconfig['name'])
dbpool = dbconn(dbconfig['name'])
def connection(func):
def inner(*arg, **kwargs):
trans = newcon.connection()
def inner(*args, **kwargs):
conn = kwargs.get('db', dbpool.connection())
try:
result = func(*arg, **kwargs, db=trans)
result = func(*args, **kwargs, db=conn)
except:
result = None
traceback.print_exc()
trans.close()
conn.close()
return result
return inner
def newtrans(func):
def inner(*arg, **kwargs):
trans = newcon.connection()
trans.begin()
try:
result = func(*arg, **kwargs, db=trans)
except:
result = None
logging.error(f'DB command failed')
traceback.print_exc()
trans.end()
trans.close()
return result
return inner
@newtrans
@connection
def setup(db=None):
dbcheck = db.query('SELECT * FROM config WHERE key = \'setup\'').dictresult()
if dbcheck == []:
settings = {
'host': 'relaydev.barkshark.xyz',
'host': 'relay.example.com',
'address': '0.0.0.0',
'port': 3621,
'name': 'Uncia Relay',
@ -143,7 +122,8 @@ def query(table, data, one=True, sort=None, db=None):
row = db.query(f"SELECT * FROM {table} WHERE {k} = '{v}' {SORT}")
try:
return row.singledict() if one else row.dictresult()
result = row.dictresult()
return result[0] if one and len(result)> 0 else result
except pg.NoResultError:
return
@ -231,7 +211,7 @@ class HashContext:
def randomgen(chars=20):
if type(chars) != int:
logging.warning(f'Invalid character length. Must be an int: {chars}')
logging.warn(f'Invalid character length. Must be an int: {chars}')
chars = 20
return ''.join(random.choices(string.ascii_letters + string.digits, k=chars))
@ -251,4 +231,4 @@ def bool_check(value):
return value
__all__ = ['newtrans', 'connection', 'HashContext', 'randomgen', 'query', 'query_all', 'query_or', 'query_and']
__all__ = ['connection', 'HashContext', 'randomgen', 'query', 'query_all', 'query_or', 'query_and']

View file

@ -3,22 +3,20 @@ import pg
from Crypto.PublicKey import RSA
from . import *
from . import bool_check as bcheck, CONFIG
from . import bool_check as bcheck, dbcache
from ..log import logging
from ..functions import LRUCache
Hash = HashContext()
Hash.setsalt()
KEY = LRUCache()
auth_code = None
@connection
def rsa_key(actor, db=None):
cachedkey = KEY.fetch(actor)
def rsa_key(actor, db=None, cached=True):
if cached == True:
cachedkey = dbcache.key.fetch(actor)
if cachedkey:
return cachedkey
if cachedkey:
return cachedkey
actor_key = query('keys', {'actor': actor})
@ -42,22 +40,22 @@ def rsa_key(actor, db=None):
'PUBKEY': RSA.importKey(actor_key['pubkey'])
})
KEY.store(actor, actor_key)
dbcache.key.store(actor, actor_key)
return actor_key
@connection
def config(data, cache=True, db=None):
if len(CONFIG.keys()) < 1 and cache:
if len(dbcache.config.keys()) < 1 and cache:
update_config()
if type(data) == list or data == 'all':
settings = {}
if data == 'all':
if CONFIG and cache:
if dbcache.config and cache:
logging.debug('Returning cached config')
return CONFIG
return dbcache.config
rows = query_all('config')
@ -68,7 +66,7 @@ def config(data, cache=True, db=None):
for k,v in data.items():
if cache:
logging.debug('Returning cached config')
row = [{'key': key, 'value': value} for key, value in CONFIG.items()]
row = [{'key': key, 'value': value} for key, value in dbcache.config.items()]
else:
query_data = {'key': k, 'value': v}
@ -83,7 +81,7 @@ def config(data, cache=True, db=None):
return settings
elif type(data) == str:
cached = CONFIG.get(data)
cached = dbcache.config.get(data)
if cached and cache:
return cached
@ -92,7 +90,7 @@ def config(data, cache=True, db=None):
if row:
value = bcheck(row['value'])
CONFIG[data] = value
dbcache.config[data] = value
return value
@ -104,7 +102,7 @@ def update_config(db=None):
key = row['key']
value = bcheck(row['value'])
CONFIG[key] = value
dbcache.config[key] = value
@connection
@ -172,11 +170,11 @@ def user(data, db=None):
@connection
def token(data, db=None):
def token(data, *args, db=None, **kwargs):
if type(data) == str:
query_string = {'token': data}
if type(data) == int:
elif type(data) == int:
query_string = {'id': data}
elif type(data) == dict:
@ -191,6 +189,10 @@ def token(data, db=None):
logging.error(f'Invalid data for get.token: {data}')
return
else:
logging.warning(f'Invalid data type: {type(data)}')
return
return query('tokens', query_string)
@ -198,6 +200,10 @@ def token(data, db=None):
def verify_password(username, password, db=None):
user_data = user(username)
if not user_data:
logging.verbose(f'Invalid user when trying to verify password: {username}')
return
return Hash.verify(password, user_data['password'])
@ -211,16 +217,26 @@ def code(action=None):
# generate an auth code if there are no admin users
if len(user('all')) < 1:
users = user('all')
if not users or len(users) < 1:
code('regen')
host = config('host')
port = config('port')
address = config('address')
if config('setup'):
logging.warn(f'There are no admin users in the database. Please register an account at https://{host}/register?code={auth_code}')
address = '127.0.0.1' if address == '0.0.0.0' else address
else:
logging.warn(f'The relay is not configured. Please set it up at https://{host}/setup?code={auth_code}')
if host:
if config('setup'):
logging.warning(f'There are no admin users in the database. Please register an account at https://{host}/register?code={auth_code}')
else:
logging.warning(f'The relay is not configured. Please set it up at http://{address}:{port}/setup?code={auth_code}')
else:
auth_code = None
# Set log level from config
log_level = config('log_level')
logging.setLevel(log_level)

View file

@ -4,7 +4,7 @@ import ujson as json
from datetime import datetime
from . import *
from . import get, bool_check, CONFIG
from . import get, bool_check, dbcache
from ..log import logging
from ..functions import format_urls
@ -13,43 +13,42 @@ Hash = HashContext()
Hash.setsalt()
@newtrans
@connection
def config(data, db=None):
configs = get.config('all', cache=False)
db.begin()
for k,v in data.items():
value = bool_check(v)
row = query('config', {'key': k})
if configs and configs.get(k) == value:
continue
data = {
'key': k,
'value': value
}
if not configs or k not in configs:
data = {
'key': k,
'value': value
}
if row:
data['id'] = row['id']
db.insert('config', data)
else:
row = query('config', {'key': k})
db.update('config', {'value': value}, id=row['id'])
db.upsert('config', data)
db.end()
get.update_config()
@newtrans
@connection
def rsa_key(name, keys, db=None):
actor_key = get.rsa_key('default')
key = {
'actor': name,
'pubkey': keys['pubkey'],
'privkey': keys['privkey']
}
if not actor_key:
db.update('keys', {
'pubkey': keys['pubkey'],
'privkey': keys['privkey']
}, actor='default')
if db.upsert('keys', key, actor=name):
dbcache.key.store(name, get.rsa_key(name, cached=False))
return True
@newtrans
@connection
def inbox(action, urls, timestamp=None, db=None):
actor, inbox, domain = format_urls(urls)
row = get.inbox(actor)
@ -76,7 +75,7 @@ def inbox(action, urls, timestamp=None, db=None):
return True
@newtrans
@connection
def request(action, urls, followid=None, db=None):
actor, inbox, domain = format_urls(urls)
row = get.request(domain)
@ -102,7 +101,7 @@ def request(action, urls, followid=None, db=None):
return True
@newtrans
@connection
def add_retry(msgid, inbox, data, headers, db=None):
row = get.retries({'msgid': msgid, 'inbox': inbox})
@ -121,7 +120,7 @@ def add_retry(msgid, inbox, data, headers, db=None):
return True
@newtrans
@connection
def del_retries(data, db=None):
if type(data) == int:
rows = [get.retries(data)]
@ -138,7 +137,7 @@ def del_retries(data, db=None):
return True
@newtrans
@connection
def ban(action, data, reason=None, db=None):
if '@' in data:
if data.startswith('@'):
@ -176,8 +175,8 @@ def ban(action, data, reason=None, db=None):
return True if db.delete(bantype, id=row['id']) else False
@newtrans
def whitelist(action, data, reason=None, db=None):
@connection
def whitelist(action, data, db=None):
domain = urlparse(data).netloc if data.startswith('https://') else data
row = get.whitelist(domain)
@ -201,7 +200,7 @@ def whitelist(action, data, reason=None, db=None):
db.delete('whitelist', id=row['id'])
@newtrans
@connection
def user(username, password, db=None):
handle = username.lower()
timestamp = datetime.now().timestamp()
@ -219,7 +218,7 @@ def user(username, password, db=None):
return db.insert('users', data)
@newtrans
@connection
def del_user(token=None, username=None, db=None):
if not username and not token:
return
@ -243,7 +242,7 @@ def del_user(token=None, username=None, db=None):
db.delete('users', id=userid)
@newtrans
@connection
def token(username, db=None):
userdata = get.user(username)
@ -259,17 +258,18 @@ def token(username, db=None):
return db.insert('tokens', tokendata)
@newtrans
@connection
def del_token(token, db=None):
row = get.token(token)
if not row:
return
db.delete('tokens', id=row['id'])
if db.delete('tokens', id=row['id']):
return True
@newtrans
@connection
def acct_name(handle, username, db=None):
data = {'username': username}
user = get.user(handle)
@ -282,7 +282,7 @@ def acct_name(handle, username, db=None):
return True
@newtrans
@connection
def password(handle, password, db=None):
user = get.user(handle)

View file

@ -27,9 +27,13 @@
{{token.timestamp}}
%td{'class': 'col2'}
%form{'action': 'https://{{config.host}}/account/token', 'method': 'post'}
%input{'type': 'hidden', 'name': 'token', 'value': '{{token.token}}'}
%input{'type': 'submit', 'value': 'Delete'}
-if current != 'active'
%form{'action': 'https://{{config.host}}/account/token', 'method': 'post'}
%input{'type': 'hidden', 'name': 'token', 'value': '{{token.token}}'}
%input{'type': 'submit', 'value': 'Delete'}
-else
n/a
%div{'class': 'section account profile'}
%h2{'class': 'title'} Display Name

View file

@ -349,7 +349,7 @@
%label Log Level
%select{'name': 'log_level'}
- for level in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
- for level in ['MERP', 'DEBUG', 'VERB', 'INFO', 'WARN', 'ERROR', 'CRIT']
-if config.log_level == level
%option{'value': '{{level}}', selected: None}
{{level}}

View file

@ -2,12 +2,13 @@
- set title = 'Setup'
- block content
- if config.setup
-if config.setup
%div{'class': 'section setup'}
%p{'class': 'sec-header'} Setup
%p<
The relay has been successfully setup. Please start the relay again and setup an admin account via the register url that shows up in the console log
-else
%div{'class': 'section setup'}
%p{'class': 'sec-header'} Setup
%p<

View file

@ -3,26 +3,102 @@ import logging as logger
import logging.config as logconf
from os import environ as env
from datetime import datetime
def verb(self, message, *args, **kws):
if self.isEnabledFor(15):
self._log(15, message, args, **kws)
LOG_BASE = '%(asctime)s %(process)d %(levelname)s'
DATE_FMT = "%Y-%m-%d %H:%M:%S" if not env.get('INVOCATION_ID') else ''
# Custom logger
class Log():
def __init__(self, minimum='INFO', datefmt='%Y-%m-%d %H:%M:%S', date=True):
self.levels = {
'CRIT': 60,
'ERROR': 50,
'WARN': 40,
'INFO': 30,
'VERB': 20,
'DEBUG': 10,
'MERP': 0
}
self.datefmt = datefmt
self.minimum = self._lvlCheck(minimum)
# make sure the minimum logging level is an int
def _lvlCheck(self, level):
try:
value = int(level)
except ValueError:
value = self.levels.get(level)
if value not in self.levels.values():
raise InvalidLevel(f'Invalid logging level: {level}')
return value
def setLevel(self, level):
self.minimum = self._lvlCheck(level)
def log(self, level, msg):
levelNum = self._lvlCheck(level)
if type(level) == int:
for k,v in self.levels.items():
if v == levelNum:
level = k
if levelNum < self.minimum:
return
date = datetime.now().strftime(self.datefmt)
output = f'{date} {level}: {msg}\n'
stdout = sys.stdout
stdout.write(output)
stdout.flush()
def critical(self, msg):
self.log('CRIT', msg)
def error(self, msg):
self.log('ERROR', msg)
def warning(self, msg):
self.log('WARN', msg)
def info(self, msg):
self.log('INFO', msg)
def verbose(self, msg):
self.log('VERB', msg)
def debug(self, msg):
self.log('DEBUG', msg)
def merp(self, msg):
self.log('MERP', msg)
class InvalidType(Exception):
'''Raise when the log level isn't a str or an int'''
class InvalidLevel(Exception):
'''Raise when an invalid logging level was specified'''
# Set logger for sanic
LOG = dict(
version=1,
disable_existing_loggers=False,
loggers={
"sanic.root": {
"level": 'INFO',
"level": 'CRITICAL',
"handlers": ["console"],
"propagate": False,
},
"sanic.error": {
"level": "INFO",
"level": "CRITICAL",
"handlers": ["error_console"],
"propagate": False,
"qualname": "sanic.error",
@ -32,7 +108,7 @@ LOG = dict(
handlers={
"console": {
"class": "logging.StreamHandler",
'level': 'INFO',
'level': 'CRITICAL',
"formatter": "generic",
"stream": sys.stdout,
},
@ -44,16 +120,13 @@ LOG = dict(
},
formatters={
"generic": {
"format": F"{LOG_BASE} %(message)s",
"datefmt": DATE_FMT,
"format": f"%(asctime)s %(process)d %(levelname)s %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S" if not env.get('INVOCATION_ID') else '',
"class": "logging.Formatter",
},
},
)
logconf.dictConfig(LOG)
logger.addLevelName(15, 'VERBOSE')
logger.Logger.verb = verb
logger.Logger.verbose = verb
logging = logger.getLogger("sanic.root")
logging = Log()

View file

@ -10,7 +10,7 @@ from .database import get, put
from .config import version, pyv
host = get.config('host', 'relaydev.barkshark.xyz')
host = get.config('host')
def defhead():
@ -239,21 +239,19 @@ def push(inbox, data, headers, *args):
response = httpclient.request('POST', inbox, body=body, headers=headers)
if response.status not in [200, 202]:
logging.warning(f'Failed to push to {inbox}: Error {response.status}')
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]:
logging.debug(f'Adding message to retries: {inbox}')
put.add_retry(url, inbox, data, orig_head)
else:
logging.debug(f'Successfully sent message to {inbox}')
logging.verbose(f'Successfully sent message to {inbox}')
put.del_retries({'inbox': inbox, 'msgid': url})
return True
except Exception as e:
logging.warning(f'Connection error when pushing to {inbox}: {e}')
logging.debug(f'Adding message to retries: {inbox}')
logging.verbose(f'Connection error when pushing to {inbox}: {e}')
put.add_retry(url, inbox, data, orig_head)

View file

@ -1,40 +1,63 @@
#!/usr/bin/env python3
import sys, ujson as json
import time, sys, ujson as json
from json.decoder import JSONDecodeError
from os.path import isfile, abspath
from datetime import datetime
from tinydb import TinyDB, Query
from configobj import ConfigObj
from urllib.parse import urlparse
from .config import stor_path
from .database import db as pgdb, get, put
from .database import get, put
oldpath = abspath('config')
if 'pleroma' in sys.argv:
import yaml
try:
with open('relay.yaml') as f:
config = yaml.load(f, Loader=yaml.SafeLoader)
try:
db = TinyDB(f'{oldpath}/db.json', ensure_ascii=False, escape_forward_slashes=False, indent=4)
except Exception as e:
print(f'Failed to open "relay.yaml": {e}')
sys.exit()
except JSONDecodeError as e:
print(f'Failed to load DB: {e}')
print(f'Exiting...')
dbfile = config['db']
domainbans = config['ap']['blocked_instances']
whitelist = config['ap']['whitelist']
settings = {
'address': config['listen'],
'port': config['port'],
'host': config['ap']['host'],
'whitelist': config['ap']['whitelist_enabled']
}
try:
jsondb = json.load(open(dbfile))
except Exception as e:
print(f'Failed to open "{dbfile}": {e}')
sys.exit()
inboxes = jsondb['relay-list']
key = {
'privkey': jsondb['actorKeys']['privateKey'],
'pubkey': jsondb['actorKeys']['publicKey']
}
else:
print('heck')
sys.exit()
query = Query()
class table:
inbox = db.table('inbox')
key = db.table('key')
message = db.table('message')
fail = db.table('fail')
discon = db.table('discon')
follows = db.table('follows')
# migrate inboxes
for row in inboxes:
if type(row) == str:
domain = urlparse(row).netloc
row = {
'actor': f'https://{domain}/actor',
'inbox': f'https://{domain}/inbox',
'domain': domain
}
for row in table.inbox.all():
if not get.inbox(row['domain']):
urls = {
'actor': row['actor'],
@ -46,63 +69,20 @@ for row in table.inbox.all():
put.inbox('add', urls, timestamp=timestamp)
db_actor_key = table.key.get(query.user == 'relay')
if not db_actor_key:
print('heck')
else:
actor_key_data = {
'actor': 'default',
'pubkey': db_actor_key['pubkey'],
'privkey': db_actor_key['privkey']
}
pgdb.upsert('keys', actor_key_data, actor='default')
# migrate actor key
put.rsa_key('default', key)
configfile = f'{oldpath}/config.ini'
listfile = f'{oldpath}/lists.ini'
infofile = f'{oldpath}/templates/info.md'
rulesfile = f'{oldpath}/templates/rules.md'
# migrate config
put.config(settings)
if isfile(infofile):
info = open(infofile).read()
if isfile(rulesfile):
rules = open(rulesfile).read()
if isfile(configfile):
config = ConfigObj(configfile)
cfg = {
'address': config['network']['listen'],
'port': config['network']['port'],
'host': config['network']['host'],
'name': config['settings']['name'],
'admin': config['settings']['admin_acct'],
'email': config['settings']['email'],
'show_domainblocks': config['settings']['show_instance_blocks'],
'show_userblocks': config['settings']['show_user_blocks'],
'whitelist': config['settings']['whitelist_enabled'],
'require_approval': config['settings']['require_approval'],
'block_relays': config['settings']['block_relays'],
'notifications': config['settings']['notif'],
'log_level': config['internals']['log_level'],
'info': info,
'rules': rules,
'setup': False
}
put.config(cfg)
# migrate domain bans
for domain in domainbans:
put.ban('add', domain)
if isfile(listfile):
config = ConfigObj(listfile)
for domain in config['blocked_instances']:
put.ban('add', domain)
for user in config['blocked_users']:
put.ban('add', user)
# migrate whitelist
for domain in whitelist:
put.whitelist('add', domain)

View file

@ -8,8 +8,8 @@ from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from .log import logging, LOG
from .config import script_path, fwsecret
from .database import get, setup, randomgen
from .config import script_path, fwsecret, development
from .database import get, setup
from .messages import run_retries
from .admin import bool_check
from .templates import build_templates
@ -72,14 +72,11 @@ class WatchHandler(FileSystemEventHandler):
build_templates()
def start_template_watcher():
def setup_template_watcher():
tplpath = f'{script_path}/frontend/templates'
observer = Observer()
observer.schedule(WatchHandler(), tplpath, recursive=False)
logging.info('Starting template watcher')
observer.start()
return observer
@ -97,8 +94,14 @@ def main():
dbport = int(get.config('port'))
build_templates()
observer = start_template_watcher()
observer = setup_template_watcher()
if bool_check(development):
logging.info('Starting template watcher')
observer.start()
logging.info(f'Starting Uncia at {dblisten}:{dbport}')
app.run(host=dblisten, port=dbport, workers=1, debug=False, access_log=False)
logging.info('Stopping template watcher')
observer.stop()

View file

@ -73,8 +73,7 @@ def sign_sigstring(sigstring, key, hashalg='SHA256'):
if not sign_key:
return
privkey = RSA.importKey(sign_key)
pkcs = PKCS1_v1_5.new(privkey)
pkcs = PKCS1_v1_5.new(sign_key['PRIVKEY'])
h = HASHES[hashalg.lower()].new()
h.update(sigstring.encode('ascii'))

View file

@ -224,8 +224,7 @@ class Home(HTTPMethodView):
class Faq(HTTPMethodView):
async def get(self, request):
data = {'msg': 'UvU', 'instances': {}}
return render('faq.html', request, data)
return render('faq.html', request, {})
class Admin(HTTPMethodView):
@ -293,9 +292,9 @@ class Account(HTTPMethodView):
if action == 'delete':
if None in [password, token, user]:
return response.redirect('/account')
return self.get(request, msg='Missing password, token, or username')
print(put.del_user(token))
put.del_user(token)
resp = response.redirect('/')
del resp.cookies['token']
return resp
@ -327,6 +326,18 @@ class Account(HTTPMethodView):
else:
return await self.get(request, msg='Failed to update display name')
if action == 'token':
form_token = request['form'].get('token')
if not form_token:
return await self.get(request, msg='Failed to provide token to delete')
if put.del_token(form_token):
return await self.get(request, msg='Deleted token')
else:
return await self.get(request, msg='Failed to delete token')
return response.redirect('/account')
@ -358,15 +369,20 @@ class Login(HTTPMethodView):
password = request['form'].get('password')
if None in [username, password]:
return await reterror(Login, request, 'Missing username or password')
return await self.get(request, msg='Missing username or password')
if not get.user(username):
return await self.get(request, msg='Invalid username')
if not get.verify_password(username, password):
return await reterror(Login, request, 'Invalid username or password')
return await self.get(request, msg='Invalid password')
tokendata = put.token(username)
if not tokendata:
return await reterror(Login, request, 'Failed to create token')
return await self.get(request, msg='Failed to create token')
print(tokendata)
resp = response.redirect('/admin')
resp.cookies['token'] = tokendata['token']