Validate sending actor when follow request is received

main
Bob Mottram 2021-12-14 13:59:42 +00:00
parent 0e0160a698
commit 23af727bec
2 changed files with 255 additions and 242 deletions

252
follow.py
View File

@ -11,11 +11,9 @@ from pprint import pprint
import os import os
from utils import hasObjectStringObject from utils import hasObjectStringObject
from utils import hasObjectStringType from utils import hasObjectStringType
from utils import hasActor
from utils import removeDomainPort from utils import removeDomainPort
from utils import hasUsersPath from utils import hasUsersPath
from utils import getFullDomain from utils import getFullDomain
from utils import isSystemAccount
from utils import getFollowersList from utils import getFollowersList
from utils import validNickname from utils import validNickname
from utils import domainPermitted from utils import domainPermitted
@ -31,7 +29,6 @@ from utils import isAccountDir
from utils import getUserPaths from utils import getUserPaths
from utils import acctDir from utils import acctDir
from utils import hasGroupType from utils import hasGroupType
from utils import isGroupAccount
from utils import localActorUrl from utils import localActorUrl
from acceptreject import createAccept from acceptreject import createAccept
from acceptreject import createReject from acceptreject import createReject
@ -39,7 +36,6 @@ from webfinger import webfingerHandle
from auth import createBasicAuthHeader from auth import createBasicAuthHeader
from session import getJson from session import getJson
from session import postJson from session import postJson
from cache import getPersonPubKey
def createInitialLastSeen(baseDir: str, httpPrefix: str) -> None: def createInitialLastSeen(baseDir: str, httpPrefix: str) -> None:
@ -418,8 +414,8 @@ def _getNoOfFollows(baseDir: str, nickname: str, domain: str,
return ctr return ctr
def _getNoOfFollowers(baseDir: str, def getNoOfFollowers(baseDir: str,
nickname: str, domain: str, authenticated: bool) -> int: nickname: str, domain: str, authenticated: bool) -> int:
"""Returns the number of followers of the given person """Returns the number of followers of the given person
""" """
return _getNoOfFollows(baseDir, nickname, domain, return _getNoOfFollows(baseDir, nickname, domain,
@ -562,9 +558,9 @@ def getFollowingFeed(baseDir: str, domain: str, port: int, path: str,
return following return following
def _followApprovalRequired(baseDir: str, nicknameToFollow: str, def followApprovalRequired(baseDir: str, nicknameToFollow: str,
domainToFollow: str, debug: bool, domainToFollow: str, debug: bool,
followRequestHandle: str) -> bool: followRequestHandle: str) -> bool:
""" Returns the policy for follower approvals """ Returns the policy for follower approvals
""" """
# has this handle already been manually approved? # has this handle already been manually approved?
@ -591,10 +587,10 @@ def _followApprovalRequired(baseDir: str, nicknameToFollow: str,
return manuallyApproveFollows return manuallyApproveFollows
def _noOfFollowRequests(baseDir: str, def noOfFollowRequests(baseDir: str,
nicknameToFollow: str, domainToFollow: str, nicknameToFollow: str, domainToFollow: str,
nickname: str, domain: str, fromPort: int, nickname: str, domain: str, fromPort: int,
followType: str) -> int: followType: str) -> int:
"""Returns the current number of follow requests """Returns the current number of follow requests
""" """
accountsDir = baseDir + '/accounts/' + \ accountsDir = baseDir + '/accounts/' + \
@ -608,7 +604,7 @@ def _noOfFollowRequests(baseDir: str,
with open(approveFollowsFilename, 'r') as f: with open(approveFollowsFilename, 'r') as f:
lines = f.readlines() lines = f.readlines()
except OSError: except OSError:
print('EX: _noOfFollowRequests ' + approveFollowsFilename) print('EX: noOfFollowRequests ' + approveFollowsFilename)
if lines: if lines:
if followType == "onion": if followType == "onion":
for fileLine in lines: for fileLine in lines:
@ -623,12 +619,12 @@ def _noOfFollowRequests(baseDir: str,
return ctr return ctr
def _storeFollowRequest(baseDir: str, def storeFollowRequest(baseDir: str,
nicknameToFollow: str, domainToFollow: str, port: int, nicknameToFollow: str, domainToFollow: str, port: int,
nickname: str, domain: str, fromPort: int, nickname: str, domain: str, fromPort: int,
followJson: {}, followJson: {},
debug: bool, personUrl: str, debug: bool, personUrl: str,
groupAccount: bool) -> bool: groupAccount: bool) -> bool:
"""Stores the follow request for later use """Stores the follow request for later use
""" """
accountsDir = baseDir + '/accounts/' + \ accountsDir = baseDir + '/accounts/' + \
@ -651,7 +647,7 @@ def _storeFollowRequest(baseDir: str,
with open(followersFilename, 'r') as fpFollowers: with open(followersFilename, 'r') as fpFollowers:
followersStr = fpFollowers.read() followersStr = fpFollowers.read()
except OSError: except OSError:
print('EX: _storeFollowRequest ' + followersFilename) print('EX: storeFollowRequest ' + followersFilename)
if approveHandle in followersStr: if approveHandle in followersStr:
alreadyFollowing = True alreadyFollowing = True
@ -696,7 +692,7 @@ def _storeFollowRequest(baseDir: str,
with open(approveFollowsFilename, 'a+') as fp: with open(approveFollowsFilename, 'a+') as fp:
fp.write(approveHandleStored + '\n') fp.write(approveHandleStored + '\n')
except OSError: except OSError:
print('EX: _storeFollowRequest 2 ' + approveFollowsFilename) print('EX: storeFollowRequest 2 ' + approveFollowsFilename)
else: else:
if debug: if debug:
print('DEBUG: ' + approveHandleStored + print('DEBUG: ' + approveHandleStored +
@ -706,7 +702,7 @@ def _storeFollowRequest(baseDir: str,
with open(approveFollowsFilename, 'w+') as fp: with open(approveFollowsFilename, 'w+') as fp:
fp.write(approveHandleStored + '\n') fp.write(approveHandleStored + '\n')
except OSError: except OSError:
print('EX: _storeFollowRequest 3 ' + approveFollowsFilename) print('EX: storeFollowRequest 3 ' + approveFollowsFilename)
# store the follow request in its own directory # store the follow request in its own directory
# We don't rely upon the inbox because items in there could expire # 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) 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, def followedAccountAccepts(session, baseDir: str, httpPrefix: str,
nicknameToFollow: str, domainToFollow: str, nicknameToFollow: str, domainToFollow: str,
port: int, port: int,
@ -1124,8 +914,8 @@ def sendFollowRequest(session, baseDir: str,
newFollowJson['to'] = followedId newFollowJson['to'] = followedId
print('Follow request: ' + str(newFollowJson)) print('Follow request: ' + str(newFollowJson))
if _followApprovalRequired(baseDir, nickname, domain, debug, if followApprovalRequired(baseDir, nickname, domain, debug,
followHandle): followHandle):
# Remove any follow requests rejected for the account being followed. # Remove any follow requests rejected for the account being followed.
# It's assumed that if you are following someone then you are # 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 # ok with them following back. If this isn't the case then a rejected

245
inbox.py
View File

@ -17,6 +17,9 @@ from languages import understoodPostLanguage
from like import updateLikesCollection from like import updateLikesCollection
from reaction import updateReactionCollection from reaction import updateReactionCollection
from reaction import validEmojiContent from reaction import validEmojiContent
from utils import domainPermitted
from utils import isGroupAccount
from utils import isSystemAccount
from utils import invalidCiphertext from utils import invalidCiphertext
from utils import removeHtml from utils import removeHtml
from utils import fileLastModified from utils import fileLastModified
@ -65,9 +68,14 @@ from httpsig import verifyPostHeaders
from session import createSession from session import createSession
from follow import followerApprovalActive from follow import followerApprovalActive
from follow import isFollowingActor from follow import isFollowingActor
from follow import receiveFollowRequest
from follow import getFollowersOfActor from follow import getFollowersOfActor
from follow import unfollowerOfAccount 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 pprint import pprint
from cache import storePersonInCache from cache import storePersonInCache
from cache import getPersonPubKey from cache import getPersonPubKey
@ -3834,6 +3842,221 @@ def _checkJsonSignature(baseDir: str, queueJson: {}) -> (bool, bool):
return hasJsonSignature, jwebsigType 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, def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
projectVersion: str, projectVersion: str,
baseDir: str, httpPrefix: str, sendThreads: [], postLog: [], baseDir: str, httpPrefix: str, sendThreads: [], postLog: [],
@ -4134,16 +4357,16 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
if debug: if debug:
print('DEBUG: checking for follow requests') print('DEBUG: checking for follow requests')
if receiveFollowRequest(session, if _receiveFollowRequest(session,
baseDir, httpPrefix, port, baseDir, httpPrefix, port,
sendThreads, postLog, sendThreads, postLog,
cachedWebfingers, cachedWebfingers,
personCache, personCache,
queueJson['post'], queueJson['post'],
federationList, federationList,
debug, projectVersion, debug, projectVersion,
maxFollowers, onionDomain, maxFollowers, onionDomain,
signingPrivateKeyPem): signingPrivateKeyPem, unitTest):
if os.path.isfile(queueFilename): if os.path.isfile(queueFilename):
try: try:
os.remove(queueFilename) os.remove(queueFilename)