mirror of https://gitlab.com/bashrc2/epicyon
Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon
commit
55cd454907
|
|
@ -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
45
auth.py
|
|
@ -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
|
||||||
|
|
|
||||||
27
daemon.py
27
daemon.py
|
|
@ -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 = {}
|
||||||
|
|
|
||||||
13
epicyon.py
13
epicyon.py
|
|
@ -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,
|
||||||
|
|
|
||||||
9
tests.py
9
tests.py
|
|
@ -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,
|
||||||
|
|
|
||||||
10
utils.py
10
utils.py
|
|
@ -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
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue