main
Bob Mottram 2021-02-16 11:47:48 +00:00
commit 98300fb5f9
29 changed files with 297 additions and 61 deletions

View File

@ -3,6 +3,6 @@ image: debian:testing
test: test:
script: script:
- apt-get update - apt-get update
- apt-get install -y python3-crypto python3-dateutil python3-idna python3-numpy python3-pil.imagetk python3-pycryptodome python3-requests python3-socks python3-setuptools python3-pyqrcode - apt-get install -y python3-cryptography python3-dateutil python3-idna python3-numpy python3-pil.imagetk python3-requests python3-socks python3-setuptools python3-pyqrcode
- python3 epicyon.py --tests - python3 epicyon.py --tests
- python3 epicyon.py --testsnetwork - python3 epicyon.py --testsnetwork

View File

@ -7,6 +7,9 @@ __email__ = "bob@freedombone.net"
__status__ = "Production" __status__ = "Production"
import os import os
from datetime import datetime
from utils import fileLastModified
from utils import setConfigParam
from utils import hasUsersPath from utils import hasUsersPath
from utils import getFullDomain from utils import getFullDomain
from utils import removeIdEnding from utils import removeIdEnding
@ -175,15 +178,27 @@ def isBlockedDomain(baseDir: str, domain: str) -> bool:
if noOfSections > 2: if noOfSections > 2:
shortDomain = domain[noOfSections-2] + '.' + domain[noOfSections-1] shortDomain = domain[noOfSections-2] + '.' + domain[noOfSections-1]
globalBlockingFilename = baseDir + '/accounts/blocking.txt' allowFilename = baseDir + '/accounts/allowedinstances.txt'
if os.path.isfile(globalBlockingFilename): if not os.path.isfile(allowFilename):
with open(globalBlockingFilename, 'r') as fpBlocked: # instance block list
blockedStr = fpBlocked.read() globalBlockingFilename = baseDir + '/accounts/blocking.txt'
if '*@' + domain in blockedStr: if os.path.isfile(globalBlockingFilename):
return True with open(globalBlockingFilename, 'r') as fpBlocked:
if shortDomain: blockedStr = fpBlocked.read()
if '*@' + shortDomain in blockedStr: if '*@' + domain in blockedStr:
return True return True
if shortDomain:
if '*@' + shortDomain in blockedStr:
return True
else:
# instance allow list
if not shortDomain:
if domain not in open(allowFilename).read():
return True
else:
if shortDomain not in open(allowFilename).read():
return True
return False return False
@ -344,3 +359,91 @@ def outboxUndoBlock(baseDir: str, httpPrefix: str,
nicknameBlocked, domainBlockedFull) nicknameBlocked, domainBlockedFull)
if debug: if debug:
print('DEBUG: post undo blocked via c2s - ' + postFilename) print('DEBUG: post undo blocked via c2s - ' + postFilename)
def setBrochMode(baseDir: str, domainFull: str, enabled: bool) -> None:
"""Broch mode can be used to lock down the instance during
a period of time when it is temporarily under attack.
For example, where an adversary is constantly spinning up new
instances.
It surveys the following lists of all accounts and uses that
to construct an instance level allow list. Anything arriving
which is then not from one of the allowed domains will be dropped
"""
allowFilename = baseDir + '/accounts/allowedinstances.txt'
if not enabled:
# remove instance allow list
if os.path.isfile(allowFilename):
os.remove(allowFilename)
print('Broch mode turned off')
else:
if os.path.isfile(allowFilename):
lastModified = fileLastModified(allowFilename)
print('Broch mode already activated ' + lastModified)
return
# generate instance allow list
allowedDomains = [domainFull]
followFiles = ('following.txt', 'followers.txt')
for subdir, dirs, files in os.walk(baseDir + '/accounts'):
for acct in dirs:
if '@' not in acct:
continue
if 'inbox@' in acct or 'news@' in acct:
continue
accountDir = os.path.join(baseDir + '/accounts', acct)
for followFileType in followFiles:
followingFilename = accountDir + '/' + followFileType
if not os.path.isfile(followingFilename):
continue
with open(followingFilename, "r") as f:
followList = f.readlines()
for handle in followList:
if '@' not in handle:
continue
handle = handle.replace('\n', '')
handleDomain = handle.split('@')[1]
if handleDomain not in allowedDomains:
allowedDomains.append(handleDomain)
break
# write the allow file
allowFile = open(allowFilename, "w+")
if allowFile:
allowFile.write(domainFull + '\n')
for d in allowedDomains:
allowFile.write(d + '\n')
allowFile.close()
print('Broch mode enabled')
setConfigParam(baseDir, "brochMode", enabled)
def brochModeLapses(baseDir: str, lapseDays=7) -> bool:
"""After broch mode is enabled it automatically
elapses after a period of time
"""
allowFilename = baseDir + '/accounts/allowedinstances.txt'
if not os.path.isfile(allowFilename):
return False
lastModified = fileLastModified(allowFilename)
modifiedDate = None
brochMode = True
try:
modifiedDate = \
datetime.strptime(lastModified, "%Y-%m-%dT%H:%M:%SZ")
except BaseException:
return brochMode
if not modifiedDate:
return brochMode
currTime = datetime.datetime.utcnow()
daysSinceBroch = (currTime - modifiedDate).days
if daysSinceBroch >= lapseDays:
try:
os.remove(allowFilename)
brochMode = False
setConfigParam(baseDir, "brochMode", brochMode)
print('Broch mode has elapsed')
except BaseException:
pass
return brochMode

