Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon

main
Bob Mottram 2021-06-09 18:17:30 +01:00
commit 55cd454907
7 changed files with 105 additions and 8 deletions

View File

@ -82,7 +82,7 @@ Type=simple
User=epicyon User=epicyon
Group=epicyon Group=epicyon
WorkingDirectory=/opt/epicyon WorkingDirectory=/opt/epicyon
ExecStart=/usr/bin/python3 /opt/epicyon/epicyon.py --port 443 --proxy 7156 --domain YOUR_DOMAIN --registration open ExecStart=/usr/bin/python3 /opt/epicyon/epicyon.py --port 443 --proxy 7156 --domain YOUR_DOMAIN --registration open --logLoginFailures
Environment=USER=epicyon Environment=USER=epicyon
Environment=PYTHONUNBUFFERED=true Environment=PYTHONUNBUFFERED=true
Restart=always Restart=always
@ -208,6 +208,8 @@ And restart the web server:
systemctl restart nginx systemctl restart nginx
``` ```
If you need to use **fail2ban** then failed login attempts can be found in *accounts/loginfailures.log*.
If you are using the [Caddy web server](https://caddyserver.com) then see *caddy.example.conf* If you are using the [Caddy web server](https://caddyserver.com) then see *caddy.example.conf*
## Running Static Analysis ## Running Static Analysis

45
auth.py
View File

@ -11,6 +11,7 @@ import hashlib
import binascii import binascii
import os import os
import secrets import secrets
import datetime
from utils import isSystemAccount from utils import isSystemAccount
from utils import hasUsersPath from utils import hasUsersPath
@ -204,3 +205,47 @@ def createPassword(length=10):
validChars = 'abcdefghijklmnopqrstuvwxyz' + \ validChars = 'abcdefghijklmnopqrstuvwxyz' + \
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
return ''.join((secrets.choice(validChars) for i in range(length))) return ''.join((secrets.choice(validChars) for i in range(length)))
def recordLoginFailure(baseDir: str, ipAddress: str,
countDict: {}, failTime: int,
logToFile: bool) -> None:
"""Keeps ip addresses and the number of times login failures
occured for them in a dict
"""
if not countDict.get(ipAddress):
while len(countDict.items()) > 100:
oldestTime = 0
oldestIP = None
for ipAddr, ipItem in countDict.items():
if oldestTime == 0 or ipItem['time'] < oldestTime:
oldestTime = ipItem['time']
oldestIP = ipAddr
if oldestIP:
del countDict[oldestIP]
countDict[ipAddress] = {
"count": 1,
"time": failTime
}
else:
countDict[ipAddress]['count'] += 1
countDict[ipAddress]['time'] = failTime
failCount = countDict[ipAddress]['count']
if failCount > 4:
print('WARN: ' + str(ipAddress) + ' failed to log in ' +
str(failCount) + ' times')
if not logToFile:
return
failureLog = baseDir + '/accounts/loginfailures.log'
writeType = 'a+'
if not os.path.isfile(failureLog):
writeType = 'w+'
currTime = datetime.datetime.utcnow()
try:
with open(failureLog, writeType) as fp:
fp.write(currTime.strftime("%Y-%m-%d %H:%M:%SZ") +
' ' + ipAddress + '\n')
except BaseException:
pass

View File

@ -101,6 +101,7 @@ from skills import noOfActorSkills
from skills import actorHasSkill from skills import actorHasSkill
from skills import actorSkillValue from skills import actorSkillValue
from skills import setActorSkillLevel from skills import setActorSkillLevel
from auth import recordLoginFailure
from auth import authorize from auth import authorize
from auth import createPassword from auth import createPassword
from auth import createBasicAuthHeader from auth import createBasicAuthHeader
@ -206,6 +207,7 @@ from shares import addShare
from shares import removeShare from shares import removeShare
from shares import expireShares from shares import expireShares
from categories import setHashtagCategory from categories import setHashtagCategory
from utils import isLocalNetworkAddress
from utils import permittedDir from utils import permittedDir
from utils import isAccountDir from utils import isAccountDir
from utils import getOccupationSkills from utils import getOccupationSkills
@ -1437,15 +1439,33 @@ class PubServer(BaseHTTPRequestHandler):
return return
authHeader = \ authHeader = \
createBasicAuthHeader(loginNickname, loginPassword) createBasicAuthHeader(loginNickname, loginPassword)
if self.headers.get('X-Forward-For'):
ipAddress = self.headers['X-Forward-For']
elif self.headers.get('X-Forwarded-For'):
ipAddress = self.headers['X-Forwarded-For']
else:
ipAddress = self.client_address[0]
if not domain.endswith('.onion'):
if not isLocalNetworkAddress(ipAddress):
print('Login attempt from IP: ' + str(ipAddress))
if not authorizeBasic(baseDir, '/users/' + if not authorizeBasic(baseDir, '/users/' +
loginNickname + '/outbox', loginNickname + '/outbox',
authHeader, False): authHeader, False):
print('Login failed: ' + loginNickname) print('Login failed: ' + loginNickname)
self._clearLoginDetails(loginNickname, callingDomain) self._clearLoginDetails(loginNickname, callingDomain)
self.server.lastLoginFailure = int(time.time()) failTime = int(time.time())
self.server.lastLoginFailure = failTime
if not domain.endswith('.onion'):
if not isLocalNetworkAddress(ipAddress):
recordLoginFailure(baseDir, ipAddress,
self.server.loginFailureCount,
failTime,
self.server.logLoginFailures)
self.server.POSTbusy = False self.server.POSTbusy = False
return return
else: else:
if self.server.loginFailureCount.get(ipAddress):
del self.server.loginFailureCount[ipAddress]
if isSuspended(baseDir, loginNickname): if isSuspended(baseDir, loginNickname):
msg = \ msg = \
htmlSuspended(self.server.cssCache, htmlSuspended(self.server.cssCache,
@ -14829,7 +14849,8 @@ def loadTokens(baseDir: str, tokensDict: {}, tokensLookup: {}) -> None:
break break
def runDaemon(city: str, def runDaemon(logLoginFailures: bool,
city: str,
showNodeInfoAccounts: bool, showNodeInfoAccounts: bool,
showNodeInfoVersion: bool, showNodeInfoVersion: bool,
brochMode: bool, brochMode: bool,
@ -15097,6 +15118,8 @@ def runDaemon(city: str,
httpd.allowDeletion = allowDeletion httpd.allowDeletion = allowDeletion
httpd.lastLoginTime = 0 httpd.lastLoginTime = 0
httpd.lastLoginFailure = 0 httpd.lastLoginFailure = 0
httpd.loginFailureCount = {}
httpd.logLoginFailures = logLoginFailures
httpd.maxReplies = maxReplies httpd.maxReplies = maxReplies
httpd.tokens = {} httpd.tokens = {}
httpd.tokensLookup = {} httpd.tokensLookup = {}

View File

@ -291,6 +291,11 @@ parser.add_argument("--iconsAsButtons",
type=str2bool, nargs='?', type=str2bool, nargs='?',
const=True, default=False, const=True, default=False,
help="Show header icons as buttons") help="Show header icons as buttons")
parser.add_argument("--logLoginFailures",
dest='logLoginFailures',
type=str2bool, nargs='?',
const=True, default=False,
help="Whether to log longin failures")
parser.add_argument("--rssIconAtTop", parser.add_argument("--rssIconAtTop",
dest='rssIconAtTop', dest='rssIconAtTop',
type=str2bool, nargs='?', type=str2bool, nargs='?',
@ -2510,6 +2515,11 @@ brochMode = \
if brochMode is not None: if brochMode is not None:
args.brochMode = bool(brochMode) args.brochMode = bool(brochMode)
logLoginFailures = \
getConfigParam(baseDir, 'logLoginFailures')
if logLoginFailures is not None:
args.logLoginFailures = bool(logLoginFailures)
showNodeInfoAccounts = \ showNodeInfoAccounts = \
getConfigParam(baseDir, 'showNodeInfoAccounts') getConfigParam(baseDir, 'showNodeInfoAccounts')
if showNodeInfoAccounts is not None: if showNodeInfoAccounts is not None:
@ -2539,7 +2549,8 @@ if setTheme(baseDir, themeName, domain,
print('Theme set to ' + themeName) print('Theme set to ' + themeName)
if __name__ == "__main__": if __name__ == "__main__":
runDaemon(args.city, runDaemon(args.logLoginFailures,
args.city,
args.showNodeInfoAccounts, args.showNodeInfoAccounts,
args.showNodeInfoVersion, args.showNodeInfoVersion,
args.brochMode, args.brochMode,

View File

@ -518,8 +518,9 @@ def createServerAlice(path: str, domain: str, port: int,
showNodeInfoAccounts = True showNodeInfoAccounts = True
showNodeInfoVersion = True showNodeInfoVersion = True
city = 'London, England' city = 'London, England'
logLoginFailures = False
print('Server running: Alice') print('Server running: Alice')
runDaemon(city, runDaemon(logLoginFailures, city,
showNodeInfoAccounts, showNodeInfoAccounts,
showNodeInfoVersion, showNodeInfoVersion,
brochMode, brochMode,
@ -620,8 +621,9 @@ def createServerBob(path: str, domain: str, port: int,
showNodeInfoAccounts = True showNodeInfoAccounts = True
showNodeInfoVersion = True showNodeInfoVersion = True
city = 'London, England' city = 'London, England'
logLoginFailures = False
print('Server running: Bob') print('Server running: Bob')
runDaemon(city, runDaemon(logLoginFailures, city,
showNodeInfoAccounts, showNodeInfoAccounts,
showNodeInfoVersion, showNodeInfoVersion,
brochMode, brochMode,
@ -677,8 +679,9 @@ def createServerEve(path: str, domain: str, port: int, federationList: [],
showNodeInfoAccounts = True showNodeInfoAccounts = True
showNodeInfoVersion = True showNodeInfoVersion = True
city = 'London, England' city = 'London, England'
logLoginFailures = False
print('Server running: Eve') print('Server running: Eve')
runDaemon(city, runDaemon(logLoginFailures, city,
showNodeInfoAccounts, showNodeInfoAccounts,
showNodeInfoVersion, showNodeInfoVersion,
brochMode, brochMode,

View File

@ -669,6 +669,16 @@ def getLocalNetworkAddresses() -> []:
return ('localhost', '127.0.', '192.168', '10.0.') return ('localhost', '127.0.', '192.168', '10.0.')
def isLocalNetworkAddress(ipAddress: str) -> bool:
"""
"""
localIPs = getLocalNetworkAddresses()
for ipAddr in localIPs:
if ipAddress.startswith(ipAddr):
return True
return False
def dangerousMarkup(content: str, allowLocalNetworkAccess: bool) -> bool: def dangerousMarkup(content: str, allowLocalNetworkAccess: bool) -> bool:
"""Returns true if the given content contains dangerous html markup """Returns true if the given content contains dangerous html markup
""" """

View File

@ -1343,7 +1343,7 @@
User=epicyon<br> User=epicyon<br>
Group=epicyon<br> Group=epicyon<br>
WorkingDirectory=/opt/epicyon<br> WorkingDirectory=/opt/epicyon<br>
ExecStart=/usr/bin/python3 /opt/epicyon/epicyon.py --port 443 --proxy 7156 --domain YOUR_DOMAIN --registration open --debug<br> ExecStart=/usr/bin/python3 /opt/epicyon/epicyon.py --port 443 --proxy 7156 --domain YOUR_DOMAIN --registration open --debug --logLoginFailures<br>
Environment=USER=epicyon<br> Environment=USER=epicyon<br>
Environment=PYTHONUNBUFFERED=true<br> Environment=PYTHONUNBUFFERED=true<br>
Restart=always<br> Restart=always<br>
@ -1492,6 +1492,9 @@
<div class="shell"> <div class="shell">
systemctl restart nginx systemctl restart nginx
</div> </div>
<p class="intro">
If you need to use <a href="https://www.fail2ban.org">fail2ban</a> then failed login attempts can be found in <b>accounts/loginfailures.log</b>.
</p>
<p class="intro"> <p class="intro">
If you are using the <a href="https://caddyserver.com">Caddy web server</a> then see <a href="https://code.freedombone.net/bashrc/epicyon/raw/main/caddy.example.conf">caddy.example.conf</a> If you are using the <a href="https://caddyserver.com">Caddy web server</a> then see <a href="https://code.freedombone.net/bashrc/epicyon/raw/main/caddy.example.conf">caddy.example.conf</a>
</p> </p>