daemon.py 147 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913
  1. __filename__ = "daemon.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "0.0.1"
  5. __maintainer__ = "Bob Mottram"
  6. __email__ = "bob@freedombone.net"
  7. __status__ = "Production"
  8. from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer
  9. #import socketserver
  10. import commentjson
  11. import json
  12. import time
  13. import base64
  14. # used for mime decoding of message POST
  15. import email.parser
  16. # for saving images
  17. from binascii import a2b_base64
  18. from hashlib import sha256
  19. from pprint import pprint
  20. from session import createSession
  21. from webfinger import webfingerMeta
  22. from webfinger import webfingerLookup
  23. from webfinger import webfingerHandle
  24. from person import registerAccount
  25. from person import personLookup
  26. from person import personBoxJson
  27. from person import createSharedInbox
  28. from person import isSuspended
  29. from person import suspendAccount
  30. from person import unsuspendAccount
  31. from person import removeAccount
  32. from person import canRemovePost
  33. from posts import outboxMessageCreateWrap
  34. from posts import savePostToBox
  35. from posts import sendToFollowers
  36. from posts import postIsAddressedToPublic
  37. from posts import sendToNamedAddresses
  38. from posts import createPublicPost
  39. from posts import createReportPost
  40. from posts import createUnlistedPost
  41. from posts import createFollowersOnlyPost
  42. from posts import createDirectMessagePost
  43. from posts import populateRepliesJson
  44. from posts import addToField
  45. from inbox import inboxPermittedMessage
  46. from inbox import inboxMessageHasParams
  47. from inbox import runInboxQueue
  48. from inbox import savePostToInboxQueue
  49. from inbox import populateReplies
  50. from follow import getFollowingFeed
  51. from follow import outboxUndoFollow
  52. from follow import sendFollowRequest
  53. from auth import authorize
  54. from auth import createPassword
  55. from auth import createBasicAuthHeader
  56. from auth import authorizeBasic
  57. from threads import threadWithTrace
  58. from media import getMediaPath
  59. from media import createMediaDirs
  60. from delete import outboxDelete
  61. from like import outboxLike
  62. from like import outboxUndoLike
  63. from blocking import outboxBlock
  64. from blocking import outboxUndoBlock
  65. from blocking import addBlock
  66. from blocking import removeBlock
  67. from blocking import addGlobalBlock
  68. from blocking import removeGlobalBlock
  69. from blocking import isBlockedHashtag
  70. from config import setConfigParam
  71. from config import getConfigParam
  72. from roles import outboxDelegate
  73. from roles import setRole
  74. from roles import clearModeratorStatus
  75. from skills import outboxSkills
  76. from availability import outboxAvailability
  77. from webinterface import htmlIndividualPost
  78. from webinterface import htmlProfile
  79. from webinterface import htmlInbox
  80. from webinterface import htmlOutbox
  81. from webinterface import htmlModeration
  82. from webinterface import htmlPostReplies
  83. from webinterface import htmlLogin
  84. from webinterface import htmlSuspended
  85. from webinterface import htmlGetLoginCredentials
  86. from webinterface import htmlNewPost
  87. from webinterface import htmlFollowConfirm
  88. from webinterface import htmlSearch
  89. from webinterface import htmlUnfollowConfirm
  90. from webinterface import htmlProfileAfterSearch
  91. from webinterface import htmlEditProfile
  92. from webinterface import htmlTermsOfService
  93. from webinterface import htmlHashtagSearch
  94. from webinterface import htmlModerationInfo
  95. from webinterface import htmlSearchSharedItems
  96. from webinterface import htmlHashtagBlocked
  97. from shares import getSharesFeedForPerson
  98. from shares import outboxShareUpload
  99. from shares import outboxUndoShareUpload
  100. from shares import addShare
  101. from utils import getNicknameFromActor
  102. from utils import getDomainFromActor
  103. from manualapprove import manualDenyFollowRequest
  104. from manualapprove import manualApproveFollowRequest
  105. from announce import createAnnounce
  106. from announce import outboxAnnounce
  107. from content import addHtmlTags
  108. from media import removeMetaData
  109. import os
  110. import sys
  111. # maximum number of posts to list in outbox feed
  112. maxPostsInFeed=12
  113. # number of follows/followers per page
  114. followsPerPage=12
  115. # number of item shares per page
  116. sharesPerPage=12
  117. def readFollowList(filename: str):
  118. """Returns a list of ActivityPub addresses to follow
  119. """
  120. followlist=[]
  121. if not os.path.isfile(filename):
  122. return followlist
  123. followUsers = open(filename, "r")
  124. for u in followUsers:
  125. if u not in followlist:
  126. nickname,domain = parseHandle(u)
  127. if nickname:
  128. followlist.append(nickname+'@'+domain)
  129. followUsers.close()
  130. return followlist
  131. class PubServer(BaseHTTPRequestHandler):
  132. protocol_version = 'HTTP/1.1'
  133. def _login_headers(self,fileFormat: str,length: int) -> None:
  134. self.send_response(200)
  135. self.send_header('Content-type', fileFormat)
  136. self.send_header('Content-Length', str(length))
  137. self.send_header('Host', self.server.domainFull)
  138. self.send_header('WWW-Authenticate', 'title="Login to Epicyon", Basic realm="epicyon"')
  139. self.end_headers()
  140. def _set_headers(self,fileFormat: str,length: int,cookie: str) -> None:
  141. self.send_response(200)
  142. self.send_header('Content-type', fileFormat)
  143. self.send_header('Content-Length', str(length))
  144. if cookie:
  145. self.send_header('Cookie', cookie)
  146. self.send_header('Host', self.server.domainFull)
  147. self.send_header('InstanceID', self.server.instanceId)
  148. self.end_headers()
  149. def _redirect_headers(self,redirect: str,cookie: str) -> None:
  150. self.send_response(303)
  151. #self.send_header('Content-type', 'text/html')
  152. if cookie:
  153. self.send_header('Cookie', cookie)
  154. self.send_header('Location', redirect)
  155. self.send_header('Host', self.server.domainFull)
  156. self.send_header('InstanceID', self.server.instanceId)
  157. self.send_header('Content-Length', '0')
  158. self.end_headers()
  159. def _404(self) -> None:
  160. msg="<html><head></head><body><h1>404 Not Found</h1></body></html>".encode('utf-8')
  161. self.send_response(404)
  162. self.send_header('Content-Type', 'text/html; charset=utf-8')
  163. self.send_header('Content-Length', str(len(msg)))
  164. self.end_headers()
  165. try:
  166. self.wfile.write(msg)
  167. except Exception as e:
  168. print('Error when showing 404')
  169. print(e)
  170. def _webfinger(self) -> bool:
  171. if not self.path.startswith('/.well-known'):
  172. return False
  173. if self.server.debug:
  174. print('DEBUG: WEBFINGER well-known')
  175. if self.server.debug:
  176. print('DEBUG: WEBFINGER host-meta')
  177. if self.path.startswith('/.well-known/host-meta'):
  178. wfResult=webfingerMeta(self.server.httpPrefix,self.server.domainFull)
  179. if wfResult:
  180. msg=wfResult.encode('utf-8')
  181. self._set_headers('application/xrd+xml',len(msg),None)
  182. self.wfile.write(msg)
  183. return
  184. if self.server.debug:
  185. print('DEBUG: WEBFINGER lookup '+self.path+' '+str(self.server.baseDir))
  186. wfResult=webfingerLookup(self.path,self.server.baseDir,self.server.port,self.server.debug)
  187. if wfResult:
  188. msg=json.dumps(wfResult).encode('utf-8')
  189. self._set_headers('application/jrd+json',len(msg),None)
  190. self.wfile.write(msg)
  191. else:
  192. if self.server.debug:
  193. print('DEBUG: WEBFINGER lookup 404 '+self.path)
  194. self._404()
  195. return True
  196. def _permittedDir(self,path: str) -> bool:
  197. """These are special paths which should not be accessible
  198. directly via GET or POST
  199. """
  200. if path.startswith('/wfendpoints') or \
  201. path.startswith('/keys') or \
  202. path.startswith('/accounts'):
  203. return False
  204. return True
  205. def _postToOutbox(self,messageJson: {}) -> bool:
  206. """post is received by the outbox
  207. Client to server message post
  208. https://www.w3.org/TR/activitypub/#client-to-server-outbox-delivery
  209. """
  210. if not messageJson.get('type'):
  211. if self.server.debug:
  212. print('DEBUG: POST to outbox has no "type" parameter')
  213. return False
  214. if not messageJson.get('object') and messageJson.get('content'):
  215. if messageJson['type']!='Create':
  216. # https://www.w3.org/TR/activitypub/#object-without-create
  217. if self.server.debug:
  218. print('DEBUG: POST to outbox - adding Create wrapper')
  219. messageJson= \
  220. outboxMessageCreateWrap(self.server.httpPrefix, \
  221. self.postToNickname, \
  222. self.server.domain, \
  223. self.server.port, \
  224. messageJson)
  225. if messageJson['type']=='Create':
  226. if not (messageJson.get('id') and \
  227. messageJson.get('type') and \
  228. messageJson.get('actor') and \
  229. messageJson.get('object') and \
  230. messageJson.get('to')):
  231. if self.server.debug:
  232. pprint(messageJson)
  233. print('DEBUG: POST to outbox - Create does not have the required parameters')
  234. return False
  235. # https://www.w3.org/TR/activitypub/#create-activity-outbox
  236. messageJson['object']['attributedTo']=messageJson['actor']
  237. if messageJson['object'].get('attachment'):
  238. attachmentIndex=0
  239. if messageJson['object']['attachment'][attachmentIndex].get('mediaType'):
  240. fileExtension='png'
  241. if messageJson['object']['attachment'][attachmentIndex]['mediaType'].endswith('jpeg'):
  242. fileExtension='jpg'
  243. if messageJson['object']['attachment'][attachmentIndex]['mediaType'].endswith('gif'):
  244. fileExtension='gif'
  245. mediaDir=self.server.baseDir+'/accounts/'+self.postToNickname+'@'+self.server.domain
  246. uploadMediaFilename=mediaDir+'/upload.'+fileExtension
  247. if not os.path.isfile(uploadMediaFilename):
  248. del messageJson['object']['attachment']
  249. else:
  250. # generate a path for the uploaded image
  251. mPath=getMediaPath()
  252. mediaPath=mPath+'/'+createPassword(32)+'.'+fileExtension
  253. createMediaDirs(self.server.baseDir,mPath)
  254. mediaFilename=self.server.baseDir+'/'+mediaPath
  255. # move the uploaded image to its new path
  256. os.rename(uploadMediaFilename,mediaFilename)
  257. # change the url of the attachment
  258. messageJson['object']['attachment'][attachmentIndex]['url']= \
  259. self.server.httpPrefix+'://'+self.server.domainFull+'/'+mediaPath
  260. permittedOutboxTypes=[
  261. 'Create','Announce','Like','Follow','Undo', \
  262. 'Update','Add','Remove','Block','Delete', \
  263. 'Delegate','Skill'
  264. ]
  265. if messageJson['type'] not in permittedOutboxTypes:
  266. if self.server.debug:
  267. print('DEBUG: POST to outbox - '+messageJson['type']+ \
  268. ' is not a permitted activity type')
  269. return False
  270. if messageJson.get('id'):
  271. postId=messageJson['id'].replace('/activity','').replace('/undo','')
  272. if self.server.debug:
  273. print('DEBUG: id attribute exists within POST to outbox')
  274. else:
  275. if self.server.debug:
  276. print('DEBUG: No id attribute within POST to outbox')
  277. postId=None
  278. if self.server.debug:
  279. pprint(messageJson)
  280. print('DEBUG: savePostToBox')
  281. savePostToBox(self.server.baseDir, \
  282. self.server.httpPrefix, \
  283. postId, \
  284. self.postToNickname, \
  285. self.server.domainFull,messageJson,'outbox')
  286. if outboxAnnounce(self.server.baseDir,messageJson,self.server.debug):
  287. if self.server.debug:
  288. print('DEBUG: Updated announcements (shares) collection for the post associated with the Announce activity')
  289. if not self.server.session:
  290. if self.server.debug:
  291. print('DEBUG: creating new session for c2s')
  292. self.server.session= \
  293. createSession(self.server.domain,self.server.port,self.server.useTor)
  294. if self.server.debug:
  295. print('DEBUG: sending c2s post to followers')
  296. sendToFollowers(self.server.session,self.server.baseDir, \
  297. self.postToNickname,self.server.domain, \
  298. self.server.port, \
  299. self.server.httpPrefix, \
  300. self.server.federationList, \
  301. self.server.sendThreads, \
  302. self.server.postLog, \
  303. self.server.cachedWebfingers, \
  304. self.server.personCache, \
  305. messageJson,self.server.debug, \
  306. self.server.projectVersion)
  307. if self.server.debug:
  308. print('DEBUG: handle any unfollow requests')
  309. outboxUndoFollow(self.server.baseDir,messageJson,self.server.debug)
  310. if self.server.debug:
  311. print('DEBUG: handle delegation requests')
  312. outboxDelegate(self.server.baseDir,self.postToNickname,messageJson,self.server.debug)
  313. if self.server.debug:
  314. print('DEBUG: handle skills changes requests')
  315. outboxSkills(self.server.baseDir,self.postToNickname,messageJson,self.server.debug)
  316. if self.server.debug:
  317. print('DEBUG: handle availability changes requests')
  318. outboxAvailability(self.server.baseDir,self.postToNickname,messageJson,self.server.debug)
  319. if self.server.debug:
  320. print('DEBUG: handle any like requests')
  321. outboxLike(self.server.baseDir,self.server.httpPrefix, \
  322. self.postToNickname,self.server.domain,self.server.port, \
  323. messageJson,self.server.debug)
  324. if self.server.debug:
  325. print('DEBUG: handle any undo like requests')
  326. outboxUndoLike(self.server.baseDir,self.server.httpPrefix, \
  327. self.postToNickname,self.server.domain,self.server.port, \
  328. messageJson,self.server.debug)
  329. if self.server.debug:
  330. print('DEBUG: handle delete requests')
  331. outboxDelete(self.server.baseDir,self.server.httpPrefix, \
  332. self.postToNickname,self.server.domain, \
  333. messageJson,self.server.debug, \
  334. self.server.allowDeletion)
  335. if self.server.debug:
  336. print('DEBUG: handle block requests')
  337. outboxBlock(self.server.baseDir,self.server.httpPrefix, \
  338. self.postToNickname,self.server.domain, \
  339. self.server.port,
  340. messageJson,self.server.debug)
  341. if self.server.debug:
  342. print('DEBUG: handle undo block requests')
  343. outboxUndoBlock(self.server.baseDir,self.server.httpPrefix, \
  344. self.postToNickname,self.server.domain, \
  345. self.server.port,
  346. messageJson,self.server.debug)
  347. if self.server.debug:
  348. print('DEBUG: handle share uploads')
  349. outboxShareUpload(self.server.baseDir,self.server.httpPrefix, \
  350. self.postToNickname,self.server.domain, \
  351. self.server.port,
  352. messageJson,self.server.debug)
  353. if self.server.debug:
  354. print('DEBUG: handle undo share uploads')
  355. outboxUndoShareUpload(self.server.baseDir,self.server.httpPrefix, \
  356. self.postToNickname,self.server.domain, \
  357. self.server.port,
  358. messageJson,self.server.debug)
  359. if self.server.debug:
  360. print('DEBUG: sending c2s post to named addresses')
  361. print('c2s sender: '+self.postToNickname+'@'+self.server.domain+':'+str(self.server.port))
  362. sendToNamedAddresses(self.server.session,self.server.baseDir, \
  363. self.postToNickname,self.server.domain, \
  364. self.server.port, \
  365. self.server.httpPrefix, \
  366. self.server.federationList, \
  367. self.server.sendThreads, \
  368. self.server.postLog, \
  369. self.server.cachedWebfingers, \
  370. self.server.personCache, \
  371. messageJson,self.server.debug, \
  372. self.server.projectVersion)
  373. return True
  374. def _updateInboxQueue(self,nickname: str,messageJson: {},messageBytes: str) -> int:
  375. """Update the inbox queue
  376. """
  377. # Check if the queue is full
  378. if len(self.server.inboxQueue)>=self.server.maxQueueLength:
  379. print('Inbox queue is full')
  380. return 1
  381. # Convert the headers needed for signature verification to dict
  382. headersDict={}
  383. headersDict['host']=self.headers['host']
  384. headersDict['signature']=self.headers['signature']
  385. if self.headers.get('Date'):
  386. headersDict['Date']=self.headers['Date']
  387. if self.headers.get('digest'):
  388. headersDict['digest']=self.headers['digest']
  389. if self.headers.get('Content-type'):
  390. headersDict['Content-type']=self.headers['Content-type']
  391. # For follow activities add a 'to' field, which is a copy of the object field
  392. messageJson,toFieldExists=addToField('Follow',messageJson,self.server.debug)
  393. # For like activities add a 'to' field, which is a copy of the actor within the object field
  394. messageJson,toFieldExists=addToField('Like',messageJson,self.server.debug)
  395. pprint(messageJson)
  396. # save the json for later queue processing
  397. queueFilename = \
  398. savePostToInboxQueue(self.server.baseDir,
  399. self.server.httpPrefix,
  400. nickname,
  401. self.server.domainFull,
  402. messageJson,
  403. messageBytes.decode('utf-8'),
  404. headersDict,
  405. self.path,
  406. self.server.debug)
  407. if queueFilename:
  408. # add json to the queue
  409. if queueFilename not in self.server.inboxQueue:
  410. self.server.inboxQueue.append(queueFilename)
  411. self.send_response(201)
  412. self.end_headers()
  413. self.server.POSTbusy=False
  414. return 0
  415. return 2
  416. def _isAuthorized(self) -> bool:
  417. # token based authenticated used by the web interface
  418. if self.headers.get('Cookie'):
  419. if '=' in self.headers['Cookie']:
  420. tokenStr=self.headers['Cookie'].split('=',1)[1]
  421. if self.server.tokensLookup.get(tokenStr):
  422. nickname=self.server.tokensLookup[tokenStr]
  423. if '/'+nickname+'/' in self.path:
  424. return True
  425. if self.path.endswith('/'+nickname):
  426. return True
  427. return False
  428. # basic auth
  429. if self.headers.get('Authorization'):
  430. if authorize(self.server.baseDir,self.path, \
  431. self.headers['Authorization'], \
  432. self.server.debug):
  433. return True
  434. return False
  435. def do_GET(self):
  436. if self.server.debug:
  437. print('DEBUG: GET from '+self.server.baseDir+ \
  438. ' path: '+self.path+' busy: '+ \
  439. str(self.server.GETbusy))
  440. if self.server.debug:
  441. print(str(self.headers))
  442. cookie=None
  443. if self.headers.get('Cookie'):
  444. cookie=self.headers['Cookie']
  445. # check authorization
  446. authorized = self._isAuthorized()
  447. if authorized:
  448. if self.server.debug:
  449. print('GET Authorization granted')
  450. else:
  451. if self.server.debug:
  452. print('GET Not authorized')
  453. # treat shared inbox paths consistently
  454. if self.path=='/sharedInbox' or self.path=='/users/inbox':
  455. self.path='/inbox'
  456. # is this a html request?
  457. htmlGET=False
  458. if self.headers.get('Accept'):
  459. if 'text/html' in self.headers['Accept']:
  460. htmlGET=True
  461. # if not authorized then show the login screen
  462. if htmlGET and self.path!='/login' and self.path!='/' and self.path!='/terms':
  463. if '/media/' not in self.path and \
  464. '/sharefiles/' not in self.path and \
  465. '/statuses/' not in self.path and \
  466. '/emoji/' not in self.path and \
  467. '/tags/' not in self.path and \
  468. '/icons/' not in self.path:
  469. divertToLoginScreen=True
  470. if self.path.startswith('/users/'):
  471. nickStr=self.path.split('/users/')[1]
  472. if '/' not in nickStr and '?' not in nickStr:
  473. divertToLoginScreen=False
  474. else:
  475. if self.path.endswith('/following') or \
  476. self.path.endswith('/followers') or \
  477. self.path.endswith('/skills') or \
  478. self.path.endswith('/roles') or \
  479. self.path.endswith('/shares'):
  480. divertToLoginScreen=False
  481. if divertToLoginScreen and not authorized:
  482. if self.server.debug:
  483. print('DEBUG: divertToLoginScreen='+str(divertToLoginScreen))
  484. print('DEBUG: authorized='+str(authorized))
  485. self.send_response(303)
  486. self.send_header('Location', '/login')
  487. self.send_header('Content-Length', '0')
  488. self.end_headers()
  489. self.server.GETbusy=False
  490. return
  491. # get css
  492. # Note that this comes before the busy flag to avoid conflicts
  493. if self.path.endswith('.css'):
  494. if os.path.isfile('epicyon-profile.css'):
  495. with open('epicyon-profile.css', 'r') as cssfile:
  496. css = cssfile.read()
  497. msg=css.encode('utf-8')
  498. self._set_headers('text/css',len(msg),cookie)
  499. self.wfile.write(msg)
  500. self.wfile.flush()
  501. return
  502. # image on login screen
  503. if self.path=='/login.png':
  504. mediaFilename= \
  505. self.server.baseDir+'/accounts/login.png'
  506. if os.path.isfile(mediaFilename):
  507. with open(mediaFilename, 'rb') as avFile:
  508. mediaBinary = avFile.read()
  509. self._set_headers('image/png',len(mediaBinary),cookie)
  510. self.wfile.write(mediaBinary)
  511. self.wfile.flush()
  512. self._404()
  513. return
  514. # login screen background image
  515. if self.path=='/login-background.png':
  516. mediaFilename= \
  517. self.server.baseDir+'/accounts/login-background.png'
  518. if os.path.isfile(mediaFilename):
  519. with open(mediaFilename, 'rb') as avFile:
  520. mediaBinary = avFile.read()
  521. self._set_headers('image/png',len(mediaBinary),cookie)
  522. self.wfile.write(mediaBinary)
  523. self.wfile.flush()
  524. return
  525. self._404()
  526. return
  527. # follow screen background image
  528. if self.path=='/follow-background.png':
  529. mediaFilename= \
  530. self.server.baseDir+'/accounts/follow-background.png'
  531. if os.path.isfile(mediaFilename):
  532. with open(mediaFilename, 'rb') as avFile:
  533. mediaBinary = avFile.read()
  534. self._set_headers('image/png',len(mediaBinary),cookie)
  535. self.wfile.write(mediaBinary)
  536. self.wfile.flush()
  537. self._404()
  538. return
  539. # emoji images
  540. if '/emoji/' in self.path:
  541. if self.path.endswith('.png') or \
  542. self.path.endswith('.jpg') or \
  543. self.path.endswith('.gif'):
  544. emojiStr=self.path.split('/emoji/')[1]
  545. emojiFilename= \
  546. self.server.baseDir+'/emoji/'+emojiStr
  547. if os.path.isfile(emojiFilename):
  548. mediaImageType='png'
  549. if emojiFilename.endswith('.png'):
  550. mediaImageType='png'
  551. elif emojiFilename.endswith('.jpg'):
  552. mediaImageType='jpeg'
  553. else:
  554. mediaImageType='gif'
  555. with open(emojiFilename, 'rb') as avFile:
  556. mediaBinary = avFile.read()
  557. self._set_headers('image/'+mediaImageType,len(mediaBinary),cookie)
  558. self.wfile.write(mediaBinary)
  559. self.wfile.flush()
  560. return
  561. self._404()
  562. return
  563. # show media
  564. # Note that this comes before the busy flag to avoid conflicts
  565. if '/media/' in self.path:
  566. if self.path.endswith('.png') or \
  567. self.path.endswith('.jpg') or \
  568. self.path.endswith('.gif'):
  569. mediaStr=self.path.split('/media/')[1]
  570. mediaFilename= \
  571. self.server.baseDir+'/media/'+mediaStr
  572. if os.path.isfile(mediaFilename):
  573. mediaFileType='png'
  574. if mediaFilename.endswith('.png'):
  575. mediaFileType='png'
  576. elif mediaFilename.endswith('.jpg'):
  577. mediaFileType='jepg'
  578. else:
  579. mediaFileType='gif'
  580. with open(mediaFilename, 'rb') as avFile:
  581. mediaBinary = avFile.read()
  582. self._set_headers('image/'+mediaFileType,len(mediaBinary),cookie)
  583. self.wfile.write(mediaBinary)
  584. self.wfile.flush()
  585. return
  586. self._404()
  587. return
  588. # show shared item images
  589. # Note that this comes before the busy flag to avoid conflicts
  590. if '/sharefiles/' in self.path:
  591. if self.path.endswith('.png') or \
  592. self.path.endswith('.jpg') or \
  593. self.path.endswith('.gif'):
  594. mediaStr=self.path.split('/sharefiles/')[1]
  595. mediaFilename= \
  596. self.server.baseDir+'/sharefiles/'+mediaStr
  597. if os.path.isfile(mediaFilename):
  598. mediaFileType='png'
  599. if mediaFilename.endswith('.png'):
  600. mediaFileType='png'
  601. elif mediaFilename.endswith('.jpg'):
  602. mediaFileType='jpeg'
  603. else:
  604. mediaFileType='gif'
  605. with open(mediaFilename, 'rb') as avFile:
  606. mediaBinary = avFile.read()
  607. self._set_headers('image/'+mediaFileType,len(mediaBinary),cookie)
  608. self.wfile.write(mediaBinary)
  609. self.wfile.flush()
  610. return
  611. self._404()
  612. return
  613. # icon images
  614. # Note that this comes before the busy flag to avoid conflicts
  615. if self.path.startswith('/icons/'):
  616. if self.path.endswith('.png'):
  617. mediaStr=self.path.split('/icons/')[1]
  618. mediaFilename= \
  619. self.server.baseDir+'/img/icons/'+mediaStr
  620. if os.path.isfile(mediaFilename):
  621. if mediaFilename.endswith('.png'):
  622. with open(mediaFilename, 'rb') as avFile:
  623. mediaBinary = avFile.read()
  624. self._set_headers('image/png',len(mediaBinary),cookie)
  625. self.wfile.write(mediaBinary)
  626. self.wfile.flush()
  627. return
  628. self._404()
  629. return
  630. # show avatar or background image
  631. # Note that this comes before the busy flag to avoid conflicts
  632. if '/users/' in self.path:
  633. if self.path.endswith('.png') or \
  634. self.path.endswith('.jpg') or \
  635. self.path.endswith('.gif'):
  636. avatarStr=self.path.split('/users/')[1]
  637. if '/' in avatarStr:
  638. avatarNickname=avatarStr.split('/')[0]
  639. avatarFile=avatarStr.split('/')[1]
  640. avatarFilename= \
  641. self.server.baseDir+'/accounts/'+ \
  642. avatarNickname+'@'+ \
  643. self.server.domain+'/'+avatarFile
  644. if os.path.isfile(avatarFilename):
  645. mediaImageType='png'
  646. if avatarFile.endswith('.png'):
  647. mediaImageType='png'
  648. elif avatarFile.endswith('.jpg'):
  649. mediaImageType='jpeg'
  650. else:
  651. mediaImageType='gif'
  652. with open(avatarFilename, 'rb') as avFile:
  653. mediaBinary = avFile.read()
  654. self._set_headers('image/'+mediaImageType,len(mediaBinary),cookie)
  655. self.wfile.write(mediaBinary)
  656. self.wfile.flush()
  657. return
  658. # This busy state helps to avoid flooding
  659. # Resources which are expected to be called from a web page
  660. # should be above this
  661. if self.server.GETbusy:
  662. currTimeGET=int(time.time())
  663. if currTimeGET-self.server.lastGET==0:
  664. if self.server.debug:
  665. print('DEBUG: GET Busy')
  666. self.send_response(429)
  667. self.end_headers()
  668. return
  669. self.server.lastGET=currTimeGET
  670. self.server.GETbusy=True
  671. if not self._permittedDir(self.path):
  672. if self.server.debug:
  673. print('DEBUG: GET Not permitted')
  674. self._404()
  675. self.server.GETbusy=False
  676. return
  677. # get webfinger endpoint for a person
  678. if self._webfinger():
  679. self.server.GETbusy=False
  680. return
  681. if self.path.startswith('/terms'):
  682. msg=htmlTermsOfService(self.server.baseDir, \
  683. self.server.httpPrefix, \
  684. self.server.domainFull).encode()
  685. self._login_headers('text/html',len(msg))
  686. self.wfile.write(msg)
  687. self.server.GETbusy=False
  688. return
  689. if self.path.startswith('/login') or self.path=='/':
  690. # request basic auth
  691. msg=htmlLogin(self.server.baseDir).encode('utf-8')
  692. self._login_headers('text/html',len(msg))
  693. self.wfile.write(msg)
  694. self.server.GETbusy=False
  695. return
  696. # follow a person from the web interface by selecting Follow on the dropdown
  697. if '/users/' in self.path:
  698. if '?follow=' in self.path:
  699. followStr=self.path.split('?follow=')[1]
  700. originPathStr=self.path.split('?follow=')[0]
  701. if ';' in followStr:
  702. followActor=followStr.split(';')[0]
  703. followProfileUrl=followStr.split(';')[1]
  704. # show the confirm follow screen
  705. msg=htmlFollowConfirm(self.server.baseDir,originPathStr,followActor,followProfileUrl).encode()
  706. self._set_headers('text/html',len(msg),cookie)
  707. self.wfile.write(msg)
  708. self.server.GETbusy=False
  709. return
  710. self._redirect_headers(originPathStr,cookie)
  711. self.server.GETbusy=False
  712. return
  713. # block a person from the web interface by selecting Block on the dropdown
  714. if '/users/' in self.path:
  715. if '?block=' in self.path:
  716. blockStr=self.path.split('?block=')[1]
  717. originPathStr=self.path.split('?block=')[0]
  718. if ';' in blockStr:
  719. blockActor=blockStr.split(';')[0]
  720. blockProfileUrl=blockStr.split(';')[1]
  721. # show the confirm block screen
  722. msg=htmlBlockConfirm(self.server.baseDir,originPathStr,blockActor,blockProfileUrl).encode()
  723. self._set_headers('text/html',len(msg),cookie)
  724. self.wfile.write(msg)
  725. self.server.GETbusy=False
  726. return
  727. self._redirect_headers(originPathStr,cookie)
  728. self.server.GETbusy=False
  729. return
  730. # hashtag search
  731. if self.path.startswith('/tags/'):
  732. pageNumber=1
  733. if '?page=' in self.path:
  734. pageNumberStr=self.path.split('?page=')[1]
  735. if pageNumberStr.isdigit():
  736. pageNumber=int(pageNumberStr)
  737. hashtag=self.path.split('/tags/')[1]
  738. if '?page=' in hashtag:
  739. hashtag=hashtag.split('?page=')[0]
  740. if isBlockedHashtag(self.server.baseDir,hashtag):
  741. msg=htmlHashtagBlocked(self.server.baseDir).encode('utf-8')
  742. self._login_headers('text/html',len(msg))
  743. self.wfile.write(msg)
  744. self.server.GETbusy=False
  745. return
  746. hashtagStr= \
  747. htmlHashtagSearch(self.server.baseDir,hashtag,pageNumber, \
  748. maxPostsInFeed,self.server.session, \
  749. self.server.cachedWebfingers, \
  750. self.server.personCache, \
  751. self.server.httpPrefix, \
  752. self.server.projectVersion)
  753. if hashtagStr:
  754. msg=hashtagStr.encode()
  755. self._set_headers('text/html',len(msg),cookie)
  756. self.wfile.write(msg)
  757. else:
  758. originPathStr=self.path.split('/tags/')[0]
  759. self._redirect_headers(originPathStr+'/search',cookie)
  760. self.server.GETbusy=False
  761. return
  762. # search for a fediverse address from the web interface by selecting search icon
  763. if htmlGET and '/users/' in self.path:
  764. if self.path.endswith('/search'):
  765. # show the search screen
  766. msg=htmlSearch(self.server.baseDir,self.path).encode()
  767. self._set_headers('text/html',len(msg),cookie)
  768. self.wfile.write(msg)
  769. self.server.GETbusy=False
  770. return
  771. # Unfollow a person from the web interface by selecting Unfollow on the dropdown
  772. if htmlGET and '/users/' in self.path:
  773. if '?unfollow=' in self.path:
  774. followStr=self.path.split('?unfollow=')[1]
  775. originPathStr=self.path.split('?unfollow=')[0]
  776. if ';' in followStr:
  777. followActor=followStr.split(';')[0]
  778. followProfileUrl=followStr.split(';')[1]
  779. # show the confirm follow screen
  780. msg=htmlUnfollowConfirm(self.server.baseDir,originPathStr,followActor,followProfileUrl).encode()
  781. self._set_headers('text/html',len(msg),cookie)
  782. self.wfile.write(msg)
  783. self.server.GETbusy=False
  784. return
  785. self._redirect_headers(originPathStr,cookie)
  786. self.server.GETbusy=False
  787. return
  788. # Unblock a person from the web interface by selecting Unblock on the dropdown
  789. if htmlGET and '/users/' in self.path:
  790. if '?unblock=' in self.path:
  791. blockStr=self.path.split('?unblock=')[1]
  792. originPathStr=self.path.split('?unblock=')[0]
  793. if ';' in blockStr:
  794. blockActor=blockStr.split(';')[0]
  795. blockProfileUrl=blockStr.split(';')[1]
  796. # show the confirm unblock screen
  797. msg=htmlUnblockConfirm(self.server.baseDir,originPathStr,blockActor,blockProfileUrl).encode()
  798. self._set_headers('text/html',len(msg),cookie)
  799. self.wfile.write(msg)
  800. self.server.GETbusy=False
  801. return
  802. self._redirect_headers(originPathStr,cookie)
  803. self.server.GETbusy=False
  804. return
  805. # announce/repeat from the web interface
  806. if htmlGET and '?repeat=' in self.path:
  807. repeatUrl=self.path.split('?repeat=')[1]
  808. actor=self.path.split('?repeat=')[0]
  809. self.postToNickname=getNicknameFromActor(actor)
  810. if not self.server.session:
  811. self.server.session= \
  812. createSession(self.server.domain,self.server.port,self.server.useTor)
  813. announceJson= \
  814. createAnnounce(self.server.session, \
  815. self.server.baseDir, \
  816. self.server.federationList, \
  817. self.postToNickname, \
  818. self.server.domain,self.server.port, \
  819. 'https://www.w3.org/ns/activitystreams#Public', \
  820. None,self.server.httpPrefix, \
  821. repeatUrl,False,False, \
  822. self.server.sendThreads, \
  823. self.server.postLog, \
  824. self.server.personCache, \
  825. self.server.cachedWebfingers, \
  826. self.server.debug, \
  827. self.server.projectVersion)
  828. if announceJson:
  829. self._postToOutbox(announceJson)
  830. self.server.GETbusy=False
  831. self._redirect_headers(actor+'/inbox',cookie)
  832. return
  833. # undo an announce/repeat from the web interface
  834. if htmlGET and '?unrepeat=' in self.path:
  835. repeatUrl=self.path.split('?unrepeat=')[1]
  836. actor=self.path.split('?unrepeat=')[0]
  837. self.postToNickname=getNicknameFromActor(actor)
  838. if not self.server.session:
  839. self.server.session= \
  840. createSession(self.server.domain,self.server.port,self.server.useTor)
  841. undoAnnounceActor=self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+self.postToNickname
  842. newUndoAnnounce = {
  843. "@context": "https://www.w3.org/ns/activitystreams",
  844. 'actor': undoAnnounceActor,
  845. 'type': 'Undo',
  846. 'cc': [undoAnnounceActor+'/followers'],
  847. 'to': ['https://www.w3.org/ns/activitystreams#Public'],
  848. 'object': {
  849. 'actor': undoAnnounceActor,
  850. 'cc': [undoAnnounceActor+'/followers'],
  851. 'object': repeatUrl,
  852. 'to': ['https://www.w3.org/ns/activitystreams#Public'],
  853. 'type': 'Announce'
  854. }
  855. }
  856. self._postToOutbox(newUndoAnnounce)
  857. self.server.GETbusy=False
  858. self._redirect_headers(actor+'/inbox',cookie)
  859. return
  860. # send a follow request approval from the web interface
  861. if authorized and '/followapprove=' in self.path and self.path.startswith('/users/'):
  862. originPathStr=self.path.split('/followapprove=')[0]
  863. followerNickname=originPathStr.replace('/users/','')
  864. followingHandle=self.path.split('/followapprove=')[1]
  865. if '@' in followingHandle:
  866. if not self.server.session:
  867. self.server.session= \
  868. createSession(self.server.domain,self.server.port,self.server.useTor)
  869. manualApproveFollowRequest(self.server.session, \
  870. self.server.baseDir, \
  871. self.server.httpPrefix, \
  872. followerNickname,self.server.domain,self.server.port, \
  873. followingHandle, \
  874. self.server.federationList, \
  875. self.server.sendThreads, \
  876. self.server.postLog, \
  877. self.server.cachedWebfingers, \
  878. self.server.personCache, \
  879. self.server.acceptedCaps, \
  880. self.server.debug, \
  881. self.server.projectVersion)
  882. self._redirect_headers(originPathStr,cookie)
  883. self.server.GETbusy=False
  884. return
  885. # deny a follow request from the web interface
  886. if authorized and '/followdeny=' in self.path and self.path.startswith('/users/'):
  887. originPathStr=self.path.split('/followdeny=')[0]
  888. followerNickname=originPathStr.replace('/users/','')
  889. followingHandle=self.path.split('/followdeny=')[1]
  890. if '@' in followingHandle:
  891. manualDenyFollowRequest(self.server.baseDir, \
  892. followerNickname,self.server.domain, \
  893. followingHandle)
  894. self._redirect_headers(originPathStr,cookie)
  895. self.server.GETbusy=False
  896. return
  897. # like from the web interface icon
  898. if htmlGET and '?like=' in self.path and '/statuses/' in self.path:
  899. likeUrl=self.path.split('?like=')[1]
  900. actor=self.path.split('?like=')[0]
  901. self.postToNickname=getNicknameFromActor(actor)
  902. if not self.server.session:
  903. self.server.session= \
  904. createSession(self.server.domain,self.server.port,self.server.useTor)
  905. likeActor=self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+self.postToNickname
  906. actorLiked=likeUrl.split('/statuses/')[0]
  907. likeJson= {
  908. "@context": "https://www.w3.org/ns/activitystreams",
  909. 'type': 'Like',
  910. 'actor': likeActor,
  911. 'object': likeUrl
  912. }
  913. self._postToOutbox(likeJson)
  914. self.server.GETbusy=False
  915. self._redirect_headers(actor+'/inbox',cookie)
  916. return
  917. # undo a like from the web interface icon
  918. if htmlGET and '?unlike=' in self.path and '/statuses/' in self.path:
  919. likeUrl=self.path.split('?unlike=')[1]
  920. actor=self.path.split('?unlike=')[0]
  921. self.postToNickname=getNicknameFromActor(actor)
  922. if not self.server.session:
  923. self.server.session= \
  924. createSession(self.server.domain,self.server.port,self.server.useTor)
  925. undoActor=self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+self.postToNickname
  926. actorLiked=likeUrl.split('/statuses/')[0]
  927. undoLikeJson= {
  928. "@context": "https://www.w3.org/ns/activitystreams",
  929. 'type': 'Undo',
  930. 'actor': undoActor,
  931. 'object': {
  932. 'type': 'Like',
  933. 'actor': undoActor,
  934. 'object': likeUrl
  935. }
  936. }
  937. self._postToOutbox(undoLikeJson)
  938. self.server.GETbusy=False
  939. self._redirect_headers(actor+'/inbox',cookie)
  940. return
  941. # delete a post from the web interface icon
  942. if htmlGET and '?delete=' in self.path:
  943. deleteUrl=self.path.split('?delete=')[1]
  944. actor=self.server.httpPrefix+'://'+self.server.domainFull+self.path.split('?delete=')[0]
  945. if self.server.allowDeletion or \
  946. deleteUrl.startswith(actor):
  947. if self.server.debug:
  948. print('DEBUG: deleteUrl='+deleteUrl)
  949. print('DEBUG: actor='+actor)
  950. if actor not in deleteUrl:
  951. # You can only delete your own posts
  952. self.server.GETbusy=False
  953. self._redirect_headers(actor+'/inbox',cookie)
  954. return
  955. self.postToNickname=getNicknameFromActor(actor)
  956. if not self.server.session:
  957. self.server.session= \
  958. createSession(self.server.domain,self.server.port,self.server.useTor)
  959. deleteActor=self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+self.postToNickname
  960. deleteJson= {
  961. "@context": "https://www.w3.org/ns/activitystreams",
  962. 'actor': actor,
  963. 'object': deleteUrl,
  964. 'to': ['https://www.w3.org/ns/activitystreams#Public',actor],
  965. 'cc': [actor+'/followers'],
  966. 'type': 'Delete'
  967. }
  968. if self.server.debug:
  969. pprint(deleteJson)
  970. self._postToOutbox(deleteJson)
  971. self.server.GETbusy=False
  972. self._redirect_headers(actor+'/inbox',cookie)
  973. return
  974. # reply from the web interface icon
  975. inReplyToUrl=None
  976. replyWithDM=False
  977. replyToList=[]
  978. if htmlGET and '?replyto=' in self.path:
  979. inReplyToUrl=self.path.split('?replyto=')[1]
  980. if '?' in inReplyToUrl:
  981. mentionsList=inReplyToUrl.split('?')
  982. for m in mentionsList:
  983. if m.startswith('mention='):
  984. replyToList.append(m.replace('mention=',''))
  985. inReplyToUrl=mentionsList[0]
  986. self.path=self.path.split('?replyto=')[0]+'/newpost'
  987. if self.server.debug:
  988. print('DEBUG: replyto path '+self.path)
  989. # replying as a direct message, for moderation posts
  990. if htmlGET and '?replydm=' in self.path:
  991. inReplyToUrl=self.path.split('?replydm=')[1]
  992. if '?' in inReplyToUrl:
  993. mentionsList=inReplyToUrl.split('?')
  994. for m in mentionsList:
  995. if m.startswith('mention='):
  996. replyToList.append(m.replace('mention=',''))
  997. inReplyToUrl=mentionsList[0]
  998. self.path=self.path.split('?replydm=')[0]+'/newdm'
  999. if self.server.debug:
  1000. print('DEBUG: replydm path '+self.path)
  1001. # edit profile in web interface
  1002. if htmlGET and '/users/' in self.path and self.path.endswith('/editprofile'):
  1003. msg=htmlEditProfile(self.server.baseDir,self.path,self.server.domain,self.server.port).encode()
  1004. self._set_headers('text/html',len(msg),cookie)
  1005. self.wfile.write(msg)
  1006. self.server.GETbusy=False
  1007. return
  1008. # Various types of new post in the web interface
  1009. if htmlGET and '/users/' in self.path and \
  1010. (self.path.endswith('/newpost') or \
  1011. self.path.endswith('/newunlisted') or \
  1012. self.path.endswith('/newfollowers') or \
  1013. self.path.endswith('/newdm') or \
  1014. self.path.endswith('/newreport') or \
  1015. '/newreport?=' in self.path or \
  1016. self.path.endswith('/newshare')):
  1017. msg=htmlNewPost(self.server.baseDir,self.path,inReplyToUrl,replyToList).encode()
  1018. self._set_headers('text/html',len(msg),cookie)
  1019. self.wfile.write(msg)
  1020. self.server.GETbusy=False
  1021. return
  1022. # get an individual post from the path /@nickname/statusnumber
  1023. if '/@' in self.path:
  1024. namedStatus=self.path.split('/@')[1]
  1025. if '/' not in namedStatus:
  1026. # show actor
  1027. nickname=namedStatus
  1028. else:
  1029. postSections=namedStatus.split('/')
  1030. if len(postSections)==2:
  1031. nickname=postSections[0]
  1032. statusNumber=postSections[1]
  1033. if len(statusNumber)>10 and statusNumber.isdigit():
  1034. postFilename= \
  1035. self.server.baseDir+'/accounts/'+nickname+'@'+self.server.domain+'/outbox/'+ \
  1036. self.server.httpPrefix+':##'+self.server.domainFull+'#users#'+nickname+'#statuses#'+statusNumber+'.json'
  1037. if os.path.isfile(postFilename):
  1038. postJsonObject={}
  1039. with open(postFilename, 'r') as fp:
  1040. postJsonObject=commentjson.load(fp)
  1041. # Only authorized viewers get to see likes on posts
  1042. # Otherwize marketers could gain more social graph info
  1043. if not authorized:
  1044. if postJsonObject.get('likes'):
  1045. postJsonObject['likes']={}
  1046. if 'text/html' in self.headers['Accept']:
  1047. msg= \
  1048. htmlIndividualPost(self.server.session, \
  1049. self.server.cachedWebfingers,self.server.personCache, \
  1050. nickname,self.server.domain,self.server.port, \
  1051. authorized,postJsonObject, \
  1052. self.server.httpPrefix, \
  1053. self.server.projectVersion).encode('utf-8')
  1054. self._set_headers('text/html',len(msg),cookie)
  1055. self.wfile.write(msg)
  1056. else:
  1057. msg=json.dumps(postJsonObject).encode('utf-8')
  1058. self._set_headers('application/json',len(msg),None)
  1059. self.wfile.write(msg)
  1060. self.server.GETbusy=False
  1061. return
  1062. else:
  1063. self._404()
  1064. self.server.GETbusy=False
  1065. return
  1066. # get replies to a post /users/nickname/statuses/number/replies
  1067. if self.path.endswith('/replies') or '/replies?page=' in self.path:
  1068. if '/statuses/' in self.path and '/users/' in self.path:
  1069. namedStatus=self.path.split('/users/')[1]
  1070. if '/' in namedStatus:
  1071. postSections=namedStatus.split('/')
  1072. if len(postSections)>=4:
  1073. if postSections[3].startswith('replies'):
  1074. nickname=postSections[0]
  1075. statusNumber=postSections[2]
  1076. if len(statusNumber)>10 and statusNumber.isdigit():
  1077. #get the replies file
  1078. boxname='outbox'
  1079. postDir=self.server.baseDir+'/accounts/'+nickname+'@'+self.server.domain+'/'+boxname
  1080. postRepliesFilename= \
  1081. postDir+'/'+ \
  1082. self.server.httpPrefix+':##'+self.server.domainFull+'#users#'+nickname+'#statuses#'+statusNumber+'.replies'
  1083. if not os.path.isfile(postRepliesFilename):
  1084. # There are no replies, so show empty collection
  1085. repliesJson = {
  1086. '@context': 'https://www.w3.org/ns/activitystreams',
  1087. 'first': self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+nickname+'/statuses/'+statusNumber+'/replies?page=true',
  1088. 'id': self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+nickname+'/statuses/'+statusNumber+'/replies',
  1089. 'last': self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+nickname+'/statuses/'+statusNumber+'/replies?page=true',
  1090. 'totalItems': 0,
  1091. 'type': 'OrderedCollection'}
  1092. if 'text/html' in self.headers['Accept']:
  1093. if not self.server.session:
  1094. if self.server.debug:
  1095. print('DEBUG: creating new session')
  1096. self.server.session= \
  1097. createSession(self.server.domain,self.server.port,self.server.useTor)
  1098. msg=htmlPostReplies(self.server.baseDir, \
  1099. self.server.session, \
  1100. self.server.cachedWebfingers, \
  1101. self.server.personCache, \
  1102. nickname, \
  1103. self.server.domain, \
  1104. self.server.port, \
  1105. repliesJson, \
  1106. self.server.httpPrefix, \
  1107. self.server.projectVersion).encode('utf-8')
  1108. self._set_headers('text/html',len(msg),cookie)
  1109. print('----------------------------------------------------')
  1110. pprint(repliesJson)
  1111. self.wfile.write(msg)
  1112. else:
  1113. msg=json.dumps(repliesJson).encode('utf-8')
  1114. self._set_headers('application/json',len(msg),None)
  1115. self.wfile.write(msg)
  1116. self.server.GETbusy=False
  1117. return
  1118. else:
  1119. # replies exist. Itterate through the text file containing message ids
  1120. repliesJson = {
  1121. '@context': 'https://www.w3.org/ns/activitystreams',
  1122. 'id': self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+nickname+'/statuses/'+statusNumber+'?page=true',
  1123. 'orderedItems': [
  1124. ],
  1125. 'partOf': self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+nickname+'/statuses/'+statusNumber,
  1126. 'type': 'OrderedCollectionPage'}
  1127. # populate the items list with replies
  1128. populateRepliesJson(self.server.baseDir, \
  1129. nickname, \
  1130. self.server.domain, \
  1131. postRepliesFilename, \
  1132. authorized, \
  1133. repliesJson)
  1134. # send the replies json
  1135. if 'text/html' in self.headers['Accept']:
  1136. if not self.server.session:
  1137. if self.server.debug:
  1138. print('DEBUG: creating new session')
  1139. self.server.session= \
  1140. createSession(self.server.domain,self.server.port,self.server.useTor)
  1141. msg=htmlPostReplies(self.server.baseDir, \
  1142. self.server.session, \
  1143. self.server.cachedWebfingers, \
  1144. self.server.personCache, \
  1145. nickname, \
  1146. self.server.domain, \
  1147. self.server.port, \
  1148. repliesJson, \
  1149. self.server.httpPrefix, \
  1150. self.server.projectVersion).encode('utf-8')
  1151. self._set_headers('text/html',len(msg),cookie)
  1152. self.wfile.write(msg)
  1153. else:
  1154. msg=json.dumps(repliesJson).encode('utf-8')
  1155. self._set_headers('application/json',len(msg),None)
  1156. self.wfile.write(msg)
  1157. self.server.GETbusy=False
  1158. return
  1159. if self.path.endswith('/roles') and '/users/' in self.path:
  1160. namedStatus=self.path.split('/users/')[1]
  1161. if '/' in namedStatus:
  1162. postSections=namedStatus.split('/')
  1163. nickname=postSections[0]
  1164. actorFilename=self.server.baseDir+'/accounts/'+nickname+'@'+self.server.domain+'.json'
  1165. if os.path.isfile(actorFilename):
  1166. with open(actorFilename, 'r') as fp:
  1167. actorJson=commentjson.load(fp)
  1168. if actorJson.get('roles'):
  1169. if 'text/html' in self.headers['Accept']:
  1170. getPerson = \
  1171. personLookup(self.server.domain,self.path.replace('/roles',''), \
  1172. self.server.baseDir)
  1173. if getPerson:
  1174. msg=htmlProfile(self.server.projectVersion, \
  1175. self.server.baseDir, \
  1176. self.server.httpPrefix, \
  1177. True, \
  1178. self.server.ocapAlways, \
  1179. getPerson,'roles', \
  1180. self.server.session, \
  1181. self.server.cachedWebfingers, \
  1182. self.server.personCache, \
  1183. actorJson['roles']).encode('utf-8')
  1184. self._set_headers('text/html',len(msg),cookie)
  1185. self.wfile.write(msg)
  1186. else:
  1187. msg=json.dumps(actorJson['roles']).encode('utf-8')
  1188. self._set_headers('application/json',len(msg),None)
  1189. self.wfile.write(msg)
  1190. self.server.GETbusy=False
  1191. return
  1192. if self.path.endswith('/skills') and '/users/' in self.path:
  1193. namedStatus=self.path.split('/users/')[1]
  1194. if '/' in namedStatus:
  1195. postSections=namedStatus.split('/')
  1196. nickname=postSections[0]
  1197. actorFilename=self.server.baseDir+'/accounts/'+nickname+'@'+self.server.domain+'.json'
  1198. if os.path.isfile(actorFilename):
  1199. with open(actorFilename, 'r') as fp:
  1200. actorJson=commentjson.load(fp)
  1201. if actorJson.get('skills'):
  1202. if 'text/html' in self.headers['Accept']:
  1203. getPerson = \
  1204. personLookup(self.server.domain,self.path.replace('/skills',''), \
  1205. self.server.baseDir)
  1206. if getPerson:
  1207. msg=htmlProfile(self.server.projectVersion, \
  1208. self.server.baseDir, \
  1209. self.server.httpPrefix, \
  1210. True, \
  1211. self.server.ocapAlways, \
  1212. getPerson,'skills', \
  1213. self.server.session, \
  1214. self.server.cachedWebfingers, \
  1215. self.server.personCache, \
  1216. actorJson['skills']).encode('utf-8')
  1217. self._set_headers('text/html',len(msg),cookie)
  1218. self.wfile.write(msg)
  1219. else:
  1220. msg=json.dumps(actorJson['skills']).encode('utf-8')
  1221. self._set_headers('application/json',len(msg),None)
  1222. self.wfile.write(msg)
  1223. self.server.GETbusy=False
  1224. return
  1225. # get an individual post from the path /users/nickname/statuses/number
  1226. if '/statuses/' in self.path and '/users/' in self.path:
  1227. namedStatus=self.path.split('/users/')[1]
  1228. if '/' in namedStatus:
  1229. postSections=namedStatus.split('/')
  1230. if len(postSections)>=3:
  1231. nickname=postSections[0]
  1232. statusNumber=postSections[2]
  1233. if len(statusNumber)>10 and statusNumber.isdigit():
  1234. postFilename= \
  1235. self.server.baseDir+'/accounts/'+nickname+'@'+self.server.domain+'/outbox/'+ \
  1236. self.server.httpPrefix+':##'+self.server.domainFull+'#users#'+nickname+'#statuses#'+statusNumber+'.json'
  1237. if os.path.isfile(postFilename):
  1238. postJsonObject={}
  1239. with open(postFilename, 'r') as fp:
  1240. postJsonObject=commentjson.load(fp)
  1241. # Only authorized viewers get to see likes on posts
  1242. # Otherwize marketers could gain more social graph info
  1243. if not authorized:
  1244. if postJsonObject.get('likes'):
  1245. postJsonObject['likes']={}
  1246. if 'text/html' in self.headers['Accept']:
  1247. msg=htmlIndividualPost(self.server.baseDir, \
  1248. self.server.session, \
  1249. self.server.cachedWebfingers,self.server.personCache, \
  1250. nickname,self.server.domain,self.server.port, \
  1251. authorized,postJsonObject, \
  1252. self.server.httpPrefix, \
  1253. self.server.projectVersion).encode('utf-8')
  1254. self._set_headers('text/html',len(msg),cookie)
  1255. self.wfile.write(msg)
  1256. else:
  1257. msg=json.dumps(postJsonObject).encode('utf-8')
  1258. self._set_headers('application/json',len(msg),None)
  1259. self.wfile.write(msg)
  1260. self.server.GETbusy=False
  1261. return
  1262. else:
  1263. self._404()
  1264. self.server.GETbusy=False
  1265. return
  1266. # get the inbox for a given person
  1267. if self.path.endswith('/inbox') or '/inbox?page=' in self.path:
  1268. if '/users/' in self.path:
  1269. if authorized:
  1270. inboxFeed=personBoxJson(self.server.baseDir, \
  1271. self.server.domain, \
  1272. self.server.port, \
  1273. self.path, \
  1274. self.server.httpPrefix, \
  1275. maxPostsInFeed, 'inbox', \
  1276. True,self.server.ocapAlways)
  1277. if inboxFeed:
  1278. if 'text/html' in self.headers['Accept']:
  1279. nickname=self.path.replace('/users/','').replace('/inbox','')
  1280. pageNumber=1
  1281. if '?page=' in nickname:
  1282. pageNumber=nickname.split('?page=')[1]
  1283. nickname=nickname.split('?page=')[0]
  1284. if pageNumber.isdigit():
  1285. pageNumber=int(pageNumber)
  1286. else:
  1287. pageNumber=1
  1288. if 'page=' not in self.path:
  1289. # if no page was specified then show the first
  1290. inboxFeed=personBoxJson(self.server.baseDir, \
  1291. self.server.domain, \
  1292. self.server.port, \
  1293. self.path+'?page=1', \
  1294. self.server.httpPrefix, \
  1295. maxPostsInFeed, 'inbox', \
  1296. True,self.server.ocapAlways)
  1297. msg=htmlInbox(pageNumber,maxPostsInFeed, \
  1298. self.server.session, \
  1299. self.server.baseDir, \
  1300. self.server.cachedWebfingers, \
  1301. self.server.personCache, \
  1302. nickname, \
  1303. self.server.domain, \
  1304. self.server.port, \
  1305. inboxFeed, \
  1306. self.server.allowDeletion, \
  1307. self.server.httpPrefix, \
  1308. self.server.projectVersion).encode('utf-8')
  1309. self._set_headers('text/html',len(msg),cookie)
  1310. self.wfile.write(msg)
  1311. else:
  1312. msg=json.dumps(inboxFeed).encode('utf-8')
  1313. self._set_headers('application/json',len(msg),None)
  1314. self.wfile.write(msg)
  1315. self.server.GETbusy=False
  1316. return
  1317. else:
  1318. if self.server.debug:
  1319. nickname=self.path.replace('/users/','').replace('/inbox','')
  1320. print('DEBUG: '+nickname+ \
  1321. ' was not authorized to access '+self.path)
  1322. if self.path!='/inbox':
  1323. # not the shared inbox
  1324. if self.server.debug:
  1325. print('DEBUG: GET access to inbox is unauthorized')
  1326. self.send_response(405)
  1327. self.end_headers()
  1328. self.server.GETbusy=False
  1329. return
  1330. # get outbox feed for a person
  1331. outboxFeed=personBoxJson(self.server.baseDir,self.server.domain, \
  1332. self.server.port,self.path, \
  1333. self.server.httpPrefix, \
  1334. maxPostsInFeed, 'outbox', \
  1335. authorized, \
  1336. self.server.ocapAlways)
  1337. if outboxFeed:
  1338. if 'text/html' in self.headers['Accept']:
  1339. nickname=self.path.replace('/users/','').replace('/outbox','')
  1340. pageNumber=1
  1341. if '?page=' in nickname:
  1342. pageNumber=nickname.split('?page=')[1]
  1343. nickname=nickname.split('?page=')[0]
  1344. if pageNumber.isdigit():
  1345. pageNumber=int(pageNumber)
  1346. else:
  1347. pageNumber=1
  1348. if 'page=' not in self.path:
  1349. # if a page wasn't specified then show the first one
  1350. outboxFeed=personBoxJson(self.server.baseDir,self.server.domain, \
  1351. self.server.port,self.path+'?page=1', \
  1352. self.server.httpPrefix, \
  1353. maxPostsInFeed, 'outbox', \
  1354. authorized, \
  1355. self.server.ocapAlways)
  1356. msg=htmlOutbox(pageNumber,maxPostsInFeed, \
  1357. self.server.session, \
  1358. self.server.baseDir, \
  1359. self.server.cachedWebfingers, \
  1360. self.server.personCache, \
  1361. nickname, \
  1362. self.server.domain, \
  1363. self.server.port, \
  1364. outboxFeed, \
  1365. self.server.allowDeletion, \
  1366. self.server.httpPrefix, \
  1367. self.server.projectVersion).encode('utf-8')
  1368. self._set_headers('text/html',len(msg),cookie)
  1369. self.wfile.write(msg)
  1370. else:
  1371. msg=json.dumps(outboxFeed).encode('utf-8')
  1372. self._set_headers('application/json',len(msg),None)
  1373. self.wfile.write(msg)
  1374. self.server.GETbusy=False
  1375. return
  1376. # get the moderation feed for a moderator
  1377. if self.path.endswith('/moderation') or '/moderation?page=' in self.path:
  1378. if '/users/' in self.path:
  1379. if authorized:
  1380. moderationFeed= \
  1381. personBoxJson(self.server.baseDir, \
  1382. self.server.domain, \
  1383. self.server.port, \
  1384. self.path, \
  1385. self.server.httpPrefix, \
  1386. maxPostsInFeed, 'moderation', \
  1387. True,self.server.ocapAlways)
  1388. if moderationFeed:
  1389. if 'text/html' in self.headers['Accept']:
  1390. nickname=self.path.replace('/users/','').replace('/moderation','')
  1391. pageNumber=1
  1392. if '?page=' in nickname:
  1393. pageNumber=nickname.split('?page=')[1]
  1394. nickname=nickname.split('?page=')[0]
  1395. if pageNumber.isdigit():
  1396. pageNumber=int(pageNumber)
  1397. else:
  1398. pageNumber=1
  1399. if 'page=' not in self.path:
  1400. # if no page was specified then show the first
  1401. moderationFeed= \
  1402. personBoxJson(self.server.baseDir, \
  1403. self.server.domain, \
  1404. self.server.port, \
  1405. self.path+'?page=1', \
  1406. self.server.httpPrefix, \
  1407. maxPostsInFeed, 'moderation', \
  1408. True,self.server.ocapAlways)
  1409. msg=htmlModeration(pageNumber,maxPostsInFeed, \
  1410. self.server.session, \
  1411. self.server.baseDir, \
  1412. self.server.cachedWebfingers, \
  1413. self.server.personCache, \
  1414. nickname, \
  1415. self.server.domain, \
  1416. self.server.port, \
  1417. moderationFeed, \
  1418. True, \
  1419. self.server.httpPrefix, \
  1420. self.server.projectVersion).encode('utf-8')
  1421. self._set_headers('text/html',len(msg),cookie)
  1422. self.wfile.write(msg)
  1423. else:
  1424. msg=json.dumps(moderationFeed).encode('utf-8')
  1425. self._set_headers('application/json',len(msg),None)
  1426. self.wfile.write(msg)
  1427. self.server.GETbusy=False
  1428. return
  1429. else:
  1430. if self.server.debug:
  1431. nickname=self.path.replace('/users/','').replace('/moderation','')
  1432. print('DEBUG: '+nickname+ \
  1433. ' was not authorized to access '+self.path)
  1434. if self.server.debug:
  1435. print('DEBUG: GET access to moderation feed is unauthorized')
  1436. self.send_response(405)
  1437. self.end_headers()
  1438. self.server.GETbusy=False
  1439. return
  1440. shares=getSharesFeedForPerson(self.server.baseDir, \
  1441. self.server.domain, \
  1442. self.server.port,self.path, \
  1443. self.server.httpPrefix, \
  1444. sharesPerPage)
  1445. if shares:
  1446. if 'text/html' in self.headers['Accept']:
  1447. if 'page=' not in self.path:
  1448. # get a page of shares, not the summary
  1449. shares=getSharesFeedForPerson(self.server.baseDir,self.server.domain, \
  1450. self.server.port,self.path+'?page=true', \
  1451. self.server.httpPrefix, \
  1452. sharesPerPage)
  1453. getPerson = personLookup(self.server.domain,self.path.replace('/shares',''), \
  1454. self.server.baseDir)
  1455. if getPerson:
  1456. if not self.server.session:
  1457. if self.server.debug:
  1458. print('DEBUG: creating new session')
  1459. self.server.session= \
  1460. createSession(self.server.domain,self.server.port,self.server.useTor)
  1461. msg=htmlProfile(self.server.projectVersion, \
  1462. self.server.baseDir, \
  1463. self.server.httpPrefix, \
  1464. authorized, \
  1465. self.server.ocapAlways, \
  1466. getPerson,'shares', \
  1467. self.server.session, \
  1468. self.server.cachedWebfingers, \
  1469. self.server.personCache, \
  1470. shares).encode('utf-8')
  1471. self._set_headers('text/html',len(msg),cookie)
  1472. self.wfile.write(msg)
  1473. self.server.GETbusy=False
  1474. return
  1475. else:
  1476. msg=json.dumps(shares).encode('utf-8')
  1477. self._set_headers('application/json',len(msg),None)
  1478. self.wfile.write(msg)
  1479. self.server.GETbusy=False
  1480. return
  1481. following=getFollowingFeed(self.server.baseDir,self.server.domain, \
  1482. self.server.port,self.path, \
  1483. self.server.httpPrefix, \
  1484. authorized,followsPerPage)
  1485. if following:
  1486. if 'text/html' in self.headers['Accept']:
  1487. if 'page=' not in self.path:
  1488. # get a page of following, not the summary
  1489. following=getFollowingFeed(self.server.baseDir,self.server.domain, \
  1490. self.server.port,self.path+'?page=true', \
  1491. self.server.httpPrefix, \
  1492. authorized,followsPerPage)
  1493. getPerson = personLookup(self.server.domain,self.path.replace('/following',''), \
  1494. self.server.baseDir)
  1495. if getPerson:
  1496. if not self.server.session:
  1497. if self.server.debug:
  1498. print('DEBUG: creating new session')
  1499. self.server.session= \
  1500. createSession(self.server.domain,self.server.port,self.server.useTor)
  1501. msg=htmlProfile(self.server.projectVersion, \
  1502. self.server.baseDir, \
  1503. self.server.httpPrefix, \
  1504. authorized, \
  1505. self.server.ocapAlways, \
  1506. getPerson,'following', \
  1507. self.server.session, \
  1508. self.server.cachedWebfingers, \
  1509. self.server.personCache, \
  1510. following).encode('utf-8')
  1511. self._set_headers('text/html',len(msg),cookie)
  1512. self.wfile.write(msg)
  1513. self.server.GETbusy=False
  1514. return
  1515. else:
  1516. msg=json.dumps(following).encode('utf-8')
  1517. self._set_headers('application/json',len(msg),None)
  1518. self.wfile.write(msg)
  1519. self.server.GETbusy=False
  1520. return
  1521. followers=getFollowingFeed(self.server.baseDir,self.server.domain, \
  1522. self.server.port,self.path, \
  1523. self.server.httpPrefix, \
  1524. authorized,followsPerPage,'followers')
  1525. if followers:
  1526. if 'text/html' in self.headers['Accept']:
  1527. if 'page=' not in self.path:
  1528. # get a page of followers, not the summary
  1529. followers=getFollowingFeed(self.server.baseDir,self.server.domain, \
  1530. self.server.port,self.path+'?page=1', \
  1531. self.server.httpPrefix, \
  1532. authorized,followsPerPage,'followers')
  1533. getPerson = personLookup(self.server.domain,self.path.replace('/followers',''), \
  1534. self.server.baseDir)
  1535. if getPerson:
  1536. if not self.server.session:
  1537. if self.server.debug:
  1538. print('DEBUG: creating new session')
  1539. self.server.session= \
  1540. createSession(self.server.domain,self.server.port,self.server.useTor)
  1541. msg=htmlProfile(self.server.projectVersion, \
  1542. self.server.baseDir, \
  1543. self.server.httpPrefix, \
  1544. authorized, \
  1545. self.server.ocapAlways, \
  1546. getPerson,'followers', \
  1547. self.server.session, \
  1548. self.server.cachedWebfingers, \
  1549. self.server.personCache, \
  1550. followers).encode('utf-8')
  1551. self._set_headers('text/html',len(msg),cookie)
  1552. self.wfile.write(msg)
  1553. self.server.GETbusy=False
  1554. return
  1555. else:
  1556. msg=json.dumps(followers).encode('utf-8')
  1557. self._set_headers('application/json',len(msg),None)
  1558. self.wfile.write(msg)
  1559. self.server.GETbusy=False
  1560. return
  1561. # look up a person
  1562. getPerson = personLookup(self.server.domain,self.path, \
  1563. self.server.baseDir)
  1564. if getPerson:
  1565. if 'text/html' in self.headers['Accept']:
  1566. if not self.server.session:
  1567. if self.server.debug:
  1568. print('DEBUG: creating new session')
  1569. self.server.session= \
  1570. createSession(self.server.domain,self.server.port,self.server.useTor)
  1571. msg=htmlProfile(self.server.projectVersion, \
  1572. self.server.baseDir, \
  1573. self.server.httpPrefix, \
  1574. authorized, \
  1575. self.server.ocapAlways, \
  1576. getPerson,'posts',
  1577. self.server.session, \
  1578. self.server.cachedWebfingers, \
  1579. self.server.personCache).encode('utf-8')
  1580. self._set_headers('text/html',len(msg),cookie)
  1581. self.wfile.write(msg)
  1582. else:
  1583. msg=json.dumps(getPerson).encode('utf-8')
  1584. self._set_headers('application/json',len(msg),None)
  1585. self.wfile.write(msg)
  1586. self.server.GETbusy=False
  1587. return
  1588. # check that a json file was requested
  1589. if not self.path.endswith('.json'):
  1590. if self.server.debug:
  1591. print('DEBUG: GET Not json: '+self.path+' '+self.server.baseDir)
  1592. self._404()
  1593. self.server.GETbusy=False
  1594. return
  1595. # check that the file exists
  1596. filename=self.server.baseDir+self.path
  1597. if os.path.isfile(filename):
  1598. with open(filename, 'r', encoding='utf-8') as File:
  1599. content = File.read()
  1600. contentJson=json.loads(content)
  1601. msg=json.dumps(contentJson).encode('utf-8')
  1602. self._set_headers('application/json',len(msg),None)
  1603. self.wfile.write(msg)
  1604. else:
  1605. if self.server.debug:
  1606. print('DEBUG: GET Unknown file')
  1607. self._404()
  1608. self.server.GETbusy=False
  1609. def do_HEAD(self):
  1610. self._set_headers('application/json',0,None)
  1611. def _receiveNewPost(self,authorized: bool,postType: str) -> bool:
  1612. # 0 = this is not a new post
  1613. # 1 = new post success
  1614. # -1 = new post failed
  1615. # 2 = new post canceled
  1616. if authorized and '/users/' in self.path and self.path.endswith('?'+postType):
  1617. if ' boundary=' in self.headers['Content-type']:
  1618. nickname=None
  1619. nicknameStr=self.path.split('/users/')[1]
  1620. if '/' in nicknameStr:
  1621. nickname=nicknameStr.split('/')[0]
  1622. else:
  1623. return -1
  1624. length = int(self.headers['Content-length'])
  1625. if length>self.server.maxPostLength:
  1626. print('POST size too large')
  1627. return -1
  1628. boundary=self.headers['Content-type'].split('boundary=')[1]
  1629. if ';' in boundary:
  1630. boundary=boundary.split(';')[0]
  1631. # Note: we don't use cgi here because it's due to be deprecated
  1632. # in Python 3.8/3.10
  1633. # Instead we use the multipart mime parser from the email module
  1634. postBytes=self.rfile.read(length)
  1635. msg = email.parser.BytesParser().parsebytes(postBytes)
  1636. # why don't we just use msg.is_multipart(), rather than splitting?
  1637. # TL;DR it doesn't work for this use case because we're not using
  1638. # email style encoding message/rfc822
  1639. messageFields=msg.get_payload(decode=False).split(boundary)
  1640. fields={}
  1641. filename=None
  1642. for f in messageFields:
  1643. if f=='--':
  1644. continue
  1645. if ' name="' in f:
  1646. postStr=f.split(' name="',1)[1]
  1647. if '"' in postStr:
  1648. postKey=postStr.split('"',1)[0]
  1649. postValueStr=postStr.split('"',1)[1]
  1650. if ';' not in postValueStr:
  1651. if '\r\n' in postValueStr:
  1652. postLines=postValueStr.split('\r\n')
  1653. postValue=''
  1654. if len(postLines)>2:
  1655. for line in range(2,len(postLines)-1):
  1656. if line>2:
  1657. postValue+='\n'
  1658. postValue+=postLines[line]
  1659. fields[postKey]=postValue
  1660. else:
  1661. # directly search the binary array for the beginning
  1662. # of an image
  1663. searchStr=b'Content-Type: image/png'
  1664. imageLocation=postBytes.find(searchStr)
  1665. filenameBase=self.server.baseDir+'/accounts/'+nickname+'@'+self.server.domain+'/upload'
  1666. if imageLocation>-1:
  1667. filename=filenameBase+'.png'
  1668. else:
  1669. searchStr=b'Content-Type: image/jpeg'
  1670. imageLocation=postBytes.find(searchStr)
  1671. if imageLocation>-1:
  1672. filename=filenameBase+'.jpg'
  1673. else:
  1674. searchStr=b'Content-Type: image/gif'
  1675. imageLocation=postBytes.find(searchStr)
  1676. if imageLocation>-1:
  1677. filename=filenameBase+'.gif'
  1678. if filename and imageLocation>-1:
  1679. # locate the beginning of the image, after any
  1680. # carriage returns
  1681. startPos=imageLocation+len(searchStr)
  1682. for offset in range(1,8):
  1683. if postBytes[startPos+offset]!=10:
  1684. if postBytes[startPos+offset]!=13:
  1685. startPos+=offset
  1686. break
  1687. fd = open(filename, 'wb')
  1688. fd.write(postBytes[startPos:])
  1689. fd.close()
  1690. # send the post
  1691. if not fields.get('message'):
  1692. return -1
  1693. if fields.get('submitPost'):
  1694. if fields['submitPost']!='Submit':
  1695. return -1
  1696. else:
  1697. return 2
  1698. if not fields.get('imageDescription'):
  1699. fields['imageDescription']=None
  1700. if not fields.get('subject'):
  1701. fields['subject']=None
  1702. if not fields.get('replyTo'):
  1703. fields['replyTo']=None
  1704. if postType=='newpost':
  1705. messageJson= \
  1706. createPublicPost(self.server.baseDir, \
  1707. nickname, \
  1708. self.server.domain,self.server.port, \
  1709. self.server.httpPrefix, \
  1710. fields['message'],False,False,False, \
  1711. filename,fields['imageDescription'],True, \
  1712. fields['replyTo'], fields['replyTo'],fields['subject'])
  1713. if messageJson:
  1714. self.postToNickname=nickname
  1715. if self._postToOutbox(messageJson):
  1716. populateReplies(self.server.baseDir, \
  1717. self.server.httpPrefix, \
  1718. self.server.domainFull, \
  1719. messageJson, \
  1720. self.server.maxReplies, \
  1721. self.server.debug)
  1722. return 1
  1723. else:
  1724. return -1
  1725. if postType=='newunlisted':
  1726. messageJson= \
  1727. createUnlistedPost(self.server.baseDir, \
  1728. nickname, \
  1729. self.server.domain,self.server.port, \
  1730. self.server.httpPrefix, \
  1731. fields['message'],False,False,False, \
  1732. filename,fields['imageDescription'],True, \
  1733. fields['replyTo'], fields['replyTo'],fields['subject'])
  1734. if messageJson:
  1735. self.postToNickname=nickname
  1736. if self._postToOutbox(messageJson):
  1737. populateReplies(self.server.baseDir, \
  1738. self.server.httpPrefix, \
  1739. self.server.domain, \
  1740. messageJson, \
  1741. self.server.maxReplies, \
  1742. self.server.debug)
  1743. return 1
  1744. else:
  1745. return -1
  1746. if postType=='newfollowers':
  1747. messageJson= \
  1748. createFollowersOnlyPost(self.server.baseDir, \
  1749. nickname, \
  1750. self.server.domain,self.server.port, \
  1751. self.server.httpPrefix, \
  1752. fields['message'],True,False,False, \
  1753. filename,fields['imageDescription'],True, \
  1754. fields['replyTo'], fields['replyTo'],fields['subject'])
  1755. if messageJson:
  1756. self.postToNickname=nickname
  1757. if self._postToOutbox(messageJson):
  1758. populateReplies(self.server.baseDir, \
  1759. self.server.httpPrefix, \
  1760. self.server.domain, \
  1761. messageJson, \
  1762. self.server.maxReplies, \
  1763. self.server.debug)
  1764. return 1
  1765. else:
  1766. return -1
  1767. if postType=='newdm':
  1768. messageJson=None
  1769. if '@' in fields['message']:
  1770. messageJson= \
  1771. createDirectMessagePost(self.server.baseDir, \
  1772. nickname, \
  1773. self.server.domain,self.server.port, \
  1774. self.server.httpPrefix, \
  1775. fields['message'],True,False,False, \
  1776. filename,fields['imageDescription'],True, \
  1777. fields['replyTo'],fields['replyTo'],fields['subject'])
  1778. if messageJson:
  1779. self.postToNickname=nickname
  1780. if self._postToOutbox(messageJson):
  1781. populateReplies(self.server.baseDir, \
  1782. self.server.httpPrefix, \
  1783. self.server.domain, \
  1784. messageJson, \
  1785. self.server.maxReplies, \
  1786. self.server.debug)
  1787. return 1
  1788. else:
  1789. return -1
  1790. if postType=='newreport':
  1791. # So as to be sure that this only goes to moderators
  1792. # and not accounts being reported we disable any
  1793. # included fediverse addresses by replacing '@' with '-at-'
  1794. fields['message']=fields['message'].replace('@','-at-')
  1795. messageJson= \
  1796. createReportPost(self.server.baseDir, \
  1797. nickname, \
  1798. self.server.domain,self.server.port, \
  1799. self.server.httpPrefix, \
  1800. fields['message'],True,False,False, \
  1801. filename,fields['imageDescription'],True, \
  1802. self.server.debug,fields['subject'])
  1803. if messageJson:
  1804. self.postToNickname=nickname
  1805. if self._postToOutbox(messageJson):
  1806. return 1
  1807. else:
  1808. return -1
  1809. if postType=='newshare':
  1810. if not fields.get('itemType'):
  1811. return False
  1812. if not fields.get('category'):
  1813. return False
  1814. if not fields.get('location'):
  1815. return False
  1816. if not fields.get('duration'):
  1817. return False
  1818. addShare(self.server.baseDir, \
  1819. self.server.httpPrefix, \
  1820. nickname, \
  1821. self.server.domain,self.server.port, \
  1822. fields['subject'], \
  1823. fields['message'], \
  1824. filename, \
  1825. fields['itemType'], \
  1826. fields['category'], \
  1827. fields['location'], \
  1828. fields['duration'],
  1829. self.server.debug)
  1830. if os.path.isfile(filename):
  1831. os.remove(filename)
  1832. self.postToNickname=nickname
  1833. if self._postToOutbox(messageJson):
  1834. return 1
  1835. else:
  1836. return -1
  1837. return -1
  1838. else:
  1839. return 0
  1840. def do_POST(self):
  1841. if self.server.debug:
  1842. print('DEBUG: POST to from '+self.server.baseDir+ \
  1843. ' path: '+self.path+' busy: '+ \
  1844. str(self.server.POSTbusy))
  1845. if self.server.POSTbusy:
  1846. currTimePOST=int(time.time())
  1847. if currTimePOST-self.server.lastPOST==0:
  1848. self.send_response(429)
  1849. self.end_headers()
  1850. return
  1851. self.server.lastPOST=currTimePOST
  1852. self.server.POSTbusy=True
  1853. if not self.headers.get('Content-type'):
  1854. print('Content-type header missing')
  1855. self.send_response(400)
  1856. self.end_headers()
  1857. self.server.POSTbusy=False
  1858. return
  1859. # remove any trailing slashes from the path
  1860. if not self.path.endswith('confirm'):
  1861. self.path=self.path.replace('/outbox/','/outbox').replace('/inbox/','/inbox').replace('/shares/','/shares').replace('/sharedInbox/','/sharedInbox')
  1862. cookie=None
  1863. if self.headers.get('Cookie'):
  1864. cookie=self.headers['Cookie']
  1865. # check authorization
  1866. authorized = self._isAuthorized()
  1867. if authorized:
  1868. if self.server.debug:
  1869. print('POST Authorization granted')
  1870. else:
  1871. if self.server.debug:
  1872. print('POST Not authorized')
  1873. print(str(self.headers))
  1874. # if this is a POST to teh outbox then check authentication
  1875. self.outboxAuthenticated=False
  1876. self.postToNickname=None
  1877. if self.path.startswith('/login'):
  1878. # get the contents of POST containing login credentials
  1879. length = int(self.headers['Content-length'])
  1880. if length>512:
  1881. print('Login failed - credentials too long')
  1882. self.send_response(401)
  1883. self.end_headers()
  1884. self.server.POSTbusy=False
  1885. return
  1886. loginParams=self.rfile.read(length).decode('utf-8')
  1887. loginNickname,loginPassword,register=htmlGetLoginCredentials(loginParams,self.server.lastLoginTime)
  1888. if loginNickname:
  1889. self.server.lastLoginTime=int(time.time())
  1890. if register:
  1891. if not registerAccount(self.server.baseDir,self.server.httpPrefix, \
  1892. self.server.domain,self.server.port, \
  1893. loginNickname,loginPassword):
  1894. self.server.POSTbusy=False
  1895. self._redirect_headers('/login',cookie)
  1896. return
  1897. authHeader=createBasicAuthHeader(loginNickname,loginPassword)
  1898. if not authorizeBasic(self.server.baseDir,'/users/'+loginNickname+'/outbox',authHeader,False):
  1899. print('Login failed: '+loginNickname)
  1900. # remove any token
  1901. if self.server.tokens.get(loginNickname):
  1902. del self.server.tokensLookup[self.server.tokens[loginNickname]]
  1903. del self.server.tokens[loginNickname]
  1904. del self.server.salts[loginNickname]
  1905. self.send_response(303)
  1906. self.send_header('Content-Length', '0')
  1907. self.send_header('Set-Cookie', 'epicyon=; SameSite=Strict')
  1908. self.send_header('Location', '/login')
  1909. self.end_headers()
  1910. self.server.POSTbusy=False
  1911. return
  1912. else:
  1913. if isSuspended(self.server.baseDir,loginNickname):
  1914. msg=htmlSuspended(self.server.baseDir).encode('utf-8')
  1915. self._login_headers('text/html',len(msg))
  1916. self.wfile.write(msg)
  1917. self.server.POSTbusy=False
  1918. return
  1919. # login success - redirect with authorization
  1920. print('Login success: '+loginNickname)
  1921. self.send_response(303)
  1922. # This produces a deterministic token based on nick+password+salt
  1923. # But notice that the salt is ephemeral, so a server reboot changes them.
  1924. # This allows you to be logged in on two or more devices with the
  1925. # same token, but also ensures that if an adversary obtains the token
  1926. # then rebooting the server is sufficient to thwart them, without
  1927. # any password changes.
  1928. if not self.server.salts.get(loginNickname):
  1929. self.server.salts[loginNickname]=createPassword(32)
  1930. self.server.tokens[loginNickname]=sha256((loginNickname+loginPassword+self.server.salts[loginNickname]).encode('utf-8')).hexdigest()
  1931. self.server.tokensLookup[self.server.tokens[loginNickname]]=loginNickname
  1932. self.send_header('Set-Cookie', 'epicyon='+self.server.tokens[loginNickname]+'; SameSite=Strict')
  1933. self.send_header('Location', '/users/'+loginNickname+'/inbox')
  1934. self.send_header('Content-Length', '0')
  1935. self.end_headers()
  1936. self.server.POSTbusy=False
  1937. return
  1938. self.send_response(200)
  1939. self.end_headers()
  1940. self.server.POSTbusy=False
  1941. return
  1942. # update of profile/avatar from web interface
  1943. if authorized and self.path.endswith('/profiledata'):
  1944. if ' boundary=' in self.headers['Content-type']:
  1945. boundary=self.headers['Content-type'].split('boundary=')[1]
  1946. if ';' in boundary:
  1947. boundary=boundary.split(';')[0]
  1948. actorStr=self.path.replace('/profiledata','').replace('/editprofile','')
  1949. nickname=getNicknameFromActor(actorStr)
  1950. if not nickname:
  1951. self._redirect_headers(actorStr,cookie)
  1952. self.server.POSTbusy=False
  1953. return
  1954. length = int(self.headers['Content-length'])
  1955. postBytes=self.rfile.read(length)
  1956. msg = email.parser.BytesParser().parsebytes(postBytes)
  1957. messageFields=msg.get_payload(decode=False).split(boundary)
  1958. fields={}
  1959. filename=None
  1960. lastImageLocation=0
  1961. for f in messageFields:
  1962. if f=='--':
  1963. continue
  1964. if ' name="' in f:
  1965. postStr=f.split(' name="',1)[1]
  1966. if '"' in postStr:
  1967. postKey=postStr.split('"',1)[0]
  1968. postValueStr=postStr.split('"',1)[1]
  1969. if ';' not in postValueStr:
  1970. if '\r\n' in postValueStr:
  1971. postLines=postValueStr.split('\r\n')
  1972. postValue=''
  1973. if len(postLines)>2:
  1974. for line in range(2,len(postLines)-1):
  1975. if line>2:
  1976. postValue+='\n'
  1977. postValue+=postLines[line]
  1978. fields[postKey]=postValue
  1979. else:
  1980. if 'filename="' not in postStr:
  1981. continue
  1982. filenameStr=postStr.split('filename="')[1]
  1983. if '"' not in filenameStr:
  1984. continue
  1985. postImageFilename=filenameStr.split('"')[0]
  1986. if '.' not in postImageFilename:
  1987. continue
  1988. # directly search the binary array for the beginning
  1989. # of an image
  1990. searchStr=b'Content-Type: image/png'
  1991. imageLocation=postBytes.find(searchStr,lastImageLocation)
  1992. filenameBase=self.server.baseDir+'/accounts/'+nickname+'@'+self.server.domain+'/'+postKey
  1993. # Note: a .temp extension is used here so that at no time is
  1994. # an image with metadata publicly exposed, even for a few mS
  1995. if imageLocation>-1:
  1996. filename=filenameBase+'.png.temp'
  1997. else:
  1998. searchStr=b'Content-Type: image/jpeg'
  1999. imageLocation=postBytes.find(searchStr,lastImageLocation)
  2000. if imageLocation>-1:
  2001. filename=filenameBase+'.jpg.temp'
  2002. else:
  2003. searchStr=b'Content-Type: image/gif'
  2004. imageLocation=postBytes.find(searchStr,lastImageLocation)
  2005. if imageLocation>-1:
  2006. filename=filenameBase+'.gif.temp'
  2007. if filename and imageLocation>-1:
  2008. # locate the beginning of the image, after any
  2009. # carriage returns
  2010. startPos=imageLocation+len(searchStr)
  2011. for offset in range(1,8):
  2012. if postBytes[startPos+offset]!=10:
  2013. if postBytes[startPos+offset]!=13:
  2014. startPos+=offset
  2015. break
  2016. # look for the end of the image
  2017. imageLocationEnd=postBytes.find(b'-------',imageLocation+1)
  2018. fd = open(filename, 'wb')
  2019. if imageLocationEnd>-1:
  2020. fd.write(postBytes[startPos:][:imageLocationEnd-startPos])
  2021. else:
  2022. fd.write(postBytes[startPos:])
  2023. fd.close()
  2024. # remove exif/metadata
  2025. removeMetaData(filename,filename.replace('.temp',''))
  2026. os.remove(filename)
  2027. lastImageLocation=imageLocation+1
  2028. actorFilename=self.server.baseDir+'/accounts/'+nickname+'@'+self.server.domain+'.json'
  2029. if os.path.isfile(actorFilename):
  2030. with open(actorFilename, 'r') as fp:
  2031. actorJson=commentjson.load(fp)
  2032. actorChanged=False
  2033. skillCtr=1
  2034. newSkills={}
  2035. while skillCtr<10:
  2036. skillName=fields.get('skillName'+str(skillCtr))
  2037. if not skillName:
  2038. skillCtr+=1
  2039. continue
  2040. skillValue=fields.get('skillValue'+str(skillCtr))
  2041. if not skillValue:
  2042. skillCtr+=1
  2043. continue
  2044. if not actorJson['skills'].get(skillName):
  2045. actorChanged=True
  2046. else:
  2047. if actorJson['skills'][skillName]!=int(skillValue):
  2048. actorChanged=True
  2049. newSkills[skillName]=int(skillValue)
  2050. skillCtr+=1
  2051. if len(actorJson['skills'].items())!=len(newSkills.items()):
  2052. actorChanged=True
  2053. actorJson['skills']=newSkills
  2054. if fields.get('preferredNickname'):
  2055. if fields['preferredNickname']!=actorJson['preferredUsername']:
  2056. actorJson['preferredUsername']=fields['preferredNickname']
  2057. actorChanged=True
  2058. if fields.get('bio'):
  2059. if fields['bio']!=actorJson['summary']:
  2060. actorTags={}
  2061. actorJson['summary']= \
  2062. addHtmlTags(self.server.baseDir, \
  2063. self.server.httpPrefix, \
  2064. nickname, \
  2065. self.server.domainFull, \
  2066. fields['bio'],[],actorTags)
  2067. if actorTags:
  2068. actorJson['tag']=[]
  2069. for tagName,tag in actorTags.items():
  2070. actorJson['tag'].append(tag)
  2071. actorChanged=True
  2072. if fields.get('moderators'):
  2073. adminNickname=getConfigParam(self.server.baseDir,'admin')
  2074. if self.path.startswith('/users/'+adminNickname+'/'):
  2075. moderatorsFile=self.server.baseDir+'/accounts/moderators.txt'
  2076. clearModeratorStatus(self.server.baseDir)
  2077. if ',' in fields['moderators']:
  2078. # if the list was given as comma separated
  2079. modFile=open(moderatorsFile,"w+")
  2080. for modNick in fields['moderators'].split(','):
  2081. modNick=modNick.strip()
  2082. if os.path.isdir(self.server.baseDir+'/accounts/'+modNick+'@'+self.server.domain):
  2083. modFile.write(modNick+'\n')
  2084. modFile.close()
  2085. for modNick in fields['moderators'].split(','):
  2086. modNick=modNick.strip()
  2087. if os.path.isdir(self.server.baseDir+'/accounts/'+modNick+'@'+self.server.domain):
  2088. setRole(self.server.baseDir,modNick,self.server.domain,'instance','moderator')
  2089. else:
  2090. # nicknames on separate lines
  2091. modFile=open(moderatorsFile,"w+")
  2092. for modNick in fields['moderators'].split('\n'):
  2093. modNick=modNick.strip()
  2094. if os.path.isdir(self.server.baseDir+'/accounts/'+modNick+'@'+self.server.domain):
  2095. modFile.write(modNick+'\n')
  2096. modFile.close()
  2097. for modNick in fields['moderators'].split('\n'):
  2098. modNick=modNick.strip()
  2099. if os.path.isdir(self.server.baseDir+'/accounts/'+modNick+'@'+self.server.domain):
  2100. setRole(self.server.baseDir,modNick,self.server.domain,'instance','moderator')
  2101. approveFollowers=False
  2102. if fields.get('approveFollowers'):
  2103. if fields['approveFollowers']=='on':
  2104. approveFollowers=True
  2105. if approveFollowers!=actorJson['manuallyApprovesFollowers']:
  2106. actorJson['manuallyApprovesFollowers']=approveFollowers
  2107. actorChanged=True
  2108. if fields.get('isBot'):
  2109. if fields['isBot']=='on':
  2110. if actorJson['type']!='Service':
  2111. actorJson['type']='Service'
  2112. actorChanged=True
  2113. else:
  2114. if actorJson['type']!='Person':
  2115. actorJson['type']='Person'
  2116. actorChanged=True
  2117. # save filtered words list
  2118. filterFilename=self.server.baseDir+'/accounts/'+nickname+'@'+self.server.domain+'/filters.txt'
  2119. if fields.get('filteredWords'):
  2120. with open(filterFilename, "w") as filterfile:
  2121. filterfile.write(fields['filteredWords'])
  2122. else:
  2123. if os.path.isfile(filterFilename):
  2124. os.remove(filterFilename)
  2125. # save blocked accounts list
  2126. blockedFilename=self.server.baseDir+'/accounts/'+nickname+'@'+self.server.domain+'/blocking.txt'
  2127. if fields.get('blocked'):
  2128. with open(blockedFilename, "w") as blockedfile:
  2129. blockedfile.write(fields['blocked'])
  2130. else:
  2131. if os.path.isfile(blockedFilename):
  2132. os.remove(blockedFilename)
  2133. # save allowed instances list
  2134. allowedInstancesFilename=self.server.baseDir+'/accounts/'+nickname+'@'+self.server.domain+'/allowedinstances.txt'
  2135. if fields.get('allowedInstances'):
  2136. with open(allowedInstancesFilename, "w") as allowedInstancesFile:
  2137. allowedInstancesFile.write(fields['allowedInstances'])
  2138. else:
  2139. if os.path.isfile(allowedInstancesFilename):
  2140. os.remove(allowedInstancesFilename)
  2141. # save actor json file within accounts
  2142. if actorChanged:
  2143. with open(actorFilename, 'w') as fp:
  2144. commentjson.dump(actorJson, fp, indent=4, sort_keys=False)
  2145. self._redirect_headers(actorStr,cookie)
  2146. self.server.POSTbusy=False
  2147. return
  2148. # moderator action buttons
  2149. if authorized and '/users/' in self.path and \
  2150. self.path.endswith('/moderationaction'):
  2151. actorStr=self.path.replace('/moderationaction','')
  2152. length = int(self.headers['Content-length'])
  2153. moderationParams=self.rfile.read(length).decode('utf-8')
  2154. print('moderationParams: '+moderationParams)
  2155. if '&' in moderationParams:
  2156. moderationText=None
  2157. moderationButton=None
  2158. for moderationStr in moderationParams.split('&'):
  2159. print('moderationStr: '+moderationStr)
  2160. if moderationStr.startswith('moderationAction'):
  2161. if '=' in moderationStr:
  2162. moderationText=moderationStr.split('=')[1].strip()
  2163. moderationText=moderationText.replace('+',' ').replace('%40','@').replace('%3A',':').replace('%23','#').strip()
  2164. elif moderationStr.startswith('submitInfo'):
  2165. msg=htmlModerationInfo(self.server.baseDir).encode('utf-8')
  2166. self._login_headers('text/html',len(msg))
  2167. self.wfile.write(msg)
  2168. self.server.POSTbusy=False
  2169. return
  2170. elif moderationStr.startswith('submitBlock'):
  2171. moderationButton='block'
  2172. elif moderationStr.startswith('submitUnblock'):
  2173. moderationButton='unblock'
  2174. elif moderationStr.startswith('submitSuspend'):
  2175. moderationButton='suspend'
  2176. elif moderationStr.startswith('submitUnsuspend'):
  2177. moderationButton='unsuspend'
  2178. elif moderationStr.startswith('submitRemove'):
  2179. moderationButton='remove'
  2180. if moderationButton and moderationText:
  2181. if self.server.debug:
  2182. print('moderationButton: '+moderationButton)
  2183. print('moderationText: '+moderationText)
  2184. nickname=moderationText
  2185. if nickname.startswith('http') or \
  2186. nickname.startswith('dat'):
  2187. nickname=getNicknameFromActor(nickname)
  2188. if '@' in nickname:
  2189. nickname=nickname.split('@')[0]
  2190. if moderationButton=='suspend':
  2191. suspendAccount(self.server.baseDir,nickname,self.server.salts)
  2192. if moderationButton=='unsuspend':
  2193. unsuspendAccount(self.server.baseDir,nickname)
  2194. if moderationButton=='block':
  2195. fullBlockDomain=None
  2196. if moderationText.startswith('http') or \
  2197. moderationText.startswith('dat'):
  2198. blockDomain,blockPort=getDomainFromActor(moderationText)
  2199. fullBlockDomain=blockDomain
  2200. if blockPort:
  2201. if blockPort!=80 and blockPort!=443:
  2202. if ':' not in blockDomain:
  2203. fullBlockDomain=blockDomain+':'+str(blockPort)
  2204. if '@' in moderationText:
  2205. fullBlockDomain=moderationText.split('@')[1]
  2206. if fullBlockDomain or nickname.startswith('#'):
  2207. addGlobalBlock(self.server.baseDir, \
  2208. nickname,fullBlockDomain)
  2209. if moderationButton=='unblock':
  2210. fullBlockDomain=None
  2211. if moderationText.startswith('http') or \
  2212. moderationText.startswith('dat'):
  2213. blockDomain,blockPort=getDomainFromActor(moderationText)
  2214. fullBlockDomain=blockDomain
  2215. if blockPort:
  2216. if blockPort!=80 and blockPort!=443:
  2217. if ':' not in blockDomain:
  2218. fullBlockDomain=blockDomain+':'+str(blockPort)
  2219. if '@' in moderationText:
  2220. fullBlockDomain=moderationText.split('@')[1]
  2221. if fullBlockDomain or nickname.startswith('#'):
  2222. removeGlobalBlock(self.server.baseDir, \
  2223. nickname,fullBlockDomain)
  2224. if moderationButton=='remove':
  2225. if '/statuses/' not in moderationText:
  2226. removeAccount(self.server.baseDir, \
  2227. nickname, \
  2228. self.server.domain, \
  2229. self.server.port)
  2230. else:
  2231. # remove a post or thread
  2232. postFilename= \
  2233. locatePost(self.server.baseDir, \
  2234. nickname,self.server.domain, \
  2235. moderationText)
  2236. if postFilename:
  2237. if canRemovePost(self.server.baseDir, \
  2238. nickname, \
  2239. self.server.domain, \
  2240. self.server.port, \
  2241. moderationText):
  2242. deletePost(self.server.baseDir, \
  2243. self.server.httpPrefix, \
  2244. nickname,self.server.omain, \
  2245. postFilename, \
  2246. self.server.debug)
  2247. self._redirect_headers(actorStr+'/moderation',cookie)
  2248. self.server.POSTbusy=False
  2249. return
  2250. # a search was made
  2251. if authorized and \
  2252. (self.path.endswith('/searchhandle') or '/searchhandle?page=' in self.path):
  2253. # get the page number
  2254. pageNumber=1
  2255. if '/searchhandle?page=' in self.path:
  2256. pageNumberStr=self.path.split('/searchhandle?page=')[1]
  2257. if pageNumberStr.isdigit():
  2258. pageNumber=int(pageNumberStr)
  2259. self.path=self.path.split('?page=')[0]
  2260. actorStr=self.path.replace('/searchhandle','')
  2261. length = int(self.headers['Content-length'])
  2262. searchParams=self.rfile.read(length).decode('utf-8')
  2263. if 'searchtext=' in searchParams:
  2264. searchStr=searchParams.split('searchtext=')[1]
  2265. if '&' in searchStr:
  2266. searchStr=searchStr.split('&')[0]
  2267. searchStr=searchStr.replace('+',' ').replace('%40','@').replace('%3A',':').replace('%23','#').strip()
  2268. if searchStr.startswith('#'):
  2269. # hashtag search
  2270. hashtagStr= \
  2271. htmlHashtagSearch(self.server.baseDir,searchStr[1:],1, \
  2272. maxPostsInFeed,self.server.session, \
  2273. self.server.cachedWebfingers, \
  2274. self.server.personCache, \
  2275. self.server.httpPrefix, \
  2276. self.server.projectVersion)
  2277. if hashtagStr:
  2278. msg=hashtagStr.encode('utf-8')
  2279. self._login_headers('text/html',len(msg))
  2280. self.wfile.write(msg)
  2281. self.server.POSTbusy=False
  2282. return
  2283. elif '@' in searchStr:
  2284. # profile search
  2285. nickname=getNicknameFromActor(self.path)
  2286. if not self.server.session:
  2287. self.server.session= \
  2288. createSession(self.server.domain, \
  2289. self.server.port, \
  2290. self.server.useTor)
  2291. profileStr= \
  2292. htmlProfileAfterSearch(self.server.baseDir, \
  2293. self.path.replace('/searchhandle',''), \
  2294. self.server.httpPrefix, \
  2295. nickname, \
  2296. self.server.domain,self.server.port, \
  2297. searchStr, \
  2298. self.server.session, \
  2299. self.server.cachedWebfingers, \
  2300. self.server.personCache, \
  2301. self.server.debug, \
  2302. self.server.projectVersion)
  2303. if profileStr:
  2304. msg=profileStr.encode('utf-8')
  2305. self._login_headers('text/html',len(msg))
  2306. self.wfile.write(msg)
  2307. self.server.POSTbusy=False
  2308. return
  2309. else:
  2310. # shared items search
  2311. sharedItemsStr= \
  2312. htmlSearchSharedItems(self.server.baseDir, \
  2313. searchStr,pageNumber, \
  2314. maxPostsInFeed,actorStr)
  2315. if sharedItemsStr:
  2316. msg=sharedItemsStr.encode('utf-8')
  2317. self._login_headers('text/html',len(msg))
  2318. self.wfile.write(msg)
  2319. self.server.POSTbusy=False
  2320. return
  2321. self._redirect_headers(actorStr,cookie)
  2322. self.server.POSTbusy=False
  2323. return
  2324. # decision to follow in the web interface is confirmed
  2325. if authorized and self.path.endswith('/followconfirm'):
  2326. originPathStr=self.path.split('/followconfirm')[0]
  2327. followerNickname=getNicknameFromActor(originPathStr)
  2328. length = int(self.headers['Content-length'])
  2329. followConfirmParams=self.rfile.read(length).decode('utf-8')
  2330. if '&submitYes=' in followConfirmParams:
  2331. followingActor=followConfirmParams.replace('%3A',':').replace('%2F','/').split('actor=')[1]
  2332. if '&' in followingActor:
  2333. followingActor=followingActor.split('&')[0]
  2334. followingNickname=getNicknameFromActor(followingActor)
  2335. followingDomain,followingPort=getDomainFromActor(followingActor)
  2336. if followerNickname==followingNickname and \
  2337. followingDomain==self.server.domain and \
  2338. followingPort==self.server.port:
  2339. if self.server.debug:
  2340. print('You cannot follow yourself!')
  2341. else:
  2342. if self.server.debug:
  2343. print('Sending follow request from '+followerNickname+' to '+followingActor)
  2344. sendFollowRequest(self.server.session, \
  2345. self.server.baseDir, \
  2346. followerNickname, \
  2347. self.server.domain,self.server.port, \
  2348. self.server.httpPrefix, \
  2349. followingNickname, \
  2350. followingDomain, \
  2351. followingPort,self.server.httpPrefix, \
  2352. False,self.server.federationList, \
  2353. self.server.sendThreads, \
  2354. self.server.postLog, \
  2355. self.server.cachedWebfingers, \
  2356. self.server.personCache, \
  2357. self.server.debug, \
  2358. self.server.projectVersion)
  2359. self._redirect_headers(originPathStr,cookie)
  2360. self.server.POSTbusy=False
  2361. return
  2362. # decision to unfollow in the web interface is confirmed
  2363. if authorized and self.path.endswith('/unfollowconfirm'):
  2364. originPathStr=self.path.split('/unfollowconfirm')[0]
  2365. followerNickname=getNicknameFromActor(originPathStr)
  2366. length = int(self.headers['Content-length'])
  2367. followConfirmParams=self.rfile.read(length).decode('utf-8')
  2368. if '&submitYes=' in followConfirmParams:
  2369. followingActor=followConfirmParams.replace('%3A',':').replace('%2F','/').split('actor=')[1]
  2370. if '&' in followingActor:
  2371. followingActor=followingActor.split('&')[0]
  2372. followingNickname=getNicknameFromActor(followingActor)
  2373. followingDomain,followingPort=getDomainFromActor(followingActor)
  2374. if followerNickname==followingNickname and \
  2375. followingDomain==self.server.domain and \
  2376. followingPort==self.server.port:
  2377. if self.server.debug:
  2378. print('You cannot unfollow yourself!')
  2379. else:
  2380. if self.server.debug:
  2381. print(followerNickname+' stops following '+followingActor)
  2382. followActor=self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+followerNickname
  2383. statusNumber,published = getStatusNumber()
  2384. followId=followActor+'/statuses/'+str(statusNumber)
  2385. unfollowJson = {
  2386. '@context': 'https://www.w3.org/ns/activitystreams',
  2387. 'id': followId+'/undo',
  2388. 'type': 'Undo',
  2389. 'actor': followActor,
  2390. 'object': {
  2391. 'id': followId,
  2392. 'type': 'Follow',
  2393. 'actor': followActor,
  2394. 'object': followingActor
  2395. }
  2396. }
  2397. pathUsersSection=self.path.split('/users/')[1]
  2398. self.postToNickname=pathUsersSection.split('/')[0]
  2399. self._postToOutbox(unfollowJson)
  2400. self._redirect_headers(originPathStr,cookie)
  2401. self.server.POSTbusy=False
  2402. return
  2403. # decision to unblock in the web interface is confirmed
  2404. if authorized and self.path.endswith('/unblockconfirm'):
  2405. originPathStr=self.path.split('/unblockconfirm')[0]
  2406. blockerNickname=getNicknameFromActor(originPathStr)
  2407. length = int(self.headers['Content-length'])
  2408. blockConfirmParams=self.rfile.read(length).decode('utf-8')
  2409. if '&submitYes=' in blockConfirmParams:
  2410. blockingActor=blockConfirmParams.replace('%3A',':').replace('%2F','/').split('actor=')[1]
  2411. if '&' in blockingActor:
  2412. blockingActor=blockingActor.split('&')[0]
  2413. blockingNickname=getNicknameFromActor(blockingActor)
  2414. blockingDomain,blockingPort=getDomainFromActor(blockingActor)
  2415. blockingDomainFull=blockingDomain
  2416. if blockingPort:
  2417. if blockingPort!=80 and blockingPort!=443:
  2418. if ':' not in blockingDomain:
  2419. blockingDomainFull=blockingDomain+':'+str(blockingPort)
  2420. if blockerNickname==blockingNickname and \
  2421. blockingDomain==self.server.domain and \
  2422. blockingPort==self.server.port:
  2423. if self.server.debug:
  2424. print('You cannot unblock yourself!')
  2425. else:
  2426. if self.server.debug:
  2427. print(blockerNickname+' stops blocking '+blockingActor)
  2428. removeBlock(self.server.baseDir,blockerNickname,self.server.domain, \
  2429. blockingNickname,blockingDomainFull)
  2430. self._redirect_headers(originPathStr,cookie)
  2431. self.server.POSTbusy=False
  2432. return
  2433. # decision to block in the web interface is confirmed
  2434. if authorized and self.path.endswith('/blockconfirm'):
  2435. originPathStr=self.path.split('/blockconfirm')[0]
  2436. blockerNickname=getNicknameFromActor(originPathStr)
  2437. length = int(self.headers['Content-length'])
  2438. blockConfirmParams=self.rfile.read(length).decode('utf-8')
  2439. if '&submitYes=' in blockConfirmParams:
  2440. blockingActor=blockConfirmParams.replace('%3A',':').replace('%2F','/').split('actor=')[1]
  2441. if '&' in blockingActor:
  2442. blockingActor=blockingActor.split('&')[0]
  2443. blockingNickname=getNicknameFromActor(blockingActor)
  2444. blockingDomain,blockingPort=getDomainFromActor(blockingActor)
  2445. blockingDomainFull=blockingDomain
  2446. if blockingPort:
  2447. if blockingPort!=80 and blockingPort!=443:
  2448. if ':' not in blockingDomain:
  2449. blockingDomainFull=blockingDomain+':'+str(blockingPort)
  2450. if blockerNickname==blockingNickname and \
  2451. blockingDomain==self.server.domain and \
  2452. blockingPort==self.server.port:
  2453. if self.server.debug:
  2454. print('You cannot block yourself!')
  2455. else:
  2456. if self.server.debug:
  2457. print('Adding block by '+blockerNickname+' of '+blockingActor)
  2458. addBlock(self.server.baseDir,blockerNickname,self.server.domain, \
  2459. blockingNickname,blockingDomainFull)
  2460. self._redirect_headers(originPathStr,cookie)
  2461. self.server.POSTbusy=False
  2462. return
  2463. postState=self._receiveNewPost(authorized,'newpost')
  2464. if postState!=0:
  2465. nickname=self.path.split('/users/')[1]
  2466. if '/' in nickname:
  2467. nickname=nickname.split('/')[0]
  2468. self._redirect_headers('/users/'+nickname+'/outbox',cookie)
  2469. self.server.POSTbusy=False
  2470. return
  2471. postState=self._receiveNewPost(authorized,'newunlisted')
  2472. if postState!=0:
  2473. nickname=self.path.split('/users/')[1]
  2474. if '/' in nickname:
  2475. nickname=nickname.split('/')[0]
  2476. self._redirect_headers('/users/'+self.postToNickname+'/outbox',cookie)
  2477. self.server.POSTbusy=False
  2478. return
  2479. postState=self._receiveNewPost(authorized,'newfollowers')
  2480. if postState!=0:
  2481. nickname=self.path.split('/users/')[1]
  2482. if '/' in nickname:
  2483. nickname=nickname.split('/')[0]
  2484. self._redirect_headers('/users/'+self.postToNickname+'/outbox',cookie)
  2485. self.server.POSTbusy=False
  2486. return
  2487. postState=self._receiveNewPost(authorized,'newdm')
  2488. if postState!=0:
  2489. nickname=self.path.split('/users/')[1]
  2490. if '/' in nickname:
  2491. nickname=nickname.split('/')[0]
  2492. self._redirect_headers('/users/'+self.postToNickname+'/outbox',cookie)
  2493. self.server.POSTbusy=False
  2494. return
  2495. postState=self._receiveNewPost(authorized,'newreport')
  2496. if postState!=0:
  2497. nickname=self.path.split('/users/')[1]
  2498. if '/' in nickname:
  2499. nickname=nickname.split('/')[0]
  2500. self._redirect_headers('/users/'+self.postToNickname+'/outbox',cookie)
  2501. self.server.POSTbusy=False
  2502. return
  2503. postState=self._receiveNewPost(authorized,'newshare')
  2504. if postState!=0:
  2505. nickname=self.path.split('/users/')[1]
  2506. if '/' in nickname:
  2507. nickname=nickname.split('/')[0]
  2508. self._redirect_headers('/users/'+self.postToNickname+'/shares',cookie)
  2509. self.server.POSTbusy=False
  2510. return
  2511. if self.path.endswith('/outbox') or self.path.endswith('/shares'):
  2512. if '/users/' in self.path:
  2513. if authorized:
  2514. self.outboxAuthenticated=True
  2515. pathUsersSection=self.path.split('/users/')[1]
  2516. self.postToNickname=pathUsersSection.split('/')[0]
  2517. if not self.outboxAuthenticated:
  2518. self.send_response(405)
  2519. self.end_headers()
  2520. self.server.POSTbusy=False
  2521. return
  2522. # check that the post is to an expected path
  2523. if not (self.path.endswith('/outbox') or \
  2524. self.path.endswith('/inbox') or \
  2525. self.path.endswith('/shares') or \
  2526. self.path.endswith('/moderationaction') or \
  2527. self.path.endswith('/caps/new') or \
  2528. self.path=='/sharedInbox'):
  2529. print('Attempt to POST to invalid path '+self.path)
  2530. self.send_response(400)
  2531. self.end_headers()
  2532. self.server.POSTbusy=False
  2533. return
  2534. # read the message and convert it into a python dictionary
  2535. length = int(self.headers['Content-length'])
  2536. if self.server.debug:
  2537. print('DEBUG: content-length: '+str(length))
  2538. if not self.headers['Content-type'].startswith('image/'):
  2539. if length>self.server.maxMessageLength:
  2540. print('Maximum message length exceeded '+str(length))
  2541. self.send_response(400)
  2542. self.end_headers()
  2543. self.server.POSTbusy=False
  2544. return
  2545. else:
  2546. if length>self.server.maxImageSize:
  2547. print('Maximum image size exceeded '+str(length))
  2548. self.send_response(400)
  2549. self.end_headers()
  2550. self.server.POSTbusy=False
  2551. return
  2552. # receive images to the outbox
  2553. if self.headers['Content-type'].startswith('image/') and \
  2554. '/users/' in self.path:
  2555. if not self.outboxAuthenticated:
  2556. if self.server.debug:
  2557. print('DEBUG: unauthenticated attempt to post image to outbox')
  2558. self.send_response(403)
  2559. self.end_headers()
  2560. self.server.POSTbusy=False
  2561. return
  2562. pathUsersSection=self.path.split('/users/')[1]
  2563. if '/' not in pathUsersSection:
  2564. self.send_response(404)
  2565. self.end_headers()
  2566. self.server.POSTbusy=False
  2567. return
  2568. self.postFromNickname=pathUsersSection.split('/')[0]
  2569. accountsDir=self.server.baseDir+'/accounts/'+self.postFromNickname+'@'+self.server.domain
  2570. if not os.path.isdir(accountsDir):
  2571. self.send_response(404)
  2572. self.end_headers()
  2573. self.server.POSTbusy=False
  2574. return
  2575. mediaBytes=self.rfile.read(length)
  2576. mediaFilenameBase=accountsDir+'/upload'
  2577. mediaFilename=mediaFilenameBase+'.png'
  2578. if self.headers['Content-type'].endswith('jpeg'):
  2579. mediaFilename=mediaFilenameBase+'.jpg'
  2580. if self.headers['Content-type'].endswith('gif'):
  2581. mediaFilename=mediaFilenameBase+'.gif'
  2582. with open(mediaFilename, 'wb') as avFile:
  2583. avFile.write(mediaBytes)
  2584. if self.server.debug:
  2585. print('DEBUG: image saved to '+mediaFilename)
  2586. self.send_response(201)
  2587. self.end_headers()
  2588. self.server.POSTbusy=False
  2589. return
  2590. # refuse to receive non-json content
  2591. if self.headers['Content-type'] != 'application/json' and \
  2592. self.headers['Content-type'] != 'application/activity+json':
  2593. print("POST is not json: "+self.headers['Content-type'])
  2594. if self.server.debug:
  2595. print(str(self.headers))
  2596. length = int(self.headers['Content-length'])
  2597. if length<self.server.maxPostLength:
  2598. unknownPost=self.rfile.read(length).decode('utf-8')
  2599. print(str(unknownPost))
  2600. self.send_response(400)
  2601. self.end_headers()
  2602. self.server.POSTbusy=False
  2603. return
  2604. if self.server.debug:
  2605. print('DEBUG: Reading message')
  2606. messageBytes=self.rfile.read(length)
  2607. messageJson=json.loads(messageBytes)
  2608. # https://www.w3.org/TR/activitypub/#object-without-create
  2609. if self.outboxAuthenticated:
  2610. if self._postToOutbox(messageJson):
  2611. if messageJson.get('id'):
  2612. self.headers['Location']= \
  2613. messageJson['id'].replace('/activity','').replace('/undo','')
  2614. self.send_response(201)
  2615. self.end_headers()
  2616. self.server.POSTbusy=False
  2617. return
  2618. else:
  2619. if self.server.debug:
  2620. print('Failed to post to outbox')
  2621. self.send_response(403)
  2622. self.end_headers()
  2623. self.server.POSTbusy=False
  2624. return
  2625. # check the necessary properties are available
  2626. if self.server.debug:
  2627. print('DEBUG: Check message has params')
  2628. if self.path.endswith('/inbox') or \
  2629. self.path=='/sharedInbox':
  2630. if not inboxMessageHasParams(messageJson):
  2631. if self.server.debug:
  2632. pprint(messageJson)
  2633. print("DEBUG: inbox message doesn't have the required parameters")
  2634. self.send_response(403)
  2635. self.end_headers()
  2636. self.server.POSTbusy=False
  2637. return
  2638. if not inboxPermittedMessage(self.server.domain, \
  2639. messageJson, \
  2640. self.server.federationList):
  2641. if self.server.debug:
  2642. # https://www.youtube.com/watch?v=K3PrSj9XEu4
  2643. print('DEBUG: Ah Ah Ah')
  2644. self.send_response(403)
  2645. self.end_headers()
  2646. self.server.POSTbusy=False
  2647. return
  2648. if self.server.debug:
  2649. pprint(messageJson)
  2650. if not self.headers.get('signature'):
  2651. if 'keyId=' not in self.headers['signature']:
  2652. if self.server.debug:
  2653. print('DEBUG: POST to inbox has no keyId in header signature parameter')
  2654. self.send_response(403)
  2655. self.end_headers()
  2656. self.server.POSTbusy=False
  2657. return
  2658. if self.server.debug:
  2659. print('DEBUG: POST saving to inbox queue')
  2660. if '/users/' in self.path:
  2661. pathUsersSection=self.path.split('/users/')[1]
  2662. if '/' not in pathUsersSection:
  2663. if self.server.debug:
  2664. print('DEBUG: This is not a users endpoint')
  2665. else:
  2666. self.postToNickname=pathUsersSection.split('/')[0]
  2667. if self.postToNickname:
  2668. queueStatus=self._updateInboxQueue(self.postToNickname,messageJson,messageBytes)
  2669. if queueStatus==0:
  2670. self.send_response(200)
  2671. self.end_headers()
  2672. self.server.POSTbusy=False
  2673. return
  2674. if queueStatus==1:
  2675. self.send_response(503)
  2676. self.end_headers()
  2677. self.server.POSTbusy=False
  2678. return
  2679. if self.server.debug:
  2680. print('_updateInboxQueue exited without doing anything')
  2681. else:
  2682. if self.server.debug:
  2683. print('self.postToNickname is None')
  2684. self.send_response(403)
  2685. self.end_headers()
  2686. self.server.POSTbusy=False
  2687. return
  2688. else:
  2689. if self.path == '/sharedInbox' or self.path == '/inbox':
  2690. print('DEBUG: POST to shared inbox')
  2691. queueStatus=self._updateInboxQueue('inbox',messageJson,messageBytes)
  2692. if queueStatus==0:
  2693. self.send_response(200)
  2694. self.end_headers()
  2695. self.server.POSTbusy=False
  2696. return
  2697. if queueStatus==1:
  2698. self.send_response(503)
  2699. self.end_headers()
  2700. self.server.POSTbusy=False
  2701. return
  2702. self.send_response(200)
  2703. self.end_headers()
  2704. self.server.POSTbusy=False
  2705. class PubServerUnitTest(PubServer):
  2706. protocol_version = 'HTTP/1.0'
  2707. def runDaemon(projectVersion, \
  2708. instanceId,clientToServer: bool, \
  2709. baseDir: str,domain: str, \
  2710. port=80,proxyPort=80,httpPrefix='https', \
  2711. fedList=[],noreply=False,nolike=False,nopics=False, \
  2712. noannounce=False,cw=False,ocapAlways=False, \
  2713. useTor=False,maxReplies=64, \
  2714. domainMaxPostsPerDay=8640,accountMaxPostsPerDay=8640, \
  2715. allowDeletion=False,debug=False,unitTest=False) -> None:
  2716. if len(domain)==0:
  2717. domain='localhost'
  2718. if '.' not in domain:
  2719. if domain != 'localhost':
  2720. print('Invalid domain: ' + domain)
  2721. return
  2722. serverAddress = ('', proxyPort)
  2723. if unitTest:
  2724. httpd = ThreadingHTTPServer(serverAddress, PubServerUnitTest)
  2725. else:
  2726. httpd = ThreadingHTTPServer(serverAddress, PubServer)
  2727. # max POST size of 10M
  2728. httpd.projectVersion=projectVersion
  2729. httpd.maxPostLength=1024*1024*10
  2730. httpd.domain=domain
  2731. httpd.port=port
  2732. httpd.domainFull=domain
  2733. if port:
  2734. if port!=80 and port!=443:
  2735. if ':' not in domain:
  2736. httpd.domainFull=domain+':'+str(port)
  2737. httpd.httpPrefix=httpPrefix
  2738. httpd.debug=debug
  2739. httpd.federationList=fedList.copy()
  2740. httpd.baseDir=baseDir
  2741. httpd.instanceId=instanceId
  2742. httpd.personCache={}
  2743. httpd.cachedWebfingers={}
  2744. httpd.useTor=useTor
  2745. httpd.session = None
  2746. httpd.sessionLastUpdate=0
  2747. httpd.lastGET=0
  2748. httpd.lastPOST=0
  2749. httpd.GETbusy=False
  2750. httpd.POSTbusy=False
  2751. httpd.receivedMessage=False
  2752. httpd.inboxQueue=[]
  2753. httpd.sendThreads=[]
  2754. httpd.postLog=[]
  2755. httpd.maxQueueLength=16
  2756. httpd.ocapAlways=ocapAlways
  2757. httpd.maxMessageLength=5000
  2758. httpd.maxImageSize=10*1024*1024
  2759. httpd.allowDeletion=allowDeletion
  2760. httpd.lastLoginTime=0
  2761. httpd.maxReplies=maxReplies
  2762. httpd.salts={}
  2763. httpd.tokens={}
  2764. httpd.tokensLookup={}
  2765. httpd.acceptedCaps=["inbox:write","objects:read"]
  2766. if noreply:
  2767. httpd.acceptedCaps.append('inbox:noreply')
  2768. if nolike:
  2769. httpd.acceptedCaps.append('inbox:nolike')
  2770. if nopics:
  2771. httpd.acceptedCaps.append('inbox:nopics')
  2772. if noannounce:
  2773. httpd.acceptedCaps.append('inbox:noannounce')
  2774. if cw:
  2775. httpd.acceptedCaps.append('inbox:cw')
  2776. if not os.path.isdir(baseDir+'/accounts/inbox@'+domain):
  2777. print('Creating shared inbox: inbox@'+domain)
  2778. createSharedInbox(baseDir,'inbox',domain,port,httpPrefix)
  2779. print('Creating inbox queue')
  2780. httpd.thrInboxQueue= \
  2781. threadWithTrace(target=runInboxQueue, \
  2782. args=(projectVersion, \
  2783. baseDir,httpPrefix,httpd.sendThreads, \
  2784. httpd.postLog,httpd.cachedWebfingers, \
  2785. httpd.personCache,httpd.inboxQueue, \
  2786. domain,port,useTor,httpd.federationList, \
  2787. httpd.ocapAlways,maxReplies, \
  2788. domainMaxPostsPerDay,accountMaxPostsPerDay, \
  2789. allowDeletion,debug,httpd.acceptedCaps),daemon=True)
  2790. httpd.thrInboxQueue.start()
  2791. if clientToServer:
  2792. print('Running ActivityPub client on ' + domain + ' port ' + str(proxyPort))
  2793. else:
  2794. print('Running ActivityPub server on ' + domain + ' port ' + str(proxyPort))
  2795. httpd.serve_forever()