View File

@ -109,6 +109,7 @@ from threads import threadWithTrace
from threads import removeDormantThreads from threads import removeDormantThreads
from media import replaceYouTube from media import replaceYouTube
from media import attachMedia from media import attachMedia
from blocking import setBrochMode
from blocking import addBlock from blocking import addBlock
from blocking import removeBlock from blocking import removeBlock
from blocking import addGlobalBlock from blocking import addGlobalBlock
@ -185,6 +186,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 getLocalNetworkAddresses
from utils import decodedHost from utils import decodedHost
from utils import isPublicPost from utils import isPublicPost
from utils import getLockedAccount from utils import getLockedAccount
@ -478,6 +480,10 @@ class PubServer(BaseHTTPRequestHandler):
if 'text/html' not in self.headers['Accept']: if 'text/html' not in self.headers['Accept']:
return False return False
if self.headers['Accept'].startswith('*'): if self.headers['Accept'].startswith('*'):
if self.headers.get('User-Agent'):
if 'ELinks' in self.headers['User-Agent'] or \
'Lynx' in self.headers['User-Agent']:
return True
return False return False
if 'json' in self.headers['Accept']: if 'json' in self.headers['Accept']:
return False return False
@ -1153,20 +1159,46 @@ class PubServer(BaseHTTPRequestHandler):
# check for blocked domains so that they can be rejected early # check for blocked domains so that they can be rejected early
messageDomain = None messageDomain = None
if messageJson.get('actor'): if not messageJson.get('actor'):
messageDomain, messagePort = \
getDomainFromActor(messageJson['actor'])
if isBlockedDomain(self.server.baseDir, messageDomain):
print('POST from blocked domain ' + messageDomain)
self._400()
self.server.POSTbusy = False
return 3
else:
print('Message arriving at inbox queue has no actor') print('Message arriving at inbox queue has no actor')
self._400() self._400()
self.server.POSTbusy = False self.server.POSTbusy = False
return 3 return 3
# actor should be a string
if not isinstance(messageJson['actor'], str):
self._400()
self.server.POSTbusy = False
return 3
# actor should look like a url
if '://' not in messageJson['actor'] or \
'.' not in messageJson['actor']:
print('POST actor does not look like a url ' +
messageJson['actor'])
self._400()
self.server.POSTbusy = False
return 3
# sent by an actor on a local network address?
if not self.server.allowLocalNetworkAccess:
localNetworkPatternList = getLocalNetworkAddresses()
for localNetworkPattern in localNetworkPatternList:
if localNetworkPattern in messageJson['actor']:
print('POST actor contains local network address ' +
messageJson['actor'])
self._400()
self.server.POSTbusy = False
return 3
messageDomain, messagePort = \
getDomainFromActor(messageJson['actor'])
if isBlockedDomain(self.server.baseDir, messageDomain):
print('POST from blocked domain ' + messageDomain)
self._400()
self.server.POSTbusy = False
return 3
# if the inbox queue is full then return a busy code # if the inbox queue is full then return a busy code
if len(self.server.inboxQueue) >= self.server.maxQueueLength: if len(self.server.inboxQueue) >= self.server.maxQueueLength:
if messageDomain: if messageDomain:
@ -4512,6 +4544,18 @@ class PubServer(BaseHTTPRequestHandler):
setConfigParam(baseDir, "verifyAllSignatures", setConfigParam(baseDir, "verifyAllSignatures",
verifyAllSignatures) verifyAllSignatures)
brochMode = False
if fields.get('brochMode'):
if fields['brochMode'] == 'on':
brochMode = True
currBrochMode = \
getConfigParam(baseDir, "brochMode")
if brochMode != currBrochMode:
setBrochMode(self.server.baseDir,
self.server.domainFull,
brochMode)
setConfigParam(baseDir, "brochMode", brochMode)
# change moderators list # change moderators list
if fields.get('moderators'): if fields.get('moderators'):
if path.startswith('/users/' + if path.startswith('/users/' +
@ -10099,9 +10143,13 @@ class PubServer(BaseHTTPRequestHandler):
# manifest for progressive web apps # manifest for progressive web apps
if '/manifest.json' in self.path: if '/manifest.json' in self.path:
self._progressiveWebAppManifest(callingDomain, if self._hasAccept(callingDomain):
GETstartTime, GETtimings) if not self._requestHTTP():
return self._progressiveWebAppManifest(callingDomain,
GETstartTime, GETtimings)
return
else:
self.path = '/'
# default newswire favicon, for links to sites which # default newswire favicon, for links to sites which
# have no favicon # have no favicon
@ -13889,7 +13937,8 @@ def loadTokens(baseDir: str, tokensDict: {}, tokensLookup: {}) -> None:
break break
def runDaemon(verifyAllSignatures: bool, def runDaemon(brochMode: bool,
verifyAllSignatures: bool,
sendThreadsTimeoutMins: int, sendThreadsTimeoutMins: int,
dormantMonths: int, dormantMonths: int,
maxNewswirePosts: int, maxNewswirePosts: int,
@ -14142,6 +14191,9 @@ def runDaemon(verifyAllSignatures: bool,
# cache to store css files # cache to store css files
httpd.cssCache = {} httpd.cssCache = {}
# whether to enable broch mode, which locks down the instance
setBrochMode(baseDir, httpd.domainFull, brochMode)
if not os.path.isdir(baseDir + '/accounts/inbox@' + domain): if not os.path.isdir(baseDir + '/accounts/inbox@' + domain):
print('Creating shared inbox: inbox@' + domain) print('Creating shared inbox: inbox@' + domain)
createSharedInbox(baseDir, 'inbox', domain, port, httpPrefix) createSharedInbox(baseDir, 'inbox', domain, port, httpPrefix)

View File

@ -106,11 +106,11 @@
--column-left-icons-margin: 0; --column-left-icons-margin: 0;
--column-right-border-width: 0; --column-right-border-width: 0;
--column-left-border-color: black; --column-left-border-color: black;
--column-left-icon-size: 20%; --column-left-icon-size: 2.1vw;
--column-left-icon-size-mobile: 10%; --column-left-icon-size-mobile: 10%;
--column-left-image-width-mobile: 40vw; --column-left-image-width-mobile: 40vw;
--column-right-image-width-mobile: 100vw; --column-right-image-width-mobile: 100vw;
--column-right-icon-size: 20%; --column-right-icon-size: 2.1vw;
--column-right-icon-size-mobile: 10%; --column-right-icon-size-mobile: 10%;
--newswire-date-color: white; --newswire-date-color: white;
--newswire-voted-background-color: black; --newswire-voted-background-color: black;

View File

@ -279,6 +279,11 @@ parser.add_argument("--verifyAllSignatures",
const=True, default=False, const=True, default=False,
help="Whether to require that all incoming " + help="Whether to require that all incoming " +
"posts have valid jsonld signatures") "posts have valid jsonld signatures")
parser.add_argument("--brochMode",
dest='brochMode',
type=str2bool, nargs='?',
const=True, default=False,
help="Enable broch mode")
parser.add_argument("--noapproval", type=str2bool, nargs='?', parser.add_argument("--noapproval", type=str2bool, nargs='?',
const=True, default=False, const=True, default=False,
help="Allow followers without approval") help="Allow followers without approval")
@ -2292,6 +2297,11 @@ verifyAllSignatures = \
if verifyAllSignatures is not None: if verifyAllSignatures is not None:
args.verifyAllSignatures = bool(verifyAllSignatures) args.verifyAllSignatures = bool(verifyAllSignatures)
brochMode = \
getConfigParam(baseDir, 'brochMode')
if brochMode is not None:
args.brochMode = bool(brochMode)
YTDomain = getConfigParam(baseDir, 'youtubedomain') YTDomain = getConfigParam(baseDir, 'youtubedomain')
if YTDomain: if YTDomain:
if '://' in YTDomain: if '://' in YTDomain:
@ -2305,7 +2315,8 @@ if setTheme(baseDir, themeName, domain, args.allowLocalNetworkAccess):
print('Theme set to ' + themeName) print('Theme set to ' + themeName)
if __name__ == "__main__": if __name__ == "__main__":
runDaemon(args.verifyAllSignatures, runDaemon(args.brochMode,
args.verifyAllSignatures,
args.sendThreadsTimeoutMins, args.sendThreadsTimeoutMins,
args.dormantMonths, args.dormantMonths,
args.maxNewswirePosts, args.maxNewswirePosts,

View File

@ -51,6 +51,7 @@ from bookmarks import updateBookmarksCollection
from bookmarks import undoBookmarksCollectionEntry from bookmarks import undoBookmarksCollectionEntry
from blocking import isBlocked from blocking import isBlocked
from blocking import isBlockedDomain from blocking import isBlockedDomain
from blocking import brochModeLapses
from filters import isFiltered from filters import isFiltered
from utils import updateAnnounceCollection from utils import updateAnnounceCollection
from utils import undoAnnounceCollectionEntry from utils import undoAnnounceCollectionEntry
@ -2518,6 +2519,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
# heartbeat to monitor whether the inbox queue is running # heartbeat to monitor whether the inbox queue is running
heartBeatCtr += 5 heartBeatCtr += 5
if heartBeatCtr >= 10: if heartBeatCtr >= 10:
# turn off broch mode after it has timed out
brochModeLapses(baseDir)
print('>>> Heartbeat Q:' + str(len(queue)) + ' ' + print('>>> Heartbeat Q:' + str(len(queue)) + ' ' +
'{:%F %T}'.format(datetime.datetime.now())) '{:%F %T}'.format(datetime.datetime.now()))
heartBeatCtr = 0 heartBeatCtr = 0

View File

@ -14,6 +14,7 @@ from posts import outboxMessageCreateWrap
from posts import savePostToBox from posts import savePostToBox
from posts import sendToFollowersThread from posts import sendToFollowersThread
from posts import sendToNamedAddresses from posts import sendToNamedAddresses
from utils import getLocalNetworkAddresses
from utils import getFullDomain from utils import getFullDomain
from utils import removeIdEnding from utils import removeIdEnding
from utils import getDomainFromActor from utils import getDomainFromActor
@ -114,6 +115,23 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
'Create does not have the "to" parameter ' + 'Create does not have the "to" parameter ' +
str(messageJson)) str(messageJson))
return False return False
# actor should be a string
if not isinstance(messageJson['actor'], str):
return False
# actor should look like a url
if '://' not in messageJson['actor'] or \
'.' not in messageJson['actor']:
return False
# sent by an actor on a local network address?
if not allowLocalNetworkAccess:
localNetworkPatternList = getLocalNetworkAddresses()
for localNetworkPattern in localNetworkPatternList:
if localNetworkPattern in messageJson['actor']:
return False
testDomain, testPort = getDomainFromActor(messageJson['actor']) testDomain, testPort = getDomainFromActor(messageJson['actor'])
testDomain = getFullDomain(testDomain, testPort) testDomain = getFullDomain(testDomain, testPort)
if isBlockedDomain(baseDir, testDomain): if isBlockedDomain(baseDir, testDomain):

6
setup.py 100644
View File

@ -0,0 +1,6 @@
#!/usr/bin/python3
import setuptools
if __name__ == "__main__":
setuptools.setup()

View File

@ -325,8 +325,10 @@ def createServerAlice(path: str, domain: str, port: int,
sendThreadsTimeoutMins = 30 sendThreadsTimeoutMins = 30
maxFollowers = 10 maxFollowers = 10
verifyAllSignatures = True verifyAllSignatures = True
brochMode = False
print('Server running: Alice') print('Server running: Alice')
runDaemon(verifyAllSignatures, runDaemon(brochMode,
verifyAllSignatures,
sendThreadsTimeoutMins, sendThreadsTimeoutMins,
dormantMonths, maxNewswirePosts, dormantMonths, maxNewswirePosts,
allowLocalNetworkAccess, allowLocalNetworkAccess,
@ -420,8 +422,10 @@ def createServerBob(path: str, domain: str, port: int,
sendThreadsTimeoutMins = 30 sendThreadsTimeoutMins = 30
maxFollowers = 10 maxFollowers = 10
verifyAllSignatures = True verifyAllSignatures = True
brochMode = False
print('Server running: Bob') print('Server running: Bob')
runDaemon(verifyAllSignatures, runDaemon(brochMode,
verifyAllSignatures,
sendThreadsTimeoutMins, sendThreadsTimeoutMins,
dormantMonths, maxNewswirePosts, dormantMonths, maxNewswirePosts,
allowLocalNetworkAccess, allowLocalNetworkAccess,
@ -469,8 +473,10 @@ def createServerEve(path: str, domain: str, port: int, federationList: [],
sendThreadsTimeoutMins = 30 sendThreadsTimeoutMins = 30
maxFollowers = 10 maxFollowers = 10
verifyAllSignatures = True verifyAllSignatures = True
brochMode = False
print('Server running: Eve') print('Server running: Eve')
runDaemon(verifyAllSignatures, runDaemon(brochMode,
verifyAllSignatures,
sendThreadsTimeoutMins, sendThreadsTimeoutMins,
dormantMonths, maxNewswirePosts, dormantMonths, maxNewswirePosts,
allowLocalNetworkAccess, allowLocalNetworkAccess,

View File

@ -368,5 +368,6 @@
"Skip to Newswire": "انتقل إلى Newswire", "Skip to Newswire": "انتقل إلى Newswire",
"Skip to Links": "تخطي إلى روابط الويب", "Skip to Links": "تخطي إلى روابط الويب",
"Publish a blog article": "نشر مقال بلوق", "Publish a blog article": "نشر مقال بلوق",
"Featured writer": "كاتب متميز" "Featured writer": "كاتب متميز",
"Broch mode": "وضع الكتيب"
} }

View File

@ -368,5 +368,6 @@
"Skip to Newswire": "Vés a Newswire", "Skip to Newswire": "Vés a Newswire",
"Skip to Links": "Vés als enllaços web", "Skip to Links": "Vés als enllaços web",
"Publish a blog article": "Publicar un article del bloc", "Publish a blog article": "Publicar un article del bloc",
"Featured writer": "Escriptor destacat" "Featured writer": "Escriptor destacat",
"Broch mode": "Mode Broch"
} }

View File

@ -368,5 +368,6 @@
"Skip to Newswire": "Neidio i Newswire", "Skip to Newswire": "Neidio i Newswire",
"Skip to Links": "Neidio i Dolenni Gwe", "Skip to Links": "Neidio i Dolenni Gwe",
"Publish a blog article": "Cyhoeddi erthygl blog", "Publish a blog article": "Cyhoeddi erthygl blog",
"Featured writer": "Awdur dan sylw" "Featured writer": "Awdur dan sylw",
"Broch mode": "Modd Broch"
} }

View File

@ -368,5 +368,6 @@
"Skip to Newswire": "Springe zu Newswire", "Skip to Newswire": "Springe zu Newswire",
"Skip to Links": "Springe zu Weblinks", "Skip to Links": "Springe zu Weblinks",
"Publish a blog article": "Veröffentlichen Sie einen Blog-Artikel", "Publish a blog article": "Veröffentlichen Sie einen Blog-Artikel",
"Featured writer": "Ausgewählter Schriftsteller" "Featured writer": "Ausgewählter Schriftsteller",
"Broch mode": "Broch-Modus"
} }

View File

@ -368,5 +368,6 @@
"Skip to Newswire": "Skip to Newswire", "Skip to Newswire": "Skip to Newswire",
"Skip to Links": "Skip to Links", "Skip to Links": "Skip to Links",
"Publish a blog article": "Publish a blog article", "Publish a blog article": "Publish a blog article",
"Featured writer": "Featured writer" "Featured writer": "Featured writer",
"Broch mode": "Broch mode"
} }

View File

@ -368,5 +368,6 @@
"Skip to Newswire": "Saltar a Newswire", "Skip to Newswire": "Saltar a Newswire",
"Skip to Links": "Saltar a enlaces web", "Skip to Links": "Saltar a enlaces web",
"Publish a blog article": "Publica un artículo de blog", "Publish a blog article": "Publica un artículo de blog",
"Featured writer": "Escritora destacada" "Featured writer": "Escritora destacada",
"Broch mode": "Modo broche"
} }

View File

@ -368,5 +368,6 @@
"Skip to Newswire": "Passer à Newswire", "Skip to Newswire": "Passer à Newswire",
"Skip to Links": "Passer aux liens Web", "Skip to Links": "Passer aux liens Web",
"Publish a blog article": "Publier un article de blog", "Publish a blog article": "Publier un article de blog",
"Featured writer": "Écrivain en vedette" "Featured writer": "Écrivain en vedette",
"Broch mode": "Mode Broch"
} }

View File

@ -368,5 +368,6 @@
"Skip to Newswire": "Scipeáil chuig Newswire", "Skip to Newswire": "Scipeáil chuig Newswire",
"Skip to Links": "Scipeáil chuig Naisc Ghréasáin", "Skip to Links": "Scipeáil chuig Naisc Ghréasáin",
"Publish a blog article": "Foilsigh alt blagála", "Publish a blog article": "Foilsigh alt blagála",
"Featured writer": "Scríbhneoir mór le rá" "Featured writer": "Scríbhneoir mór le rá",
"Broch mode": "Modh broch"
} }

View File

@ -368,5 +368,6 @@
"Skip to Newswire": "Newswire पर जाएं", "Skip to Newswire": "Newswire पर जाएं",
"Skip to Links": "वेब लिंक पर जाएं", "Skip to Links": "वेब लिंक पर जाएं",
"Publish a blog article": "एक ब्लॉग लेख प्रकाशित करें", "Publish a blog article": "एक ब्लॉग लेख प्रकाशित करें",
"Featured writer": "फीचर्ड लेखक" "Featured writer": "फीचर्ड लेखक",
"Broch mode": "ब्रोच मोड"
} }

View File

@ -368,5 +368,6 @@
"Skip to Newswire": "Passa a Newswire", "Skip to Newswire": "Passa a Newswire",
"Skip to Links": "Passa a collegamenti Web", "Skip to Links": "Passa a collegamenti Web",
"Publish a blog article": "Pubblica un articolo sul blog", "Publish a blog article": "Pubblica un articolo sul blog",
"Featured writer": "Scrittore in primo piano" "Featured writer": "Scrittore in primo piano",
"Broch mode": "Modalità Broch"
} }

