diff --git a/daemon.py b/daemon.py index 32693fd4..466b65c2 100644 --- a/daemon.py +++ b/daemon.py @@ -11,6 +11,7 @@ from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer import commentjson import json import time +from hashlib import sha256 from pprint import pprint from session import createSession from webfinger import webfingerMeta @@ -91,12 +92,10 @@ class PubServer(BaseHTTPRequestHandler): self.send_header('WWW-Authenticate', 'title="Login to Epicyon", Basic realm="epicyon"') self.end_headers() - def _set_headers(self,fileFormat: str,authHeader: str) -> None: + def _set_headers(self,fileFormat: str) -> None: self.send_response(200) self.send_header('Content-type', fileFormat) self.send_header('Host', self.server.domainFull) - if authHeader: - self.send_header('Authorization', authHeader) self.end_headers() def _404(self) -> None: @@ -105,7 +104,7 @@ class PubServer(BaseHTTPRequestHandler): self.end_headers() self.wfile.write("

404 Not Found

".encode('utf-8')) - def _webfinger(self,authHeader: str) -> bool: + def _webfinger(self) -> bool: if not self.path.startswith('/.well-known'): return False if self.server.debug: @@ -116,7 +115,7 @@ class PubServer(BaseHTTPRequestHandler): if self.path.startswith('/.well-known/host-meta'): wfResult=webfingerMeta() if wfResult: - self._set_headers('application/xrd+xml',authHeader) + self._set_headers('application/xrd+xml') self.wfile.write(wfResult.encode('utf-8')) return @@ -124,7 +123,7 @@ class PubServer(BaseHTTPRequestHandler): print('DEBUG: WEBFINGER lookup '+self.path+' '+str(self.server.baseDir)) wfResult=webfingerLookup(self.path,self.server.baseDir,self.server.port,self.server.debug) if wfResult: - self._set_headers('application/jrd+json',authHeader) + self._set_headers('application/jrd+json') self.wfile.write(json.dumps(wfResult).encode('utf-8')) else: if self.server.debug: @@ -343,6 +342,18 @@ class PubServer(BaseHTTPRequestHandler): return 2 def _isAuthorized(self) -> bool: + # token based authenticated used by the web interface + if self.headers.get('Cookie'): + if '=' in self.headers['Cookie']: + tokenStr=self.headers['Cookie'].split('=',1)[1] + if self.server.tokensLookup.get(tokenStr): + nickname=self.server.tokensLookup[tokenStr] + if '/'+nickname+'/' in self.path: + return True + if self.path.endswith('/'+nickname): + return True + return False + # basic auth if self.headers.get('Authorization'): if authorize(self.server.baseDir,self.path, \ self.headers['Authorization'], \ @@ -356,16 +367,17 @@ class PubServer(BaseHTTPRequestHandler): ' path: '+self.path+' busy: '+ \ str(self.server.GETbusy)) - # get the auth header as a local variable - authHeader=None + if self.server.debug: + print(str(self.headers)) + + # check authorization authorized = self._isAuthorized() if authorized: - authHeader=self.headers['Authorization'] if self.server.debug: - print('Authorized') + print('GET Authorization granted') else: if self.server.debug: - print('Not authorized') + print('GET Not authorized') # get css # Note that this comes before the busy flag to avoid conflicts @@ -373,7 +385,7 @@ class PubServer(BaseHTTPRequestHandler): if os.path.isfile('epicyon-profile.css'): with open('epicyon-profile.css', 'r') as cssfile: css = cssfile.read() - self._set_headers('text/css',authHeader) + self._set_headers('text/css') self.wfile.write(css.encode('utf-8')) return # image on login screen @@ -381,7 +393,7 @@ class PubServer(BaseHTTPRequestHandler): mediaFilename= \ self.server.baseDir+'/accounts/login.png' if os.path.isfile(mediaFilename): - self._set_headers('image/png',authHeader) + self._set_headers('image/png') with open(mediaFilename, 'rb') as avFile: mediaBinary = avFile.read() self.wfile.write(mediaBinary) @@ -397,11 +409,11 @@ class PubServer(BaseHTTPRequestHandler): self.server.baseDir+'/media/'+mediaStr if os.path.isfile(mediaFilename): if mediaFilename.endswith('.png'): - self._set_headers('image/png',authHeader) + self._set_headers('image/png') elif mediaFilename.endswith('.jpg'): - self._set_headers('image/jpeg',authHeader) + self._set_headers('image/jpeg') else: - self._set_headers('image/gif',authHeader) + self._set_headers('image/gif') with open(mediaFilename, 'rb') as avFile: mediaBinary = avFile.read() self.wfile.write(mediaBinary) @@ -419,11 +431,11 @@ class PubServer(BaseHTTPRequestHandler): self.server.baseDir+'/sharefiles/'+mediaStr if os.path.isfile(mediaFilename): if mediaFilename.endswith('.png'): - self._set_headers('image/png',authHeader) + self._set_headers('image/png') elif mediaFilename.endswith('.jpg'): - self._set_headers('image/jpeg',authHeader) + self._set_headers('image/jpeg') else: - self._set_headers('image/gif',authHeader) + self._set_headers('image/gif') with open(mediaFilename, 'rb') as avFile: mediaBinary = avFile.read() self.wfile.write(mediaBinary) @@ -446,11 +458,11 @@ class PubServer(BaseHTTPRequestHandler): self.server.domain+'/'+avatarFile if os.path.isfile(avatarFilename): if avatarFile.endswith('.png'): - self._set_headers('image/png',authHeader) + self._set_headers('image/png') elif avatarFile.endswith('.jpg'): - self._set_headers('image/jpeg',authHeader) + self._set_headers('image/jpeg') else: - self._set_headers('image/gif',authHeader) + self._set_headers('image/gif') with open(avatarFilename, 'rb') as avFile: avBinary = avFile.read() self.wfile.write(avBinary) @@ -466,7 +478,7 @@ class PubServer(BaseHTTPRequestHandler): print('DEBUG: GET Busy') self.send_response(429) if authorized: - self.send_header('Authorization', authHeader) + self.send_header('Authorization') self.end_headers() return self.server.lastGET=currTimeGET @@ -479,7 +491,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy=False return # get webfinger endpoint for a person - if self._webfinger(authHeader): + if self._webfinger(): self.server.GETbusy=False return @@ -518,14 +530,14 @@ class PubServer(BaseHTTPRequestHandler): if postJsonObject.get('likes'): postJsonObject['likes']={} if 'text/html' in self.headers['Accept']: - self._set_headers('text/html',authHeader) + self._set_headers('text/html') if authorized: - self.send_header('Authorization', authHeader) + self.send_header('Authorization') self.wfile.write(htmlIndividualPost(postJsonObject).encode('utf-8')) else: - self._set_headers('application/json',authHeader) + self._set_headers('application/json') if authorized: - self.send_header('Authorization', authHeader) + self.send_header('Authorization') self.wfile.write(json.dumps(postJsonObject).encode('utf-8')) self.server.GETbusy=False return @@ -563,10 +575,10 @@ class PubServer(BaseHTTPRequestHandler): 'totalItems': 0, 'type': 'OrderedCollection'} if 'text/html' in self.headers['Accept']: - self._set_headers('text/html',authHeader) + self._set_headers('text/html') self.wfile.write(htmlPostReplies(repliesJson).encode('utf-8')) else: - self._set_headers('application/json',authHeader) + self._set_headers('application/json') self.wfile.write(json.dumps(repliesJson).encode('utf-8')) self.server.GETbusy=False return @@ -634,10 +646,10 @@ class PubServer(BaseHTTPRequestHandler): repliesJson['orderedItems'].append(postJsonObject) # send the replies json if 'text/html' in self.headers['Accept']: - self._set_headers('text/html',authHeader) + self._set_headers('text/html') self.wfile.write(htmlPostReplies(repliesJson).encode('utf-8')) else: - self._set_headers('application/json',authHeader) + self._set_headers('application/json') self.wfile.write(json.dumps(repliesJson).encode('utf-8')) self.server.GETbusy=False return @@ -657,7 +669,7 @@ class PubServer(BaseHTTPRequestHandler): personLookup(self.server.domain,self.path.replace('/roles',''), \ self.server.baseDir) if getPerson: - self._set_headers('text/html',authHeader) + self._set_headers('text/html') self.wfile.write(htmlProfile(self.server.baseDir, \ self.server.httpPrefix, \ True, \ @@ -668,7 +680,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.personCache, \ actorJson['roles']).encode('utf-8')) else: - self._set_headers('application/json',authHeader) + self._set_headers('application/json') self.wfile.write(json.dumps(actorJson['roles']).encode('utf-8')) self.server.GETbusy=False return @@ -688,7 +700,7 @@ class PubServer(BaseHTTPRequestHandler): personLookup(self.server.domain,self.path.replace('/skills',''), \ self.server.baseDir) if getPerson: - self._set_headers('text/html',authHeader) + self._set_headers('text/html') self.wfile.write(htmlProfile(self.server.baseDir, \ self.server.httpPrefix, \ True, \ @@ -699,7 +711,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.personCache, \ actorJson['skills']).encode('utf-8')) else: - self._set_headers('application/json',authHeader) + self._set_headers('application/json') self.wfile.write(json.dumps(actorJson['skills']).encode('utf-8')) self.server.GETbusy=False return @@ -729,10 +741,10 @@ class PubServer(BaseHTTPRequestHandler): if postJsonObject.get('likes'): postJsonObject['likes']={} if 'text/html' in self.headers['Accept']: - self._set_headers('text/html',authHeader) + self._set_headers('text/html') self.wfile.write(htmlIndividualPost(postJsonObject).encode('utf-8')) else: - self._set_headers('application/json',authHeader) + self._set_headers('application/json') self.wfile.write(json.dumps(postJsonObject).encode('utf-8')) self.server.GETbusy=False return @@ -765,7 +777,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.httpPrefix, \ maxPostsInFeed, 'inbox', \ True,self.server.ocapAlways) - self._set_headers('text/html',authHeader) + self._set_headers('text/html') self.wfile.write(htmlInbox(self.server.session, \ self.server.baseDir, \ self.server.cachedWebfingers, \ @@ -774,7 +786,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.domain, \ inboxFeed).encode('utf-8')) else: - self._set_headers('application/json',authHeader) + self._set_headers('application/json') self.wfile.write(json.dumps(inboxFeed).encode('utf-8')) self.server.GETbusy=False return @@ -810,7 +822,7 @@ class PubServer(BaseHTTPRequestHandler): authorized, \ self.server.ocapAlways) - self._set_headers('text/html',authHeader) + self._set_headers('text/html') self.wfile.write(htmlOutbox(self.server.session, \ self.server.baseDir, \ self.server.cachedWebfingers, \ @@ -819,7 +831,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.domain, \ outboxFeed).encode('utf-8')) else: - self._set_headers('application/json',authHeader) + self._set_headers('application/json') self.wfile.write(json.dumps(outboxFeed).encode('utf-8')) self.server.GETbusy=False return @@ -846,7 +858,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.session= \ createSession(self.server.domain,self.server.port,self.server.useTor) - self._set_headers('text/html',authHeader) + self._set_headers('text/html') self.wfile.write(htmlProfile(self.server.baseDir, \ self.server.httpPrefix, \ authorized, \ @@ -859,7 +871,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy=False return else: - self._set_headers('application/json',authHeader) + self._set_headers('application/json') self.wfile.write(json.dumps(shares).encode('utf-8')) self.server.GETbusy=False return @@ -885,7 +897,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.session= \ createSession(self.server.domain,self.server.port,self.server.useTor) - self._set_headers('text/html',authHeader) + self._set_headers('text/html') self.wfile.write(htmlProfile(self.server.baseDir, \ self.server.httpPrefix, \ authorized, \ @@ -898,7 +910,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy=False return else: - self._set_headers('application/json',authHeader) + self._set_headers('application/json') self.wfile.write(json.dumps(following).encode('utf-8')) self.server.GETbusy=False return @@ -935,7 +947,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy=False return else: - self._set_headers('application/json',authHeader) + self._set_headers('application/json') self.wfile.write(json.dumps(followers).encode('utf-8')) self.server.GETbusy=False return @@ -949,7 +961,7 @@ class PubServer(BaseHTTPRequestHandler): print('DEBUG: creating new session') self.server.session= \ createSession(self.server.domain,self.server.port,self.server.useTor) - self._set_headers('text/html',authHeader) + self._set_headers('text/html') self.wfile.write(htmlProfile(self.server.baseDir, \ self.server.httpPrefix, \ authorized, \ @@ -959,7 +971,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.cachedWebfingers, \ self.server.personCache).encode('utf-8')) else: - self._set_headers('application/json',authHeader) + self._set_headers('application/json') self.wfile.write(json.dumps(getPerson).encode('utf-8')) self.server.GETbusy=False return @@ -973,7 +985,7 @@ class PubServer(BaseHTTPRequestHandler): # check that the file exists filename=self.server.baseDir+self.path if os.path.isfile(filename): - self._set_headers('application/json',authHeader) + self._set_headers('application/json') with open(filename, 'r', encoding='utf-8') as File: content = File.read() contentJson=json.loads(content) @@ -1031,7 +1043,14 @@ class PubServer(BaseHTTPRequestHandler): authHeader=createBasicAuthHeader(loginNickname,loginPassword) if not authorizeBasic(self.server.baseDir,'/users/'+loginNickname+'/outbox',authHeader,False): print('Login failed: '+loginNickname) + # remove any token + if self.server.tokens.get(loginNickname): + del self.server.tokensLookup[self.server.tokens[loginNickname]] + del self.server.tokens[loginNickname] + del self.server.salts[loginNickname] self.send_response(401) + self.send_header('Content-type', 'text/html; charset=utf-8') + self.send_header('Set-Cookie', 'epicyon=; SameSite=Strict') self.end_headers() self.server.POSTbusy=False return @@ -1039,8 +1058,12 @@ class PubServer(BaseHTTPRequestHandler): # login success - redirect with authorization print('Login success: '+loginNickname) self.send_response(303) - self.send_header('Location', self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+loginNickname+'/outbox') - self.send_header('Authorization', authHeader) + if not self.server.salts.get(loginNickname): + self.server.salts[loginNickname]=createPassword(32) + self.server.tokens[loginNickname]=str(sha256((loginNickname+loginPassword+self.server.salts[loginNickname]).encode('utf-8'))) + self.server.tokensLookup[self.server.tokens[loginNickname]]=loginNickname + self.send_header('Set-Cookie', 'epicyon='+self.server.tokens[loginNickname]+'; SameSite=Strict') + self.send_header('Location', '/users/'+loginNickname+'/outbox') self.end_headers() self.server.POSTbusy=False return @@ -1286,6 +1309,9 @@ def runDaemon(clientToServer: bool,baseDir: str,domain: str, \ httpd.maxImageSize=10*1024*1024 httpd.allowDeletion=allowDeletion httpd.lastLoginTime=0 + httpd.salts={} + httpd.tokens={} + httpd.tokensLookup={} httpd.acceptedCaps=["inbox:write","objects:read"] if noreply: httpd.acceptedCaps.append('inbox:noreply')