renaming keys
This commit is contained in:
parent
912046663c
commit
5ebc9ed080
12
config.ini
12
config.ini
|
@ -1,5 +1,5 @@
|
|||
# example stargazer config, nothing special here
|
||||
listen = 127.0.0.1
|
||||
# listening on 0.0.0.0 binds to all interfaces, while 127.0.0.1 only to localhost
|
||||
listen = 0.0.0.0
|
||||
|
||||
[:tls]
|
||||
store = certs
|
||||
|
@ -10,3 +10,11 @@ root = public
|
|||
[localhost:/cgi]
|
||||
root = public
|
||||
cgi = on
|
||||
|
||||
# repeated configuration for your domain
|
||||
[example.com]
|
||||
root = public
|
||||
|
||||
[example.com:/cgi]
|
||||
root = public
|
||||
cgi = on
|
|
@ -4,6 +4,8 @@ After linked, key cannot be unlinked. It can only be deleted.
|
|||
You need to have at least one key linked.
|
||||
When you link new key to the existing account, a random token is generated for authentication for short period of time. That token is burned after use.
|
||||
You can request your account to be deleted. To proceed, you have to solve simple anti-csrf challenge.
|
||||
When the key is linked with account, a display name is being automatically set from certificate's issuer field.
|
||||
You can change display name of your key. Because of nature of Gemini query strings, it's a two-step process: first, the key hash is temporary stored in database, actual renaming happens in the second step.
|
||||
|
||||
Access points:
|
||||
/cgi - cgi scripts root
|
||||
|
@ -35,6 +37,18 @@ Access points:
|
|||
match
|
||||
empty - redirect to index.gmi
|
||||
anticsrf+hash - verify & redirect to index.gmi | warning
|
||||
/rename-request.gmi
|
||||
no CC - request CC
|
||||
mismatch - invalid CC
|
||||
match
|
||||
empty - which key would you like to rename?
|
||||
anticsrf+hash - verify & redirect to rename.gmi | warning
|
||||
/rename.gmi
|
||||
no CC - request CC
|
||||
mismatch - invalid CC
|
||||
match
|
||||
empty - choose your name for "[old name]" | redirect to rename-request.gmi
|
||||
string - rename & redirect to index.gmi
|
||||
/delete.gmi - delete your account
|
||||
no CC - request CC
|
||||
mismatch - invalid CC
|
||||
|
@ -53,7 +67,8 @@ CREATE TABLE IF NOT EXISTS users (
|
|||
request_delete VARCHAR(16),
|
||||
request_delete_time INTEGER,
|
||||
anticsrf VARCHAR(4),
|
||||
anticsrf_time INTEGER
|
||||
anticsrf_time INTEGER,
|
||||
request_rename VARCHAR(255)
|
||||
)
|
||||
CREATE TABLE IF NOT EXISTS keys (
|
||||
hash VARCHAR(255) PRIMARY KEY,
|
||||
|
|
|
@ -20,11 +20,18 @@ class auth:
|
|||
ANTIC_EXPIRE = 60*60*24
|
||||
|
||||
hash = None
|
||||
certName = None
|
||||
username = None
|
||||
anticsrf = False
|
||||
|
||||
# User row cache. It's enough to ask database once and update cache only when asked for outdated or missing columns.
|
||||
user = {}
|
||||
userOutdated = []
|
||||
anticsrf = False
|
||||
|
||||
# User keys cache indexed by hashes
|
||||
keys = {}
|
||||
keysOutdated = ["all"]
|
||||
# getKeys always returns all keys owned by user.
|
||||
|
||||
def __init__(self, dbFile):
|
||||
"""
|
||||
|
@ -45,7 +52,8 @@ class auth:
|
|||
request_delete VARCHAR(16),
|
||||
request_delete_time INTEGER,
|
||||
anticsrf VARCHAR(4),
|
||||
anticsrf_time INTEGER
|
||||
anticsrf_time INTEGER,
|
||||
request_rename VARCHAR(255)
|
||||
)
|
||||
""")
|
||||
self.cur.execute("""
|
||||
|
@ -59,21 +67,27 @@ class auth:
|
|||
)
|
||||
""")
|
||||
|
||||
# TODO: database migration
|
||||
# self.migrateDatabase()
|
||||
self.garbageCollector()
|
||||
|
||||
def garbageCollector(self):
|
||||
"""
|
||||
delete all unlinked keys
|
||||
delete all unlinked keys and expired tokens
|
||||
"""
|
||||
# garbageCollector is intended to run before caching initialization
|
||||
self.cur.execute("DELETE FROM keys WHERE user IS NULL")
|
||||
self.cur.execute("UPDATE users SET link_token = NULL, link_token_time = NULL WHERE link_token_time + ? - strftime('%s') <= 0", (self.LINK_EXPIRE, ))
|
||||
# field request_rename expire together with anticsrf
|
||||
self.cur.execute("UPDATE users SET anticsrf = NULL, anticsrf_time = NULL, request_rename = NULL WHERE anticsrf_time + ? - strftime('%s') <= 0", (self.ANTIC_EXPIRE, ))
|
||||
self.con.commit()
|
||||
|
||||
def passKey(self, hash):
|
||||
def passKey(self, hash, name=None):
|
||||
"""
|
||||
pass given key to the object
|
||||
"""
|
||||
self.hash = hash
|
||||
self.certName = name
|
||||
|
||||
key = self.fetchKey()
|
||||
if (not key):
|
||||
|
@ -92,19 +106,48 @@ class auth:
|
|||
return None
|
||||
|
||||
res = self.cur.execute("SELECT keys.*, users.name as username FROM users, keys WHERE users.id = keys.user AND keys.hash = ?", (self.hash, ))
|
||||
return res.fetchone()
|
||||
res = res.fetchone()
|
||||
|
||||
def getKeys(self):
|
||||
if (res):
|
||||
self.keys[self.hash] = res.copy()
|
||||
del self.keys[self.hash]['username']
|
||||
del self.keys[self.hash]['hash']
|
||||
|
||||
return res
|
||||
|
||||
def getKeys(self, require=[]):
|
||||
"""
|
||||
get all keys of current user
|
||||
"require" argument controls which fields must be up to date - if ommited, there are no requirements!
|
||||
"""
|
||||
if (not self.username):
|
||||
return None
|
||||
|
||||
res = self.cur.execute("SELECT * FROM users, keys WHERE users.id = keys.user AND users.name = ?", (self.username, ))
|
||||
return res.fetchall()
|
||||
outdated = False
|
||||
if ('all' not in self.keysOutdated):
|
||||
for key in require:
|
||||
if (key in self.keysOutdated):
|
||||
outdated = True
|
||||
break
|
||||
else:
|
||||
self.keysOutdated.remove('all')
|
||||
outdated = True
|
||||
|
||||
def userInfo(self, key):
|
||||
if (outdated):
|
||||
res = self.cur.execute("SELECT keys.* FROM users, keys WHERE users.id = keys.user AND users.name = ?", (self.username, ))
|
||||
res = res.fetchall()
|
||||
|
||||
self.keys = {}
|
||||
for key in res:
|
||||
self.keys[key['hash']] = key
|
||||
del key['hash']
|
||||
self.keysOutdated.clear()
|
||||
|
||||
if (DEBUG):
|
||||
logging.warning({"keys": self.keys, "keysOutdated": self.keysOutdated})
|
||||
return self.keys
|
||||
|
||||
def userInfo(self, column):
|
||||
"""
|
||||
get user row from database/cache
|
||||
"""
|
||||
|
@ -112,24 +155,36 @@ class auth:
|
|||
return None
|
||||
|
||||
if (DEBUG):
|
||||
logging.warning({"user": self.user, "userOutdated": self.userOutdated, "key": key})
|
||||
logging.warning({"user": self.user, "userOutdated": self.userOutdated, "requested": column})
|
||||
|
||||
if (key in self.user and key not in self.userOutdated):
|
||||
return self.user[key]
|
||||
if (column in self.user and column not in self.userOutdated):
|
||||
return self.user[column]
|
||||
|
||||
res = self.cur.execute("SELECT * FROM users WHERE users.name = ?", (self.username, ))
|
||||
self.user = res.fetchone()
|
||||
self.userOutdated.clear()
|
||||
return self.user[key]
|
||||
return self.user[column]
|
||||
|
||||
def updateUserInfo(self, key, value):
|
||||
def updateUserInfo(self, column, value):
|
||||
"""
|
||||
handy function for quick update self.user and self.userOutdated
|
||||
for outdating it's enough to self.userOutdated.append(key)
|
||||
"""
|
||||
self.user[key] = value
|
||||
while (key in self.userOutdated): # remove duplicates
|
||||
self.userOutdated.remove(key)
|
||||
self.user[column] = value
|
||||
while (column in self.userOutdated): # remove duplicates
|
||||
self.userOutdated.remove(column)
|
||||
|
||||
def updateKeyInfo(self, hash, column, value):
|
||||
"""
|
||||
handy function (at the moment almost inevitable) for quick update self.keys and self.keysOutdated
|
||||
for outdating it's enough to self.keysOutdated.append(key)
|
||||
"""
|
||||
if (hash not in self.keys):
|
||||
self.keys[hash] = {}
|
||||
|
||||
self.keys[hash][column] = value
|
||||
while (column in self.keysOutdated): # remove duplicates
|
||||
self.keysOutdated.remove(column)
|
||||
|
||||
def genAnticSRF(self):
|
||||
"""
|
||||
|
@ -156,7 +211,6 @@ class auth:
|
|||
"""
|
||||
check antic cross-site request forgery token validity
|
||||
Remider: there's one token per session
|
||||
TODO: anticsrf expiration
|
||||
"""
|
||||
if (not self.username):
|
||||
return None
|
||||
|
@ -173,13 +227,13 @@ class auth:
|
|||
def registerKey(self):
|
||||
"""
|
||||
insert new key into database (will be deleted if not linked)
|
||||
TODO: autoset common name
|
||||
"""
|
||||
if (not self.hash):
|
||||
return None
|
||||
|
||||
self.cur.execute("INSERT INTO keys (hash) VALUES (?)", (self.hash, ))
|
||||
self.cur.execute("INSERT INTO keys (hash, name) VALUES (?, ?)", (self.hash, self.certName))
|
||||
# unlinked key is deleted anyway, so why don't wait with commit until linking and avoid unneccessary disk IO?
|
||||
self.updateKeyInfo(self.hash, 'name', self.certName)
|
||||
|
||||
def updateKey(self):
|
||||
"""
|
||||
|
@ -190,6 +244,7 @@ class auth:
|
|||
|
||||
self.cur.execute("UPDATE keys SET last_seen = strftime('%s') WHERE hash = ?", (self.hash, ))
|
||||
self.con.commit()
|
||||
self.keysOutdated.append('last_seen')
|
||||
|
||||
SUCCESS = 0
|
||||
NAME_IN_USE = 1
|
||||
|
@ -218,6 +273,7 @@ class auth:
|
|||
# now the key is protected from autodeletion
|
||||
self.con.commit()
|
||||
self.username = username
|
||||
self.updateKeyInfo(self.hash, 'user', uid)
|
||||
|
||||
return self.SUCCESS
|
||||
|
||||
|
@ -273,6 +329,7 @@ class auth:
|
|||
self.cur.execute("UPDATE keys SET user = ? WHERE hash = ?", (res['id'], self.hash))
|
||||
self.burnLink(res['name'])
|
||||
self.con.commit()
|
||||
self.updateKeyInfo(self.hash, 'user', res['id'])
|
||||
self.username = res['name']
|
||||
return self.SUCCESS
|
||||
else:
|
||||
|
@ -288,12 +345,45 @@ class auth:
|
|||
if (hash == self.hash):
|
||||
return self.KEY_IN_USE
|
||||
|
||||
myKeys = self.getKeys()
|
||||
for key in myKeys:
|
||||
if (key['hash'] == hash):
|
||||
self.cur.execute("DELETE FROM keys WHERE hash = ?", (hash, ))
|
||||
self.con.commit()
|
||||
return self.SUCCESS
|
||||
if (hash in self.getKeys()):
|
||||
self.cur.execute("DELETE FROM keys WHERE hash = ?", (hash, ))
|
||||
self.con.commit()
|
||||
del self.keys[hash]
|
||||
return self.SUCCESS
|
||||
|
||||
return self.NOT_FOUND
|
||||
|
||||
def requestRename(self, hash):
|
||||
"""
|
||||
prepare for changing name of given key
|
||||
"""
|
||||
if (not self.username):
|
||||
return None
|
||||
|
||||
if (hash in self.getKeys()):
|
||||
self.cur.execute("UPDATE users SET request_rename = ? WHERE name = ?", (hash, self.username))
|
||||
self.con.commit()
|
||||
self.user['request_rename'] = hash
|
||||
return self.SUCCESS
|
||||
|
||||
return self.NOT_FOUND
|
||||
|
||||
def renameKey(self, name):
|
||||
"""
|
||||
Change the display name of given key
|
||||
"""
|
||||
if (not self.username):
|
||||
return None
|
||||
|
||||
hash = self.userInfo('request_rename')
|
||||
# key hash in database is probably good, but's better safe than sorry
|
||||
if (hash in self.getKeys()):
|
||||
self.cur.execute("UPDATE keys SET name = ? WHERE hash = ?", (name, hash))
|
||||
self.cur.execute("UPDATE users SET request_rename = NULL WHERE name = ?", (self.username, ))
|
||||
self.con.commit()
|
||||
self.updateKeyInfo(hash, 'name', name)
|
||||
self.updateUserInfo('request_rename', None)
|
||||
return self.SUCCESS
|
||||
|
||||
return self.NOT_FOUND
|
||||
|
||||
|
@ -302,6 +392,11 @@ if (__name__ == '__main__'):
|
|||
import sys
|
||||
if (len(sys.argv) > 1):
|
||||
auth(sys.argv[1])
|
||||
print({"enableRegistration": auth.ENABLE_REGISTRATION})
|
||||
print({
|
||||
"enableRegistration": auth.ENABLE_REGISTRATION,
|
||||
"linkExpire": auth.LINK_EXPIRE,
|
||||
"anticExpire": auth.ANTIC_EXPIRE,
|
||||
"debug": DEBUG
|
||||
})
|
||||
else:
|
||||
print('Database file not specified')
|
|
@ -10,12 +10,13 @@ if (not hash):
|
|||
# no CC
|
||||
print('60 Authentication is required\r\n')
|
||||
exit()
|
||||
certName = os.environ.get('REMOTE_USER')
|
||||
|
||||
print('20 text/gemini\r\n')
|
||||
|
||||
from auth import auth
|
||||
auth = auth('data/data.db')
|
||||
auth.passKey(hash)
|
||||
auth.passKey(hash, certName)
|
||||
|
||||
if (not auth.username):
|
||||
# mismatch
|
||||
|
@ -28,8 +29,9 @@ else:
|
|||
print('Hello, {}!'.format(auth.username))
|
||||
print('## Your keys')
|
||||
|
||||
for key in auth.getKeys():
|
||||
hash = key['hash']
|
||||
myKeys = auth.getKeys(['last_seen'])
|
||||
for hash in myKeys:
|
||||
key = myKeys[hash]
|
||||
lastSeen = datetime.fromtimestamp(key['last_seen'])
|
||||
current = hash == auth.hash
|
||||
|
||||
|
@ -43,6 +45,7 @@ else:
|
|||
|
||||
print('hash:', hash)
|
||||
print('last seen:', lastSeen)
|
||||
print('=> rename-request.gmi?{} rename'.format(auth.genAnticSRF() + hash))
|
||||
if (not current):
|
||||
print('=> unlink.gmi?{} unlink'.format(auth.genAnticSRF() + hash))
|
||||
|
||||
|
@ -59,4 +62,5 @@ else:
|
|||
print('Token {} will expire in {}:{}'.format(linkToken, zero(minutes), zero(seconds)))
|
||||
print('=> link.gmi?cancel cancel')
|
||||
else:
|
||||
print('---')
|
||||
print('=> link.gmi link new key')
|
|
@ -10,10 +10,11 @@ if (not hash):
|
|||
# no CC
|
||||
print('60 Authentication is required\r\n')
|
||||
exit()
|
||||
certName = os.environ.get('REMOTE_USER')
|
||||
|
||||
from auth import auth
|
||||
auth = auth('data/data.db')
|
||||
auth.passKey(hash)
|
||||
auth.passKey(hash, certName)
|
||||
|
||||
query = os.environ.get('QUERY_STRING')
|
||||
|
||||
|
|
|
@ -10,10 +10,11 @@ if (not hash):
|
|||
# no CC
|
||||
print('60 Authentication is required\r\n')
|
||||
exit()
|
||||
certName = os.environ.get('REMOTE_USER')
|
||||
|
||||
from auth import auth
|
||||
auth = auth('data/data.db')
|
||||
auth.passKey(hash)
|
||||
auth.passKey(hash, certName)
|
||||
|
||||
if (auth.username):
|
||||
# match
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
#!/usr/bin/python3
|
||||
import os
|
||||
import sys
|
||||
|
||||
os.chdir('..')
|
||||
sys.path.append('lib')
|
||||
|
||||
hash = os.environ.get('TLS_CLIENT_HASH')
|
||||
if (not hash):
|
||||
# no CC
|
||||
print('60 Authentication is required\r\n')
|
||||
exit()
|
||||
certName = os.environ.get('REMOTE_USER')
|
||||
|
||||
from auth import auth
|
||||
auth = auth('data/data.db')
|
||||
auth.passKey(hash, certName)
|
||||
|
||||
if (not auth.username):
|
||||
# mismatch
|
||||
print('61 Unknown key\r\n')
|
||||
else:
|
||||
# match
|
||||
query = os.environ.get('QUERY_STRING')
|
||||
|
||||
if (not query):
|
||||
# empty
|
||||
from datetime import datetime
|
||||
print('20 text/gemini\r\n')
|
||||
print('Which key would you like to rename?')
|
||||
|
||||
myKeys = auth.getKeys(['last_seen'])
|
||||
for hash in myKeys:
|
||||
key = myKeys['hash']
|
||||
lastSeen = datetime.fromtimestamp(key['last_seen'])
|
||||
current = hash == auth.hash
|
||||
|
||||
name = key['name']
|
||||
name = '"' + name + '"' if name else '[no name]'
|
||||
|
||||
label = '=> rename-request.gmi?{} {}'.format(auth.genAnticSRF() + hash, name)
|
||||
if (current):
|
||||
label += ' (currently used)'
|
||||
print(label)
|
||||
|
||||
print('hash:', hash)
|
||||
print('last seen:', lastSeen)
|
||||
else:
|
||||
anticsrf = query[:4]
|
||||
hash = query[4:]
|
||||
|
||||
if (not anticsrf or not hash):
|
||||
print('59 What are you trying to do?\r\n')
|
||||
exit()
|
||||
|
||||
# anticsrf+hash
|
||||
if (auth.checkAnticSRF(anticsrf)):
|
||||
res = auth.requestRename(hash)
|
||||
if (res == auth.SUCCESS):
|
||||
print('30 rename.gmi\r\n')
|
||||
elif (res == auth.NOT_FOUND):
|
||||
print('20 text/gemini\r\n')
|
||||
print('Failed to rename non-existing key, or key which does not belong to you.')
|
||||
print('=> index.gmi back to home')
|
||||
else:
|
||||
print('40 Unknown error\r\n')
|
||||
else:
|
||||
print('50 Bad Antic SRF (security)\r\n')
|
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/python3
|
||||
import os
|
||||
import sys
|
||||
|
||||
os.chdir('..')
|
||||
sys.path.append('lib')
|
||||
|
||||
hash = os.environ.get('TLS_CLIENT_HASH')
|
||||
if (not hash):
|
||||
# no CC
|
||||
print('60 Authentication is required\r\n')
|
||||
exit()
|
||||
certName = os.environ.get('REMOTE_USER')
|
||||
|
||||
from auth import auth
|
||||
auth = auth('data/data.db')
|
||||
auth.passKey(hash, certName)
|
||||
|
||||
if (not auth.username):
|
||||
# mismatch
|
||||
print('61 Unknown key\r\n')
|
||||
else:
|
||||
# match
|
||||
name = os.environ.get('QUERY_STRING')
|
||||
if (not name):
|
||||
# empty
|
||||
if (auth.userInfo('request_rename')):
|
||||
# TODO: tell which key are you renaming
|
||||
print('10 Choose new name for your key\r\n')
|
||||
else:
|
||||
print('30 rename-request.gmi\r\n')
|
||||
else:
|
||||
# string
|
||||
res = auth.renameKey(name)
|
||||
if (res == auth.SUCCESS):
|
||||
print('30 index.gmi\r\n')
|
||||
elif (res == auth.NOT_FOUND):
|
||||
print('20 text/gemini\r\n')
|
||||
print('Failed to rename non-existing key, or key which does not belong to you.')
|
||||
print('=> index.gmi back to home')
|
||||
else:
|
||||
print('40 Unknown error\r\n')
|
|
@ -10,10 +10,11 @@ if (not hash):
|
|||
# no CC
|
||||
print('60 Authentication is required\r\n')
|
||||
exit()
|
||||
certName = os.environ.get('REMOTE_USER')
|
||||
|
||||
from auth import auth
|
||||
auth = auth('data/data.db')
|
||||
auth.passKey(hash)
|
||||
auth.passKey(hash, certName)
|
||||
|
||||
if (not auth.username):
|
||||
# mismatch
|
||||
|
|
|
@ -10,14 +10,16 @@ hash = os.environ.get('TLS_CLIENT_HASH')
|
|||
if (not hash):
|
||||
print('60 Authentication is required\r\n')
|
||||
exit()
|
||||
certName = os.environ.get('REMOTE_USER')
|
||||
|
||||
print('20 text/gemini\r\n')
|
||||
|
||||
from auth import auth
|
||||
auth = auth('data/data.db')
|
||||
auth.passKey(hash)
|
||||
auth.passKey(hash, certName)
|
||||
|
||||
print('Your hash:', auth.hash)
|
||||
print('Your common name:', certName)
|
||||
if (auth.username):
|
||||
print('Your username:', auth.username)
|
||||
print('=> account/index.gmi manage your account')
|
||||
|
|
Reference in New Issue