View File

@ -368,5 +368,6 @@
"Skip to Newswire": "Newswireにスキップ", "Skip to Newswire": "Newswireにスキップ",
"Skip to Links": "Webリンクにスキップ", "Skip to Links": "Webリンクにスキップ",
"Publish a blog article": "ブログ記事を公開する", "Publish a blog article": "ブログ記事を公開する",
"Featured writer": "注目の作家" "Featured writer": "注目の作家",
"Broch mode": "ブロッホモード"
} }

View File

@ -364,5 +364,6 @@
"Skip to Newswire": "Skip to Newswire", "Skip to Newswire": "Skip to Newswire",
"Skip to Links": "Skip to Links", "Skip to Links": "Skip to Links",
"Publish a blog article": "Publish a blog article", "Publish a blog article": "Publish a blog article",
"Featured writer": "Featured writer" "Featured writer": "Featured writer",
"Broch mode": "Broch mode"
} }

View File

@ -368,5 +368,6 @@
"Skip to Newswire": "Pular para Newswire", "Skip to Newswire": "Pular para Newswire",
"Skip to Links": "Pular para links da web", "Skip to Links": "Pular para links da web",
"Publish a blog article": "Publique um artigo de blog", "Publish a blog article": "Publique um artigo de blog",
"Featured writer": "Escritor em destaque" "Featured writer": "Escritor em destaque",
"Broch mode": "Modo broch"
} }

