From 23af727becaf00f2de99ea4aea333702f9623f4a Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 14 Dec 2021 13:59:42 +0000 Subject: [PATCH] Validate sending actor when follow request is received --- follow.py | 252 +++++------------------------------------------------- inbox.py | 245 +++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 255 insertions(+), 242 deletions(-) diff --git a/follow.py b/follow.py index a5840dbcf..a908fcf24 100644 --- a/follow.py +++ b/follow.py @@ -11,11 +11,9 @@ from pprint import pprint import os from utils import hasObjectStringObject from utils import hasObjectStringType -from utils import hasActor from utils import removeDomainPort from utils import hasUsersPath from utils import getFullDomain -from utils import isSystemAccount from utils import getFollowersList from utils import validNickname from utils import domainPermitted @@ -31,7 +29,6 @@ from utils import isAccountDir from utils import getUserPaths from utils import acctDir from utils import hasGroupType -from utils import isGroupAccount from utils import localActorUrl from acceptreject import createAccept from acceptreject import createReject @@ -39,7 +36,6 @@ from webfinger import webfingerHandle from auth import createBasicAuthHeader from session import getJson from session import postJson -from cache import getPersonPubKey def createInitialLastSeen(baseDir: str, httpPrefix: str) -> None: @@ -418,8 +414,8 @@ def _getNoOfFollows(baseDir: str, nickname: str, domain: str, return ctr -def _getNoOfFollowers(baseDir: str, - nickname: str, domain: str, authenticated: bool) -> int: +def getNoOfFollowers(baseDir: str, + nickname: str, domain: str, authenticated: bool) -> int: """Returns the number of followers of the given person """ return _getNoOfFollows(baseDir, nickname, domain, @@ -562,9 +558,9 @@ def getFollowingFeed(baseDir: str, domain: str, port: int, path: str, return following -def _followApprovalRequired(baseDir: str, nicknameToFollow: str, - domainToFollow: str, debug: bool, - followRequestHandle: str) -> bool: +def followApprovalRequired(baseDir: str, nicknameToFollow: str, + domainToFollow: str, debug: bool, + followRequestHandle: str) -> bool: """ Returns the policy for follower approvals """ # has this handle already been manually approved? @@ -591,10 +587,10 @@ def _followApprovalRequired(baseDir: str, nicknameToFollow: str, return manuallyApproveFollows -def _noOfFollowRequests(baseDir: str, - nicknameToFollow: str, domainToFollow: str, - nickname: str, domain: str, fromPort: int, - followType: str) -> int: +def noOfFollowRequests(baseDir: str, + nicknameToFollow: str, domainToFollow: str, + nickname: str, domain: str, fromPort: int, + followType: str) -> int: """Returns the current number of follow requests """ accountsDir = baseDir + '/accounts/' + \ @@ -608,7 +604,7 @@ def _noOfFollowRequests(baseDir: str, with open(approveFollowsFilename, 'r') as f: lines = f.readlines() except OSError: - print('EX: _noOfFollowRequests ' + approveFollowsFilename) + print('EX: noOfFollowRequests ' + approveFollowsFilename) if lines: if followType == "onion": for fileLine in lines: @@ -623,12 +619,12 @@ def _noOfFollowRequests(baseDir: str, return ctr -def _storeFollowRequest(baseDir: str, - nicknameToFollow: str, domainToFollow: str, port: int, - nickname: str, domain: str, fromPort: int, - followJson: {}, - debug: bool, personUrl: str, - groupAccount: bool) -> bool: +def storeFollowRequest(baseDir: str, + nicknameToFollow: str, domainToFollow: str, port: int, + nickname: str, domain: str, fromPort: int, + followJson: {}, + debug: bool, personUrl: str, + groupAccount: bool) -> bool: """Stores the follow request for later use """ accountsDir = baseDir + '/accounts/' + \ @@ -651,7 +647,7 @@ def _storeFollowRequest(baseDir: str, with open(followersFilename, 'r') as fpFollowers: followersStr = fpFollowers.read() except OSError: - print('EX: _storeFollowRequest ' + followersFilename) + print('EX: storeFollowRequest ' + followersFilename) if approveHandle in followersStr: alreadyFollowing = True @@ -696,7 +692,7 @@ def _storeFollowRequest(baseDir: str, with open(approveFollowsFilename, 'a+') as fp: fp.write(approveHandleStored + '\n') except OSError: - print('EX: _storeFollowRequest 2 ' + approveFollowsFilename) + print('EX: storeFollowRequest 2 ' + approveFollowsFilename) else: if debug: print('DEBUG: ' + approveHandleStored + @@ -706,7 +702,7 @@ def _storeFollowRequest(baseDir: str, with open(approveFollowsFilename, 'w+') as fp: fp.write(approveHandleStored + '\n') except OSError: - print('EX: _storeFollowRequest 3 ' + approveFollowsFilename) + print('EX: storeFollowRequest 3 ' + approveFollowsFilename) # store the follow request in its own directory # We don't rely upon the inbox because items in there could expire @@ -717,212 +713,6 @@ def _storeFollowRequest(baseDir: str, return saveJson(followJson, followActivityfilename) -def receiveFollowRequest(session, baseDir: str, httpPrefix: str, - port: int, sendThreads: [], postLog: [], - cachedWebfingers: {}, personCache: {}, - messageJson: {}, federationList: [], - debug: bool, projectVersion: str, - maxFollowers: int, onionDomain: str, - signingPrivateKeyPem: str) -> bool: - """Receives a follow request within the POST section of HTTPServer - """ - if not messageJson['type'].startswith('Follow'): - if not messageJson['type'].startswith('Join'): - return False - print('Receiving follow request') - if not hasActor(messageJson, debug): - return False - if not hasUsersPath(messageJson['actor']): - if debug: - print('DEBUG: users/profile/accounts/channel missing from actor') - return False - domain, tempPort = getDomainFromActor(messageJson['actor']) - fromPort = port - domainFull = getFullDomain(domain, tempPort) - if tempPort: - fromPort = tempPort - if not domainPermitted(domain, federationList): - if debug: - print('DEBUG: follower from domain not permitted - ' + domain) - return False - nickname = getNicknameFromActor(messageJson['actor']) - if not nickname: - # single user instance - nickname = 'dev' - if debug: - print('DEBUG: follow request does not contain a ' + - 'nickname. Assuming single user instance.') - if not messageJson.get('to'): - messageJson['to'] = messageJson['object'] - if not hasUsersPath(messageJson['object']): - if debug: - print('DEBUG: users/profile/channel/accounts ' + - 'not found within object') - return False - domainToFollow, tempPort = getDomainFromActor(messageJson['object']) - if not domainPermitted(domainToFollow, federationList): - if debug: - print('DEBUG: follow domain not permitted ' + domainToFollow) - return True - domainToFollowFull = getFullDomain(domainToFollow, tempPort) - nicknameToFollow = getNicknameFromActor(messageJson['object']) - if not nicknameToFollow: - if debug: - print('DEBUG: follow request does not contain a ' + - 'nickname for the account followed') - return True - if isSystemAccount(nicknameToFollow): - if debug: - print('DEBUG: Cannot follow system account - ' + - nicknameToFollow) - return True - if maxFollowers > 0: - if _getNoOfFollowers(baseDir, - nicknameToFollow, domainToFollow, - True) > maxFollowers: - print('WARN: ' + nicknameToFollow + - ' has reached their maximum number of followers') - return True - handleToFollow = nicknameToFollow + '@' + domainToFollow - if domainToFollow == domain: - if not os.path.isdir(baseDir + '/accounts/' + handleToFollow): - if debug: - print('DEBUG: followed account not found - ' + - baseDir + '/accounts/' + handleToFollow) - return True - - if isFollowerOfPerson(baseDir, - nicknameToFollow, domainToFollowFull, - nickname, domainFull): - if debug: - print('DEBUG: ' + nickname + '@' + domain + - ' is already a follower of ' + - nicknameToFollow + '@' + domainToFollow) - return True - - # what is the followers policy? - approveHandle = nickname + '@' + domainFull - if _followApprovalRequired(baseDir, nicknameToFollow, - domainToFollow, debug, approveHandle): - print('Follow approval is required') - if domain.endswith('.onion'): - if _noOfFollowRequests(baseDir, - nicknameToFollow, domainToFollow, - nickname, domain, fromPort, - 'onion') > 5: - print('Too many follow requests from onion addresses') - return False - elif domain.endswith('.i2p'): - if _noOfFollowRequests(baseDir, - nicknameToFollow, domainToFollow, - nickname, domain, fromPort, - 'i2p') > 5: - print('Too many follow requests from i2p addresses') - return False - else: - if _noOfFollowRequests(baseDir, - nicknameToFollow, domainToFollow, - nickname, domain, fromPort, - '') > 10: - print('Too many follow requests') - return False - - # Get the actor for the follower and add it to the cache. - # Getting their public key has the same result - if debug: - print('Obtaining the following actor: ' + messageJson['actor']) - if not getPersonPubKey(baseDir, session, messageJson['actor'], - personCache, debug, projectVersion, - httpPrefix, domainToFollow, onionDomain, - signingPrivateKeyPem): - if debug: - print('Unable to obtain following actor: ' + - messageJson['actor']) - - groupAccount = \ - hasGroupType(baseDir, messageJson['actor'], personCache) - if groupAccount and isGroupAccount(baseDir, nickname, domain): - print('Group cannot follow a group') - return False - - print('Storing follow request for approval') - return _storeFollowRequest(baseDir, - nicknameToFollow, domainToFollow, port, - nickname, domain, fromPort, - messageJson, debug, messageJson['actor'], - groupAccount) - else: - print('Follow request does not require approval ' + approveHandle) - # update the followers - accountToBeFollowed = \ - acctDir(baseDir, nicknameToFollow, domainToFollow) - if os.path.isdir(accountToBeFollowed): - followersFilename = accountToBeFollowed + '/followers.txt' - - # for actors which don't follow the mastodon - # /users/ path convention store the full actor - if '/users/' not in messageJson['actor']: - approveHandle = messageJson['actor'] - - # Get the actor for the follower and add it to the cache. - # Getting their public key has the same result - if debug: - print('Obtaining the following actor: ' + messageJson['actor']) - if not getPersonPubKey(baseDir, session, messageJson['actor'], - personCache, debug, projectVersion, - httpPrefix, domainToFollow, onionDomain, - signingPrivateKeyPem): - if debug: - print('Unable to obtain following actor: ' + - messageJson['actor']) - - print('Updating followers file: ' + - followersFilename + ' adding ' + approveHandle) - if os.path.isfile(followersFilename): - if approveHandle not in open(followersFilename).read(): - groupAccount = \ - hasGroupType(baseDir, - messageJson['actor'], personCache) - if debug: - print(approveHandle + ' / ' + messageJson['actor'] + - ' is Group: ' + str(groupAccount)) - if groupAccount and \ - isGroupAccount(baseDir, nickname, domain): - print('Group cannot follow a group') - return False - try: - with open(followersFilename, 'r+') as followersFile: - content = followersFile.read() - if approveHandle + '\n' not in content: - followersFile.seek(0, 0) - if not groupAccount: - followersFile.write(approveHandle + - '\n' + content) - else: - followersFile.write('!' + approveHandle + - '\n' + content) - except Exception as e: - print('WARN: ' + - 'Failed to write entry to followers file ' + - str(e)) - else: - try: - with open(followersFilename, 'w+') as followersFile: - followersFile.write(approveHandle + '\n') - except OSError: - print('EX: unable to write ' + followersFilename) - - print('Beginning follow accept') - return followedAccountAccepts(session, baseDir, httpPrefix, - nicknameToFollow, domainToFollow, port, - nickname, domain, fromPort, - messageJson['actor'], federationList, - messageJson, sendThreads, postLog, - cachedWebfingers, personCache, - debug, projectVersion, True, - signingPrivateKeyPem) - - def followedAccountAccepts(session, baseDir: str, httpPrefix: str, nicknameToFollow: str, domainToFollow: str, port: int, @@ -1124,8 +914,8 @@ def sendFollowRequest(session, baseDir: str, newFollowJson['to'] = followedId print('Follow request: ' + str(newFollowJson)) - if _followApprovalRequired(baseDir, nickname, domain, debug, - followHandle): + if followApprovalRequired(baseDir, nickname, domain, debug, + followHandle): # Remove any follow requests rejected for the account being followed. # It's assumed that if you are following someone then you are # ok with them following back. If this isn't the case then a rejected diff --git a/inbox.py b/inbox.py index f19adbf78..215dfd3be 100644 --- a/inbox.py +++ b/inbox.py @@ -17,6 +17,9 @@ from languages import understoodPostLanguage from like import updateLikesCollection from reaction import updateReactionCollection from reaction import validEmojiContent +from utils import domainPermitted +from utils import isGroupAccount +from utils import isSystemAccount from utils import invalidCiphertext from utils import removeHtml from utils import fileLastModified @@ -65,9 +68,14 @@ from httpsig import verifyPostHeaders from session import createSession from follow import followerApprovalActive from follow import isFollowingActor -from follow import receiveFollowRequest from follow import getFollowersOfActor from follow import unfollowerOfAccount +from follow import isFollowerOfPerson +from follow import followedAccountAccepts +from follow import storeFollowRequest +from follow import noOfFollowRequests +from follow import getNoOfFollowers +from follow import followApprovalRequired from pprint import pprint from cache import storePersonInCache from cache import getPersonPubKey @@ -3834,6 +3842,221 @@ def _checkJsonSignature(baseDir: str, queueJson: {}) -> (bool, bool): return hasJsonSignature, jwebsigType +def _receiveFollowRequest(session, baseDir: str, httpPrefix: str, + port: int, sendThreads: [], postLog: [], + cachedWebfingers: {}, personCache: {}, + messageJson: {}, federationList: [], + debug: bool, projectVersion: str, + maxFollowers: int, onionDomain: str, + signingPrivateKeyPem: str, unitTest: bool) -> bool: + """Receives a follow request within the POST section of HTTPServer + """ + if not messageJson['type'].startswith('Follow'): + if not messageJson['type'].startswith('Join'): + return False + print('Receiving follow request') + if not hasActor(messageJson, debug): + return False + if not hasUsersPath(messageJson['actor']): + if debug: + print('DEBUG: users/profile/accounts/channel missing from actor') + return False + domain, tempPort = getDomainFromActor(messageJson['actor']) + fromPort = port + domainFull = getFullDomain(domain, tempPort) + if tempPort: + fromPort = tempPort + if not domainPermitted(domain, federationList): + if debug: + print('DEBUG: follower from domain not permitted - ' + domain) + return False + nickname = getNicknameFromActor(messageJson['actor']) + if not nickname: + # single user instance + nickname = 'dev' + if debug: + print('DEBUG: follow request does not contain a ' + + 'nickname. Assuming single user instance.') + if not messageJson.get('to'): + messageJson['to'] = messageJson['object'] + if not hasUsersPath(messageJson['object']): + if debug: + print('DEBUG: users/profile/channel/accounts ' + + 'not found within object') + return False + domainToFollow, tempPort = getDomainFromActor(messageJson['object']) + if not domainPermitted(domainToFollow, federationList): + if debug: + print('DEBUG: follow domain not permitted ' + domainToFollow) + return True + domainToFollowFull = getFullDomain(domainToFollow, tempPort) + nicknameToFollow = getNicknameFromActor(messageJson['object']) + if not nicknameToFollow: + if debug: + print('DEBUG: follow request does not contain a ' + + 'nickname for the account followed') + return True + if isSystemAccount(nicknameToFollow): + if debug: + print('DEBUG: Cannot follow system account - ' + + nicknameToFollow) + return True + if maxFollowers > 0: + if getNoOfFollowers(baseDir, + nicknameToFollow, domainToFollow, + True) > maxFollowers: + print('WARN: ' + nicknameToFollow + + ' has reached their maximum number of followers') + return True + handleToFollow = nicknameToFollow + '@' + domainToFollow + if domainToFollow == domain: + if not os.path.isdir(baseDir + '/accounts/' + handleToFollow): + if debug: + print('DEBUG: followed account not found - ' + + baseDir + '/accounts/' + handleToFollow) + return True + + if isFollowerOfPerson(baseDir, + nicknameToFollow, domainToFollowFull, + nickname, domainFull): + if debug: + print('DEBUG: ' + nickname + '@' + domain + + ' is already a follower of ' + + nicknameToFollow + '@' + domainToFollow) + return True + + approveHandle = nickname + '@' + domainFull + + # is the actor sending the request valid? + if not validSendingActor(session, baseDir, + nicknameToFollow, domainToFollow, + personCache, messageJson, + signingPrivateKeyPem, debug, unitTest): + print('REJECT spam follow request ' + approveHandle) + return False + + # what is the followers policy? + if followApprovalRequired(baseDir, nicknameToFollow, + domainToFollow, debug, approveHandle): + print('Follow approval is required') + if domain.endswith('.onion'): + if noOfFollowRequests(baseDir, + nicknameToFollow, domainToFollow, + nickname, domain, fromPort, + 'onion') > 5: + print('Too many follow requests from onion addresses') + return False + elif domain.endswith('.i2p'): + if noOfFollowRequests(baseDir, + nicknameToFollow, domainToFollow, + nickname, domain, fromPort, + 'i2p') > 5: + print('Too many follow requests from i2p addresses') + return False + else: + if noOfFollowRequests(baseDir, + nicknameToFollow, domainToFollow, + nickname, domain, fromPort, + '') > 10: + print('Too many follow requests') + return False + + # Get the actor for the follower and add it to the cache. + # Getting their public key has the same result + if debug: + print('Obtaining the following actor: ' + messageJson['actor']) + if not getPersonPubKey(baseDir, session, messageJson['actor'], + personCache, debug, projectVersion, + httpPrefix, domainToFollow, onionDomain, + signingPrivateKeyPem): + if debug: + print('Unable to obtain following actor: ' + + messageJson['actor']) + + groupAccount = \ + hasGroupType(baseDir, messageJson['actor'], personCache) + if groupAccount and isGroupAccount(baseDir, nickname, domain): + print('Group cannot follow a group') + return False + + print('Storing follow request for approval') + return storeFollowRequest(baseDir, + nicknameToFollow, domainToFollow, port, + nickname, domain, fromPort, + messageJson, debug, messageJson['actor'], + groupAccount) + else: + print('Follow request does not require approval ' + approveHandle) + # update the followers + accountToBeFollowed = \ + acctDir(baseDir, nicknameToFollow, domainToFollow) + if os.path.isdir(accountToBeFollowed): + followersFilename = accountToBeFollowed + '/followers.txt' + + # for actors which don't follow the mastodon + # /users/ path convention store the full actor + if '/users/' not in messageJson['actor']: + approveHandle = messageJson['actor'] + + # Get the actor for the follower and add it to the cache. + # Getting their public key has the same result + if debug: + print('Obtaining the following actor: ' + messageJson['actor']) + if not getPersonPubKey(baseDir, session, messageJson['actor'], + personCache, debug, projectVersion, + httpPrefix, domainToFollow, onionDomain, + signingPrivateKeyPem): + if debug: + print('Unable to obtain following actor: ' + + messageJson['actor']) + + print('Updating followers file: ' + + followersFilename + ' adding ' + approveHandle) + if os.path.isfile(followersFilename): + if approveHandle not in open(followersFilename).read(): + groupAccount = \ + hasGroupType(baseDir, + messageJson['actor'], personCache) + if debug: + print(approveHandle + ' / ' + messageJson['actor'] + + ' is Group: ' + str(groupAccount)) + if groupAccount and \ + isGroupAccount(baseDir, nickname, domain): + print('Group cannot follow a group') + return False + try: + with open(followersFilename, 'r+') as followersFile: + content = followersFile.read() + if approveHandle + '\n' not in content: + followersFile.seek(0, 0) + if not groupAccount: + followersFile.write(approveHandle + + '\n' + content) + else: + followersFile.write('!' + approveHandle + + '\n' + content) + except Exception as e: + print('WARN: ' + + 'Failed to write entry to followers file ' + + str(e)) + else: + try: + with open(followersFilename, 'w+') as followersFile: + followersFile.write(approveHandle + '\n') + except OSError: + print('EX: unable to write ' + followersFilename) + + print('Beginning follow accept') + return followedAccountAccepts(session, baseDir, httpPrefix, + nicknameToFollow, domainToFollow, port, + nickname, domain, fromPort, + messageJson['actor'], federationList, + messageJson, sendThreads, postLog, + cachedWebfingers, personCache, + debug, projectVersion, True, + signingPrivateKeyPem) + + def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, projectVersion: str, baseDir: str, httpPrefix: str, sendThreads: [], postLog: [], @@ -4134,16 +4357,16 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, if debug: print('DEBUG: checking for follow requests') - if receiveFollowRequest(session, - baseDir, httpPrefix, port, - sendThreads, postLog, - cachedWebfingers, - personCache, - queueJson['post'], - federationList, - debug, projectVersion, - maxFollowers, onionDomain, - signingPrivateKeyPem): + if _receiveFollowRequest(session, + baseDir, httpPrefix, port, + sendThreads, postLog, + cachedWebfingers, + personCache, + queueJson['post'], + federationList, + debug, projectVersion, + maxFollowers, onionDomain, + signingPrivateKeyPem, unitTest): if os.path.isfile(queueFilename): try: os.remove(queueFilename)