View File

@ -368,5 +368,6 @@
"Skip to Newswire": "Перейти к ленте новостей", "Skip to Newswire": "Перейти к ленте новостей",
"Skip to Links": "Перейти к веб-ссылкам", "Skip to Links": "Перейти к веб-ссылкам",
"Publish a blog article": "Опубликовать статью в блоге", "Publish a blog article": "Опубликовать статью в блоге",
"Featured writer": "Избранный писатель" "Featured writer": "Избранный писатель",
"Broch mode": "Брош режим"
} }

View File

@ -368,5 +368,6 @@
"Skip to Newswire": "跳到新闻专线", "Skip to Newswire": "跳到新闻专线",
"Skip to Links": "跳到网页链接", "Skip to Links": "跳到网页链接",
"Publish a blog article": "发布博客文章", "Publish a blog article": "发布博客文章",
"Featured writer": "特色作家" "Featured writer": "特色作家",
"Broch mode": "断点模式"
} }

View File

@ -605,6 +605,12 @@ def urlPermitted(url: str, federationList: []):
return False return False
def getLocalNetworkAddresses() -> []:
"""Returns patterns for local network address detection
"""
return ('localhost', '127.0.', '192.168', '10.0.')
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
""" """
@ -615,7 +621,7 @@ def dangerousMarkup(content: str, allowLocalNetworkAccess: bool) -> bool:
contentSections = content.split('<') contentSections = content.split('<')
invalidPartials = () invalidPartials = ()
if not allowLocalNetworkAccess: if not allowLocalNetworkAccess:
invalidPartials = ('localhost', '127.0.', '192.168', '10.0.') invalidPartials = getLocalNetworkAddresses()
invalidStrings = ('script', 'canvas', 'style', 'abbr', invalidStrings = ('script', 'canvas', 'style', 'abbr',
'frame', 'iframe', 'html', 'body', 'frame', 'iframe', 'html', 'body',
'hr', 'allow-popups', 'allow-scripts') 'hr', 'allow-popups', 'allow-scripts')

View File

@ -1278,6 +1278,16 @@ def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str,
' <input type="checkbox" class="profilecheckbox" ' + \ ' <input type="checkbox" class="profilecheckbox" ' + \
'name="verifyallsignatures"> ' + \ 'name="verifyallsignatures"> ' + \
translate['Verify all signatures'] + '<br>\n' translate['Verify all signatures'] + '<br>\n'
if getConfigParam(baseDir, "brochMode"):
instanceStr += \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="brochMode" checked> ' + \
translate['Broch mode'] + '<br>\n'
else:
instanceStr += \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="brochMode"> ' + \
translate['Broch mode'] + '<br>\n'
instanceStr += '</div>' instanceStr += '</div>'
moderators = '' moderators = ''

View File

@ -416,19 +416,19 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str,
translate['Mod'] translate['Mod']
navLinks = { navLinks = {
menuProfile: '/users/' + nickname, menuProfile: '/users/' + nickname,
menuInbox: usersPath + '/inbox#timeline', menuInbox: usersPath + '/inbox#timelineposts',
menuSearch: usersPath + '/search', menuSearch: usersPath + '/search',
menuNewPost: usersPath + '/newpost', menuNewPost: usersPath + '/newpost',
menuCalendar: usersPath + '/calendar', menuCalendar: usersPath + '/calendar',
menuDM: usersPath + '/dm#timeline', menuDM: usersPath + '/dm#timelineposts',
menuReplies: usersPath + '/tlreplies#timeline', menuReplies: usersPath + '/tlreplies#timelineposts',
menuOutbox: usersPath + '/inbox#timeline', menuOutbox: usersPath + '/inbox#timelineposts',
menuBookmarks: usersPath + '/tlbookmarks#timeline', menuBookmarks: usersPath + '/tlbookmarks#timelineposts',
menuShares: usersPath + '/tlshares#timeline', menuShares: usersPath + '/tlshares#timelineposts',
menuBlogs: usersPath + '/tlblogs#timeline', menuBlogs: usersPath + '/tlblogs#timelineposts',
# menuEvents: usersPath + '/tlevents#timeline', # menuEvents: usersPath + '/tlevents#timelineposts',
menuNewswire: '#newswire', menuNewswire: usersPath + '/newswiremobile',
menuLinks: '#links' menuLinks: usersPath + '/linksmobile'
} }
if moderator: if moderator:
navLinks[menuModeration] = usersPath + '/moderation#modtimeline' navLinks[menuModeration] = usersPath + '/moderation#modtimeline'
@ -502,7 +502,7 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str,
calendarImage, followApprovals, calendarImage, followApprovals,
iconsAsButtons) iconsAsButtons)
tlStr += ' <div id="timeline" class="timeline-posts">\n' tlStr += ' <div id="timelineposts" class="timeline-posts">\n'
# second row of buttons for moderator actions # second row of buttons for moderator actions
if moderator and boxName == 'moderation': if moderator and boxName == 'moderation':

View File

@ -892,26 +892,26 @@ def htmlKeyboardNavigation(banner: str, links: {},
followApprovals=False) -> str: followApprovals=False) -> str:
"""Given a set of links return the html for keyboard navigation """Given a set of links return the html for keyboard navigation
""" """
htmlStr = '<div class="transparent"><ul>' htmlStr = '<div class="transparent"><ul>\n'
if banner: if banner:
htmlStr += '<pre aria-label="">' + banner + '<br><br></pre>' htmlStr += '<pre aria-label="">\n' + banner + '\n<br><br></pre>\n'
if subHeading: if subHeading:
htmlStr += '<strong><label class="transparent">' + \ htmlStr += '<strong><label class="transparent">' + \
subHeading + '</label></strong><br>' subHeading + '</label></strong><br>\n'
# show new follower approvals # show new follower approvals
if usersPath and translate and followApprovals: if usersPath and translate and followApprovals:
htmlStr += '<strong><label class="transparent">' + \ htmlStr += '<strong><label class="transparent">' + \
'<a href="' + usersPath + '/followers#timeline">' + \ '<a href="' + usersPath + '/followers#timeline">' + \
translate['Approve follow requests'] + '</a>' + \ translate['Approve follow requests'] + '</a>' + \
'</label></strong><br><br>' '</label></strong><br><br>\n'
# show the list of links # show the list of links
for title, url in links.items(): for title, url in links.items():
htmlStr += '<li><label class="transparent">' + \ htmlStr += '<li><label class="transparent">' + \
'<a href="' + str(url) + '">' + \ '<a href="' + str(url) + '">' + \
str(title) + '</a></label></li>' str(title) + '</a></label></li>\n'
htmlStr += '</ul></div>' htmlStr += '</ul></div>\n'
return htmlStr return htmlStr

View File

@ -1,6 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="description" content="ActivityPub server written in Python, HTML and CSS, and suitable for self-hosting on single board computers">
<meta name="keywords" content="ActivityPub, Fediverse, Python, HTML, CSS">
<meta name="author" content="Bob Mottram">
<style> <style>
@charset "UTF-8"; @charset "UTF-8";
@ -1141,6 +1144,9 @@
} }
</style> </style>
<head>
<title>Epicyon ActivityPub server</title>
</head>
<body> <body>
<div class="timeline-banner"></div> <div class="timeline-banner"></div>
<center> <center>