daemon.py 408 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971797279737974797579767977797879797980798179827983798479857986798779887989799079917992799379947995799679977998799980008001800280038004800580068007800880098010801180128013801480158016801780188019802080218022802380248025802680278028802980308031803280338034803580368037803880398040804180428043804480458046804780488049805080518052805380548055805680578058805980608061806280638064806580668067806880698070807180728073807480758076807780788079808080818082808380848085808680878088808980908091809280938094809580968097809880998100810181028103810481058106810781088109811081118112811381148115811681178118811981208121812281238124812581268127812881298130813181328133813481358136813781388139814081418142814381448145814681478148814981508151815281538154815581568157815881598160816181628163816481658166816781688169817081718172817381748175817681778178817981808181818281838184818581868187818881898190819181928193819481958196819781988199820082018202820382048205820682078208820982108211821282138214821582168217821882198220822182228223822482258226822782288229823082318232823382348235823682378238823982408241824282438244824582468247824882498250825182528253825482558256825782588259826082618262826382648265826682678268826982708271827282738274827582768277827882798280828182828283828482858286828782888289829082918292829382948295829682978298829983008301830283038304830583068307830883098310831183128313831483158316831783188319832083218322832383248325832683278328832983308331833283338334833583368337833883398340834183428343834483458346834783488349835083518352835383548355835683578358835983608361836283638364836583668367836883698370837183728373837483758376837783788379838083818382838383848385838683878388838983908391839283938394839583968397839883998400840184028403840484058406840784088409841084118412841384148415841684178418841984208421842284238424842584268427842884298430843184328433843484358436843784388439844084418442844384448445844684478448844984508451845284538454845584568457845884598460846184628463846484658466846784688469847084718472847384748475847684778478847984808481848284838484848584868487848884898490849184928493849484958496849784988499850085018502850385048505850685078508850985108511851285138514851585168517851885198520852185228523852485258526852785288529853085318532853385348535853685378538853985408541854285438544854585468547854885498550855185528553
  1. __filename__ = "daemon.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "1.1.0"
  5. __maintainer__ = "Bob Mottram"
  6. __email__ = "bob@freedombone.net"
  7. __status__ = "Production"
  8. from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
  9. import sys
  10. import json
  11. import time
  12. import locale
  13. import urllib.parse
  14. from socket import error as SocketError
  15. import errno
  16. from functools import partial
  17. import pyqrcode
  18. # for saving images
  19. from hashlib import sha256
  20. from hashlib import sha1
  21. from session import createSession
  22. from webfinger import parseHandle
  23. from webfinger import webfingerMeta
  24. from webfinger import webfingerNodeInfo
  25. from webfinger import webfingerLookup
  26. from webfinger import webfingerUpdate
  27. from metadata import metaDataInstance
  28. from metadata import metaDataNodeInfo
  29. from pgp import getEmailAddress
  30. from pgp import setEmailAddress
  31. from pgp import getPGPpubKey
  32. from pgp import getPGPfingerprint
  33. from pgp import setPGPpubKey
  34. from pgp import setPGPfingerprint
  35. from xmpp import getXmppAddress
  36. from xmpp import setXmppAddress
  37. from ssb import getSSBAddress
  38. from ssb import setSSBAddress
  39. from tox import getToxAddress
  40. from tox import setToxAddress
  41. from matrix import getMatrixAddress
  42. from matrix import setMatrixAddress
  43. from donate import getDonationUrl
  44. from donate import setDonationUrl
  45. from person import savePersonQrcode
  46. from person import randomizeActorImages
  47. from person import personUpgradeActor
  48. from person import activateAccount
  49. from person import deactivateAccount
  50. from person import registerAccount
  51. from person import personLookup
  52. from person import personBoxJson
  53. from person import createSharedInbox
  54. from person import isSuspended
  55. from person import suspendAccount
  56. from person import unsuspendAccount
  57. from person import removeAccount
  58. from person import canRemovePost
  59. from person import personSnooze
  60. from person import personUnsnooze
  61. from posts import mutePost
  62. from posts import unmutePost
  63. from posts import createQuestionPost
  64. from posts import createPublicPost
  65. from posts import createBlogPost
  66. from posts import createReportPost
  67. from posts import createUnlistedPost
  68. from posts import createFollowersOnlyPost
  69. from posts import createDirectMessagePost
  70. from posts import populateRepliesJson
  71. from posts import addToField
  72. from posts import expireCache
  73. from inbox import clearQueueItems
  74. from inbox import inboxPermittedMessage
  75. from inbox import inboxMessageHasParams
  76. from inbox import runInboxQueue
  77. from inbox import runInboxQueueWatchdog
  78. from inbox import savePostToInboxQueue
  79. from inbox import populateReplies
  80. from inbox import getPersonPubKey
  81. from follow import getFollowingFeed
  82. from follow import sendFollowRequest
  83. from auth import authorize
  84. from auth import createPassword
  85. from auth import createBasicAuthHeader
  86. from auth import authorizeBasic
  87. from auth import storeBasicCredentials
  88. from threads import threadWithTrace
  89. from threads import removeDormantThreads
  90. from media import replaceYouTube
  91. from media import attachMedia
  92. from blocking import addBlock
  93. from blocking import removeBlock
  94. from blocking import addGlobalBlock
  95. from blocking import removeGlobalBlock
  96. from blocking import isBlockedHashtag
  97. from blocking import isBlockedDomain
  98. from blocking import getDomainBlocklist
  99. from config import setConfigParam
  100. from config import getConfigParam
  101. from roles import setRole
  102. from roles import clearModeratorStatus
  103. from blog import htmlBlogPageRSS2
  104. from blog import htmlBlogPageRSS3
  105. from blog import htmlBlogView
  106. from blog import htmlBlogPage
  107. from blog import htmlBlogPost
  108. from blog import htmlEditBlog
  109. from webinterface import htmlFollowingList
  110. from webinterface import getBlogAddress
  111. from webinterface import setBlogAddress
  112. from webinterface import htmlCalendarDeleteConfirm
  113. from webinterface import htmlDeletePost
  114. from webinterface import htmlAbout
  115. from webinterface import htmlRemoveSharedItem
  116. from webinterface import htmlInboxDMs
  117. from webinterface import htmlInboxReplies
  118. from webinterface import htmlInboxMedia
  119. from webinterface import htmlInboxBlogs
  120. from webinterface import htmlUnblockConfirm
  121. from webinterface import htmlPersonOptions
  122. from webinterface import htmlIndividualPost
  123. from webinterface import htmlProfile
  124. from webinterface import htmlInbox
  125. from webinterface import htmlBookmarks
  126. from webinterface import htmlShares
  127. from webinterface import htmlOutbox
  128. from webinterface import htmlModeration
  129. from webinterface import htmlPostReplies
  130. from webinterface import htmlLogin
  131. from webinterface import htmlSuspended
  132. from webinterface import htmlGetLoginCredentials
  133. from webinterface import htmlNewPost
  134. from webinterface import htmlFollowConfirm
  135. from webinterface import htmlCalendar
  136. from webinterface import htmlSearch
  137. from webinterface import htmlSearchEmoji
  138. from webinterface import htmlSearchEmojiTextEntry
  139. from webinterface import htmlUnfollowConfirm
  140. from webinterface import htmlProfileAfterSearch
  141. from webinterface import htmlEditProfile
  142. from webinterface import htmlTermsOfService
  143. from webinterface import htmlSkillsSearch
  144. from webinterface import htmlHistorySearch
  145. from webinterface import htmlHashtagSearch
  146. from webinterface import htmlModerationInfo
  147. from webinterface import htmlSearchSharedItems
  148. from webinterface import htmlHashtagBlocked
  149. from shares import getSharesFeedForPerson
  150. from shares import addShare
  151. from shares import removeShare
  152. from shares import expireShares
  153. from utils import updateLikesCollection
  154. from utils import undoLikesCollectionEntry
  155. from utils import deletePost
  156. from utils import isBlogPost
  157. from utils import removeAvatarFromCache
  158. from utils import locatePost
  159. from utils import getCachedPostFilename
  160. from utils import removePostFromCache
  161. from utils import getNicknameFromActor
  162. from utils import getDomainFromActor
  163. from utils import getStatusNumber
  164. from utils import urlPermitted
  165. from utils import loadJson
  166. from utils import saveJson
  167. from manualapprove import manualDenyFollowRequest
  168. from manualapprove import manualApproveFollowRequest
  169. from announce import createAnnounce
  170. from content import replaceEmojiFromTags
  171. from content import addHtmlTags
  172. from content import extractMediaInFormPOST
  173. from content import saveMediaInFormPOST
  174. from content import extractTextFieldsInPOST
  175. from media import removeMetaData
  176. from cache import storePersonInCache
  177. from cache import getPersonFromCache
  178. from httpsig import verifyPostHeaders
  179. from theme import setTheme
  180. from theme import getTheme
  181. from schedule import runPostSchedule
  182. from schedule import runPostScheduleWatchdog
  183. from schedule import removeScheduledPosts
  184. from outbox import postMessageToOutbox
  185. from happening import removeCalendarEvent
  186. from bookmarks import bookmark
  187. from bookmarks import undoBookmark
  188. from petnames import setPetName
  189. from followingCalendar import addPersonToCalendar
  190. from followingCalendar import removePersonFromCalendar
  191. import os
  192. # maximum number of posts to list in outbox feed
  193. maxPostsInFeed = 12
  194. # reduced posts for media feed because it can take a while
  195. maxPostsInMediaFeed = 6
  196. # Blogs can be longer, so don't show many per page
  197. maxPostsInBlogsFeed = 4
  198. # Maximum number of entries in returned rss.xml
  199. maxPostsInRSSFeed = 10
  200. # number of follows/followers per page
  201. followsPerPage = 12
  202. # number of item shares per page
  203. sharesPerPage = 12
  204. def saveDomainQrcode(baseDir: str, httpPrefix: str,
  205. domainFull: str, scale=6) -> None:
  206. """Saves a qrcode image for the domain name
  207. This helps to transfer onion or i2p domains to a mobile device
  208. """
  209. qrcodeFilename = baseDir + '/accounts/qrcode.png'
  210. url = pyqrcode.create(httpPrefix + '://' + domainFull)
  211. url.png(qrcodeFilename, scale)
  212. def readFollowList(filename: str) -> None:
  213. """Returns a list of ActivityPub addresses to follow
  214. """
  215. followlist = []
  216. if not os.path.isfile(filename):
  217. return followlist
  218. followUsers = open(filename, "r")
  219. for u in followUsers:
  220. if u not in followlist:
  221. nickname, domain = parseHandle(u)
  222. if nickname:
  223. followlist.append(nickname + '@' + domain)
  224. followUsers.close()
  225. return followlist
  226. class PubServer(BaseHTTPRequestHandler):
  227. protocol_version = 'HTTP/1.1'
  228. def _pathIsImage(self) -> bool:
  229. if self.path.endswith('.png') or \
  230. self.path.endswith('.jpg') or \
  231. self.path.endswith('.gif') or \
  232. self.path.endswith('.webp'):
  233. return True
  234. return False
  235. def _pathIsVideo(self) -> bool:
  236. if self.path.endswith('.ogv') or \
  237. self.path.endswith('.mp4'):
  238. return True
  239. return False
  240. def _pathIsAudio(self) -> bool:
  241. if self.path.endswith('.ogg') or \
  242. self.path.endswith('.mp3'):
  243. return True
  244. return False
  245. def handle_error(self, request, client_address):
  246. print('ERROR: http server error: ' + str(request) + ', ' +
  247. str(client_address))
  248. pass
  249. def _isMinimal(self, nickname: str) -> bool:
  250. """Returns true if minimal buttons should be shown
  251. for the given account
  252. """
  253. accountDir = self.server.baseDir + '/accounts/' + \
  254. nickname + '@' + self.server.domain
  255. if not os.path.isdir(accountDir):
  256. return False
  257. minimalFilename = accountDir + '/minimal'
  258. if os.path.isfile(minimalFilename):
  259. return True
  260. return False
  261. def _setMinimal(self, nickname: str, minimal: bool) -> None:
  262. """Sets whether an account should display minimal buttons
  263. """
  264. accountDir = self.server.baseDir + '/accounts/' + \
  265. nickname + '@' + self.server.domain
  266. if not os.path.isdir(accountDir):
  267. return
  268. minimalFilename = accountDir + '/minimal'
  269. minimalFileExists = os.path.isfile(minimalFilename)
  270. if not minimal and minimalFileExists:
  271. os.remove(minimalFilename)
  272. elif minimal and not minimalFileExists:
  273. with open(minimalFilename, 'w') as fp:
  274. fp.write('\n')
  275. def _sendReplyToQuestion(self, nickname: str, messageId: str,
  276. answer: str) -> None:
  277. """Sends a reply to a question
  278. """
  279. votesFilename = self.server.baseDir + '/accounts/' + \
  280. nickname + '@' + self.server.domain + '/questions.txt'
  281. if os.path.isfile(votesFilename):
  282. # have we already voted on this?
  283. if messageId in open(votesFilename).read():
  284. print('Already voted on message ' + messageId)
  285. return
  286. print('Voting on message ' + messageId)
  287. print('Vote for: ' + answer)
  288. messageJson = \
  289. createPublicPost(self.server.baseDir,
  290. nickname,
  291. self.server.domain, self.server.port,
  292. self.server.httpPrefix,
  293. answer, False, False, False,
  294. None, None, None, True,
  295. messageId, messageId, None,
  296. False, None, None, None)
  297. if messageJson:
  298. # name field contains the answer
  299. messageJson['object']['name'] = answer
  300. if self._postToOutbox(messageJson, __version__, nickname):
  301. postFilename = \
  302. locatePost(self.server.baseDir, nickname,
  303. self.server.domain, messageId)
  304. if postFilename:
  305. postJsonObject = loadJson(postFilename)
  306. if postJsonObject:
  307. populateReplies(self.server.baseDir,
  308. self.server.httpPrefix,
  309. self.server.domainFull,
  310. postJsonObject,
  311. self.server.maxReplies,
  312. self.server.debug)
  313. # record the vote
  314. votesFile = open(votesFilename, 'a+')
  315. if votesFile:
  316. votesFile.write(messageId + '\n')
  317. votesFile.close()
  318. # ensure that the cached post is removed if it exists,
  319. # so that it then will be recreated
  320. cachedPostFilename = \
  321. getCachedPostFilename(self.server.baseDir,
  322. nickname,
  323. self.server.domain,
  324. postJsonObject)
  325. if cachedPostFilename:
  326. if os.path.isfile(cachedPostFilename):
  327. os.remove(cachedPostFilename)
  328. # remove from memory cache
  329. removePostFromCache(postJsonObject,
  330. self.server.recentPostsCache)
  331. else:
  332. print('ERROR: unable to post vote to outbox')
  333. else:
  334. print('ERROR: unable to create vote')
  335. def _removePostInteractions(self, postJsonObject: {}) -> None:
  336. """Removes potentially sensitive interactions from a post
  337. This is the type of thing which would be of interest to marketers
  338. or of saleable value to them. eg. Knowing who likes who or what.
  339. """
  340. if postJsonObject.get('likes'):
  341. postJsonObject['likes'] = {'items': []}
  342. if postJsonObject.get('shares'):
  343. postJsonObject['shares'] = {}
  344. if postJsonObject.get('replies'):
  345. postJsonObject['replies'] = {}
  346. if postJsonObject.get('bookmarks'):
  347. postJsonObject['bookmarks'] = {}
  348. if not postJsonObject.get('object'):
  349. return
  350. if not isinstance(postJsonObject['object'], dict):
  351. return
  352. if postJsonObject['object'].get('likes'):
  353. postJsonObject['object']['likes'] = {'items': []}
  354. if postJsonObject['object'].get('shares'):
  355. postJsonObject['object']['shares'] = {}
  356. if postJsonObject['object'].get('replies'):
  357. postJsonObject['object']['replies'] = {}
  358. if postJsonObject['object'].get('bookmarks'):
  359. postJsonObject['object']['bookmarks'] = {}
  360. def _requestHTTP(self) -> bool:
  361. """Should a http response be given?
  362. """
  363. if not self.headers.get('Accept'):
  364. return False
  365. if self.server.debug:
  366. print('ACCEPT: ' + self.headers['Accept'])
  367. if 'image/' in self.headers['Accept']:
  368. if 'text/html' not in self.headers['Accept']:
  369. return False
  370. if 'video/' in self.headers['Accept']:
  371. if 'text/html' not in self.headers['Accept']:
  372. return False
  373. if 'audio/' in self.headers['Accept']:
  374. if 'text/html' not in self.headers['Accept']:
  375. return False
  376. if self.headers['Accept'].startswith('*'):
  377. return False
  378. if 'json' in self.headers['Accept']:
  379. return False
  380. return True
  381. def _fetchAuthenticated(self) -> bool:
  382. """http authentication of GET requests for json
  383. """
  384. if not self.server.authenticatedFetch:
  385. return True
  386. # check that the headers are signed
  387. if not self.headers.get('signature'):
  388. if self.server.debug:
  389. print('WARN: authenticated fetch, ' +
  390. 'GET has no signature in headers')
  391. return False
  392. # get the keyId
  393. keyId = None
  394. signatureParams = self.headers['signature'].split(',')
  395. for signatureItem in signatureParams:
  396. if signatureItem.startswith('keyId='):
  397. if '"' in signatureItem:
  398. keyId = signatureItem.split('"')[1]
  399. break
  400. if not keyId:
  401. if self.server.debug:
  402. print('WARN: authenticated fetch, ' +
  403. 'failed to obtain keyId from signature')
  404. return False
  405. # is the keyId (actor) valid?
  406. if not urlPermitted(keyId, self.server.federationList, "inbox:read"):
  407. if self.server.debug:
  408. print('Authorized fetch failed: ' + keyId +
  409. ' is not permitted')
  410. return False
  411. # make sure we have a session
  412. if not self.server.session:
  413. print('DEBUG: creating new session during authenticated fetch')
  414. self.server.session = createSession(self.server.proxyType)
  415. if not self.server.session:
  416. print('ERROR: GET failed to create session during ' +
  417. 'authenticated fetch')
  418. return False
  419. # obtain the public key
  420. pubKey = \
  421. getPersonPubKey(self.server.baseDir, self.server.session, keyId,
  422. self.server.personCache, self.server.debug,
  423. __version__, self.server.httpPrefix,
  424. self.server.domain, self.server.onionDomain)
  425. if not pubKey:
  426. if self.server.debug:
  427. print('DEBUG: Authenticated fetch failed to ' +
  428. 'obtain public key for ' + keyId)
  429. return False
  430. # it is assumed that there will be no message body on
  431. # authenticated fetches and also consequently no digest
  432. GETrequestBody = ''
  433. GETrequestDigest = None
  434. # verify the GET request without any digest
  435. if verifyPostHeaders(self.server.httpPrefix,
  436. pubKey, self.headers,
  437. self.path, True,
  438. GETrequestDigest,
  439. GETrequestBody,
  440. self.server.debug):
  441. return True
  442. return False
  443. def _login_headers(self, fileFormat: str, length: int,
  444. callingDomain: str) -> None:
  445. self.send_response(200)
  446. self.send_header('Content-type', fileFormat)
  447. self.send_header('Content-Length', str(length))
  448. self.send_header('Host', callingDomain)
  449. self.send_header('WWW-Authenticate',
  450. 'title="Login to Epicyon", Basic realm="epicyon"')
  451. self.send_header('X-Robots-Tag', 'noindex')
  452. self.end_headers()
  453. def _logout_headers(self, fileFormat: str, length: int,
  454. callingDomain: str) -> None:
  455. self.send_response(200)
  456. self.send_header('Content-type', fileFormat)
  457. self.send_header('Content-Length', str(length))
  458. self.send_header('Set-Cookie', 'epicyon=; SameSite=Strict')
  459. self.send_header('Host', callingDomain)
  460. self.send_header('WWW-Authenticate',
  461. 'title="Login to Epicyon", Basic realm="epicyon"')
  462. self.send_header('X-Robots-Tag', 'noindex')
  463. self.end_headers()
  464. def _set_headers_base(self, fileFormat: str, length: int, cookie: str,
  465. callingDomain: str) -> None:
  466. self.send_response(200)
  467. self.send_header('Content-type', fileFormat)
  468. if length > -1:
  469. self.send_header('Content-Length', str(length))
  470. if cookie:
  471. cookieStr = cookie
  472. if 'HttpOnly;' not in cookieStr:
  473. if self.server.httpPrefix == 'https':
  474. cookieStr += '; Secure'
  475. cookieStr += '; HttpOnly; SameSite=Strict'
  476. self.send_header('Cookie', cookieStr)
  477. self.send_header('Host', callingDomain)
  478. self.send_header('InstanceID', self.server.instanceId)
  479. self.send_header('X-Robots-Tag', 'noindex')
  480. self.send_header('X-Clacks-Overhead', 'GNU Natalie Nguyen')
  481. self.send_header('Accept-Ranges', 'none')
  482. def _set_headers(self, fileFormat: str, length: int, cookie: str,
  483. callingDomain: str) -> None:
  484. self._set_headers_base(fileFormat, length, cookie, callingDomain)
  485. self.send_header('Cache-Control', 'public, max-age=0')
  486. self.end_headers()
  487. def _set_headers_head(self, fileFormat: str, length: int, etag: str,
  488. callingDomain: str) -> None:
  489. self._set_headers_base(fileFormat, length, None, callingDomain)
  490. if etag:
  491. self.send_header('ETag', etag)
  492. self.end_headers()
  493. def _set_headers_etag(self, mediaFilename: str, fileFormat: str,
  494. data, cookie: str, callingDomain: str) -> None:
  495. self._set_headers_base(fileFormat, len(data), cookie, callingDomain)
  496. self.send_header('Cache-Control', 'public, max-age=86400')
  497. etag = None
  498. if os.path.isfile(mediaFilename + '.etag'):
  499. try:
  500. with open(mediaFilename + '.etag', 'r') as etagFile:
  501. etag = etagFile.read()
  502. except BaseException:
  503. pass
  504. if not etag:
  505. etag = sha1(data).hexdigest() # nosec
  506. try:
  507. with open(mediaFilename + '.etag', 'w') as etagFile:
  508. etagFile.write(etag)
  509. except BaseException:
  510. pass
  511. if etag:
  512. self.send_header('ETag', etag)
  513. self.end_headers()
  514. def _etag_exists(self, mediaFilename: str) -> bool:
  515. """Does an etag header exist for the given file?
  516. """
  517. etagHeader = 'If-None-Match'
  518. if not self.headers.get(etagHeader):
  519. etagHeader = 'if-none-match'
  520. if not self.headers.get(etagHeader):
  521. etagHeader = 'If-none-match'
  522. if self.headers.get(etagHeader):
  523. oldEtag = self.headers['If-None-Match']
  524. if os.path.isfile(mediaFilename + '.etag'):
  525. # load the etag from file
  526. currEtag = ''
  527. try:
  528. with open(mediaFilename, 'r') as etagFile:
  529. currEtag = etagFile.read()
  530. except BaseException:
  531. pass
  532. if oldEtag == currEtag:
  533. # The file has not changed
  534. return True
  535. return False
  536. def _redirect_headers(self, redirect: str, cookie: str,
  537. callingDomain: str) -> None:
  538. if '://' not in redirect:
  539. print('REDIRECT ERROR: redirect is not an absolute url ' +
  540. redirect)
  541. self.send_response(303)
  542. if cookie:
  543. cookieStr = cookie.replace('SET:', '').strip()
  544. if 'HttpOnly;' not in cookieStr:
  545. if self.server.httpPrefix == 'https':
  546. cookieStr += '; Secure'
  547. cookieStr += '; HttpOnly; SameSite=Strict'
  548. if not cookie.startswith('SET:'):
  549. self.send_header('Cookie', cookieStr)
  550. else:
  551. self.send_header('Set-Cookie', cookieStr)
  552. self.send_header('Location', redirect)
  553. self.send_header('Host', callingDomain)
  554. self.send_header('InstanceID', self.server.instanceId)
  555. self.send_header('Content-Length', '0')
  556. self.send_header('X-Robots-Tag', 'noindex')
  557. self.end_headers()
  558. def _httpReturnCode(self, httpCode: int, httpDescription: str,
  559. longDescription: str) -> None:
  560. msg = \
  561. '<html><head><title>' + str(httpCode) + '</title></head>' \
  562. '<body bgcolor="linen" text="black">' \
  563. '<div style="font-size: 400px; ' \
  564. 'text-align: center;">' + str(httpCode) + '</div>' \
  565. '<div style="font-size: 128px; ' \
  566. 'text-align: center; font-variant: ' \
  567. 'small-caps;">' + httpDescription + '</div>' \
  568. '<div style="text-align: center;">' + longDescription + '</div>' \
  569. '</body></html>'
  570. msg = msg.encode('utf-8')
  571. self.send_response(httpCode)
  572. self.send_header('Content-Type', 'text/html; charset=utf-8')
  573. self.send_header('Content-Length', str(len(msg)))
  574. self.send_header('X-Robots-Tag', 'noindex')
  575. self.end_headers()
  576. try:
  577. self.wfile.write(msg)
  578. except Exception as e:
  579. print('Error when showing ' + str(httpCode))
  580. print(e)
  581. def _200(self) -> None:
  582. if self.server.translate:
  583. self._httpReturnCode(200, self.server.translate['Ok'],
  584. self.server.translate['This is nothing ' +
  585. 'less than an utter ' +
  586. 'triumph'])
  587. else:
  588. self._httpReturnCode(200, 'Ok',
  589. 'This is nothing less ' +
  590. 'than an utter triumph')
  591. def _404(self) -> None:
  592. if self.server.translate:
  593. self._httpReturnCode(404, self.server.translate['Not Found'],
  594. self.server.translate['These are not the ' +
  595. 'droids you are ' +
  596. 'looking for'])
  597. else:
  598. self._httpReturnCode(404, 'Not Found',
  599. 'These are not the ' +
  600. 'droids you are ' +
  601. 'looking for')
  602. def _304(self) -> None:
  603. if self.server.translate:
  604. self._httpReturnCode(304, self.server.translate['Not changed'],
  605. self.server.translate['The contents of ' +
  606. 'your local cache ' +
  607. 'are up to date'])
  608. else:
  609. self._httpReturnCode(304, 'Not changed',
  610. 'The contents of ' +
  611. 'your local cache ' +
  612. 'are up to date')
  613. def _400(self) -> None:
  614. if self.server.translate:
  615. self._httpReturnCode(400, self.server.translate['Bad Request'],
  616. self.server.translate['Better luck ' +
  617. 'next time'])
  618. else:
  619. self._httpReturnCode(400, 'Bad Request',
  620. 'Better luck next time')
  621. def _503(self) -> None:
  622. if self.server.translate:
  623. self._httpReturnCode(503, self.server.translate['Unavailable'],
  624. self.server.translate['The server is busy. ' +
  625. 'Please try again ' +
  626. 'later'])
  627. else:
  628. self._httpReturnCode(503, 'Unavailable',
  629. 'The server is busy. Please try again ' +
  630. 'later')
  631. def _write(self, msg) -> None:
  632. tries = 0
  633. while tries < 5:
  634. try:
  635. self.wfile.write(msg)
  636. break
  637. except Exception as e:
  638. print(e)
  639. time.sleep(1)
  640. tries += 1
  641. def _robotsTxt(self) -> bool:
  642. if not self.path.lower().startswith('/robot'):
  643. return False
  644. msg = 'User-agent: *\nDisallow: /'
  645. msg = msg.encode('utf-8')
  646. self._set_headers('text/plain; charset=utf-8', len(msg),
  647. None, self.server.domainFull)
  648. self._write(msg)
  649. return True
  650. def _hasAccept(self, callingDomain: str) -> bool:
  651. if self.headers.get('Accept') or callingDomain.endswith('.b32.i2p'):
  652. if not self.headers.get('Accept'):
  653. self.headers['Accept'] = \
  654. 'text/html,application/xhtml+xml,' \
  655. 'application/xml;q=0.9,image/webp,*/*;q=0.8'
  656. return True
  657. return False
  658. def _mastoApi(self, callingDomain: str) -> bool:
  659. """This is a vestigil mastodon API for the purpose
  660. of returning an empty result to sites like
  661. https://mastopeek.app-dist.eu
  662. """
  663. if not self.path.startswith('/api/v1/'):
  664. return False
  665. if self.server.debug:
  666. print('DEBUG: mastodon api ' + self.path)
  667. if self.path == '/api/v1/instance':
  668. adminNickname = getConfigParam(self.server.baseDir, 'admin')
  669. instanceDescriptionShort = \
  670. getConfigParam(self.server.baseDir,
  671. 'instanceDescriptionShort')
  672. instanceDescription = getConfigParam(self.server.baseDir,
  673. 'instanceDescription')
  674. instanceTitle = getConfigParam(self.server.baseDir,
  675. 'instanceTitle')
  676. instanceJson = \
  677. metaDataInstance(instanceTitle,
  678. instanceDescriptionShort,
  679. instanceDescription,
  680. self.server.httpPrefix,
  681. self.server.baseDir,
  682. adminNickname,
  683. self.server.domain,
  684. self.server.domainFull,
  685. self.server.registration,
  686. self.server.systemLanguage,
  687. self.server.projectVersion)
  688. msg = json.dumps(instanceJson).encode('utf-8')
  689. if self._hasAccept(callingDomain):
  690. if 'application/ld+json' in self.headers['Accept']:
  691. self._set_headers('application/ld+json', len(msg),
  692. None, callingDomain)
  693. else:
  694. self._set_headers('application/json', len(msg),
  695. None, callingDomain)
  696. else:
  697. self._set_headers('application/ld+json', len(msg),
  698. None, callingDomain)
  699. self._write(msg)
  700. print('instance metadata sent')
  701. return True
  702. if self.path.startswith('/api/v1/instance/peers'):
  703. # This is just a dummy result.
  704. # Showing the full list of peers would have privacy implications.
  705. # On a large instance you are somewhat lost in the crowd, but on
  706. # small instances a full list of peers would convey a lot of
  707. # information about the interests of a small number of accounts
  708. msg = json.dumps(['mastodon.social',
  709. self.server.domainFull]).encode('utf-8')
  710. if self._hasAccept(callingDomain):
  711. if 'application/ld+json' in self.headers['Accept']:
  712. self._set_headers('application/ld+json', len(msg),
  713. None, callingDomain)
  714. else:
  715. self._set_headers('application/json', len(msg),
  716. None, callingDomain)
  717. else:
  718. self._set_headers('application/ld+json', len(msg),
  719. None, callingDomain)
  720. self._write(msg)
  721. print('instance peers metadata sent')
  722. return True
  723. if self.path.startswith('/api/v1/instance/activity'):
  724. # This is just a dummy result.
  725. msg = json.dumps([]).encode('utf-8')
  726. if self._hasAccept(callingDomain):
  727. if 'application/ld+json' in self.headers['Accept']:
  728. self._set_headers('application/ld+json', len(msg),
  729. None, callingDomain)
  730. else:
  731. self._set_headers('application/json', len(msg),
  732. None, callingDomain)
  733. else:
  734. self._set_headers('application/ld+json', len(msg),
  735. None, callingDomain)
  736. self._write(msg)
  737. print('instance activity metadata sent')
  738. return True
  739. self._404()
  740. return True
  741. def _nodeinfo(self, callingDomain: str) -> bool:
  742. if not self.path.startswith('/nodeinfo/2.0'):
  743. return False
  744. if self.server.debug:
  745. print('DEBUG: nodeinfo ' + self.path)
  746. info = metaDataNodeInfo(self.server.baseDir,
  747. self.server.registration,
  748. self.server.projectVersion)
  749. if info:
  750. msg = json.dumps(info).encode('utf-8')
  751. if self._hasAccept(callingDomain):
  752. if 'application/ld+json' in self.headers['Accept']:
  753. self._set_headers('application/ld+json', len(msg),
  754. None, callingDomain)
  755. else:
  756. self._set_headers('application/json', len(msg),
  757. None, callingDomain)
  758. else:
  759. self._set_headers('application/ld+json', len(msg),
  760. None, callingDomain)
  761. self._write(msg)
  762. print('nodeinfo sent')
  763. return True
  764. self._404()
  765. return True
  766. def _webfinger(self, callingDomain: str) -> bool:
  767. if not self.path.startswith('/.well-known'):
  768. return False
  769. if self.server.debug:
  770. print('DEBUG: WEBFINGER well-known')
  771. if self.server.debug:
  772. print('DEBUG: WEBFINGER host-meta')
  773. if self.path.startswith('/.well-known/host-meta'):
  774. if callingDomain.endswith('.onion') and \
  775. self.server.onionDomain:
  776. wfResult = \
  777. webfingerMeta('http', self.server.onionDomain)
  778. elif (callingDomain.endswith('.i2p') and
  779. self.server.i2pDomain):
  780. wfResult = \
  781. webfingerMeta('http', self.server.i2pDomain)
  782. else:
  783. wfResult = \
  784. webfingerMeta(self.server.httpPrefix,
  785. self.server.domainFull)
  786. if wfResult:
  787. msg = wfResult.encode('utf-8')
  788. self._set_headers('application/xrd+xml', len(msg),
  789. None, callingDomain)
  790. self._write(msg)
  791. return True
  792. self._404()
  793. return True
  794. if self.path.startswith('/.well-known/nodeinfo'):
  795. if callingDomain.endswith('.onion') and \
  796. self.server.onionDomain:
  797. wfResult = \
  798. webfingerNodeInfo('http', self.server.onionDomain)
  799. elif (callingDomain.endswith('.i2p') and
  800. self.server.i2pDomain):
  801. wfResult = \
  802. webfingerNodeInfo('http', self.server.i2pDomain)
  803. else:
  804. wfResult = \
  805. webfingerNodeInfo(self.server.httpPrefix,
  806. self.server.domainFull)
  807. if wfResult:
  808. msg = json.dumps(wfResult).encode('utf-8')
  809. if self._hasAccept(callingDomain):
  810. if 'application/ld+json' in self.headers['Accept']:
  811. self._set_headers('application/ld+json', len(msg),
  812. None, callingDomain)
  813. else:
  814. self._set_headers('application/json', len(msg),
  815. None, callingDomain)
  816. else:
  817. self._set_headers('application/ld+json', len(msg),
  818. None, callingDomain)
  819. self._write(msg)
  820. return True
  821. self._404()
  822. return True
  823. if self.server.debug:
  824. print('DEBUG: WEBFINGER lookup ' + self.path + ' ' +
  825. str(self.server.baseDir))
  826. wfResult = \
  827. webfingerLookup(self.path, self.server.baseDir,
  828. self.server.domain, self.server.onionDomain,
  829. self.server.port, self.server.debug)
  830. if wfResult:
  831. msg = json.dumps(wfResult).encode('utf-8')
  832. self._set_headers('application/jrd+json', len(msg),
  833. None, callingDomain)
  834. self._write(msg)
  835. else:
  836. if self.server.debug:
  837. print('DEBUG: WEBFINGER lookup 404 ' + self.path)
  838. self._404()
  839. return True
  840. def _permittedDir(self, path: str) -> bool:
  841. """These are special paths which should not be accessible
  842. directly via GET or POST
  843. """
  844. if path.startswith('/wfendpoints') or \
  845. path.startswith('/keys') or \
  846. path.startswith('/accounts'):
  847. return False
  848. return True
  849. def _postToOutbox(self, messageJson: {}, version: str,
  850. postToNickname=None) -> bool:
  851. """post is received by the outbox
  852. Client to server message post
  853. https://www.w3.org/TR/activitypub/#client-to-server-outbox-delivery
  854. """
  855. if postToNickname:
  856. print('Posting to nickname ' + postToNickname)
  857. self.postToNickname = postToNickname
  858. return postMessageToOutbox(messageJson, self.postToNickname,
  859. self.server, self.server.baseDir,
  860. self.server.httpPrefix,
  861. self.server.domain,
  862. self.server.domainFull,
  863. self.server.onionDomain,
  864. self.server.i2pDomain,
  865. self.server.port,
  866. self.server.recentPostsCache,
  867. self.server.followersThreads,
  868. self.server.federationList,
  869. self.server.sendThreads,
  870. self.server.postLog,
  871. self.server.cachedWebfingers,
  872. self.server.personCache,
  873. self.server.allowDeletion,
  874. self.server.proxyType, version,
  875. self.server.debug)
  876. def _postToOutboxThread(self, messageJson: {}) -> bool:
  877. """Creates a thread to send a post
  878. """
  879. accountOutboxThreadName = self.postToNickname
  880. if not accountOutboxThreadName:
  881. accountOutboxThreadName = '*'
  882. if self.server.outboxThread.get(accountOutboxThreadName):
  883. print('Waiting for previous outbox thread to end')
  884. waitCtr = 0
  885. thName = accountOutboxThreadName
  886. while self.server.outboxThread[thName].isAlive() and waitCtr < 8:
  887. time.sleep(1)
  888. waitCtr += 1
  889. if waitCtr >= 8:
  890. self.server.outboxThread[accountOutboxThreadName].kill()
  891. print('Creating outbox thread')
  892. self.server.outboxThread[accountOutboxThreadName] = \
  893. threadWithTrace(target=self._postToOutbox,
  894. args=(messageJson.copy(), __version__),
  895. daemon=True)
  896. print('Starting outbox thread')
  897. self.server.outboxThread[accountOutboxThreadName].start()
  898. return True
  899. def _updateInboxQueue(self, nickname: str, messageJson: {},
  900. messageBytes: str) -> int:
  901. """Update the inbox queue
  902. """
  903. if self.server.restartInboxQueueInProgress:
  904. self._503()
  905. print('Message arrrived but currently restarting inbox queue')
  906. self.server.POSTbusy = False
  907. return 2
  908. # check for blocked domains so that they can be rejected early
  909. messageDomain = None
  910. if messageJson.get('actor'):
  911. messageDomain, messagePort = \
  912. getDomainFromActor(messageJson['actor'])
  913. if isBlockedDomain(self.server.baseDir, messageDomain):
  914. print('POST from blocked domain ' + messageDomain)
  915. self._400()
  916. self.server.POSTbusy = False
  917. return 3
  918. # if the inbox queue is full then return a busy code
  919. if len(self.server.inboxQueue) >= self.server.maxQueueLength:
  920. if messageDomain:
  921. print('Queue: Inbox queue is full. Incoming post from ' +
  922. messageJson['actor'])
  923. else:
  924. print('Queue: Inbox queue is full')
  925. self._503()
  926. clearQueueItems(self.server.baseDir, self.server.inboxQueue)
  927. if not self.server.restartInboxQueueInProgress:
  928. self.server.restartInboxQueue = True
  929. self.server.POSTbusy = False
  930. return 2
  931. # Convert the headers needed for signature verification to dict
  932. headersDict = {}
  933. headersDict['host'] = self.headers['host']
  934. headersDict['signature'] = self.headers['signature']
  935. if self.headers.get('Date'):
  936. headersDict['Date'] = self.headers['Date']
  937. if self.headers.get('digest'):
  938. headersDict['digest'] = self.headers['digest']
  939. if self.headers.get('Content-type'):
  940. headersDict['Content-type'] = self.headers['Content-type']
  941. if self.headers.get('Content-Length'):
  942. headersDict['Content-Length'] = self.headers['Content-Length']
  943. elif self.headers.get('content-length'):
  944. headersDict['content-length'] = self.headers['content-length']
  945. # For follow activities add a 'to' field, which is a copy
  946. # of the object field
  947. messageJson, toFieldExists = \
  948. addToField('Follow', messageJson, self.server.debug)
  949. # For like activities add a 'to' field, which is a copy of
  950. # the actor within the object field
  951. messageJson, toFieldExists = \
  952. addToField('Like', messageJson, self.server.debug)
  953. beginSaveTime = time.time()
  954. # save the json for later queue processing
  955. queueFilename = \
  956. savePostToInboxQueue(self.server.baseDir,
  957. self.server.httpPrefix,
  958. nickname,
  959. self.server.domainFull,
  960. messageJson,
  961. messageBytes.decode('utf-8'),
  962. headersDict,
  963. self.path,
  964. self.server.debug)
  965. if queueFilename:
  966. # add json to the queue
  967. if queueFilename not in self.server.inboxQueue:
  968. self.server.inboxQueue.append(queueFilename)
  969. if self.server.debug:
  970. timeDiff = int((time.time() - beginSaveTime) * 1000)
  971. if timeDiff > 200:
  972. print('SLOW: slow save of inbox queue item ' +
  973. queueFilename + ' took ' + str(timeDiff) + ' mS')
  974. self.send_response(201)
  975. self.end_headers()
  976. self.server.POSTbusy = False
  977. return 0
  978. self._503()
  979. self.server.POSTbusy = False
  980. return 1
  981. def _isAuthorized(self) -> bool:
  982. if self.path.startswith('/icons/') or \
  983. self.path.startswith('/avatars/') or \
  984. self.path.startswith('/favicon.ico'):
  985. return False
  986. # token based authenticated used by the web interface
  987. if self.headers.get('Cookie'):
  988. if self.headers['Cookie'].startswith('epicyon='):
  989. tokenStr = self.headers['Cookie'].split('=', 1)[1].strip()
  990. if ';' in tokenStr:
  991. tokenStr = tokenStr.split(';')[0].strip()
  992. if self.server.tokensLookup.get(tokenStr):
  993. nickname = self.server.tokensLookup[tokenStr]
  994. # default to the inbox of the person
  995. if self.path == '/':
  996. self.path = '/users/' + nickname + '/inbox'
  997. # check that the path contains the same nickname
  998. # as the cookie otherwise it would be possible
  999. # to be authorized to use an account you don't own
  1000. if '/' + nickname + '/' in self.path:
  1001. return True
  1002. if '/' + nickname + '?' in self.path:
  1003. return True
  1004. if self.path.endswith('/'+nickname):
  1005. return True
  1006. print('AUTH: nickname ' + nickname +
  1007. ' was not found in path ' + self.path)
  1008. return False
  1009. if self.server.debug:
  1010. print('AUTH: epicyon cookie ' +
  1011. 'authorization failed, header=' +
  1012. self.headers['Cookie'].replace('epicyon=', '') +
  1013. ' tokenStr=' + tokenStr + ' tokens=' +
  1014. str(self.server.tokensLookup))
  1015. return False
  1016. print('AUTH: Header cookie was not authorized')
  1017. return False
  1018. # basic auth
  1019. if self.headers.get('Authorization'):
  1020. if authorize(self.server.baseDir, self.path,
  1021. self.headers['Authorization'],
  1022. self.server.debug):
  1023. return True
  1024. print('AUTH: Basic auth did not authorize ' +
  1025. self.headers['Authorization'])
  1026. return False
  1027. def _clearLoginDetails(self, nickname: str, callingDomain: str):
  1028. """Clears login details for the given account
  1029. """
  1030. # remove any token
  1031. if self.server.tokens.get(nickname):
  1032. del self.server.tokensLookup[self.server.tokens[nickname]]
  1033. del self.server.tokens[nickname]
  1034. self._redirect_headers(self.server.httpPrefix + '://' +
  1035. self.server.domainFull + '/login',
  1036. 'epicyon=; SameSite=Strict',
  1037. callingDomain)
  1038. def _benchmarkGETtimings(self, GETstartTime, GETtimings: [], getID: int):
  1039. """Updates a list containing how long each segment of GET takes
  1040. """
  1041. if self.server.debug:
  1042. timeDiff = int((time.time() - GETstartTime) * 1000)
  1043. logEvent = False
  1044. if timeDiff > 100:
  1045. logEvent = True
  1046. if GETtimings:
  1047. timeDiff = int(timeDiff - int(GETtimings[-1]))
  1048. GETtimings.append(str(timeDiff))
  1049. if logEvent:
  1050. ctr = 1
  1051. for timeDiff in GETtimings:
  1052. print('GET TIMING|' + str(ctr) + '|' + timeDiff)
  1053. ctr += 1
  1054. def _benchmarkPOSTtimings(self, POSTstartTime, POSTtimings: [],
  1055. postID: int):
  1056. """Updates a list containing how long each segment of POST takes
  1057. """
  1058. if self.server.debug:
  1059. timeDiff = int((time.time() - POSTstartTime) * 1000)
  1060. logEvent = False
  1061. if timeDiff > 100:
  1062. logEvent = True
  1063. if POSTtimings:
  1064. timeDiff = int(timeDiff - int(POSTtimings[-1]))
  1065. POSTtimings.append(str(timeDiff))
  1066. if logEvent:
  1067. ctr = 1
  1068. for timeDiff in POSTtimings:
  1069. print('POST TIMING|' + str(ctr) + '|' + timeDiff)
  1070. ctr += 1
  1071. def _pathContainsBlogLink(self, baseDir: str,
  1072. httpPrefix: str, domain: str,
  1073. domainFull: str, path: str) -> (str, str):
  1074. """If the path contains a blog entry then return its filename
  1075. """
  1076. if '/users/' not in path:
  1077. return None, None
  1078. userEnding = path.split('/users/', 1)[1]
  1079. if '/' not in userEnding:
  1080. return None, None
  1081. userEnding2 = userEnding.split('/')
  1082. nickname = userEnding2[0]
  1083. if len(userEnding2) != 2:
  1084. return None, None
  1085. if len(userEnding2[1]) < 14:
  1086. return None, None
  1087. userEnding2[1] = userEnding2[1].strip()
  1088. if not userEnding2[1].isdigit():
  1089. return None, None
  1090. # check for blog posts
  1091. blogIndexFilename = baseDir + '/accounts/' + \
  1092. nickname + '@' + domain + '/tlblogs.index'
  1093. if not os.path.isfile(blogIndexFilename):
  1094. return None, None
  1095. if '#' + userEnding2[1] + '.' not in open(blogIndexFilename).read():
  1096. return None, None
  1097. messageId = httpPrefix + '://' + domainFull + \
  1098. '/users/' + nickname + '/statuses/' + userEnding2[1]
  1099. return locatePost(baseDir, nickname, domain, messageId), nickname
  1100. def do_GET(self):
  1101. callingDomain = self.server.domainFull
  1102. if self.headers.get('Host'):
  1103. callingDomain = self.headers['Host']
  1104. if self.server.onionDomain:
  1105. if callingDomain != self.server.domain and \
  1106. callingDomain != self.server.domainFull and \
  1107. callingDomain != self.server.onionDomain:
  1108. print('GET domain blocked: ' + callingDomain)
  1109. self._400()
  1110. return
  1111. else:
  1112. if callingDomain != self.server.domain and \
  1113. callingDomain != self.server.domainFull:
  1114. print('GET domain blocked: ' + callingDomain)
  1115. self._400()
  1116. return
  1117. GETstartTime = time.time()
  1118. GETtimings = []
  1119. # Since fediverse crawlers are quite active,
  1120. # make returning info to them high priority
  1121. # get nodeinfo endpoint
  1122. if self._nodeinfo(callingDomain):
  1123. return
  1124. self._benchmarkGETtimings(GETstartTime, GETtimings, 1)
  1125. # minimal mastodon api
  1126. if self._mastoApi(callingDomain):
  1127. return
  1128. self._benchmarkGETtimings(GETstartTime, GETtimings, 2)
  1129. if self.path == '/logout':
  1130. msg = \
  1131. htmlLogin(self.server.translate,
  1132. self.server.baseDir, False).encode('utf-8')
  1133. self._logout_headers('text/html', len(msg), callingDomain)
  1134. self._write(msg)
  1135. return
  1136. self._benchmarkGETtimings(GETstartTime, GETtimings, 3)
  1137. # replace https://domain/@nick with https://domain/users/nick
  1138. if self.path.startswith('/@'):
  1139. self.path = self.path.replace('/@', '/users/')
  1140. # redirect music to #nowplaying list
  1141. if self.path == '/music' or self.path == '/nowplaying':
  1142. self.path = '/tags/nowplaying'
  1143. if self.server.debug:
  1144. print('DEBUG: GET from ' + self.server.baseDir +
  1145. ' path: ' + self.path + ' busy: ' +
  1146. str(self.server.GETbusy))
  1147. if self.server.debug:
  1148. print(str(self.headers))
  1149. cookie = None
  1150. if self.headers.get('Cookie'):
  1151. cookie = self.headers['Cookie']
  1152. self._benchmarkGETtimings(GETstartTime, GETtimings, 4)
  1153. # favicon image
  1154. if 'favicon.ico' in self.path:
  1155. favType = 'image/x-icon'
  1156. favFilename = 'favicon.ico'
  1157. if self._hasAccept(callingDomain):
  1158. if 'image/webp' in self.headers['Accept']:
  1159. favType = 'image/webp'
  1160. favFilename = 'favicon.webp'
  1161. # custom favicon
  1162. faviconFilename = \
  1163. self.server.baseDir + '/' + favFilename
  1164. if not os.path.isfile(faviconFilename):
  1165. # default favicon
  1166. faviconFilename = \
  1167. self.server.baseDir + '/img/icons/' + favFilename
  1168. if self._etag_exists(faviconFilename):
  1169. # The file has not changed
  1170. self._304()
  1171. return
  1172. if self.server.iconsCache.get(favFilename):
  1173. favBinary = self.server.iconsCache[favFilename]
  1174. self._set_headers_etag(faviconFilename,
  1175. favType,
  1176. favBinary, cookie,
  1177. callingDomain)
  1178. self._write(favBinary)
  1179. return
  1180. else:
  1181. if os.path.isfile(faviconFilename):
  1182. with open(faviconFilename, 'rb') as favFile:
  1183. favBinary = favFile.read()
  1184. self._set_headers_etag(faviconFilename,
  1185. favType,
  1186. favBinary, cookie,
  1187. callingDomain)
  1188. self._write(favBinary)
  1189. self.server.iconsCache[favFilename] = favBinary
  1190. return
  1191. self._404()
  1192. return
  1193. # check authorization
  1194. authorized = self._isAuthorized()
  1195. if self.server.debug:
  1196. if authorized:
  1197. print('GET Authorization granted')
  1198. else:
  1199. print('GET Not authorized')
  1200. self._benchmarkGETtimings(GETstartTime, GETtimings, 5)
  1201. if not self.server.session:
  1202. print('Starting new session during GET')
  1203. self.server.session = createSession(self.server.proxyType)
  1204. if not self.server.session:
  1205. print('ERROR: GET failed to create session duing GET')
  1206. self._404()
  1207. return
  1208. self._benchmarkGETtimings(GETstartTime, GETtimings, 6)
  1209. # is this a html request?
  1210. htmlGET = False
  1211. if self._hasAccept(callingDomain):
  1212. if self._requestHTTP():
  1213. htmlGET = True
  1214. else:
  1215. if self.headers.get('Connection'):
  1216. # https://developer.mozilla.org/en-US/
  1217. # docs/Web/HTTP/Protocol_upgrade_mechanism
  1218. if self.headers.get('Upgrade'):
  1219. print('HTTP Connection request: ' +
  1220. self.headers['Upgrade'])
  1221. else:
  1222. print('HTTP Connection request: ' +
  1223. self.headers['Connection'])
  1224. self._200()
  1225. else:
  1226. print('WARN: No Accept header ' + str(self.headers))
  1227. self._400()
  1228. return
  1229. # get fonts
  1230. if htmlGET and '/fonts/' in self.path:
  1231. fontStr = self.path.split('/fonts/')[1]
  1232. if fontStr.endswith('.otf') or \
  1233. fontStr.endswith('.ttf') or \
  1234. fontStr.endswith('.woff') or \
  1235. fontStr.endswith('.woff2'):
  1236. if fontStr.endswith('.otf'):
  1237. fontType = 'font/otf'
  1238. elif fontStr.endswith('.ttf'):
  1239. fontType = 'font/ttf'
  1240. elif fontStr.endswith('.woff'):
  1241. fontType = 'font/woff'
  1242. else:
  1243. fontType = 'font/woff2'
  1244. fontFilename = \
  1245. self.server.baseDir + '/fonts/' + fontStr
  1246. if self._etag_exists(fontFilename):
  1247. # The file has not changed
  1248. self._304()
  1249. return
  1250. if self.server.fontsCache.get(fontStr):
  1251. fontBinary = self.server.fontsCache[fontStr]
  1252. self._set_headers_etag(fontFilename,
  1253. fontType,
  1254. fontBinary, cookie,
  1255. callingDomain)
  1256. self._write(fontBinary)
  1257. return
  1258. else:
  1259. if os.path.isfile(fontFilename):
  1260. with open(fontFilename, 'rb') as avFile:
  1261. fontBinary = avFile.read()
  1262. self._set_headers_etag(fontFilename,
  1263. fontType,
  1264. fontBinary, cookie,
  1265. callingDomain)
  1266. self._write(fontBinary)
  1267. self.server.fontsCache[fontStr] = fontBinary
  1268. return
  1269. self._404()
  1270. return
  1271. self._benchmarkGETtimings(GETstartTime, GETtimings, 7)
  1272. # treat shared inbox paths consistently
  1273. if self.path == '/sharedInbox' or \
  1274. self.path == '/users/inbox' or \
  1275. self.path == '/actor/inbox' or \
  1276. self.path == '/users/'+self.server.domain:
  1277. # if shared inbox is not enabled
  1278. if not self.server.enableSharedInbox:
  1279. self._503()
  1280. return
  1281. self.path = '/inbox'
  1282. self._benchmarkGETtimings(GETstartTime, GETtimings, 8)
  1283. # RSS 2.0
  1284. if self.path.startswith('/blog/') and \
  1285. self.path.endswith('/rss.xml'):
  1286. nickname = self.path.split('/blog/')[1]
  1287. if '/' in nickname:
  1288. nickname = nickname.split('/')[0]
  1289. if not nickname.startswith('rss.'):
  1290. if os.path.isdir(self.server.baseDir +
  1291. '/accounts/' + nickname +
  1292. '@' + self.server.domain):
  1293. if not self.server.session:
  1294. print('Starting new session during RSS request')
  1295. self.server.session = \
  1296. createSession(self.server.proxyType)
  1297. if not self.server.session:
  1298. print('ERROR: GET failed to create session ' +
  1299. 'during RSS request')
  1300. self._404()
  1301. return
  1302. msg = \
  1303. htmlBlogPageRSS2(authorized,
  1304. self.server.session,
  1305. self.server.baseDir,
  1306. self.server.httpPrefix,
  1307. self.server.translate,
  1308. nickname,
  1309. self.server.domain,
  1310. self.server.port,
  1311. maxPostsInRSSFeed, 1)
  1312. if msg is not None:
  1313. msg = msg.encode('utf-8')
  1314. self._set_headers('text/xml', len(msg),
  1315. cookie, callingDomain)
  1316. self._write(msg)
  1317. return
  1318. self._404()
  1319. return
  1320. # RSS 3.0
  1321. if self.path.startswith('/blog/') and \
  1322. self.path.endswith('/rss.txt'):
  1323. nickname = self.path.split('/blog/')[1]
  1324. if '/' in nickname:
  1325. nickname = nickname.split('/')[0]
  1326. if not nickname.startswith('rss.'):
  1327. if os.path.isdir(self.server.baseDir +
  1328. '/accounts/' + nickname +
  1329. '@' + self.server.domain):
  1330. if not self.server.session:
  1331. print('Starting new session during RSS3 request')
  1332. self.server.session = \
  1333. createSession(self.server.proxyType)
  1334. if not self.server.session:
  1335. print('ERROR: GET failed to create session ' +
  1336. 'during RSS3 request')
  1337. self._404()
  1338. return
  1339. msg = \
  1340. htmlBlogPageRSS3(authorized,
  1341. self.server.session,
  1342. self.server.baseDir,
  1343. self.server.httpPrefix,
  1344. self.server.translate,
  1345. nickname,
  1346. self.server.domain,
  1347. self.server.port,
  1348. maxPostsInRSSFeed, 1)
  1349. if msg is not None:
  1350. msg = msg.encode('utf-8')
  1351. self._set_headers('text/plain; charset=utf-8',
  1352. len(msg), cookie, callingDomain)
  1353. self._write(msg)
  1354. return
  1355. self._404()
  1356. return
  1357. # show the main blog page
  1358. if htmlGET and (self.path == '/blog' or
  1359. self.path == '/blog/' or
  1360. self.path == '/blogs' or
  1361. self.path == '/blogs/'):
  1362. if '/rss.xml' not in self.path:
  1363. if not self.server.session:
  1364. print('Starting new session during blog view')
  1365. self.server.session = \
  1366. createSession(self.server.proxyType)
  1367. if not self.server.session:
  1368. print('ERROR: GET failed to create session ' +
  1369. 'during blog view')
  1370. self._404()
  1371. return
  1372. msg = htmlBlogView(authorized,
  1373. self.server.session,
  1374. self.server.baseDir,
  1375. self.server.httpPrefix,
  1376. self.server.translate,
  1377. self.server.domain,
  1378. self.server.port,
  1379. maxPostsInBlogsFeed)
  1380. if msg is not None:
  1381. msg = msg.encode('utf-8')
  1382. self._set_headers('text/html', len(msg),
  1383. cookie, callingDomain)
  1384. self._write(msg)
  1385. return
  1386. self._404()
  1387. return
  1388. # show a particular page of blog entries
  1389. # for a particular account
  1390. if htmlGET and self.path.startswith('/blog/'):
  1391. if '/rss.xml' not in self.path:
  1392. pageNumber = 1
  1393. nickname = self.path.split('/blog/')[1]
  1394. if '/' in nickname:
  1395. nickname = nickname.split('/')[0]
  1396. if '?' in nickname:
  1397. nickname = nickname.split('?')[0]
  1398. if '?page=' in self.path:
  1399. pageNumberStr = self.path.split('?page=')[1]
  1400. if '?' in pageNumberStr:
  1401. pageNumberStr = pageNumberStr.split('?')[0]
  1402. if '#' in pageNumberStr:
  1403. pageNumberStr = pageNumberStr.split('#')[0]
  1404. if pageNumberStr.isdigit():
  1405. pageNumber = int(pageNumberStr)
  1406. if pageNumber < 1:
  1407. pageNumber = 1
  1408. elif pageNumber > 10:
  1409. pageNumber = 10
  1410. if not self.server.session:
  1411. print('Starting new session during blog page')
  1412. self.server.session = \
  1413. createSession(self.server.proxyType)
  1414. if not self.server.session:
  1415. print('ERROR: GET failed to create session ' +
  1416. 'during blog page')
  1417. self._404()
  1418. return
  1419. msg = htmlBlogPage(authorized,
  1420. self.server.session,
  1421. self.server.baseDir,
  1422. self.server.httpPrefix,
  1423. self.server.translate,
  1424. nickname,
  1425. self.server.domain, self.server.port,
  1426. maxPostsInBlogsFeed, pageNumber)
  1427. if msg is not None:
  1428. msg = msg.encode('utf-8')
  1429. self._set_headers('text/html', len(msg),
  1430. cookie, callingDomain)
  1431. self._write(msg)
  1432. return
  1433. self._404()
  1434. return
  1435. if htmlGET and '/users/' in self.path:
  1436. # show the person options screen with view/follow/block/report
  1437. if '?options=' in self.path:
  1438. optionsStr = self.path.split('?options=')[1]
  1439. originPathStr = self.path.split('?options=')[0]
  1440. if ';' in optionsStr:
  1441. pageNumber = 1
  1442. optionsList = optionsStr.split(';')
  1443. optionsActor = optionsList[0]
  1444. optionsPageNumber = optionsList[1]
  1445. optionsProfileUrl = optionsList[2]
  1446. if optionsPageNumber.isdigit():
  1447. pageNumber = int(optionsPageNumber)
  1448. optionsLink = None
  1449. if len(optionsList) > 3:
  1450. optionsLink = optionsList[3]
  1451. donateUrl = None
  1452. PGPpubKey = None
  1453. PGPfingerprint = None
  1454. xmppAddress = None
  1455. matrixAddress = None
  1456. blogAddress = None
  1457. toxAddress = None
  1458. ssbAddress = None
  1459. emailAddress = None
  1460. actorJson = getPersonFromCache(self.server.baseDir,
  1461. optionsActor,
  1462. self.server.personCache)
  1463. if actorJson:
  1464. donateUrl = getDonationUrl(actorJson)
  1465. xmppAddress = getXmppAddress(actorJson)
  1466. matrixAddress = getMatrixAddress(actorJson)
  1467. ssbAddress = getSSBAddress(actorJson)
  1468. blogAddress = getBlogAddress(actorJson)
  1469. toxAddress = getToxAddress(actorJson)
  1470. emailAddress = getEmailAddress(actorJson)
  1471. PGPpubKey = getPGPpubKey(actorJson)
  1472. PGPfingerprint = getPGPfingerprint(actorJson)
  1473. msg = htmlPersonOptions(self.server.translate,
  1474. self.server.baseDir,
  1475. self.server.domain,
  1476. originPathStr,
  1477. optionsActor,
  1478. optionsProfileUrl,
  1479. optionsLink,
  1480. pageNumber, donateUrl,
  1481. xmppAddress, matrixAddress,
  1482. ssbAddress, blogAddress,
  1483. toxAddress,
  1484. PGPpubKey, PGPfingerprint,
  1485. emailAddress).encode('utf-8')
  1486. self._set_headers('text/html', len(msg),
  1487. cookie, callingDomain)
  1488. self._write(msg)
  1489. return
  1490. if callingDomain.endswith('.onion') and \
  1491. self.server.onionDomain:
  1492. originPathStrAbsolute = \
  1493. 'http://' + self.server.onionDomain + originPathStr
  1494. elif (callingDomain.endswith('.i2p') and
  1495. self.server.i2pDomain):
  1496. originPathStrAbsolute = \
  1497. 'http://' + self.server.i2pDomain + originPathStr
  1498. else:
  1499. originPathStrAbsolute = \
  1500. self.server.httpPrefix + '://' + \
  1501. self.server.domainFull + originPathStr
  1502. self._redirect_headers(originPathStrAbsolute, cookie,
  1503. callingDomain)
  1504. return
  1505. # show blog post
  1506. blogFilename, nickname = \
  1507. self._pathContainsBlogLink(self.server.baseDir,
  1508. self.server.httpPrefix,
  1509. self.server.domain,
  1510. self.server.domainFull,
  1511. self.path)
  1512. if blogFilename and nickname:
  1513. postJsonObject = loadJson(blogFilename)
  1514. if isBlogPost(postJsonObject):
  1515. msg = htmlBlogPost(authorized,
  1516. self.server.baseDir,
  1517. self.server.httpPrefix,
  1518. self.server.translate,
  1519. nickname, self.server.domain,
  1520. self.server.domainFull,
  1521. postJsonObject)
  1522. if msg is not None:
  1523. msg = msg.encode('utf-8')
  1524. self._set_headers('text/html', len(msg),
  1525. cookie, callingDomain)
  1526. self._write(msg)
  1527. return
  1528. self._404()
  1529. return
  1530. self._benchmarkGETtimings(GETstartTime, GETtimings, 9)
  1531. # remove a shared item
  1532. if htmlGET and '?rmshare=' in self.path:
  1533. shareName = self.path.split('?rmshare=')[1]
  1534. shareName = urllib.parse.unquote(shareName.strip())
  1535. usersPath = self.path.split('?rmshare=')[0]
  1536. actor = \
  1537. self.server.httpPrefix + '://' + \
  1538. self.server.domainFull + usersPath
  1539. msg = htmlRemoveSharedItem(self.server.translate,
  1540. self.server.baseDir,
  1541. actor, shareName).encode('utf-8')
  1542. if not msg:
  1543. if callingDomain.endswith('.onion') and \
  1544. self.server.onionDomain:
  1545. actor = 'http://' + self.server.onionDomain + usersPath
  1546. elif (callingDomain.endswith('.i2p') and
  1547. self.server.i2pDomain):
  1548. actor = 'http://' + self.server.i2pDomain + usersPath
  1549. self._redirect_headers(actor + '/tlshares',
  1550. cookie, callingDomain)
  1551. return
  1552. self._set_headers('text/html', len(msg),
  1553. cookie, callingDomain)
  1554. self._write(msg)
  1555. return
  1556. self._benchmarkGETtimings(GETstartTime, GETtimings, 10)
  1557. if self.path.startswith('/terms'):
  1558. if callingDomain.endswith('.onion') and \
  1559. self.server.onionDomain:
  1560. msg = htmlTermsOfService(self.server.baseDir, 'http',
  1561. self.server.onionDomain)
  1562. elif (callingDomain.endswith('.i2p') and
  1563. self.server.i2pDomain):
  1564. msg = htmlTermsOfService(self.server.baseDir, 'http',
  1565. self.server.i2pDomain)
  1566. else:
  1567. msg = htmlTermsOfService(self.server.baseDir,
  1568. self.server.httpPrefix,
  1569. self.server.domainFull)
  1570. msg = msg.encode('utf-8')
  1571. self._login_headers('text/html', len(msg), callingDomain)
  1572. self._write(msg)
  1573. return
  1574. self._benchmarkGETtimings(GETstartTime, GETtimings, 11)
  1575. # show a list of who you are following
  1576. if htmlGET and authorized and '/users/' in self.path and \
  1577. self.path.endswith('/followingaccounts'):
  1578. nickname = getNicknameFromActor(self.path)
  1579. followingFilename = \
  1580. self.server.baseDir + '/accounts/' + \
  1581. nickname + '@' + self.server.domain + '/following.txt'
  1582. if not os.path.isfile(followingFilename):
  1583. self._404()
  1584. return
  1585. msg = htmlFollowingList(self.server.baseDir, followingFilename)
  1586. self._login_headers('text/html', len(msg), callingDomain)
  1587. self._write(msg.encode('utf-8'))
  1588. return
  1589. if self.path.startswith('/about'):
  1590. if callingDomain.endswith('.onion'):
  1591. msg = \
  1592. htmlAbout(self.server.baseDir, 'http',
  1593. self.server.onionDomain,
  1594. None)
  1595. elif callingDomain.endswith('.i2p'):
  1596. msg = \
  1597. htmlAbout(self.server.baseDir, 'http',
  1598. self.server.i2pDomain,
  1599. None)
  1600. else:
  1601. msg = \
  1602. htmlAbout(self.server.baseDir,
  1603. self.server.httpPrefix,
  1604. self.server.domainFull,
  1605. self.server.onionDomain)
  1606. msg = msg.encode('utf-8')
  1607. self._login_headers('text/html', len(msg), callingDomain)
  1608. self._write(msg)
  1609. return
  1610. self._benchmarkGETtimings(GETstartTime, GETtimings, 12)
  1611. # send robots.txt if asked
  1612. if self._robotsTxt():
  1613. return
  1614. self._benchmarkGETtimings(GETstartTime, GETtimings, 13)
  1615. # if not authorized then show the login screen
  1616. if htmlGET and self.path != '/login' and \
  1617. not self._pathIsImage() and self.path != '/':
  1618. if '/media/' not in self.path and \
  1619. '/sharefiles/' not in self.path and \
  1620. '/statuses/' not in self.path and \
  1621. '/emoji/' not in self.path and \
  1622. '/tags/' not in self.path and \
  1623. '/avatars/' not in self.path and \
  1624. '/icons/' not in self.path:
  1625. divertToLoginScreen = True
  1626. if self.path.startswith('/users/'):
  1627. nickStr = self.path.split('/users/')[1]
  1628. if '/' not in nickStr and '?' not in nickStr:
  1629. divertToLoginScreen = False
  1630. else:
  1631. if self.path.endswith('/following') or \
  1632. self.path.endswith('/followers') or \
  1633. self.path.endswith('/skills') or \
  1634. self.path.endswith('/roles') or \
  1635. self.path.endswith('/shares'):
  1636. divertToLoginScreen = False
  1637. if divertToLoginScreen and not authorized:
  1638. if self.server.debug:
  1639. print('DEBUG: divertToLoginScreen=' +
  1640. str(divertToLoginScreen))
  1641. print('DEBUG: authorized=' + str(authorized))
  1642. print('DEBUG: path=' + self.path)
  1643. if callingDomain.endswith('.onion') and \
  1644. self.server.onionDomain:
  1645. self._redirect_headers('http://' +
  1646. self.server.onionDomain +
  1647. '/login',
  1648. None, callingDomain)
  1649. elif (callingDomain.endswith('.i2p') and
  1650. self.server.i2pDomain):
  1651. self._redirect_headers('http://' +
  1652. self.server.i2pDomain +
  1653. '/login',
  1654. None, callingDomain)
  1655. else:
  1656. self._redirect_headers(self.server.httpPrefix + '://' +
  1657. self.server.domainFull +
  1658. '/login', None, callingDomain)
  1659. return
  1660. self._benchmarkGETtimings(GETstartTime, GETtimings, 14)
  1661. # get css
  1662. # Note that this comes before the busy flag to avoid conflicts
  1663. if self.path.endswith('.css'):
  1664. if os.path.isfile('epicyon-profile.css'):
  1665. tries = 0
  1666. while tries < 5:
  1667. try:
  1668. with open('epicyon-profile.css', 'r') as cssfile:
  1669. css = cssfile.read()
  1670. break
  1671. except Exception as e:
  1672. print(e)
  1673. time.sleep(1)
  1674. tries += 1
  1675. msg = css.encode('utf-8')
  1676. self._set_headers('text/css', len(msg),
  1677. cookie, callingDomain)
  1678. self._write(msg)
  1679. return
  1680. self._404()
  1681. return
  1682. self._benchmarkGETtimings(GETstartTime, GETtimings, 15)
  1683. # image on login screen or qrcode
  1684. if self.path == '/login.png' or \
  1685. self.path == '/login.gif' or \
  1686. self.path == '/login.webp' or \
  1687. self.path == '/login.jpeg' or \
  1688. self.path == '/login.jpg' or \
  1689. self.path == '/qrcode.png':
  1690. mediaFilename = \
  1691. self.server.baseDir + '/accounts' + self.path
  1692. if os.path.isfile(mediaFilename):
  1693. if self._etag_exists(mediaFilename):
  1694. # The file has not changed
  1695. self._304()
  1696. return
  1697. tries = 0
  1698. mediaBinary = None
  1699. while tries < 5:
  1700. try:
  1701. with open(mediaFilename, 'rb') as avFile:
  1702. mediaBinary = avFile.read()
  1703. break
  1704. except Exception as e:
  1705. print(e)
  1706. time.sleep(1)
  1707. tries += 1
  1708. if mediaBinary:
  1709. self._set_headers_etag(mediaFilename,
  1710. 'image/png',
  1711. mediaBinary, cookie,
  1712. callingDomain)
  1713. self._write(mediaBinary)
  1714. return
  1715. self._404()
  1716. return
  1717. self._benchmarkGETtimings(GETstartTime, GETtimings, 16)
  1718. # login screen background image
  1719. if self.path == '/login-background.png':
  1720. mediaFilename = \
  1721. self.server.baseDir + '/accounts/login-background.png'
  1722. if os.path.isfile(mediaFilename):
  1723. if self._etag_exists(mediaFilename):
  1724. # The file has not changed
  1725. self._304()
  1726. return
  1727. tries = 0
  1728. mediaBinary = None
  1729. while tries < 5:
  1730. try:
  1731. with open(mediaFilename, 'rb') as avFile:
  1732. mediaBinary = avFile.read()
  1733. break
  1734. except Exception as e:
  1735. print(e)
  1736. time.sleep(1)
  1737. tries += 1
  1738. if mediaBinary:
  1739. self._set_headers_etag(mediaFilename, 'image/png',
  1740. mediaBinary, cookie,
  1741. callingDomain)
  1742. self._write(mediaBinary)
  1743. return
  1744. self._404()
  1745. return
  1746. # QR code for account handle
  1747. if '/users/' in self.path and \
  1748. self.path.endswith('/qrcode.png'):
  1749. nickname = getNicknameFromActor(self.path)
  1750. savePersonQrcode(self.server.baseDir,
  1751. nickname, self.server.domain,
  1752. self.server.port)
  1753. mediaFilename = \
  1754. self.server.baseDir + '/accounts/' + \
  1755. nickname + '@' + self.server.domain + '/qrcode.png'
  1756. if os.path.isfile(mediaFilename):
  1757. if self._etag_exists(mediaFilename):
  1758. # The file has not changed
  1759. self._304()
  1760. return
  1761. tries = 0
  1762. mediaBinary = None
  1763. while tries < 5:
  1764. try:
  1765. with open(mediaFilename, 'rb') as avFile:
  1766. mediaBinary = avFile.read()
  1767. break
  1768. except Exception as e:
  1769. print(e)
  1770. time.sleep(1)
  1771. tries += 1
  1772. if mediaBinary:
  1773. self._set_headers_etag(mediaFilename, 'image/png',
  1774. mediaBinary, cookie,
  1775. callingDomain)
  1776. self._write(mediaBinary)
  1777. return
  1778. self._404()
  1779. return
  1780. # search screen banner image
  1781. if '/users/' in self.path and \
  1782. self.path.endswith('/search_banner.png'):
  1783. nickname = getNicknameFromActor(self.path)
  1784. mediaFilename = \
  1785. self.server.baseDir + '/accounts/' + \
  1786. nickname + '@' + self.server.domain + '/search_banner.png'
  1787. if os.path.isfile(mediaFilename):
  1788. if self._etag_exists(mediaFilename):
  1789. # The file has not changed
  1790. self._304()
  1791. return
  1792. tries = 0
  1793. mediaBinary = None
  1794. while tries < 5:
  1795. try:
  1796. with open(mediaFilename, 'rb') as avFile:
  1797. mediaBinary = avFile.read()
  1798. break
  1799. except Exception as e:
  1800. print(e)
  1801. time.sleep(1)
  1802. tries += 1
  1803. if mediaBinary:
  1804. self._set_headers_etag(mediaFilename, 'image/png',
  1805. mediaBinary, cookie,
  1806. callingDomain)
  1807. self._write(mediaBinary)
  1808. return
  1809. self._404()
  1810. return
  1811. self._benchmarkGETtimings(GETstartTime, GETtimings, 17)
  1812. # follow screen background image
  1813. if self.path == '/follow-background.png':
  1814. mediaFilename = \
  1815. self.server.baseDir + '/accounts/follow-background.png'
  1816. if os.path.isfile(mediaFilename):
  1817. if self._etag_exists(mediaFilename):
  1818. # The file has not changed
  1819. self._304()
  1820. return
  1821. tries = 0
  1822. mediaBinary = None
  1823. while tries < 5:
  1824. try:
  1825. with open(mediaFilename, 'rb') as avFile:
  1826. mediaBinary = avFile.read()
  1827. break
  1828. except Exception as e:
  1829. print(e)
  1830. time.sleep(1)
  1831. tries += 1
  1832. if mediaBinary:
  1833. self._set_headers_etag(mediaFilename, 'image/png',
  1834. mediaBinary, cookie,
  1835. callingDomain)
  1836. self._write(mediaBinary)
  1837. return
  1838. self._404()
  1839. return
  1840. self._benchmarkGETtimings(GETstartTime, GETtimings, 18)
  1841. # emoji images
  1842. if '/emoji/' in self.path:
  1843. if self._pathIsImage():
  1844. emojiStr = self.path.split('/emoji/')[1]
  1845. emojiFilename = \
  1846. self.server.baseDir + '/emoji/' + emojiStr
  1847. if os.path.isfile(emojiFilename):
  1848. if self._etag_exists(emojiFilename):
  1849. # The file has not changed
  1850. self._304()
  1851. return
  1852. mediaImageType = 'png'
  1853. if emojiFilename.endswith('.png'):
  1854. mediaImageType = 'png'
  1855. elif emojiFilename.endswith('.jpg'):
  1856. mediaImageType = 'jpeg'
  1857. elif emojiFilename.endswith('.webp'):
  1858. mediaImageType = 'webp'
  1859. else:
  1860. mediaImageType = 'gif'
  1861. with open(emojiFilename, 'rb') as avFile:
  1862. mediaBinary = avFile.read()
  1863. self._set_headers_etag(emojiFilename,
  1864. 'image/' + mediaImageType,
  1865. mediaBinary, cookie,
  1866. callingDomain)
  1867. self._write(mediaBinary)
  1868. return
  1869. self._404()
  1870. return
  1871. self._benchmarkGETtimings(GETstartTime, GETtimings, 19)
  1872. # show media
  1873. # Note that this comes before the busy flag to avoid conflicts
  1874. if '/media/' in self.path:
  1875. if self._pathIsImage() or \
  1876. self._pathIsVideo() or \
  1877. self._pathIsAudio():
  1878. mediaStr = self.path.split('/media/')[1]
  1879. mediaFilename = \
  1880. self.server.baseDir + '/media/' + mediaStr
  1881. if os.path.isfile(mediaFilename):
  1882. if self._etag_exists(mediaFilename):
  1883. # The file has not changed
  1884. self._304()
  1885. return
  1886. mediaFileType = 'image/png'
  1887. if mediaFilename.endswith('.png'):
  1888. mediaFileType = 'image/png'
  1889. elif mediaFilename.endswith('.jpg'):
  1890. mediaFileType = 'image/jpeg'
  1891. elif mediaFilename.endswith('.gif'):
  1892. mediaFileType = 'image/gif'
  1893. elif mediaFilename.endswith('.webp'):
  1894. mediaFileType = 'image/webp'
  1895. elif mediaFilename.endswith('.mp4'):
  1896. mediaFileType = 'video/mp4'
  1897. elif mediaFilename.endswith('.ogv'):
  1898. mediaFileType = 'video/ogv'
  1899. elif mediaFilename.endswith('.mp3'):
  1900. mediaFileType = 'audio/mpeg'
  1901. elif mediaFilename.endswith('.ogg'):
  1902. mediaFileType = 'audio/ogg'
  1903. with open(mediaFilename, 'rb') as avFile:
  1904. mediaBinary = avFile.read()
  1905. self._set_headers_etag(mediaFilename, mediaFileType,
  1906. mediaBinary, cookie,
  1907. callingDomain)
  1908. self._write(mediaBinary)
  1909. return
  1910. self._404()
  1911. return
  1912. self._benchmarkGETtimings(GETstartTime, GETtimings, 20)
  1913. # show shared item images
  1914. # Note that this comes before the busy flag to avoid conflicts
  1915. if '/sharefiles/' in self.path:
  1916. if self._pathIsImage():
  1917. mediaStr = self.path.split('/sharefiles/')[1]
  1918. mediaFilename = \
  1919. self.server.baseDir + '/sharefiles/' + mediaStr
  1920. if os.path.isfile(mediaFilename):
  1921. if self._etag_exists(mediaFilename):
  1922. # The file has not changed
  1923. self._304()
  1924. return
  1925. mediaFileType = 'png'
  1926. if mediaFilename.endswith('.png'):
  1927. mediaFileType = 'png'
  1928. elif mediaFilename.endswith('.jpg'):
  1929. mediaFileType = 'jpeg'
  1930. elif mediaFilename.endswith('.webp'):
  1931. mediaFileType = 'webp'
  1932. else:
  1933. mediaFileType = 'gif'
  1934. with open(mediaFilename, 'rb') as avFile:
  1935. mediaBinary = avFile.read()
  1936. self._set_headers_etag(mediaFilename,
  1937. 'image/' + mediaFileType,
  1938. mediaBinary, cookie,
  1939. callingDomain)
  1940. self._write(mediaBinary)
  1941. return
  1942. self._404()
  1943. return
  1944. self._benchmarkGETtimings(GETstartTime, GETtimings, 21)
  1945. # icon images
  1946. # Note that this comes before the busy flag to avoid conflicts
  1947. if self.path.startswith('/icons/'):
  1948. if self.path.endswith('.png'):
  1949. mediaStr = self.path.split('/icons/')[1]
  1950. mediaFilename = \
  1951. self.server.baseDir + '/img/icons/' + mediaStr
  1952. if self._etag_exists(mediaFilename):
  1953. # The file has not changed
  1954. self._304()
  1955. return
  1956. if self.server.iconsCache.get(mediaStr):
  1957. mediaBinary = self.server.iconsCache[mediaStr]
  1958. self._set_headers_etag(mediaFilename,
  1959. 'image/png',
  1960. mediaBinary, cookie,
  1961. callingDomain)
  1962. self._write(mediaBinary)
  1963. return
  1964. else:
  1965. if os.path.isfile(mediaFilename):
  1966. with open(mediaFilename, 'rb') as avFile:
  1967. mediaBinary = avFile.read()
  1968. self._set_headers_etag(mediaFilename,
  1969. 'image/png',
  1970. mediaBinary, cookie,
  1971. callingDomain)
  1972. self._write(mediaBinary)
  1973. self.server.iconsCache[mediaStr] = mediaBinary
  1974. return
  1975. self._404()
  1976. return
  1977. self._benchmarkGETtimings(GETstartTime, GETtimings, 22)
  1978. # cached avatar images
  1979. # Note that this comes before the busy flag to avoid conflicts
  1980. if self.path.startswith('/avatars/'):
  1981. mediaFilename = \
  1982. self.server.baseDir + '/cache/' + self.path
  1983. if os.path.isfile(mediaFilename):
  1984. if self._etag_exists(mediaFilename):
  1985. # The file has not changed
  1986. self._304()
  1987. return
  1988. with open(mediaFilename, 'rb') as avFile:
  1989. mediaBinary = avFile.read()
  1990. if mediaFilename.endswith('.png'):
  1991. self._set_headers_etag(mediaFilename,
  1992. 'image/png',
  1993. mediaBinary, cookie,
  1994. callingDomain)
  1995. elif mediaFilename.endswith('.jpg'):
  1996. self._set_headers_etag(mediaFilename,
  1997. 'image/jpeg',
  1998. mediaBinary, cookie,
  1999. callingDomain)
  2000. elif mediaFilename.endswith('.gif'):
  2001. self._set_headers_etag(mediaFilename,
  2002. 'image/gif',
  2003. mediaBinary, cookie,
  2004. callingDomain)
  2005. elif mediaFilename.endswith('.webp'):
  2006. self._set_headers_etag(mediaFilename,
  2007. 'image/webp',
  2008. mediaBinary, cookie,
  2009. callingDomain)
  2010. else:
  2011. # default to jpeg
  2012. self._set_headers_etag(mediaFilename,
  2013. 'image/jpeg',
  2014. mediaBinary, cookie,
  2015. callingDomain)
  2016. # self._404()
  2017. return
  2018. self._write(mediaBinary)
  2019. return
  2020. self._404()
  2021. return
  2022. self._benchmarkGETtimings(GETstartTime, GETtimings, 23)
  2023. # show avatar or background image
  2024. # Note that this comes before the busy flag to avoid conflicts
  2025. if '/users/' in self.path:
  2026. if self._pathIsImage():
  2027. avatarStr = self.path.split('/users/')[1]
  2028. if '/' in avatarStr and '.temp.' not in self.path:
  2029. avatarNickname = avatarStr.split('/')[0]
  2030. avatarFile = avatarStr.split('/')[1]
  2031. # remove any numbers, eg. avatar123.png becomes avatar.png
  2032. if avatarFile.startswith('avatar'):
  2033. avatarFile = 'avatar.' + avatarFile.split('.')[1]
  2034. elif avatarFile.startswith('image'):
  2035. avatarFile = 'image.'+avatarFile.split('.')[1]
  2036. avatarFilename = \
  2037. self.server.baseDir + '/accounts/' + \
  2038. avatarNickname + '@' + \
  2039. self.server.domain + '/' + avatarFile
  2040. if os.path.isfile(avatarFilename):
  2041. if self._etag_exists(avatarFilename):
  2042. # The file has not changed
  2043. self._304()
  2044. return
  2045. mediaImageType = 'png'
  2046. if avatarFile.endswith('.png'):
  2047. mediaImageType = 'png'
  2048. elif avatarFile.endswith('.jpg'):
  2049. mediaImageType = 'jpeg'
  2050. elif avatarFile.endswith('.gif'):
  2051. mediaImageType = 'gif'
  2052. else:
  2053. mediaImageType = 'webp'
  2054. with open(avatarFilename, 'rb') as avFile:
  2055. mediaBinary = avFile.read()
  2056. self._set_headers_etag(avatarFilename,
  2057. 'image/' + mediaImageType,
  2058. mediaBinary, cookie,
  2059. callingDomain)
  2060. self._write(mediaBinary)
  2061. return
  2062. self._benchmarkGETtimings(GETstartTime, GETtimings, 24)
  2063. # This busy state helps to avoid flooding
  2064. # Resources which are expected to be called from a web page
  2065. # should be above this
  2066. if self.server.GETbusy:
  2067. currTimeGET = int(time.time())
  2068. if currTimeGET - self.server.lastGET == 0:
  2069. if self.server.debug:
  2070. print('DEBUG: GET Busy')
  2071. self.send_response(429)
  2072. self.end_headers()
  2073. return
  2074. self.server.lastGET = currTimeGET
  2075. self.server.GETbusy = True
  2076. self._benchmarkGETtimings(GETstartTime, GETtimings, 25)
  2077. if not self._permittedDir(self.path):
  2078. if self.server.debug:
  2079. print('DEBUG: GET Not permitted')
  2080. self._404()
  2081. self.server.GETbusy = False
  2082. return
  2083. # get webfinger endpoint for a person
  2084. if self._webfinger(callingDomain):
  2085. self.server.GETbusy = False
  2086. return
  2087. self._benchmarkGETtimings(GETstartTime, GETtimings, 26)
  2088. if self.path.startswith('/login') or \
  2089. (self.path == '/' and not authorized):
  2090. # request basic auth
  2091. msg = htmlLogin(self.server.translate,
  2092. self.server.baseDir).encode('utf-8')
  2093. self._login_headers('text/html', len(msg), callingDomain)
  2094. self._write(msg)
  2095. self.server.GETbusy = False
  2096. return
  2097. self._benchmarkGETtimings(GETstartTime, GETtimings, 27)
  2098. # hashtag search
  2099. if self.path.startswith('/tags/') or \
  2100. (authorized and '/tags/' in self.path):
  2101. pageNumber = 1
  2102. if '?page=' in self.path:
  2103. pageNumberStr = self.path.split('?page=')[1]
  2104. if '#' in pageNumberStr:
  2105. pageNumberStr = pageNumberStr.split('#')[0]
  2106. if pageNumberStr.isdigit():
  2107. pageNumber = int(pageNumberStr)
  2108. hashtag = self.path.split('/tags/')[1]
  2109. if '?page=' in hashtag:
  2110. hashtag = hashtag.split('?page=')[0]
  2111. if isBlockedHashtag(self.server.baseDir, hashtag):
  2112. msg = htmlHashtagBlocked(self.server.baseDir).encode('utf-8')
  2113. self._login_headers('text/html', len(msg), callingDomain)
  2114. self._write(msg)
  2115. self.server.GETbusy = False
  2116. return
  2117. nickname = None
  2118. if '/users/' in self.path:
  2119. actor = \
  2120. self.server.httpPrefix + '://' + \
  2121. self.server.domainFull + self.path
  2122. nickname = \
  2123. getNicknameFromActor(actor)
  2124. hashtagStr = \
  2125. htmlHashtagSearch(nickname,
  2126. self.server.domain, self.server.port,
  2127. self.server.recentPostsCache,
  2128. self.server.maxRecentPosts,
  2129. self.server.translate,
  2130. self.server.baseDir, hashtag, pageNumber,
  2131. maxPostsInFeed, self.server.session,
  2132. self.server.cachedWebfingers,
  2133. self.server.personCache,
  2134. self.server.httpPrefix,
  2135. self.server.projectVersion)
  2136. if hashtagStr:
  2137. msg = hashtagStr.encode('utf-8')
  2138. self._set_headers('text/html', len(msg),
  2139. cookie, callingDomain)
  2140. self._write(msg)
  2141. else:
  2142. originPathStr = self.path.split('/tags/')[0]
  2143. originPathStrAbsolute = \
  2144. self.server.httpPrefix + '://' + \
  2145. self.server.domainFull + originPathStr
  2146. if callingDomain.endswith('.onion') and \
  2147. self.server.onionDomain:
  2148. originPathStrAbsolute = 'http://' + \
  2149. self.server.onionDomain + originPathStr
  2150. elif (callingDomain.endswith('.i2p') and
  2151. self.server.onionDomain):
  2152. originPathStrAbsolute = 'http://' + \
  2153. self.server.i2pDomain + originPathStr
  2154. self._redirect_headers(originPathStrAbsolute + '/search',
  2155. cookie, callingDomain)
  2156. self.server.GETbusy = False
  2157. return
  2158. self._benchmarkGETtimings(GETstartTime, GETtimings, 28)
  2159. # show or hide buttons in the web interface
  2160. if htmlGET and '/users/' in self.path and \
  2161. self.path.endswith('/minimal') and \
  2162. authorized:
  2163. nickname = self.path.split('/users/')[1]
  2164. if '/' in nickname:
  2165. nickname = nickname.split('/')[0]
  2166. self._setMinimal(nickname, not self._isMinimal(nickname))
  2167. if not (self.server.mediaInstance or
  2168. self.server.blogsInstance):
  2169. self.path = '/users/' + nickname + '/inbox'
  2170. else:
  2171. if self.server.blogsInstance:
  2172. self.path = '/users/' + nickname + '/tlblogs'
  2173. else:
  2174. self.path = '/users/' + nickname + '/tlmedia'
  2175. # search for a fediverse address, shared item or emoji
  2176. # from the web interface by selecting search icon
  2177. if htmlGET and '/users/' in self.path:
  2178. if self.path.endswith('/search') or \
  2179. '/search?' in self.path:
  2180. if '?' in self.path:
  2181. self.path = self.path.split('?')[0]
  2182. # show the search screen
  2183. msg = htmlSearch(self.server.translate,
  2184. self.server.baseDir, self.path,
  2185. self.server.domain).encode('utf-8')
  2186. self._set_headers('text/html', len(msg), cookie, callingDomain)
  2187. self._write(msg)
  2188. self.server.GETbusy = False
  2189. return
  2190. self._benchmarkGETtimings(GETstartTime, GETtimings, 29)
  2191. # Show the calendar for a user
  2192. if htmlGET and '/users/' in self.path:
  2193. if '/calendar' in self.path:
  2194. # show the calendar screen
  2195. msg = htmlCalendar(self.server.translate,
  2196. self.server.baseDir, self.path,
  2197. self.server.httpPrefix,
  2198. self.server.domainFull).encode('utf-8')
  2199. self._set_headers('text/html', len(msg), cookie, callingDomain)
  2200. self._write(msg)
  2201. self.server.GETbusy = False
  2202. return
  2203. # Show confirmation for deleting a calendar event
  2204. if htmlGET and '/users/' in self.path:
  2205. if '/eventdelete' in self.path and \
  2206. '?time=' in self.path and \
  2207. '?id=' in self.path:
  2208. postId = self.path.split('?id=')[1]
  2209. if '?' in postId:
  2210. postId = postId.split('?')[0]
  2211. postTime = self.path.split('?time=')[1]
  2212. if '?' in postTime:
  2213. postTime = postTime.split('?')[0]
  2214. postYear = self.path.split('?year=')[1]
  2215. if '?' in postYear:
  2216. postYear = postYear.split('?')[0]
  2217. postMonth = self.path.split('?month=')[1]
  2218. if '?' in postMonth:
  2219. postMonth = postMonth.split('?')[0]
  2220. postDay = self.path.split('?day=')[1]
  2221. if '?' in postDay:
  2222. postDay = postDay.split('?')[0]
  2223. # show the confirmation screen screen
  2224. msg = htmlCalendarDeleteConfirm(self.server.translate,
  2225. self.server.baseDir,
  2226. self.path,
  2227. self.server.httpPrefix,
  2228. self.server.domainFull,
  2229. postId, postTime,
  2230. postYear, postMonth, postDay)
  2231. if not msg:
  2232. actor = \
  2233. self.server.httpPrefix + '://' + \
  2234. self.server.domainFull + \
  2235. self.path.split('/eventdelete')[0]
  2236. if callingDomain.endswith('.onion') and \
  2237. self.server.onionDomain:
  2238. actor = \
  2239. 'http://' + self.server.onionDomain + \
  2240. self.path.split('/eventdelete')[0]
  2241. elif (callingDomain.endswith('.i2p') and
  2242. self.server.i2pDomain):
  2243. actor = \
  2244. 'http://' + self.server.i2pDomain + \
  2245. self.path.split('/eventdelete')[0]
  2246. self._redirect_headers(actor + '/calendar',
  2247. cookie, callingDomain)
  2248. return
  2249. msg = msg.encode('utf-8')
  2250. self._set_headers('text/html', len(msg),
  2251. cookie, callingDomain)
  2252. self._write(msg)
  2253. self.server.GETbusy = False
  2254. return
  2255. self._benchmarkGETtimings(GETstartTime, GETtimings, 30)
  2256. # search for emoji by name
  2257. if htmlGET and '/users/' in self.path:
  2258. if self.path.endswith('/searchemoji'):
  2259. # show the search screen
  2260. msg = htmlSearchEmojiTextEntry(self.server.translate,
  2261. self.server.baseDir,
  2262. self.path).encode('utf-8')
  2263. self._set_headers('text/html', len(msg),
  2264. cookie, callingDomain)
  2265. self._write(msg)
  2266. self.server.GETbusy = False
  2267. return
  2268. self._benchmarkGETtimings(GETstartTime, GETtimings, 31)
  2269. repeatPrivate = False
  2270. if htmlGET and '?repeatprivate=' in self.path:
  2271. repeatPrivate = True
  2272. self.path = self.path.replace('?repeatprivate=', '?repeat=')
  2273. # announce/repeat from the web interface
  2274. if htmlGET and '?repeat=' in self.path:
  2275. pageNumber = 1
  2276. repeatUrl = self.path.split('?repeat=')[1]
  2277. if '?' in repeatUrl:
  2278. repeatUrl = repeatUrl.split('?')[0]
  2279. timelineBookmark = ''
  2280. if '?bm=' in self.path:
  2281. timelineBookmark = self.path.split('?bm=')[1]
  2282. if '?' in timelineBookmark:
  2283. timelineBookmark = timelineBookmark.split('?')[0]
  2284. timelineBookmark = '#' + timelineBookmark
  2285. if '?page=' in self.path:
  2286. pageNumberStr = self.path.split('?page=')[1]
  2287. if '?' in pageNumberStr:
  2288. pageNumberStr = pageNumberStr.split('?')[0]
  2289. if '#' in pageNumberStr:
  2290. pageNumberStr = pageNumberStr.split('#')[0]
  2291. if pageNumberStr.isdigit():
  2292. pageNumber = int(pageNumberStr)
  2293. timelineStr = 'inbox'
  2294. if '?tl=' in self.path:
  2295. timelineStr = self.path.split('?tl=')[1]
  2296. if '?' in timelineStr:
  2297. timelineStr = timelineStr.split('?')[0]
  2298. actor = self.path.split('?repeat=')[0]
  2299. self.postToNickname = getNicknameFromActor(actor)
  2300. if not self.postToNickname:
  2301. print('WARN: unable to find nickname in ' + actor)
  2302. self.server.GETbusy = False
  2303. actorAbsolute = \
  2304. self.server.httpPrefix + '://' + \
  2305. self.server.domainFull+actor
  2306. if callingDomain.endswith('.onion') and \
  2307. self.server.onionDomain:
  2308. actorAbsolute = 'http://' + self.server.onionDomain + actor
  2309. elif (callingDomain.endswith('.i2p') and
  2310. self.server.i2pDomain):
  2311. actorAbsolute = 'http://' + self.server.i2pDomain + actor
  2312. self._redirect_headers(actorAbsolute + '/' + timelineStr +
  2313. '?page=' + str(pageNumber), cookie,
  2314. callingDomain)
  2315. return
  2316. if not self.server.session:
  2317. print('Starting new session during repeat button')
  2318. self.server.session = createSession(self.server.proxyType)
  2319. if not self.server.session:
  2320. print('ERROR: GET failed to create session ' +
  2321. 'during repeat button')
  2322. self._404()
  2323. self.server.GETbusy = False
  2324. return
  2325. self.server.actorRepeat = self.path.split('?actor=')[1]
  2326. announceToStr = \
  2327. self.server.httpPrefix + '://' + \
  2328. self.server.domain + '/users/' + \
  2329. self.postToNickname + '/followers'
  2330. if not repeatPrivate:
  2331. announceToStr = 'https://www.w3.org/ns/activitystreams#Public'
  2332. announceJson = \
  2333. createAnnounce(self.server.session,
  2334. self.server.baseDir,
  2335. self.server.federationList,
  2336. self.postToNickname,
  2337. self.server.domain, self.server.port,
  2338. announceToStr,
  2339. None, self.server.httpPrefix,
  2340. repeatUrl, False, False,
  2341. self.server.sendThreads,
  2342. self.server.postLog,
  2343. self.server.personCache,
  2344. self.server.cachedWebfingers,
  2345. self.server.debug,
  2346. self.server.projectVersion)
  2347. if announceJson:
  2348. self._postToOutboxThread(announceJson)
  2349. self.server.GETbusy = False
  2350. actorAbsolute = self.server.httpPrefix + '://' + \
  2351. self.server.domainFull + actor
  2352. if callingDomain.endswith('.onion') and \
  2353. self.server.onionDomain:
  2354. actorAbsolute = 'http://' + self.server.onionDomain + actor
  2355. elif (callingDomain.endswith('.i2p') and
  2356. self.server.i2pDomain):
  2357. actorAbsolute = 'http://' + self.server.i2pDomain + actor
  2358. self._redirect_headers(actorAbsolute + '/' +
  2359. timelineStr + '?page=' +
  2360. str(pageNumber) +
  2361. timelineBookmark, cookie, callingDomain)
  2362. return
  2363. self._benchmarkGETtimings(GETstartTime, GETtimings, 32)
  2364. # unrepeatPrivate = False
  2365. if htmlGET and '?unrepeatprivate=' in self.path:
  2366. self.path = self.path.replace('?unrepeatprivate=', '?unrepeat=')
  2367. # unrepeatPrivate = True
  2368. # undo an announce/repeat from the web interface
  2369. if htmlGET and '?unrepeat=' in self.path:
  2370. pageNumber = 1
  2371. repeatUrl = self.path.split('?unrepeat=')[1]
  2372. if '?' in repeatUrl:
  2373. repeatUrl = repeatUrl.split('?')[0]
  2374. timelineBookmark = ''
  2375. if '?bm=' in self.path:
  2376. timelineBookmark = self.path.split('?bm=')[1]
  2377. if '?' in timelineBookmark:
  2378. timelineBookmark = timelineBookmark.split('?')[0]
  2379. timelineBookmark = '#' + timelineBookmark
  2380. if '?page=' in self.path:
  2381. pageNumberStr = self.path.split('?page=')[1]
  2382. if '?' in pageNumberStr:
  2383. pageNumberStr = pageNumberStr.split('?')[0]
  2384. if '#' in pageNumberStr:
  2385. pageNumberStr = pageNumberStr.split('#')[0]
  2386. if pageNumberStr.isdigit():
  2387. pageNumber = int(pageNumberStr)
  2388. timelineStr = 'inbox'
  2389. if '?tl=' in self.path:
  2390. timelineStr = self.path.split('?tl=')[1]
  2391. if '?' in timelineStr:
  2392. timelineStr = timelineStr.split('?')[0]
  2393. actor = self.path.split('?unrepeat=')[0]
  2394. self.postToNickname = getNicknameFromActor(actor)
  2395. if not self.postToNickname:
  2396. print('WARN: unable to find nickname in ' + actor)
  2397. self.server.GETbusy = False
  2398. actorAbsolute = self.server.httpPrefix + '://' + \
  2399. self.server.domainFull + actor
  2400. if callingDomain.endswith('.onion') and \
  2401. self.server.onionDomain:
  2402. actorAbsolute = 'http://' + self.server.onionDomain + actor
  2403. elif (callingDomain.endswith('.i2p') and
  2404. self.server.i2pDomain):
  2405. actorAbsolute = 'http://' + self.server.i2pDomain + actor
  2406. self._redirect_headers(actorAbsolute + '/' +
  2407. timelineStr + '?page=' +
  2408. str(pageNumber), cookie,
  2409. callingDomain)
  2410. return
  2411. if not self.server.session:
  2412. print('Starting new session during undo repeat')
  2413. self.server.session = createSession(self.server.proxyType)
  2414. if not self.server.session:
  2415. print('ERROR: GET failed to create session ' +
  2416. 'during undo repeat')
  2417. self._404()
  2418. self.server.GETbusy = False
  2419. return
  2420. undoAnnounceActor = \
  2421. self.server.httpPrefix + '://' + self.server.domainFull + \
  2422. '/users/' + self.postToNickname
  2423. unRepeatToStr = 'https://www.w3.org/ns/activitystreams#Public'
  2424. newUndoAnnounce = {
  2425. "@context": "https://www.w3.org/ns/activitystreams",
  2426. 'actor': undoAnnounceActor,
  2427. 'type': 'Undo',
  2428. 'cc': [undoAnnounceActor+'/followers'],
  2429. 'to': [unRepeatToStr],
  2430. 'object': {
  2431. 'actor': undoAnnounceActor,
  2432. 'cc': [undoAnnounceActor+'/followers'],
  2433. 'object': repeatUrl,
  2434. 'to': [unRepeatToStr],
  2435. 'type': 'Announce'
  2436. }
  2437. }
  2438. self._postToOutboxThread(newUndoAnnounce)
  2439. self.server.GETbusy = False
  2440. actorAbsolute = self.server.httpPrefix + '://' + \
  2441. self.server.domainFull + actor
  2442. if callingDomain.endswith('.onion') and \
  2443. self.server.onionDomain:
  2444. actorAbsolute = 'http://' + self.server.onionDomain + actor
  2445. elif (callingDomain.endswith('.i2p') and
  2446. self.server.onionDomain):
  2447. actorAbsolute = 'http://' + self.server.i2pDomain + actor
  2448. self._redirect_headers(actorAbsolute + '/' +
  2449. timelineStr + '?page=' +
  2450. str(pageNumber) +
  2451. timelineBookmark, cookie, callingDomain)
  2452. return
  2453. self._benchmarkGETtimings(GETstartTime, GETtimings, 33)
  2454. # send a follow request approval from the web interface
  2455. if authorized and '/followapprove=' in self.path and \
  2456. self.path.startswith('/users/'):
  2457. originPathStr = self.path.split('/followapprove=')[0]
  2458. followerNickname = originPathStr.replace('/users/', '')
  2459. followingHandle = self.path.split('/followapprove=')[1]
  2460. if '@' in followingHandle:
  2461. if not self.server.session:
  2462. print('Starting new session during follow approval')
  2463. self.server.session = createSession(self.server.proxyType)
  2464. if not self.server.session:
  2465. print('ERROR: GET failed to create session ' +
  2466. 'during follow approval')
  2467. self._404()
  2468. self.server.GETbusy = False
  2469. return
  2470. manualApproveFollowRequest(self.server.session,
  2471. self.server.baseDir,
  2472. self.server.httpPrefix,
  2473. followerNickname,
  2474. self.server.domain,
  2475. self.server.port,
  2476. followingHandle,
  2477. self.server.federationList,
  2478. self.server.sendThreads,
  2479. self.server.postLog,
  2480. self.server.cachedWebfingers,
  2481. self.server.personCache,
  2482. self.server.acceptedCaps,
  2483. self.server.debug,
  2484. self.server.projectVersion)
  2485. originPathStrAbsolute = \
  2486. self.server.httpPrefix + '://' + \
  2487. self.server.domainFull + originPathStr
  2488. if callingDomain.endswith('.onion') and \
  2489. self.server.onionDomain:
  2490. originPathStrAbsolute = \
  2491. 'http://' + self.server.onionDomain + originPathStr
  2492. elif (callingDomain.endswith('.i2p') and
  2493. self.server.i2pDomain):
  2494. originPathStrAbsolute = \
  2495. 'http://' + self.server.i2pDomain + originPathStr
  2496. self._redirect_headers(originPathStrAbsolute,
  2497. cookie, callingDomain)
  2498. self.server.GETbusy = False
  2499. return
  2500. self._benchmarkGETtimings(GETstartTime, GETtimings, 34)
  2501. # deny a follow request from the web interface
  2502. if authorized and '/followdeny=' in self.path and \
  2503. self.path.startswith('/users/'):
  2504. originPathStr = self.path.split('/followdeny=')[0]
  2505. followerNickname = originPathStr.replace('/users/', '')
  2506. followingHandle = self.path.split('/followdeny=')[1]
  2507. if '@' in followingHandle:
  2508. manualDenyFollowRequest(self.server.session,
  2509. self.server.baseDir,
  2510. self.server.httpPrefix,
  2511. followerNickname,
  2512. self.server.domain,
  2513. self.server.port,
  2514. followingHandle,
  2515. self.server.federationList,
  2516. self.server.sendThreads,
  2517. self.server.postLog,
  2518. self.server.cachedWebfingers,
  2519. self.server.personCache,
  2520. self.server.debug,
  2521. self.server.projectVersion)
  2522. originPathStrAbsolute = \
  2523. self.server.httpPrefix + '://' + \
  2524. self.server.domainFull + originPathStr
  2525. if callingDomain.endswith('.onion') and \
  2526. self.server.onionDomain:
  2527. originPathStrAbsolute = 'http://' + \
  2528. self.server.onionDomain + originPathStr
  2529. elif (callingDomain.endswith('.i2p') and
  2530. self.server.i2pDomain):
  2531. originPathStrAbsolute = 'http://' + \
  2532. self.server.i2pDomain + originPathStr
  2533. self._redirect_headers(originPathStrAbsolute,
  2534. cookie, callingDomain)
  2535. self.server.GETbusy = False
  2536. return
  2537. self._benchmarkGETtimings(GETstartTime, GETtimings, 35)
  2538. # like from the web interface icon
  2539. if htmlGET and '?like=' in self.path:
  2540. pageNumber = 1
  2541. likeUrl = self.path.split('?like=')[1]
  2542. if '?' in likeUrl:
  2543. likeUrl = likeUrl.split('?')[0]
  2544. timelineBookmark = ''
  2545. if '?bm=' in self.path:
  2546. timelineBookmark = self.path.split('?bm=')[1]
  2547. if '?' in timelineBookmark:
  2548. timelineBookmark = timelineBookmark.split('?')[0]
  2549. timelineBookmark = '#' + timelineBookmark
  2550. actor = self.path.split('?like=')[0]
  2551. if '?page=' in self.path:
  2552. pageNumberStr = self.path.split('?page=')[1]
  2553. if '?' in pageNumberStr:
  2554. pageNumberStr = pageNumberStr.split('?')[0]
  2555. if '#' in pageNumberStr:
  2556. pageNumberStr = pageNumberStr.split('#')[0]
  2557. if pageNumberStr.isdigit():
  2558. pageNumber = int(pageNumberStr)
  2559. timelineStr = 'inbox'
  2560. if '?tl=' in self.path:
  2561. timelineStr = self.path.split('?tl=')[1]
  2562. if '?' in timelineStr:
  2563. timelineStr = timelineStr.split('?')[0]
  2564. self.postToNickname = getNicknameFromActor(actor)
  2565. if not self.postToNickname:
  2566. print('WARN: unable to find nickname in ' + actor)
  2567. self.server.GETbusy = False
  2568. actorAbsolute = \
  2569. self.server.httpPrefix + '://' + \
  2570. self.server.domainFull+actor
  2571. if callingDomain.endswith('.onion') and \
  2572. self.server.onionDomain:
  2573. actorAbsolute = 'http://' + self.server.onionDomain + actor
  2574. elif (callingDomain.endswith('.i2p') and
  2575. self.server.i2pDomain):
  2576. actorAbsolute = 'http://' + self.server.i2pDomain + actor
  2577. self._redirect_headers(actorAbsolute + '/' + timelineStr +
  2578. '?page=' + str(pageNumber) +
  2579. timelineBookmark, cookie,
  2580. callingDomain)
  2581. return
  2582. if not self.server.session:
  2583. print('Starting new session during like')
  2584. self.server.session = createSession(self.server.proxyType)
  2585. if not self.server.session:
  2586. print('ERROR: GET failed to create session during like')
  2587. self._404()
  2588. self.server.GETbusy = False
  2589. return
  2590. likeActor = \
  2591. self.server.httpPrefix + '://' + \
  2592. self.server.domainFull + '/users/' + self.postToNickname
  2593. actorLiked = self.path.split('?actor=')[1]
  2594. if '?' in actorLiked:
  2595. actorLiked = actorLiked.split('?')[0]
  2596. likeJson = {
  2597. "@context": "https://www.w3.org/ns/activitystreams",
  2598. 'type': 'Like',
  2599. 'actor': likeActor,
  2600. 'to': [actorLiked],
  2601. 'object': likeUrl
  2602. }
  2603. # directly like the post file
  2604. likedPostFilename = locatePost(self.server.baseDir,
  2605. self.postToNickname,
  2606. self.server.domain,
  2607. likeUrl)
  2608. if likedPostFilename:
  2609. if self.server.debug:
  2610. print('Updating likes for ' + likedPostFilename)
  2611. updateLikesCollection(self.server.recentPostsCache,
  2612. self.server.baseDir,
  2613. likedPostFilename, likeUrl,
  2614. likeActor, self.server.domain,
  2615. self.server.debug)
  2616. else:
  2617. print('WARN: unable to locate file for liked post ' +
  2618. likeUrl)
  2619. # send out the like to followers
  2620. self._postToOutbox(likeJson, self.server.projectVersion)
  2621. self.server.GETbusy = False
  2622. actorAbsolute = \
  2623. self.server.httpPrefix + '://' + \
  2624. self.server.domainFull + actor
  2625. if callingDomain.endswith('.onion') and \
  2626. self.server.onionDomain:
  2627. actorAbsolute = 'http://' + self.server.onionDomain + actor
  2628. elif (callingDomain.endswith('.i2p') and
  2629. self.server.i2pDomain):
  2630. actorAbsolute = 'http://' + self.server.i2pDomain + actor
  2631. self._redirect_headers(actorAbsolute + '/' + timelineStr +
  2632. '?page=' + str(pageNumber) +
  2633. timelineBookmark, cookie,
  2634. callingDomain)
  2635. return
  2636. self._benchmarkGETtimings(GETstartTime, GETtimings, 36)
  2637. # undo a like from the web interface icon
  2638. if htmlGET and '?unlike=' in self.path:
  2639. pageNumber = 1
  2640. likeUrl = self.path.split('?unlike=')[1]
  2641. if '?' in likeUrl:
  2642. likeUrl = likeUrl.split('?')[0]
  2643. timelineBookmark = ''
  2644. if '?bm=' in self.path:
  2645. timelineBookmark = self.path.split('?bm=')[1]
  2646. if '?' in timelineBookmark:
  2647. timelineBookmark = timelineBookmark.split('?')[0]
  2648. timelineBookmark = '#' + timelineBookmark
  2649. if '?page=' in self.path:
  2650. pageNumberStr = self.path.split('?page=')[1]
  2651. if '?' in pageNumberStr:
  2652. pageNumberStr = pageNumberStr.split('?')[0]
  2653. if '#' in pageNumberStr:
  2654. pageNumberStr = pageNumberStr.split('#')[0]
  2655. if pageNumberStr.isdigit():
  2656. pageNumber = int(pageNumberStr)
  2657. timelineStr = 'inbox'
  2658. if '?tl=' in self.path:
  2659. timelineStr = self.path.split('?tl=')[1]
  2660. if '?' in timelineStr:
  2661. timelineStr = timelineStr.split('?')[0]
  2662. actor = self.path.split('?unlike=')[0]
  2663. self.postToNickname = getNicknameFromActor(actor)
  2664. if not self.postToNickname:
  2665. print('WARN: unable to find nickname in ' + actor)
  2666. self.server.GETbusy = False
  2667. actorAbsolute = \
  2668. self.server.httpPrefix + '://' + \
  2669. self.server.domainFull + actor
  2670. if callingDomain.endswith('.onion') and \
  2671. self.server.onionDomain:
  2672. actorAbsolute = 'http://' + self.server.onionDomain + actor
  2673. elif (callingDomain.endswith('.i2p') and
  2674. self.server.onionDomain):
  2675. actorAbsolute = 'http://' + self.server.i2pDomain + actor
  2676. self._redirect_headers(actorAbsolute + '/' + timelineStr +
  2677. '?page=' + str(pageNumber), cookie,
  2678. callingDomain)
  2679. return
  2680. if not self.server.session:
  2681. print('Starting new session during undo like')
  2682. self.server.session = createSession(self.server.proxyType)
  2683. if not self.server.session:
  2684. print('ERROR: GET failed to create session ' +
  2685. 'during undo like')
  2686. self._404()
  2687. self.server.GETbusy = False
  2688. return
  2689. undoActor = \
  2690. self.server.httpPrefix + '://' + \
  2691. self.server.domainFull + '/users/' + self.postToNickname
  2692. actorLiked = self.path.split('?actor=')[1]
  2693. if '?' in actorLiked:
  2694. actorLiked = actorLiked.split('?')[0]
  2695. undoLikeJson = {
  2696. "@context": "https://www.w3.org/ns/activitystreams",
  2697. 'type': 'Undo',
  2698. 'actor': undoActor,
  2699. 'to': [actorLiked],
  2700. 'object': {
  2701. 'type': 'Like',
  2702. 'actor': undoActor,
  2703. 'to': [actorLiked],
  2704. 'object': likeUrl
  2705. }
  2706. }
  2707. # directly undo the like within the post file
  2708. likedPostFilename = locatePost(self.server.baseDir,
  2709. self.postToNickname,
  2710. self.server.domain,
  2711. likeUrl)
  2712. if likedPostFilename:
  2713. if self.server.debug:
  2714. print('Removing likes for ' + likedPostFilename)
  2715. undoLikesCollectionEntry(self.server.recentPostsCache,
  2716. self.server.baseDir,
  2717. likedPostFilename, likeUrl,
  2718. undoActor, self.server.domain,
  2719. self.server.debug)
  2720. # send out the undo like to followers
  2721. self._postToOutbox(undoLikeJson, self.server.projectVersion)
  2722. self.server.GETbusy = False
  2723. actorAbsolute = self.server.httpPrefix + '://' + \
  2724. self.server.domainFull+actor
  2725. if callingDomain.endswith('.onion') and \
  2726. self.server.onionDomain:
  2727. actorAbsolute = 'http://' + self.server.onionDomain + actor
  2728. elif (callingDomain.endswith('.i2p') and
  2729. self.server.onionDomain):
  2730. actorAbsolute = 'http://' + self.server.i2pDomain + actor
  2731. self._redirect_headers(actorAbsolute + '/' + timelineStr +
  2732. '?page=' + str(pageNumber) +
  2733. timelineBookmark, cookie,
  2734. callingDomain)
  2735. return
  2736. self._benchmarkGETtimings(GETstartTime, GETtimings, 36)
  2737. # bookmark from the web interface icon
  2738. if htmlGET and '?bookmark=' in self.path:
  2739. pageNumber = 1
  2740. bookmarkUrl = self.path.split('?bookmark=')[1]
  2741. if '?' in bookmarkUrl:
  2742. bookmarkUrl = bookmarkUrl.split('?')[0]
  2743. timelineBookmark = ''
  2744. if '?bm=' in self.path:
  2745. timelineBookmark = self.path.split('?bm=')[1]
  2746. if '?' in timelineBookmark:
  2747. timelineBookmark = timelineBookmark.split('?')[0]
  2748. timelineBookmark = '#' + timelineBookmark
  2749. actor = self.path.split('?bookmark=')[0]
  2750. if '?page=' in self.path:
  2751. pageNumberStr = self.path.split('?page=')[1]
  2752. if '?' in pageNumberStr:
  2753. pageNumberStr = pageNumberStr.split('?')[0]
  2754. if '#' in pageNumberStr:
  2755. pageNumberStr = pageNumberStr.split('#')[0]
  2756. if pageNumberStr.isdigit():
  2757. pageNumber = int(pageNumberStr)
  2758. timelineStr = 'inbox'
  2759. if '?tl=' in self.path:
  2760. timelineStr = self.path.split('?tl=')[1]
  2761. if '?' in timelineStr:
  2762. timelineStr = timelineStr.split('?')[0]
  2763. self.postToNickname = getNicknameFromActor(actor)
  2764. if not self.postToNickname:
  2765. print('WARN: unable to find nickname in ' + actor)
  2766. self.server.GETbusy = False
  2767. actorAbsolute = \
  2768. self.server.httpPrefix + '://' + \
  2769. self.server.domainFull+actor
  2770. if callingDomain.endswith('.onion') and \
  2771. self.server.onionDomain:
  2772. actorAbsolute = 'http://' + self.server.onionDomain + actor
  2773. elif (callingDomain.endswith('.i2p') and
  2774. self.server.i2pDomain):
  2775. actorAbsolute = 'http://' + self.server.i2pDomain + actor
  2776. self._redirect_headers(actorAbsolute + '/' + timelineStr +
  2777. '?page=' + str(pageNumber), cookie,
  2778. callingDomain)
  2779. return
  2780. if not self.server.session:
  2781. print('Starting new session during bookmark')
  2782. self.server.session = createSession(self.server.proxyType)
  2783. if not self.server.session:
  2784. print('ERROR: GET failed to create session ' +
  2785. 'during bookmark')
  2786. self._404()
  2787. self.server.GETbusy = False
  2788. return
  2789. bookmarkActor = \
  2790. self.server.httpPrefix + '://' + \
  2791. self.server.domainFull + '/users/' + self.postToNickname
  2792. ccList = []
  2793. bookmark(self.server.recentPostsCache,
  2794. self.server.session,
  2795. self.server.baseDir,
  2796. self.server.federationList,
  2797. self.postToNickname,
  2798. self.server.domain, self.server.port,
  2799. ccList,
  2800. self.server.httpPrefix,
  2801. bookmarkUrl, bookmarkActor, False,
  2802. self.server.sendThreads,
  2803. self.server.postLog,
  2804. self.server.personCache,
  2805. self.server.cachedWebfingers,
  2806. self.server.debug,
  2807. self.server.projectVersion)
  2808. # self._postToOutbox(bookmarkJson, self.server.projectVersion)
  2809. self.server.GETbusy = False
  2810. actorAbsolute = \
  2811. self.server.httpPrefix + '://' + self.server.domainFull + actor
  2812. if callingDomain.endswith('.onion') and \
  2813. self.server.onionDomain:
  2814. actorAbsolute = 'http://' + self.server.onionDomain + actor
  2815. elif (callingDomain.endswith('.i2p') and
  2816. self.server.i2pDomain):
  2817. actorAbsolute = 'http://' + self.server.i2pDomain + actor
  2818. self._redirect_headers(actorAbsolute + '/' + timelineStr +
  2819. '?page=' + str(pageNumber) +
  2820. timelineBookmark, cookie,
  2821. callingDomain)
  2822. return
  2823. # undo a bookmark from the web interface icon
  2824. if htmlGET and '?unbookmark=' in self.path:
  2825. pageNumber = 1
  2826. bookmarkUrl = self.path.split('?unbookmark=')[1]
  2827. if '?' in bookmarkUrl:
  2828. bookmarkUrl = bookmarkUrl.split('?')[0]
  2829. timelineBookmark = ''
  2830. if '?bm=' in self.path:
  2831. timelineBookmark = self.path.split('?bm=')[1]
  2832. if '?' in timelineBookmark:
  2833. timelineBookmark = timelineBookmark.split('?')[0]
  2834. timelineBookmark = '#' + timelineBookmark
  2835. if '?page=' in self.path:
  2836. pageNumberStr = self.path.split('?page=')[1]
  2837. if '?' in pageNumberStr:
  2838. pageNumberStr = pageNumberStr.split('?')[0]
  2839. if '#' in pageNumberStr:
  2840. pageNumberStr = pageNumberStr.split('#')[0]
  2841. if pageNumberStr.isdigit():
  2842. pageNumber = int(pageNumberStr)
  2843. timelineStr = 'inbox'
  2844. if '?tl=' in self.path:
  2845. timelineStr = self.path.split('?tl=')[1]
  2846. if '?' in timelineStr:
  2847. timelineStr = timelineStr.split('?')[0]
  2848. actor = self.path.split('?unbookmark=')[0]
  2849. self.postToNickname = getNicknameFromActor(actor)
  2850. if not self.postToNickname:
  2851. print('WARN: unable to find nickname in ' + actor)
  2852. self.server.GETbusy = False
  2853. actorAbsolute = \
  2854. self.server.httpPrefix + '://' + \
  2855. self.server.domainFull + actor
  2856. if callingDomain.endswith('.onion') and \
  2857. self.server.onionDomain:
  2858. actorAbsolute = 'http://' + \
  2859. self.server.onionDomain + actor
  2860. elif (callingDomain.endswith('.i2p') and
  2861. self.server.i2pDomain):
  2862. actorAbsolute = 'http://' + self.server.i2pDomain + actor
  2863. self._redirect_headers(actorAbsolute + '/' + timelineStr +
  2864. '?page=' + str(pageNumber), cookie,
  2865. callingDomain)
  2866. return
  2867. if not self.server.session:
  2868. print('Starting new session during undo bookmark')
  2869. self.server.session = createSession(self.server.proxyType)
  2870. if not self.server.session:
  2871. print('ERROR: GET failed to create session ' +
  2872. 'during undo bookmark')
  2873. self._404()
  2874. self.server.GETbusy = False
  2875. return
  2876. undoActor = \
  2877. self.server.httpPrefix + '://' + \
  2878. self.server.domainFull + '/users/' + self.postToNickname
  2879. ccList = []
  2880. undoBookmark(self.server.recentPostsCache,
  2881. self.server.session,
  2882. self.server.baseDir,
  2883. self.server.federationList,
  2884. self.postToNickname,
  2885. self.server.domain, self.server.port,
  2886. ccList,
  2887. self.server.httpPrefix,
  2888. bookmarkUrl, undoActor, False,
  2889. self.server.sendThreads,
  2890. self.server.postLog,
  2891. self.server.personCache,
  2892. self.server.cachedWebfingers,
  2893. self.server.debug,
  2894. self.server.projectVersion)
  2895. # self._postToOutbox(undoBookmarkJson, self.server.projectVersion)
  2896. self.server.GETbusy = False
  2897. actorAbsolute = \
  2898. self.server.httpPrefix + '://' + self.server.domainFull + actor
  2899. if callingDomain.endswith('.onion') and \
  2900. self.server.onionDomain:
  2901. actorAbsolute = 'http://' + self.server.onionDomain + actor
  2902. elif (callingDomain.endswith('.i2p') and
  2903. self.server.i2pDomain):
  2904. actorAbsolute = 'http://' + self.server.i2pDomain + actor
  2905. self._redirect_headers(actorAbsolute + '/' + timelineStr +
  2906. '?page=' + str(pageNumber) +
  2907. timelineBookmark, cookie,
  2908. callingDomain)
  2909. return
  2910. self._benchmarkGETtimings(GETstartTime, GETtimings, 37)
  2911. # delete a post from the web interface icon
  2912. if htmlGET and '?delete=' in self.path:
  2913. pageNumber = 1
  2914. if '?page=' in self.path:
  2915. pageNumberStr = self.path.split('?page=')[1]
  2916. if '?' in pageNumberStr:
  2917. pageNumberStr = pageNumberStr.split('?')[0]
  2918. if '#' in pageNumberStr:
  2919. pageNumberStr = pageNumberStr.split('#')[0]
  2920. if pageNumberStr.isdigit():
  2921. pageNumber = int(pageNumberStr)
  2922. deleteUrl = self.path.split('?delete=')[1]
  2923. if '?' in deleteUrl:
  2924. deleteUrl = deleteUrl.split('?')[0]
  2925. timelineStr = self.server.defaultTimeline
  2926. if '?tl=' in self.path:
  2927. timelineStr = self.path.split('?tl=')[1]
  2928. if '?' in timelineStr:
  2929. timelineStr = timelineStr.split('?')[0]
  2930. usersPath = self.path.split('?delete=')[0]
  2931. actor = \
  2932. self.server.httpPrefix + '://' + \
  2933. self.server.domainFull + usersPath
  2934. if self.server.allowDeletion or \
  2935. deleteUrl.startswith(actor):
  2936. if self.server.debug:
  2937. print('DEBUG: deleteUrl=' + deleteUrl)
  2938. print('DEBUG: actor=' + actor)
  2939. if actor not in deleteUrl:
  2940. # You can only delete your own posts
  2941. self.server.GETbusy = False
  2942. if callingDomain.endswith('.onion') and \
  2943. self.server.onionDomain:
  2944. actor = 'http://' + self.server.onionDomain + usersPath
  2945. elif (callingDomain.endswith('.i2p') and
  2946. self.server.i2pDomain):
  2947. actor = 'http://' + self.server.i2pDomain + usersPath
  2948. self._redirect_headers(actor + '/' + timelineStr,
  2949. cookie, callingDomain)
  2950. return
  2951. self.postToNickname = getNicknameFromActor(actor)
  2952. if not self.postToNickname:
  2953. print('WARN: unable to find nickname in ' + actor)
  2954. self.server.GETbusy = False
  2955. if callingDomain.endswith('.onion') and \
  2956. self.server.onionDomain:
  2957. actor = 'http://' + self.server.onionDomain + usersPath
  2958. elif (callingDomain.endswith('.i2p') and
  2959. self.server.i2pDomain):
  2960. actor = 'http://' + self.server.i2pDomain + usersPath
  2961. self._redirect_headers(actor + '/' + timelineStr,
  2962. cookie, callingDomain)
  2963. return
  2964. if not self.server.session:
  2965. print('Starting new session during delete')
  2966. self.server.session = createSession(self.server.proxyType)
  2967. if not self.server.session:
  2968. print('ERROR: GET failed to create session ' +
  2969. 'during delete')
  2970. self._404()
  2971. self.server.GETbusy = False
  2972. return
  2973. deleteStr = \
  2974. htmlDeletePost(self.server.recentPostsCache,
  2975. self.server.maxRecentPosts,
  2976. self.server.translate, pageNumber,
  2977. self.server.session, self.server.baseDir,
  2978. deleteUrl, self.server.httpPrefix,
  2979. __version__, self.server.cachedWebfingers,
  2980. self.server.personCache)
  2981. if deleteStr:
  2982. self._set_headers('text/html', len(deleteStr),
  2983. cookie, callingDomain)
  2984. self._write(deleteStr.encode('utf-8'))
  2985. self.server.GETbusy = False
  2986. return
  2987. self.server.GETbusy = False
  2988. if callingDomain.endswith('.onion') and \
  2989. self.server.onionDomain:
  2990. actor = 'http://' + self.server.onionDomain + usersPath
  2991. elif (callingDomain.endswith('.i2p') and
  2992. self.server.i2pDomain):
  2993. actor = 'http://' + self.server.i2pDomain + usersPath
  2994. self._redirect_headers(actor + '/' + timelineStr,
  2995. cookie, callingDomain)
  2996. return
  2997. # mute a post from the web interface icon
  2998. if htmlGET and '?mute=' in self.path:
  2999. pageNumber = 1
  3000. if '?page=' in self.path:
  3001. pageNumberStr = self.path.split('?page=')[1]
  3002. if '?' in pageNumberStr:
  3003. pageNumberStr = pageNumberStr.split('?')[0]
  3004. if '#' in pageNumberStr:
  3005. pageNumberStr = pageNumberStr.split('#')[0]
  3006. if pageNumberStr.isdigit():
  3007. pageNumber = int(pageNumberStr)
  3008. muteUrl = self.path.split('?mute=')[1]
  3009. if '?' in muteUrl:
  3010. muteUrl = muteUrl.split('?')[0]
  3011. timelineBookmark = ''
  3012. if '?bm=' in self.path:
  3013. timelineBookmark = self.path.split('?bm=')[1]
  3014. if '?' in timelineBookmark:
  3015. timelineBookmark = timelineBookmark.split('?')[0]
  3016. timelineBookmark = '#' + timelineBookmark
  3017. timelineStr = self.server.defaultTimeline
  3018. if '?tl=' in self.path:
  3019. timelineStr = self.path.split('?tl=')[1]
  3020. if '?' in timelineStr:
  3021. timelineStr = timelineStr.split('?')[0]
  3022. actor = \
  3023. self.server.httpPrefix + '://' + \
  3024. self.server.domainFull + self.path.split('?mute=')[0]
  3025. nickname = getNicknameFromActor(actor)
  3026. mutePost(self.server.baseDir, nickname, self.server.domain,
  3027. muteUrl, self.server.recentPostsCache)
  3028. self.server.GETbusy = False
  3029. if callingDomain.endswith('.onion') and \
  3030. self.server.onionDomain:
  3031. actor = \
  3032. 'http://' + self.server.onionDomain + \
  3033. self.path.split('?mute=')[0]
  3034. elif (callingDomain.endswith('.i2p') and
  3035. self.server.i2pDomain):
  3036. actor = \
  3037. 'http://' + self.server.i2pDomain + \
  3038. self.path.split('?mute=')[0]
  3039. self._redirect_headers(actor + '/' +
  3040. timelineStr + timelineBookmark,
  3041. cookie, callingDomain)
  3042. return
  3043. # unmute a post from the web interface icon
  3044. if htmlGET and '?unmute=' in self.path:
  3045. pageNumber = 1
  3046. if '?page=' in self.path:
  3047. pageNumberStr = self.path.split('?page=')[1]
  3048. if '?' in pageNumberStr:
  3049. pageNumberStr = pageNumberStr.split('?')[0]
  3050. if '#' in pageNumberStr:
  3051. pageNumberStr = pageNumberStr.split('#')[0]
  3052. if pageNumberStr.isdigit():
  3053. pageNumber = int(pageNumberStr)
  3054. muteUrl = self.path.split('?unmute=')[1]
  3055. if '?' in muteUrl:
  3056. muteUrl = muteUrl.split('?')[0]
  3057. timelineBookmark = ''
  3058. if '?bm=' in self.path:
  3059. timelineBookmark = self.path.split('?bm=')[1]
  3060. if '?' in timelineBookmark:
  3061. timelineBookmark = timelineBookmark.split('?')[0]
  3062. timelineBookmark = '#' + timelineBookmark
  3063. timelineStr = self.server.defaultTimeline
  3064. if '?tl=' in self.path:
  3065. timelineStr = self.path.split('?tl=')[1]
  3066. if '?' in timelineStr:
  3067. timelineStr = timelineStr.split('?')[0]
  3068. actor = \
  3069. self.server.httpPrefix + '://' + \
  3070. self.server.domainFull + self.path.split('?unmute=')[0]
  3071. nickname = getNicknameFromActor(actor)
  3072. unmutePost(self.server.baseDir,
  3073. nickname,
  3074. self.server.domain,
  3075. muteUrl,
  3076. self.server.recentPostsCache)
  3077. self.server.GETbusy = False
  3078. if callingDomain.endswith('.onion') and \
  3079. self.server.onionDomain:
  3080. actor = \
  3081. 'http://' + \
  3082. self.server.onionDomain + self.path.split('?unmute=')[0]
  3083. elif (callingDomain.endswith('.i2p') and
  3084. self.server.i2pDomain):
  3085. actor = \
  3086. 'http://' + \
  3087. self.server.i2pDomain + self.path.split('?unmute=')[0]
  3088. self._redirect_headers(actor + '/' + timelineStr +
  3089. timelineBookmark,
  3090. cookie, callingDomain)
  3091. return
  3092. # reply from the web interface icon
  3093. inReplyToUrl = None
  3094. # replyWithDM = False
  3095. replyToList = []
  3096. replyPageNumber = 1
  3097. shareDescription = None
  3098. # replytoActor = None
  3099. if htmlGET:
  3100. # public reply
  3101. if '?replyto=' in self.path:
  3102. inReplyToUrl = self.path.split('?replyto=')[1]
  3103. if '?' in inReplyToUrl:
  3104. mentionsList = inReplyToUrl.split('?')
  3105. for m in mentionsList:
  3106. if m.startswith('mention='):
  3107. replyHandle = m.replace('mention=', '')
  3108. if replyHandle not in replyToList:
  3109. replyToList.append(replyHandle)
  3110. if m.startswith('page='):
  3111. replyPageStr = m.replace('page=', '')
  3112. if replyPageStr.isdigit():
  3113. replyPageNumber = int(replyPageStr)
  3114. # if m.startswith('actor='):
  3115. # replytoActor = m.replace('actor=', '')
  3116. inReplyToUrl = mentionsList[0]
  3117. self.path = self.path.split('?replyto=')[0] + '/newpost'
  3118. if self.server.debug:
  3119. print('DEBUG: replyto path ' + self.path)
  3120. # reply to followers
  3121. if '?replyfollowers=' in self.path:
  3122. inReplyToUrl = self.path.split('?replyfollowers=')[1]
  3123. if '?' in inReplyToUrl:
  3124. mentionsList = inReplyToUrl.split('?')
  3125. for m in mentionsList:
  3126. if m.startswith('mention='):
  3127. replyHandle = m.replace('mention=', '')
  3128. if m.replace('mention=', '') not in replyToList:
  3129. replyToList.append(replyHandle)
  3130. if m.startswith('page='):
  3131. replyPageStr = m.replace('page=', '')
  3132. if replyPageStr.isdigit():
  3133. replyPageNumber = int(replyPageStr)
  3134. # if m.startswith('actor='):
  3135. # replytoActor = m.replace('actor=', '')
  3136. inReplyToUrl = mentionsList[0]
  3137. self.path = self.path.split('?replyfollowers=')[0] + \
  3138. '/newfollowers'
  3139. if self.server.debug:
  3140. print('DEBUG: replyfollowers path ' + self.path)
  3141. # replying as a direct message,
  3142. # for moderation posts or the dm timeline
  3143. if '?replydm=' in self.path:
  3144. inReplyToUrl = self.path.split('?replydm=')[1]
  3145. if '?' in inReplyToUrl:
  3146. mentionsList = inReplyToUrl.split('?')
  3147. for m in mentionsList:
  3148. if m.startswith('mention='):
  3149. replyHandle = m.replace('mention=', '')
  3150. if m.replace('mention=', '') not in replyToList:
  3151. replyToList.append(m.replace('mention=', ''))
  3152. if m.startswith('page='):
  3153. replyPageStr = m.replace('page=', '')
  3154. if replyPageStr.isdigit():
  3155. replyPageNumber = int(replyPageStr)
  3156. # if m.startswith('actor='):
  3157. # replytoActor = m.replace('actor=', '')
  3158. inReplyToUrl = mentionsList[0]
  3159. if inReplyToUrl.startswith('sharedesc:'):
  3160. shareDescription = \
  3161. inReplyToUrl.replace('sharedesc:', '')
  3162. shareDescription = \
  3163. urllib.parse.unquote(shareDescription.strip())
  3164. self.path = self.path.split('?replydm=')[0]+'/newdm'
  3165. if self.server.debug:
  3166. print('DEBUG: replydm path ' + self.path)
  3167. # Edit a blog post
  3168. if authorized and \
  3169. '/tlblogs' in self.path and \
  3170. '?editblogpost=' in self.path and \
  3171. '?actor=' in self.path:
  3172. messageId = self.path.split('?editblogpost=')[1]
  3173. if '?' in messageId:
  3174. messageId = messageId.split('?')[0]
  3175. actor = self.path.split('?actor=')[1]
  3176. if '?' in actor:
  3177. actor = actor.split('?')[0]
  3178. nickname = getNicknameFromActor(self.path)
  3179. if nickname == actor:
  3180. postUrl = \
  3181. self.server.httpPrefix + '://' + \
  3182. self.server.domainFull + '/users/' + nickname + \
  3183. '/statuses/' + messageId
  3184. msg = htmlEditBlog(self.server.mediaInstance,
  3185. self.server.translate,
  3186. self.server.baseDir,
  3187. self.server.httpPrefix,
  3188. self.path,
  3189. replyPageNumber,
  3190. nickname, self.server.domain,
  3191. postUrl)
  3192. if msg:
  3193. msg = msg.encode('utf-8')
  3194. self._set_headers('text/html', len(msg),
  3195. cookie, callingDomain)
  3196. self._write(msg)
  3197. self.server.GETbusy = False
  3198. return
  3199. # edit profile in web interface
  3200. if '/users/' in self.path and self.path.endswith('/editprofile'):
  3201. msg = htmlEditProfile(self.server.translate,
  3202. self.server.baseDir,
  3203. self.path, self.server.domain,
  3204. self.server.port,
  3205. self.server.httpPrefix).encode('utf-8')
  3206. if msg:
  3207. self._set_headers('text/html', len(msg),
  3208. cookie, callingDomain)
  3209. self._write(msg)
  3210. else:
  3211. self._404()
  3212. self.server.GETbusy = False
  3213. return
  3214. # Various types of new post in the web interface
  3215. if ('/users/' in self.path and
  3216. (self.path.endswith('/newpost') or
  3217. self.path.endswith('/newblog') or
  3218. self.path.endswith('/newunlisted') or
  3219. self.path.endswith('/newfollowers') or
  3220. self.path.endswith('/newdm') or
  3221. self.path.endswith('/newreminder') or
  3222. self.path.endswith('/newreport') or
  3223. self.path.endswith('/newquestion') or
  3224. self.path.endswith('/newshare'))):
  3225. nickname = getNicknameFromActor(self.path)
  3226. msg = htmlNewPost(self.server.mediaInstance,
  3227. self.server.translate,
  3228. self.server.baseDir,
  3229. self.server.httpPrefix,
  3230. self.path, inReplyToUrl,
  3231. replyToList,
  3232. shareDescription,
  3233. replyPageNumber,
  3234. nickname, self.server.domain,
  3235. self.server.domainFull).encode('utf-8')
  3236. if not msg:
  3237. print('Error replying to ' + inReplyToUrl)
  3238. self._404()
  3239. self.server.GETbusy = False
  3240. return
  3241. self._set_headers('text/html', len(msg),
  3242. cookie, callingDomain)
  3243. self._write(msg)
  3244. self.server.GETbusy = False
  3245. return
  3246. self._benchmarkGETtimings(GETstartTime, GETtimings, 38)
  3247. # get an individual post from the path /@nickname/statusnumber
  3248. if '/@' in self.path:
  3249. namedStatus = self.path.split('/@')[1]
  3250. if '/' not in namedStatus:
  3251. # show actor
  3252. nickname = namedStatus
  3253. else:
  3254. postSections = namedStatus.split('/')
  3255. if len(postSections) == 2:
  3256. nickname = postSections[0]
  3257. statusNumber = postSections[1]
  3258. if len(statusNumber) > 10 and statusNumber.isdigit():
  3259. postFilename = \
  3260. self.server.baseDir + '/accounts/' + \
  3261. nickname + '@' + \
  3262. self.server.domain + '/outbox/' + \
  3263. self.server.httpPrefix + ':##' + \
  3264. self.server.domainFull + '#users#' + \
  3265. nickname + '#statuses#' + \
  3266. statusNumber + '.json'
  3267. if os.path.isfile(postFilename):
  3268. postJsonObject = loadJson(postFilename)
  3269. loadedPost = False
  3270. if postJsonObject:
  3271. loadedPost = True
  3272. else:
  3273. postJsonObject = {}
  3274. if loadedPost:
  3275. # Only authorized viewers get to see likes
  3276. # on posts. Otherwize marketers could gain
  3277. # more social graph info
  3278. if not authorized:
  3279. pjo = postJsonObject
  3280. self._removePostInteractions(pjo)
  3281. if self._requestHTTP():
  3282. recentPostsCache = \
  3283. self.server.recentPostsCache
  3284. maxRecentPosts = \
  3285. self.server.maxRecentPosts
  3286. translate = \
  3287. self.server.translate
  3288. cachedWebfingers = \
  3289. self.server.cachedWebfingers
  3290. personCache = \
  3291. self.server.personCache
  3292. httpPrefix = \
  3293. self.server.httpPrefix
  3294. projectVersion = \
  3295. self.server.projectVersion
  3296. msg = \
  3297. htmlIndividualPost(recentPostsCache,
  3298. maxRecentPosts,
  3299. translate,
  3300. self.server.session,
  3301. cachedWebfingers,
  3302. personCache,
  3303. nickname,
  3304. self.server.domain,
  3305. self.server.port,
  3306. authorized,
  3307. postJsonObject,
  3308. httpPrefix,
  3309. projectVersion)
  3310. msg = msg.encode('utf-8')
  3311. self._set_headers('text/html', len(msg),
  3312. cookie, callingDomain)
  3313. self._write(msg)
  3314. else:
  3315. if self._fetchAuthenticated():
  3316. msg = json.dumps(postJsonObject,
  3317. ensure_ascii=False)
  3318. msg = msg.encode('utf-8')
  3319. self._set_headers('application/json',
  3320. len(msg),
  3321. None, callingDomain)
  3322. self._write(msg)
  3323. else:
  3324. self._404()
  3325. self.server.GETbusy = False
  3326. return
  3327. else:
  3328. self._404()
  3329. self.server.GETbusy = False
  3330. return
  3331. self._benchmarkGETtimings(GETstartTime, GETtimings, 39)
  3332. # get replies to a post /users/nickname/statuses/number/replies
  3333. if self.path.endswith('/replies') or '/replies?page=' in self.path:
  3334. if '/statuses/' in self.path and '/users/' in self.path:
  3335. namedStatus = self.path.split('/users/')[1]
  3336. if '/' in namedStatus:
  3337. postSections = namedStatus.split('/')
  3338. if len(postSections) >= 4:
  3339. if postSections[3].startswith('replies'):
  3340. nickname = postSections[0]
  3341. statusNumber = postSections[2]
  3342. if len(statusNumber) > 10 and \
  3343. statusNumber.isdigit():
  3344. boxname = 'outbox'
  3345. # get the replies file
  3346. postDir = \
  3347. self.server.baseDir + '/accounts/' + \
  3348. nickname + '@' + self.server.domain+'/' + \
  3349. boxname
  3350. postRepliesFilename = \
  3351. postDir + '/' + \
  3352. self.server.httpPrefix + ':##' + \
  3353. self.server.domainFull + '#users#' + \
  3354. nickname + '#statuses#' + \
  3355. statusNumber + '.replies'
  3356. if not os.path.isfile(postRepliesFilename):
  3357. # There are no replies,
  3358. # so show empty collection
  3359. contextStr = \
  3360. 'https://www.w3.org/ns/activitystreams'
  3361. firstStr = \
  3362. self.server.httpPrefix + \
  3363. '://' + self.server.domainFull + \
  3364. '/users/' + nickname + \
  3365. '/statuses/' + statusNumber + \
  3366. '/replies?page=true'
  3367. idStr = \
  3368. self.server.httpPrefix + \
  3369. '://' + self.server.domainFull + \
  3370. '/users/' + nickname + \
  3371. '/statuses/' + statusNumber + \
  3372. '/replies'
  3373. lastStr = \
  3374. self.server.httpPrefix + \
  3375. '://' + self.server.domainFull + \
  3376. '/users/' + nickname + \
  3377. '/statuses/' + statusNumber + \
  3378. '/replies?page=true'
  3379. repliesJson = {
  3380. '@context': contextStr,
  3381. 'first': firstStr,
  3382. 'id': idStr,
  3383. 'last': lastStr,
  3384. 'totalItems': 0,
  3385. 'type': 'OrderedCollection'
  3386. }
  3387. if self._requestHTTP():
  3388. if not self.server.session:
  3389. print('DEBUG: ' +
  3390. 'creating new session ' +
  3391. 'during get replies')
  3392. proxyType = \
  3393. self.server.proxyType
  3394. self.server.session = \
  3395. createSession(proxyType)
  3396. if not self.server.session:
  3397. print('ERROR: GET failed to ' +
  3398. 'create session ' +
  3399. 'during get replies')
  3400. self._404()
  3401. self.server.GETbusy = False
  3402. return
  3403. recentPostsCache = \
  3404. self.server.recentPostsCache
  3405. maxRecentPosts = \
  3406. self.server.maxRecentPosts
  3407. translate = \
  3408. self.server.translate
  3409. baseDir = \
  3410. self.server.baseDir
  3411. session = \
  3412. self.server.session
  3413. cachedWebfingers = \
  3414. self.server.cachedWebfingers
  3415. personCache = \
  3416. self.server.personCache
  3417. httpPrefix = \
  3418. self.server.httpPrefix
  3419. projectVersion = \
  3420. self.server.projectVersion
  3421. msg = \
  3422. htmlPostReplies(recentPostsCache,
  3423. maxRecentPosts,
  3424. translate,
  3425. baseDir,
  3426. session,
  3427. cachedWebfingers,
  3428. personCache,
  3429. nickname,
  3430. self.server.domain,
  3431. self.server.port,
  3432. repliesJson,
  3433. httpPrefix,
  3434. projectVersion)
  3435. msg = msg.encode('utf-8')
  3436. self._set_headers('text/html',
  3437. len(msg),
  3438. cookie,
  3439. callingDomain)
  3440. self._write(msg)
  3441. else:
  3442. if self._fetchAuthenticated():
  3443. msg = \
  3444. json.dumps(repliesJson,
  3445. ensure_ascii=False)
  3446. msg = msg.encode('utf-8')
  3447. protocolStr = 'application/json'
  3448. self._set_headers(protocolStr,
  3449. len(msg), None,
  3450. callingDomain)
  3451. self._write(msg)
  3452. else:
  3453. self._404()
  3454. self.server.GETbusy = False
  3455. return
  3456. else:
  3457. # replies exist. Itterate through the
  3458. # text file containing message ids
  3459. contextStr = \
  3460. 'https://www.w3.org/ns/activitystreams'
  3461. idStr = \
  3462. self.server.httpPrefix + \
  3463. '://' + self.server.domainFull + \
  3464. '/users/' + nickname + '/statuses/' + \
  3465. statusNumber + '?page=true'
  3466. partOfStr = \
  3467. self.server.httpPrefix + \
  3468. '://' + self.server.domainFull + \
  3469. '/users/' + nickname + \
  3470. '/statuses/' + statusNumber
  3471. repliesJson = {
  3472. '@context': contextStr,
  3473. 'id': idStr,
  3474. 'orderedItems': [
  3475. ],
  3476. 'partOf': partOfStr,
  3477. 'type': 'OrderedCollectionPage'
  3478. }
  3479. # populate the items list with replies
  3480. populateRepliesJson(self.server.baseDir,
  3481. nickname,
  3482. self.server.domain,
  3483. postRepliesFilename,
  3484. authorized,
  3485. repliesJson)
  3486. # send the replies json
  3487. if self._requestHTTP():
  3488. if not self.server.session:
  3489. print('DEBUG: ' +
  3490. 'creating new session ' +
  3491. 'during get replies 2')
  3492. proxyType = self.server.proxyType
  3493. self.server.session = \
  3494. createSession(proxyType)
  3495. if not self.server.session:
  3496. print('ERROR: GET failed to ' +
  3497. 'create session ' +
  3498. 'during get replies 2')
  3499. self._404()
  3500. self.server.GETbusy = False
  3501. return
  3502. recentPostsCache = \
  3503. self.server.recentPostsCache
  3504. maxRecentPosts = \
  3505. self.server.maxRecentPosts
  3506. translate = \
  3507. self.server.translate
  3508. baseDir = \
  3509. self.server.baseDir
  3510. session = \
  3511. self.server.session
  3512. cachedWebfingers = \
  3513. self.server.cachedWebfingers
  3514. personCache = \
  3515. self.server.personCache
  3516. httpPrefix = \
  3517. self.server.httpPrefix
  3518. projectVersion = \
  3519. self.server.projectVersion
  3520. msg = \
  3521. htmlPostReplies(recentPostsCache,
  3522. maxRecentPosts,
  3523. translate,
  3524. baseDir,
  3525. session,
  3526. cachedWebfingers,
  3527. personCache,
  3528. nickname,
  3529. self.server.domain,
  3530. self.server.port,
  3531. repliesJson,
  3532. httpPrefix,
  3533. projectVersion)
  3534. msg = msg.encode('utf-8')
  3535. self._set_headers('text/html',
  3536. len(msg),
  3537. cookie,
  3538. callingDomain)
  3539. self._write(msg)
  3540. else:
  3541. if self._fetchAuthenticated():
  3542. msg = \
  3543. json.dumps(repliesJson,
  3544. ensure_ascii=False)
  3545. msg = msg.encode('utf-8')
  3546. protocolStr = 'application/json'
  3547. self._set_headers(protocolStr,
  3548. len(msg),
  3549. None,
  3550. callingDomain)
  3551. self._write(msg)
  3552. else:
  3553. self._404()
  3554. self.server.GETbusy = False
  3555. return
  3556. self._benchmarkGETtimings(GETstartTime, GETtimings, 40)
  3557. if self.path.endswith('/roles') and '/users/' in self.path:
  3558. namedStatus = self.path.split('/users/')[1]
  3559. if '/' in namedStatus:
  3560. postSections = namedStatus.split('/')
  3561. nickname = postSections[0]
  3562. actorFilename = \
  3563. self.server.baseDir + '/accounts/' + \
  3564. nickname + '@' + self.server.domain + '.json'
  3565. if os.path.isfile(actorFilename):
  3566. actorJson = loadJson(actorFilename)
  3567. if actorJson:
  3568. if actorJson.get('roles'):
  3569. if self._requestHTTP():
  3570. getPerson = \
  3571. personLookup(self.server.domain,
  3572. self.path.replace('/roles',
  3573. ''),
  3574. self.server.baseDir)
  3575. if getPerson:
  3576. defaultTimeline = \
  3577. self.server.defaultTimeline
  3578. recentPostsCache = \
  3579. self.server.recentPostsCache
  3580. cachedWebfingers = \
  3581. self.server.cachedWebfingers
  3582. msg = \
  3583. htmlProfile(defaultTimeline,
  3584. recentPostsCache,
  3585. self.server.maxRecentPosts,
  3586. self.server.translate,
  3587. self.server.projectVersion,
  3588. self.server.baseDir,
  3589. self.server.httpPrefix,
  3590. True,
  3591. self.server.ocapAlways,
  3592. getPerson, 'roles',
  3593. self.server.session,
  3594. cachedWebfingers,
  3595. self.server.personCache,
  3596. actorJson['roles'],
  3597. None, None)
  3598. msg = msg.encode('utf-8')
  3599. self._set_headers('text/html', len(msg),
  3600. cookie, callingDomain)
  3601. self._write(msg)
  3602. else:
  3603. if self._fetchAuthenticated():
  3604. msg = json.dumps(actorJson['roles'],
  3605. ensure_ascii=False)
  3606. msg = msg.encode('utf-8')
  3607. self._set_headers('application/json',
  3608. len(msg),
  3609. None, callingDomain)
  3610. self._write(msg)
  3611. else:
  3612. self._404()
  3613. self.server.GETbusy = False
  3614. return
  3615. # show skills on the profile page
  3616. if self.path.endswith('/skills') and '/users/' in self.path:
  3617. namedStatus = self.path.split('/users/')[1]
  3618. if '/' in namedStatus:
  3619. postSections = namedStatus.split('/')
  3620. nickname = postSections[0]
  3621. actorFilename = \
  3622. self.server.baseDir + '/accounts/' + \
  3623. nickname + '@' + self.server.domain + '.json'
  3624. if os.path.isfile(actorFilename):
  3625. actorJson = loadJson(actorFilename)
  3626. if actorJson:
  3627. if actorJson.get('skills'):
  3628. if self._requestHTTP():
  3629. getPerson = \
  3630. personLookup(self.server.domain,
  3631. self.path.replace('/skills',
  3632. ''),
  3633. self.server.baseDir)
  3634. if getPerson:
  3635. defaultTimeline = \
  3636. self.server.defaultTimeline
  3637. recentPostsCache = \
  3638. self.server.recentPostsCache
  3639. cachedWebfingers = \
  3640. self.server.cachedWebfingers
  3641. msg = \
  3642. htmlProfile(defaultTimeline,
  3643. recentPostsCache,
  3644. self.server.maxRecentPosts,
  3645. self.server.translate,
  3646. self.server.projectVersion,
  3647. self.server.baseDir,
  3648. self.server.httpPrefix,
  3649. True,
  3650. self.server.ocapAlways,
  3651. getPerson, 'skills',
  3652. self.server.session,
  3653. cachedWebfingers,
  3654. self.server.personCache,
  3655. actorJson['skills'],
  3656. None, None)
  3657. msg = msg.encode('utf-8')
  3658. self._set_headers('text/html',
  3659. len(msg),
  3660. cookie,
  3661. callingDomain)
  3662. self._write(msg)
  3663. else:
  3664. if self._fetchAuthenticated():
  3665. msg = json.dumps(actorJson['skills'],
  3666. ensure_ascii=False)
  3667. msg = msg.encode('utf-8')
  3668. self._set_headers('application/json',
  3669. len(msg),
  3670. None,
  3671. callingDomain)
  3672. self._write(msg)
  3673. else:
  3674. self._404()
  3675. self.server.GETbusy = False
  3676. return
  3677. actor = self.path.replace('/skills', '')
  3678. actorAbsolute = self.server.httpPrefix + '://' + \
  3679. self.server.domainFull + actor
  3680. if callingDomain.endswith('.onion') and \
  3681. self.server.onionDomain:
  3682. actorAbsolute = 'http://' + self.server.onionDomain + actor
  3683. elif (callingDomain.endswith('.i2p') and
  3684. self.server.i2pDomain):
  3685. actorAbsolute = 'http://' + self.server.i2pDomain + actor
  3686. self._redirect_headers(actorAbsolute, cookie, callingDomain)
  3687. self.server.GETbusy = False
  3688. return
  3689. self._benchmarkGETtimings(GETstartTime, GETtimings, 41)
  3690. # get an individual post from the path
  3691. # /users/nickname/statuses/number
  3692. if '/statuses/' in self.path and '/users/' in self.path:
  3693. namedStatus = self.path.split('/users/')[1]
  3694. if '/' in namedStatus:
  3695. postSections = namedStatus.split('/')
  3696. if len(postSections) >= 3:
  3697. nickname = postSections[0]
  3698. statusNumber = postSections[2]
  3699. if len(statusNumber) > 10 and statusNumber.isdigit():
  3700. postFilename = \
  3701. self.server.baseDir + '/accounts/' + \
  3702. nickname + '@' + \
  3703. self.server.domain + '/outbox/' + \
  3704. self.server.httpPrefix + ':##' + \
  3705. self.server.domainFull + '#users#' + \
  3706. nickname + '#statuses#' + \
  3707. statusNumber + '.json'
  3708. if os.path.isfile(postFilename):
  3709. postJsonObject = loadJson(postFilename)
  3710. if not postJsonObject:
  3711. self.send_response(429)
  3712. self.end_headers()
  3713. self.server.GETbusy = False
  3714. return
  3715. else:
  3716. # Only authorized viewers get to see likes
  3717. # on posts
  3718. # Otherwize marketers could gain more social
  3719. # graph info
  3720. if not authorized:
  3721. pjo = postJsonObject
  3722. self._removePostInteractions(pjo)
  3723. if self._requestHTTP():
  3724. recentPostsCache = \
  3725. self.server.recentPostsCache
  3726. maxRecentPosts = \
  3727. self.server.maxRecentPosts
  3728. translate = \
  3729. self.server.translate
  3730. cachedWebfingers = \
  3731. self.server.cachedWebfingers
  3732. personCache = \
  3733. self.server.personCache
  3734. httpPrefix = \
  3735. self.server.httpPrefix
  3736. projectVersion = \
  3737. self.server.projectVersion
  3738. msg = \
  3739. htmlIndividualPost(recentPostsCache,
  3740. maxRecentPosts,
  3741. translate,
  3742. self.server.baseDir,
  3743. self.server.session,
  3744. cachedWebfingers,
  3745. personCache,
  3746. nickname,
  3747. self.server.domain,
  3748. self.server.port,
  3749. authorized,
  3750. postJsonObject,
  3751. httpPrefix,
  3752. projectVersion)
  3753. msg = msg.encode('utf-8')
  3754. self._set_headers('text/html',
  3755. len(msg),
  3756. cookie,
  3757. callingDomain)
  3758. self._write(msg)
  3759. else:
  3760. if self._fetchAuthenticated():
  3761. msg = json.dumps(postJsonObject,
  3762. ensure_ascii=False)
  3763. msg = msg.encode('utf-8')
  3764. self._set_headers('application/json',
  3765. len(msg),
  3766. None, callingDomain)
  3767. self._write(msg)
  3768. else:
  3769. self._404()
  3770. self.server.GETbusy = False
  3771. return
  3772. else:
  3773. self._404()
  3774. self.server.GETbusy = False
  3775. return
  3776. self._benchmarkGETtimings(GETstartTime, GETtimings, 42)
  3777. # get the inbox for a given person
  3778. if self.path.endswith('/inbox') or '/inbox?page=' in self.path:
  3779. if '/users/' in self.path:
  3780. if authorized:
  3781. inboxFeed = \
  3782. personBoxJson(self.server.recentPostsCache,
  3783. self.server.session,
  3784. self.server.baseDir,
  3785. self.server.domain,
  3786. self.server.port,
  3787. self.path,
  3788. self.server.httpPrefix,
  3789. maxPostsInFeed, 'inbox',
  3790. authorized,
  3791. self.server.ocapAlways)
  3792. if inboxFeed:
  3793. if self._requestHTTP():
  3794. nickname = self.path.replace('/users/', '')
  3795. nickname = nickname.replace('/inbox', '')
  3796. pageNumber = 1
  3797. if '?page=' in nickname:
  3798. pageNumber = nickname.split('?page=')[1]
  3799. nickname = nickname.split('?page=')[0]
  3800. if pageNumber.isdigit():
  3801. pageNumber = int(pageNumber)
  3802. else:
  3803. pageNumber = 1
  3804. if 'page=' not in self.path:
  3805. # if no page was specified then show the first
  3806. inboxFeed = \
  3807. personBoxJson(self.server.recentPostsCache,
  3808. self.server.session,
  3809. self.server.baseDir,
  3810. self.server.domain,
  3811. self.server.port,
  3812. self.path + '?page=1',
  3813. self.server.httpPrefix,
  3814. maxPostsInFeed, 'inbox',
  3815. authorized,
  3816. self.server.ocapAlways)
  3817. msg = htmlInbox(self.server.defaultTimeline,
  3818. self.server.recentPostsCache,
  3819. self.server.maxRecentPosts,
  3820. self.server.translate,
  3821. pageNumber, maxPostsInFeed,
  3822. self.server.session,
  3823. self.server.baseDir,
  3824. self.server.cachedWebfingers,
  3825. self.server.personCache,
  3826. nickname,
  3827. self.server.domain,
  3828. self.server.port,
  3829. inboxFeed,
  3830. self.server.allowDeletion,
  3831. self.server.httpPrefix,
  3832. self.server.projectVersion,
  3833. self._isMinimal(nickname))
  3834. msg = msg.encode('utf-8')
  3835. self._set_headers('text/html',
  3836. len(msg),
  3837. cookie, callingDomain)
  3838. self._write(msg)
  3839. else:
  3840. # don't need authenticated fetch here because
  3841. # there is already the authorization check
  3842. msg = json.dumps(inboxFeed, ensure_ascii=False)
  3843. msg = msg.encode('utf-8')
  3844. self._set_headers('application/json',
  3845. len(msg),
  3846. None, callingDomain)
  3847. self._write(msg)
  3848. self.server.GETbusy = False
  3849. return
  3850. else:
  3851. if self.server.debug:
  3852. nickname = self.path.replace('/users/', '')
  3853. nickname = nickname.replace('/inbox', '')
  3854. print('DEBUG: ' + nickname +
  3855. ' was not authorized to access ' + self.path)
  3856. if self.path != '/inbox':
  3857. # not the shared inbox
  3858. if self.server.debug:
  3859. print('DEBUG: GET access to inbox is unauthorized')
  3860. self.send_response(405)
  3861. self.end_headers()
  3862. self.server.GETbusy = False
  3863. return
  3864. self._benchmarkGETtimings(GETstartTime, GETtimings, 43)
  3865. # get the direct messages for a given person
  3866. if self.path.endswith('/dm') or '/dm?page=' in self.path:
  3867. if '/users/' in self.path:
  3868. if authorized:
  3869. inboxDMFeed = \
  3870. personBoxJson(self.server.recentPostsCache,
  3871. self.server.session,
  3872. self.server.baseDir,
  3873. self.server.domain,
  3874. self.server.port,
  3875. self.path,
  3876. self.server.httpPrefix,
  3877. maxPostsInFeed, 'dm',
  3878. authorized,
  3879. self.server.ocapAlways)
  3880. if inboxDMFeed:
  3881. if self._requestHTTP():
  3882. nickname = self.path.replace('/users/', '')
  3883. nickname = nickname.replace('/dm', '')
  3884. pageNumber = 1
  3885. if '?page=' in nickname:
  3886. pageNumber = nickname.split('?page=')[1]
  3887. nickname = nickname.split('?page=')[0]
  3888. if pageNumber.isdigit():
  3889. pageNumber = int(pageNumber)
  3890. else:
  3891. pageNumber = 1
  3892. if 'page=' not in self.path:
  3893. # if no page was specified then show the first
  3894. inboxDMFeed = \
  3895. personBoxJson(self.server.recentPostsCache,
  3896. self.server.session,
  3897. self.server.baseDir,
  3898. self.server.domain,
  3899. self.server.port,
  3900. self.path+'?page=1',
  3901. self.server.httpPrefix,
  3902. maxPostsInFeed, 'dm',
  3903. authorized,
  3904. self.server.ocapAlways)
  3905. msg = \
  3906. htmlInboxDMs(self.server.defaultTimeline,
  3907. self.server.recentPostsCache,
  3908. self.server.maxRecentPosts,
  3909. self.server.translate,
  3910. pageNumber, maxPostsInFeed,
  3911. self.server.session,
  3912. self.server.baseDir,
  3913. self.server.cachedWebfingers,
  3914. self.server.personCache,
  3915. nickname,
  3916. self.server.domain,
  3917. self.server.port,
  3918. inboxDMFeed,
  3919. self.server.allowDeletion,
  3920. self.server.httpPrefix,
  3921. self.server.projectVersion,
  3922. self._isMinimal(nickname))
  3923. msg = msg.encode('utf-8')
  3924. self._set_headers('text/html',
  3925. len(msg),
  3926. cookie, callingDomain)
  3927. self._write(msg)
  3928. else:
  3929. # don't need authenticated fetch here because
  3930. # there is already the authorization check
  3931. msg = json.dumps(inboxDMFeed, ensure_ascii=False)
  3932. msg = msg.encode('utf-8')
  3933. self._set_headers('application/json',
  3934. len(msg),
  3935. None, callingDomain)
  3936. self._write(msg)
  3937. self.server.GETbusy = False
  3938. return
  3939. else:
  3940. if self.server.debug:
  3941. nickname = self.path.replace('/users/', '')
  3942. nickname = nickname.replace('/dm', '')
  3943. print('DEBUG: ' + nickname +
  3944. ' was not authorized to access ' + self.path)
  3945. if self.path != '/dm':
  3946. # not the DM inbox
  3947. if self.server.debug:
  3948. print('DEBUG: GET access to inbox is unauthorized')
  3949. self.send_response(405)
  3950. self.end_headers()
  3951. self.server.GETbusy = False
  3952. return
  3953. self._benchmarkGETtimings(GETstartTime, GETtimings, 44)
  3954. # get the replies for a given person
  3955. if self.path.endswith('/tlreplies') or '/tlreplies?page=' in self.path:
  3956. if '/users/' in self.path:
  3957. if authorized:
  3958. inboxRepliesFeed = \
  3959. personBoxJson(self.server.recentPostsCache,
  3960. self.server.session,
  3961. self.server.baseDir,
  3962. self.server.domain,
  3963. self.server.port,
  3964. self.path,
  3965. self.server.httpPrefix,
  3966. maxPostsInFeed, 'tlreplies',
  3967. True, self.server.ocapAlways)
  3968. if not inboxRepliesFeed:
  3969. inboxRepliesFeed = []
  3970. if self._requestHTTP():
  3971. nickname = self.path.replace('/users/', '')
  3972. nickname = nickname.replace('/tlreplies', '')
  3973. pageNumber = 1
  3974. if '?page=' in nickname:
  3975. pageNumber = nickname.split('?page=')[1]
  3976. nickname = nickname.split('?page=')[0]
  3977. if pageNumber.isdigit():
  3978. pageNumber = int(pageNumber)
  3979. else:
  3980. pageNumber = 1
  3981. if 'page=' not in self.path:
  3982. # if no page was specified then show the first
  3983. inboxRepliesFeed = \
  3984. personBoxJson(self.server.recentPostsCache,
  3985. self.server.session,
  3986. self.server.baseDir,
  3987. self.server.domain,
  3988. self.server.port,
  3989. self.path + '?page=1',
  3990. self.server.httpPrefix,
  3991. maxPostsInFeed, 'tlreplies',
  3992. True, self.server.ocapAlways)
  3993. msg = \
  3994. htmlInboxReplies(self.server.defaultTimeline,
  3995. self.server.recentPostsCache,
  3996. self.server.maxRecentPosts,
  3997. self.server.translate,
  3998. pageNumber, maxPostsInFeed,
  3999. self.server.session,
  4000. self.server.baseDir,
  4001. self.server.cachedWebfingers,
  4002. self.server.personCache,
  4003. nickname,
  4004. self.server.domain,
  4005. self.server.port,
  4006. inboxRepliesFeed,
  4007. self.server.allowDeletion,
  4008. self.server.httpPrefix,
  4009. self.server.projectVersion,
  4010. self._isMinimal(nickname))
  4011. msg = msg.encode('utf-8')
  4012. self._set_headers('text/html',
  4013. len(msg),
  4014. cookie, callingDomain)
  4015. self._write(msg)
  4016. else:
  4017. # don't need authenticated fetch here because there is
  4018. # already the authorization check
  4019. msg = json.dumps(inboxRepliesFeed,
  4020. ensure_ascii=False)
  4021. msg = msg.encode('utf-8')
  4022. self._set_headers('application/json',
  4023. len(msg),
  4024. None, callingDomain)
  4025. self._write(msg)
  4026. self.server.GETbusy = False
  4027. return
  4028. else:
  4029. if self.server.debug:
  4030. nickname = self.path.replace('/users/', '')
  4031. nickname = nickname.replace('/tlreplies', '')
  4032. print('DEBUG: ' + nickname +
  4033. ' was not authorized to access ' + self.path)
  4034. if self.path != '/tlreplies':
  4035. # not the replies inbox
  4036. if self.server.debug:
  4037. print('DEBUG: GET access to inbox is unauthorized')
  4038. self.send_response(405)
  4039. self.end_headers()
  4040. self.server.GETbusy = False
  4041. return
  4042. self._benchmarkGETtimings(GETstartTime, GETtimings, 45)
  4043. # get the media for a given person
  4044. if self.path.endswith('/tlmedia') or '/tlmedia?page=' in self.path:
  4045. if '/users/' in self.path:
  4046. if authorized:
  4047. inboxMediaFeed = \
  4048. personBoxJson(self.server.recentPostsCache,
  4049. self.server.session,
  4050. self.server.baseDir,
  4051. self.server.domain,
  4052. self.server.port,
  4053. self.path,
  4054. self.server.httpPrefix,
  4055. maxPostsInMediaFeed, 'tlmedia',
  4056. True, self.server.ocapAlways)
  4057. if not inboxMediaFeed:
  4058. inboxMediaFeed = []
  4059. if self._requestHTTP():
  4060. nickname = self.path.replace('/users/', '')
  4061. nickname = nickname.replace('/tlmedia', '')
  4062. pageNumber = 1
  4063. if '?page=' in nickname:
  4064. pageNumber = nickname.split('?page=')[1]
  4065. nickname = nickname.split('?page=')[0]
  4066. if pageNumber.isdigit():
  4067. pageNumber = int(pageNumber)
  4068. else:
  4069. pageNumber = 1
  4070. if 'page=' not in self.path:
  4071. # if no page was specified then show the first
  4072. inboxMediaFeed = \
  4073. personBoxJson(self.server.recentPostsCache,
  4074. self.server.session,
  4075. self.server.baseDir,
  4076. self.server.domain,
  4077. self.server.port,
  4078. self.path + '?page=1',
  4079. self.server.httpPrefix,
  4080. maxPostsInMediaFeed, 'tlmedia',
  4081. True, self.server.ocapAlways)
  4082. msg = \
  4083. htmlInboxMedia(self.server.defaultTimeline,
  4084. self.server.recentPostsCache,
  4085. self.server.maxRecentPosts,
  4086. self.server.translate,
  4087. pageNumber, maxPostsInMediaFeed,
  4088. self.server.session,
  4089. self.server.baseDir,
  4090. self.server.cachedWebfingers,
  4091. self.server.personCache,
  4092. nickname,
  4093. self.server.domain,
  4094. self.server.port,
  4095. inboxMediaFeed,
  4096. self.server.allowDeletion,
  4097. self.server.httpPrefix,
  4098. self.server.projectVersion,
  4099. self._isMinimal(nickname))
  4100. msg = msg.encode('utf-8')
  4101. self._set_headers('text/html',
  4102. len(msg),
  4103. cookie, callingDomain)
  4104. self._write(msg)
  4105. else:
  4106. # don't need authenticated fetch here because there is
  4107. # already the authorization check
  4108. msg = json.dumps(inboxMediaFeed,
  4109. ensure_ascii=False)
  4110. msg = msg.encode('utf-8')
  4111. self._set_headers('application/json',
  4112. len(msg),
  4113. None, callingDomain)
  4114. self._write(msg)
  4115. self.server.GETbusy = False
  4116. return
  4117. else:
  4118. if self.server.debug:
  4119. nickname = self.path.replace('/users/', '')
  4120. nickname = nickname.replace('/tlmedia', '')
  4121. print('DEBUG: ' + nickname +
  4122. ' was not authorized to access ' + self.path)
  4123. if self.path != '/tlmedia':
  4124. # not the media inbox
  4125. if self.server.debug:
  4126. print('DEBUG: GET access to inbox is unauthorized')
  4127. self.send_response(405)
  4128. self.end_headers()
  4129. self.server.GETbusy = False
  4130. return
  4131. # get the blogs for a given person
  4132. if self.path.endswith('/tlblogs') or '/tlblogs?page=' in self.path:
  4133. if '/users/' in self.path:
  4134. if authorized:
  4135. inboxBlogsFeed = \
  4136. personBoxJson(self.server.recentPostsCache,
  4137. self.server.session,
  4138. self.server.baseDir,
  4139. self.server.domain,
  4140. self.server.port,
  4141. self.path,
  4142. self.server.httpPrefix,
  4143. maxPostsInBlogsFeed, 'tlblogs',
  4144. True, self.server.ocapAlways)
  4145. if not inboxBlogsFeed:
  4146. inboxBlogsFeed = []
  4147. if self._requestHTTP():
  4148. nickname = self.path.replace('/users/', '')
  4149. nickname = nickname.replace('/tlblogs', '')
  4150. pageNumber = 1
  4151. if '?page=' in nickname:
  4152. pageNumber = nickname.split('?page=')[1]
  4153. nickname = nickname.split('?page=')[0]
  4154. if pageNumber.isdigit():
  4155. pageNumber = int(pageNumber)
  4156. else:
  4157. pageNumber = 1
  4158. if 'page=' not in self.path:
  4159. # if no page was specified then show the first
  4160. inboxBlogsFeed = \
  4161. personBoxJson(self.server.recentPostsCache,
  4162. self.server.session,
  4163. self.server.baseDir,
  4164. self.server.domain,
  4165. self.server.port,
  4166. self.path + '?page=1',
  4167. self.server.httpPrefix,
  4168. maxPostsInBlogsFeed, 'tlblogs',
  4169. True, self.server.ocapAlways)
  4170. msg = \
  4171. htmlInboxBlogs(self.server.defaultTimeline,
  4172. self.server.recentPostsCache,
  4173. self.server.maxRecentPosts,
  4174. self.server.translate,
  4175. pageNumber, maxPostsInBlogsFeed,
  4176. self.server.session,
  4177. self.server.baseDir,
  4178. self.server.cachedWebfingers,
  4179. self.server.personCache,
  4180. nickname,
  4181. self.server.domain,
  4182. self.server.port,
  4183. inboxBlogsFeed,
  4184. self.server.allowDeletion,
  4185. self.server.httpPrefix,
  4186. self.server.projectVersion,
  4187. self._isMinimal(nickname))
  4188. msg = msg.encode('utf-8')
  4189. self._set_headers('text/html',
  4190. len(msg),
  4191. cookie, callingDomain)
  4192. self._write(msg)
  4193. else:
  4194. # don't need authenticated fetch here because there is
  4195. # already the authorization check
  4196. msg = json.dumps(inboxBlogsFeed,
  4197. ensure_ascii=False)
  4198. msg = msg.encode('utf-8')
  4199. self._set_headers('application/json',
  4200. len(msg),
  4201. None, callingDomain)
  4202. self._write(msg)
  4203. self.server.GETbusy = False
  4204. return
  4205. else:
  4206. if self.server.debug:
  4207. nickname = self.path.replace('/users/', '')
  4208. nickname = nickname.replace('/tlblogs', '')
  4209. print('DEBUG: ' + nickname +
  4210. ' was not authorized to access ' + self.path)
  4211. if self.path != '/tlblogs':
  4212. # not the blogs inbox
  4213. if self.server.debug:
  4214. print('DEBUG: GET access to blogs is unauthorized')
  4215. self.send_response(405)
  4216. self.end_headers()
  4217. self.server.GETbusy = False
  4218. return
  4219. self._benchmarkGETtimings(GETstartTime, GETtimings, 46)
  4220. # get the shared items timeline for a given person
  4221. if self.path.endswith('/tlshares') or '/tlshares?page=' in self.path:
  4222. if '/users/' in self.path:
  4223. if authorized:
  4224. if self._requestHTTP():
  4225. nickname = self.path.replace('/users/', '')
  4226. nickname = nickname.replace('/tlshares', '')
  4227. pageNumber = 1
  4228. if '?page=' in nickname:
  4229. pageNumber = nickname.split('?page=')[1]
  4230. nickname = nickname.split('?page=')[0]
  4231. if pageNumber.isdigit():
  4232. pageNumber = int(pageNumber)
  4233. else:
  4234. pageNumber = 1
  4235. msg = \
  4236. htmlShares(self.server.defaultTimeline,
  4237. self.server.recentPostsCache,
  4238. self.server.maxRecentPosts,
  4239. self.server.translate,
  4240. pageNumber, maxPostsInFeed,
  4241. self.server.session,
  4242. self.server.baseDir,
  4243. self.server.cachedWebfingers,
  4244. self.server.personCache,
  4245. nickname,
  4246. self.server.domain,
  4247. self.server.port,
  4248. self.server.allowDeletion,
  4249. self.server.httpPrefix,
  4250. self.server.projectVersion)
  4251. msg = msg.encode('utf-8')
  4252. self._set_headers('text/html',
  4253. len(msg),
  4254. cookie, callingDomain)
  4255. self._write(msg)
  4256. self.server.GETbusy = False
  4257. return
  4258. # not the shares timeline
  4259. if self.server.debug:
  4260. print('DEBUG: GET access to shares timeline is unauthorized')
  4261. self.send_response(405)
  4262. self.end_headers()
  4263. self.server.GETbusy = False
  4264. return
  4265. # get the bookmarks for a given person
  4266. if self.path.endswith('/tlbookmarks') or \
  4267. '/tlbookmarks?page=' in self.path or \
  4268. self.path.endswith('/bookmarks') or \
  4269. '/bookmarks?page=' in self.path:
  4270. if '/users/' in self.path:
  4271. if authorized:
  4272. bookmarksFeed = \
  4273. personBoxJson(self.server.recentPostsCache,
  4274. self.server.session,
  4275. self.server.baseDir,
  4276. self.server.domain,
  4277. self.server.port,
  4278. self.path,
  4279. self.server.httpPrefix,
  4280. maxPostsInFeed, 'tlbookmarks',
  4281. authorized, self.server.ocapAlways)
  4282. if bookmarksFeed:
  4283. if self._requestHTTP():
  4284. nickname = self.path.replace('/users/', '')
  4285. nickname = nickname.replace('/tlbookmarks', '')
  4286. nickname = nickname.replace('/bookmarks', '')
  4287. pageNumber = 1
  4288. if '?page=' in nickname:
  4289. pageNumber = nickname.split('?page=')[1]
  4290. nickname = nickname.split('?page=')[0]
  4291. if pageNumber.isdigit():
  4292. pageNumber = int(pageNumber)
  4293. else:
  4294. pageNumber = 1
  4295. if 'page=' not in self.path:
  4296. # if no page was specified then show the first
  4297. bookmarksFeed = \
  4298. personBoxJson(self.server.recentPostsCache,
  4299. self.server.session,
  4300. self.server.baseDir,
  4301. self.server.domain,
  4302. self.server.port,
  4303. self.path + '?page=1',
  4304. self.server.httpPrefix,
  4305. maxPostsInFeed,
  4306. 'tlbookmarks',
  4307. authorized,
  4308. self.server.ocapAlways)
  4309. msg = \
  4310. htmlBookmarks(self.server.defaultTimeline,
  4311. self.server.recentPostsCache,
  4312. self.server.maxRecentPosts,
  4313. self.server.translate,
  4314. pageNumber, maxPostsInFeed,
  4315. self.server.session,
  4316. self.server.baseDir,
  4317. self.server.cachedWebfingers,
  4318. self.server.personCache,
  4319. nickname,
  4320. self.server.domain,
  4321. self.server.port,
  4322. bookmarksFeed,
  4323. self.server.allowDeletion,
  4324. self.server.httpPrefix,
  4325. self.server.projectVersion,
  4326. self._isMinimal(nickname))
  4327. msg = msg.encode('utf-8')
  4328. self._set_headers('text/html',
  4329. len(msg),
  4330. cookie, callingDomain)
  4331. self._write(msg)
  4332. else:
  4333. # don't need authenticated fetch here because
  4334. # there is already the authorization check
  4335. msg = json.dumps(inboxFeed,
  4336. ensure_ascii=False)
  4337. msg = msg.encode('utf-8')
  4338. self._set_headers('application/json',
  4339. len(msg),
  4340. None, callingDomain)
  4341. self._write(msg)
  4342. self.server.GETbusy = False
  4343. return
  4344. else:
  4345. if self.server.debug:
  4346. nickname = self.path.replace('/users/', '')
  4347. nickname = nickname.replace('/tlbookmarks', '')
  4348. nickname = nickname.replace('/bookmarks', '')
  4349. print('DEBUG: ' + nickname +
  4350. ' was not authorized to access ' + self.path)
  4351. if self.server.debug:
  4352. print('DEBUG: GET access to bookmarks is unauthorized')
  4353. self.send_response(405)
  4354. self.end_headers()
  4355. self.server.GETbusy = False
  4356. return
  4357. self._benchmarkGETtimings(GETstartTime, GETtimings, 47)
  4358. # get outbox feed for a person
  4359. outboxFeed = \
  4360. personBoxJson(self.server.recentPostsCache,
  4361. self.server.session,
  4362. self.server.baseDir, self.server.domain,
  4363. self.server.port, self.path,
  4364. self.server.httpPrefix,
  4365. maxPostsInFeed, 'outbox',
  4366. authorized,
  4367. self.server.ocapAlways)
  4368. if outboxFeed:
  4369. if self._requestHTTP():
  4370. nickname = \
  4371. self.path.replace('/users/', '').replace('/outbox', '')
  4372. pageNumber = 1
  4373. if '?page=' in nickname:
  4374. pageNumber = nickname.split('?page=')[1]
  4375. nickname = nickname.split('?page=')[0]
  4376. if pageNumber.isdigit():
  4377. pageNumber = int(pageNumber)
  4378. else:
  4379. pageNumber = 1
  4380. if 'page=' not in self.path:
  4381. # if a page wasn't specified then show the first one
  4382. outboxFeed = \
  4383. personBoxJson(self.server.recentPostsCache,
  4384. self.server.session,
  4385. self.server.baseDir,
  4386. self.server.domain,
  4387. self.server.port,
  4388. self.path + '?page=1',
  4389. self.server.httpPrefix,
  4390. maxPostsInFeed, 'outbox',
  4391. authorized,
  4392. self.server.ocapAlways)
  4393. msg = \
  4394. htmlOutbox(self.server.defaultTimeline,
  4395. self.server.recentPostsCache,
  4396. self.server.maxRecentPosts,
  4397. self.server.translate,
  4398. pageNumber, maxPostsInFeed,
  4399. self.server.session,
  4400. self.server.baseDir,
  4401. self.server.cachedWebfingers,
  4402. self.server.personCache,
  4403. nickname,
  4404. self.server.domain,
  4405. self.server.port,
  4406. outboxFeed,
  4407. self.server.allowDeletion,
  4408. self.server.httpPrefix,
  4409. self.server.projectVersion,
  4410. self._isMinimal(nickname))
  4411. msg = msg.encode('utf-8')
  4412. self._set_headers('text/html',
  4413. len(msg),
  4414. cookie, callingDomain)
  4415. self._write(msg)
  4416. else:
  4417. if self._fetchAuthenticated():
  4418. msg = json.dumps(outboxFeed,
  4419. ensure_ascii=False)
  4420. msg = msg.encode('utf-8')
  4421. self._set_headers('application/json',
  4422. len(msg),
  4423. None, callingDomain)
  4424. self._write(msg)
  4425. else:
  4426. self._404()
  4427. self.server.GETbusy = False
  4428. return
  4429. self._benchmarkGETtimings(GETstartTime, GETtimings, 48)
  4430. # get the moderation feed for a moderator
  4431. if self.path.endswith('/moderation') or \
  4432. '/moderation?page=' in self.path:
  4433. if '/users/' in self.path:
  4434. if authorized:
  4435. moderationFeed = \
  4436. personBoxJson(self.server.recentPostsCache,
  4437. self.server.session,
  4438. self.server.baseDir,
  4439. self.server.domain,
  4440. self.server.port,
  4441. self.path,
  4442. self.server.httpPrefix,
  4443. maxPostsInFeed, 'moderation',
  4444. True, self.server.ocapAlways)
  4445. if moderationFeed:
  4446. if self._requestHTTP():
  4447. nickname = self.path.replace('/users/', '')
  4448. nickname = nickname.replace('/moderation', '')
  4449. pageNumber = 1
  4450. if '?page=' in nickname:
  4451. pageNumber = nickname.split('?page=')[1]
  4452. nickname = nickname.split('?page=')[0]
  4453. if pageNumber.isdigit():
  4454. pageNumber = int(pageNumber)
  4455. else:
  4456. pageNumber = 1
  4457. if 'page=' not in self.path:
  4458. # if no page was specified then show the first
  4459. moderationFeed = \
  4460. personBoxJson(self.server.recentPostsCache,
  4461. self.server.session,
  4462. self.server.baseDir,
  4463. self.server.domain,
  4464. self.server.port,
  4465. self.path + '?page=1',
  4466. self.server.httpPrefix,
  4467. maxPostsInFeed, 'moderation',
  4468. True, self.server.ocapAlways)
  4469. msg = \
  4470. htmlModeration(self.server.defaultTimeline,
  4471. self.server.recentPostsCache,
  4472. self.server.maxRecentPosts,
  4473. self.server.translate,
  4474. pageNumber, maxPostsInFeed,
  4475. self.server.session,
  4476. self.server.baseDir,
  4477. self.server.cachedWebfingers,
  4478. self.server.personCache,
  4479. nickname,
  4480. self.server.domain,
  4481. self.server.port,
  4482. moderationFeed,
  4483. True,
  4484. self.server.httpPrefix,
  4485. self.server.projectVersion)
  4486. msg = msg.encode('utf-8')
  4487. self._set_headers('text/html',
  4488. len(msg),
  4489. cookie, callingDomain)
  4490. self._write(msg)
  4491. else:
  4492. # don't need authenticated fetch here because
  4493. # there is already the authorization check
  4494. msg = json.dumps(moderationFeed,
  4495. ensure_ascii=False)
  4496. msg = msg.encode('utf-8')
  4497. self._set_headers('application/json',
  4498. len(msg),
  4499. None, callingDomain)
  4500. self._write(msg)
  4501. self.server.GETbusy = False
  4502. return
  4503. else:
  4504. if self.server.debug:
  4505. nickname = self.path.replace('/users/', '')
  4506. nickname = nickname.replace('/moderation', '')
  4507. print('DEBUG: ' + nickname +
  4508. ' was not authorized to access ' + self.path)
  4509. if self.server.debug:
  4510. print('DEBUG: GET access to moderation feed is unauthorized')
  4511. self.send_response(405)
  4512. self.end_headers()
  4513. self.server.GETbusy = False
  4514. return
  4515. self._benchmarkGETtimings(GETstartTime, GETtimings, 49)
  4516. shares = \
  4517. getSharesFeedForPerson(self.server.baseDir,
  4518. self.server.domain,
  4519. self.server.port, self.path,
  4520. self.server.httpPrefix,
  4521. sharesPerPage)
  4522. if shares:
  4523. if self._requestHTTP():
  4524. pageNumber = 1
  4525. if '?page=' not in self.path:
  4526. searchPath = self.path
  4527. # get a page of shares, not the summary
  4528. shares = \
  4529. getSharesFeedForPerson(self.server.baseDir,
  4530. self.server.domain,
  4531. self.server.port,
  4532. self.path + '?page=true',
  4533. self.server.httpPrefix,
  4534. sharesPerPage)
  4535. else:
  4536. pageNumberStr = self.path.split('?page=')[1]
  4537. if '#' in pageNumberStr:
  4538. pageNumberStr = pageNumberStr.split('#')[0]
  4539. if pageNumberStr.isdigit():
  4540. pageNumber = int(pageNumberStr)
  4541. searchPath = self.path.split('?page=')[0]
  4542. getPerson = \
  4543. personLookup(self.server.domain,
  4544. searchPath.replace('/shares', ''),
  4545. self.server.baseDir)
  4546. if getPerson:
  4547. if not self.server.session:
  4548. print('Starting new session during profile')
  4549. self.server.session = \
  4550. createSession(self.server.proxyType)
  4551. if not self.server.session:
  4552. print('ERROR: GET failed to create session ' +
  4553. 'during profile')
  4554. self._404()
  4555. self.server.GETbusy = False
  4556. return
  4557. msg = \
  4558. htmlProfile(self.server.defaultTimeline,
  4559. self.server.recentPostsCache,
  4560. self.server.maxRecentPosts,
  4561. self.server.translate,
  4562. self.server.projectVersion,
  4563. self.server.baseDir,
  4564. self.server.httpPrefix,
  4565. authorized,
  4566. self.server.ocapAlways,
  4567. getPerson, 'shares',
  4568. self.server.session,
  4569. self.server.cachedWebfingers,
  4570. self.server.personCache,
  4571. shares,
  4572. pageNumber, sharesPerPage)
  4573. msg = msg.encode('utf-8')
  4574. self._set_headers('text/html',
  4575. len(msg),
  4576. cookie, callingDomain)
  4577. self._write(msg)
  4578. self.server.GETbusy = False
  4579. return
  4580. else:
  4581. if self._fetchAuthenticated():
  4582. msg = json.dumps(shares,
  4583. ensure_ascii=False)
  4584. msg = msg.encode('utf-8')
  4585. self._set_headers('application/json',
  4586. len(msg),
  4587. None, callingDomain)
  4588. self._write(msg)
  4589. else:
  4590. self._404()
  4591. self.server.GETbusy = False
  4592. return
  4593. self._benchmarkGETtimings(GETstartTime, GETtimings, 50)
  4594. following = \
  4595. getFollowingFeed(self.server.baseDir, self.server.domain,
  4596. self.server.port, self.path,
  4597. self.server.httpPrefix,
  4598. authorized, followsPerPage)
  4599. if following:
  4600. if self._requestHTTP():
  4601. pageNumber = 1
  4602. if '?page=' not in self.path:
  4603. searchPath = self.path
  4604. # get a page of following, not the summary
  4605. following = \
  4606. getFollowingFeed(self.server.baseDir,
  4607. self.server.domain,
  4608. self.server.port,
  4609. self.path + '?page=true',
  4610. self.server.httpPrefix,
  4611. authorized, followsPerPage)
  4612. else:
  4613. pageNumberStr = self.path.split('?page=')[1]
  4614. if '#' in pageNumberStr:
  4615. pageNumberStr = pageNumberStr.split('#')[0]
  4616. if pageNumberStr.isdigit():
  4617. pageNumber = int(pageNumberStr)
  4618. searchPath = self.path.split('?page=')[0]
  4619. getPerson = \
  4620. personLookup(self.server.domain,
  4621. searchPath.replace('/following', ''),
  4622. self.server.baseDir)
  4623. if getPerson:
  4624. if not self.server.session:
  4625. print('Starting new session during following')
  4626. self.server.session = \
  4627. createSession(self.server.proxyType)
  4628. if not self.server.session:
  4629. print('ERROR: GET failed to create session ' +
  4630. 'during following')
  4631. self._404()
  4632. self.server.GETbusy = False
  4633. return
  4634. msg = \
  4635. htmlProfile(self.server.defaultTimeline,
  4636. self.server.recentPostsCache,
  4637. self.server.maxRecentPosts,
  4638. self.server.translate,
  4639. self.server.projectVersion,
  4640. self.server.baseDir,
  4641. self.server.httpPrefix,
  4642. authorized,
  4643. self.server.ocapAlways,
  4644. getPerson, 'following',
  4645. self.server.session,
  4646. self.server.cachedWebfingers,
  4647. self.server.personCache,
  4648. following,
  4649. pageNumber,
  4650. followsPerPage).encode('utf-8')
  4651. self._set_headers('text/html',
  4652. len(msg), cookie, callingDomain)
  4653. self._write(msg)
  4654. self.server.GETbusy = False
  4655. return
  4656. else:
  4657. if self._fetchAuthenticated():
  4658. msg = json.dumps(following,
  4659. ensure_ascii=False).encode('utf-8')
  4660. self._set_headers('application/json',
  4661. len(msg),
  4662. None, callingDomain)
  4663. self._write(msg)
  4664. else:
  4665. self._404()
  4666. self.server.GETbusy = False
  4667. return
  4668. self._benchmarkGETtimings(GETstartTime, GETtimings, 51)
  4669. followers = \
  4670. getFollowingFeed(self.server.baseDir, self.server.domain,
  4671. self.server.port, self.path,
  4672. self.server.httpPrefix,
  4673. authorized, followsPerPage, 'followers')
  4674. if followers:
  4675. if self._requestHTTP():
  4676. pageNumber = 1
  4677. if '?page=' not in self.path:
  4678. searchPath = self.path
  4679. # get a page of followers, not the summary
  4680. followers = \
  4681. getFollowingFeed(self.server.baseDir,
  4682. self.server.domain,
  4683. self.server.port,
  4684. self.path + '?page=1',
  4685. self.server.httpPrefix,
  4686. authorized, followsPerPage,
  4687. 'followers')
  4688. else:
  4689. pageNumberStr = self.path.split('?page=')[1]
  4690. if '#' in pageNumberStr:
  4691. pageNumberStr = pageNumberStr.split('#')[0]
  4692. if pageNumberStr.isdigit():
  4693. pageNumber = int(pageNumberStr)
  4694. searchPath = self.path.split('?page=')[0]
  4695. getPerson = \
  4696. personLookup(self.server.domain,
  4697. searchPath.replace('/followers', ''),
  4698. self.server.baseDir)
  4699. if getPerson:
  4700. if not self.server.session:
  4701. print('Starting new session during following2')
  4702. self.server.session = \
  4703. createSession(self.server.proxyType)
  4704. if not self.server.session:
  4705. print('ERROR: GET failed to create session ' +
  4706. 'during following2')
  4707. self._404()
  4708. self.server.GETbusy = False
  4709. return
  4710. msg = \
  4711. htmlProfile(self.server.defaultTimeline,
  4712. self.server.recentPostsCache,
  4713. self.server.maxRecentPosts,
  4714. self.server.translate,
  4715. self.server.projectVersion,
  4716. self.server.baseDir,
  4717. self.server.httpPrefix,
  4718. authorized,
  4719. self.server.ocapAlways,
  4720. getPerson, 'followers',
  4721. self.server.session,
  4722. self.server.cachedWebfingers,
  4723. self.server.personCache,
  4724. followers,
  4725. pageNumber,
  4726. followsPerPage).encode('utf-8')
  4727. self._set_headers('text/html',
  4728. len(msg),
  4729. cookie, callingDomain)
  4730. self._write(msg)
  4731. self.server.GETbusy = False
  4732. return
  4733. else:
  4734. if self._fetchAuthenticated():
  4735. msg = json.dumps(followers,
  4736. ensure_ascii=False).encode('utf-8')
  4737. self._set_headers('application/json',
  4738. len(msg),
  4739. None, callingDomain)
  4740. self._write(msg)
  4741. else:
  4742. self._404()
  4743. self.server.GETbusy = False
  4744. return
  4745. self._benchmarkGETtimings(GETstartTime, GETtimings, 52)
  4746. # look up a person
  4747. getPerson = \
  4748. personLookup(self.server.domain, self.path,
  4749. self.server.baseDir)
  4750. if getPerson:
  4751. if self._requestHTTP():
  4752. if not self.server.session:
  4753. print('Starting new session during person lookup')
  4754. self.server.session = \
  4755. createSession(self.server.proxyType)
  4756. if not self.server.session:
  4757. print('ERROR: GET failed to create session ' +
  4758. 'during person lookup')
  4759. self._404()
  4760. self.server.GETbusy = False
  4761. return
  4762. msg = \
  4763. htmlProfile(self.server.defaultTimeline,
  4764. self.server.recentPostsCache,
  4765. self.server.maxRecentPosts,
  4766. self.server.translate,
  4767. self.server.projectVersion,
  4768. self.server.baseDir,
  4769. self.server.httpPrefix,
  4770. authorized,
  4771. self.server.ocapAlways,
  4772. getPerson, 'posts',
  4773. self.server.session,
  4774. self.server.cachedWebfingers,
  4775. self.server.personCache,
  4776. None, None).encode('utf-8')
  4777. self._set_headers('text/html',
  4778. len(msg),
  4779. cookie, callingDomain)
  4780. self._write(msg)
  4781. else:
  4782. if self._fetchAuthenticated():
  4783. msg = json.dumps(getPerson,
  4784. ensure_ascii=False).encode('utf-8')
  4785. self._set_headers('application/json',
  4786. len(msg),
  4787. None, callingDomain)
  4788. self._write(msg)
  4789. else:
  4790. self._404()
  4791. self.server.GETbusy = False
  4792. return
  4793. self._benchmarkGETtimings(GETstartTime, GETtimings, 53)
  4794. # check that a json file was requested
  4795. if not self.path.endswith('.json'):
  4796. if self.server.debug:
  4797. print('DEBUG: GET Not json: ' + self.path +
  4798. ' ' + self.server.baseDir)
  4799. self._404()
  4800. self.server.GETbusy = False
  4801. return
  4802. if not self._fetchAuthenticated():
  4803. if self.server.debug:
  4804. print('WARN: Unauthenticated GET')
  4805. self._404()
  4806. self.server.GETbusy = False
  4807. return
  4808. self._benchmarkGETtimings(GETstartTime, GETtimings, 54)
  4809. # check that the file exists
  4810. filename = self.server.baseDir + self.path
  4811. if os.path.isfile(filename):
  4812. with open(filename, 'r', encoding='utf-8') as File:
  4813. content = File.read()
  4814. contentJson = json.loads(content)
  4815. msg = json.dumps(contentJson,
  4816. ensure_ascii=False).encode('utf-8')
  4817. self._set_headers('application/json',
  4818. len(msg),
  4819. None, callingDomain)
  4820. self._write(msg)
  4821. else:
  4822. if self.server.debug:
  4823. print('DEBUG: GET Unknown file')
  4824. self._404()
  4825. self.server.GETbusy = False
  4826. self._benchmarkGETtimings(GETstartTime, GETtimings, 55)
  4827. def do_HEAD(self):
  4828. callingDomain = self.server.domainFull
  4829. if self.headers.get('Host'):
  4830. callingDomain = self.headers['Host']
  4831. if self.server.onionDomain:
  4832. if callingDomain != self.server.domain and \
  4833. callingDomain != self.server.domainFull and \
  4834. callingDomain != self.server.onionDomain:
  4835. print('HEAD domain blocked: ' + callingDomain)
  4836. self._400()
  4837. return
  4838. else:
  4839. if callingDomain != self.server.domain and \
  4840. callingDomain != self.server.domainFull:
  4841. print('HEAD domain blocked: ' + callingDomain)
  4842. self._400()
  4843. return
  4844. checkPath = self.path
  4845. etag = None
  4846. fileLength = -1
  4847. if '/media/' in self.path:
  4848. if self._pathIsImage() or \
  4849. self._pathIsVideo() or \
  4850. self._pathIsAudio():
  4851. mediaStr = self.path.split('/media/')[1]
  4852. mediaFilename = \
  4853. self.server.baseDir + '/media/' + mediaStr
  4854. if os.path.isfile(mediaFilename):
  4855. checkPath = mediaFilename
  4856. fileLength = os.path.getsize(mediaFilename)
  4857. mediaTagFilename = mediaFilename + '.etag'
  4858. if os.path.isfile(mediaTagFilename):
  4859. try:
  4860. with open(mediaTagFilename, 'r') as etagFile:
  4861. etag = etagFile.read()
  4862. except BaseException:
  4863. pass
  4864. else:
  4865. with open(mediaFilename, 'rb') as avFile:
  4866. mediaBinary = avFile.read()
  4867. etag = sha1(mediaBinary).hexdigest() # nosec
  4868. try:
  4869. with open(mediaTagFilename, 'w') as etagFile:
  4870. etagFile.write(etag)
  4871. except BaseException:
  4872. pass
  4873. mediaFileType = 'application/json'
  4874. if checkPath.endswith('.png'):
  4875. mediaFileType = 'image/png'
  4876. elif checkPath.endswith('.jpg'):
  4877. mediaFileType = 'image/jpeg'
  4878. elif checkPath.endswith('.gif'):
  4879. mediaFileType = 'image/gif'
  4880. elif checkPath.endswith('.webp'):
  4881. mediaFileType = 'image/webp'
  4882. elif checkPath.endswith('.mp4'):
  4883. mediaFileType = 'video/mp4'
  4884. elif checkPath.endswith('.ogv'):
  4885. mediaFileType = 'video/ogv'
  4886. elif checkPath.endswith('.mp3'):
  4887. mediaFileType = 'audio/mpeg'
  4888. elif checkPath.endswith('.ogg'):
  4889. mediaFileType = 'audio/ogg'
  4890. self._set_headers_head(mediaFileType, fileLength,
  4891. etag, callingDomain)
  4892. def _receiveNewPostProcess(self, postType: str, path: str, headers: {},
  4893. length: int, postBytes, boundary: str) -> int:
  4894. # Note: this needs to happen synchronously
  4895. # 0=this is not a new post
  4896. # 1=new post success
  4897. # -1=new post failed
  4898. # 2=new post canceled
  4899. if self.server.debug:
  4900. print('DEBUG: receiving POST')
  4901. if ' boundary=' in headers['Content-Type']:
  4902. if self.server.debug:
  4903. print('DEBUG: receiving POST headers ' +
  4904. headers['Content-Type'])
  4905. nickname = None
  4906. nicknameStr = path.split('/users/')[1]
  4907. if '/' in nicknameStr:
  4908. nickname = nicknameStr.split('/')[0]
  4909. else:
  4910. return -1
  4911. length = int(headers['Content-Length'])
  4912. if length > self.server.maxPostLength:
  4913. print('POST size too large')
  4914. return -1
  4915. boundary = headers['Content-Type'].split('boundary=')[1]
  4916. if ';' in boundary:
  4917. boundary = boundary.split(';')[0]
  4918. # Note: we don't use cgi here because it's due to be deprecated
  4919. # in Python 3.8/3.10
  4920. # Instead we use the multipart mime parser from the email module
  4921. if self.server.debug:
  4922. print('DEBUG: extracting media from POST')
  4923. mediaBytes, postBytes = \
  4924. extractMediaInFormPOST(postBytes, boundary, 'attachpic')
  4925. if self.server.debug:
  4926. if mediaBytes:
  4927. print('DEBUG: media was found. ' +
  4928. str(len(mediaBytes)) + ' bytes')
  4929. else:
  4930. print('DEBUG: no media was found in POST')
  4931. # Note: a .temp extension is used here so that at no time is
  4932. # an image with metadata publicly exposed, even for a few mS
  4933. filenameBase = \
  4934. self.server.baseDir + '/accounts/' + \
  4935. nickname + '@' + self.server.domain + '/upload.temp'
  4936. filename, attachmentMediaType = \
  4937. saveMediaInFormPOST(mediaBytes, self.server.debug,
  4938. filenameBase)
  4939. if self.server.debug:
  4940. if filename:
  4941. print('DEBUG: POST media filename is ' + filename)
  4942. else:
  4943. print('DEBUG: no media filename in POST')
  4944. if filename:
  4945. if filename.endswith('.png') or \
  4946. filename.endswith('.jpg') or \
  4947. filename.endswith('.webp') or \
  4948. filename.endswith('.gif'):
  4949. if self.server.debug:
  4950. print('DEBUG: POST media removing metadata')
  4951. postImageFilename = filename.replace('.temp', '')
  4952. removeMetaData(filename, postImageFilename)
  4953. if os.path.isfile(postImageFilename):
  4954. print('POST media saved to ' + postImageFilename)
  4955. else:
  4956. print('ERROR: POST media could not be saved to ' +
  4957. postImageFilename)
  4958. else:
  4959. if os.path.isfile(filename):
  4960. os.rename(filename, filename.replace('.temp', ''))
  4961. fields = \
  4962. extractTextFieldsInPOST(postBytes, boundary,
  4963. self.server.debug)
  4964. if self.server.debug:
  4965. if fields:
  4966. print('DEBUG: text field extracted from POST ' +
  4967. str(fields))
  4968. else:
  4969. print('WARN: no text fields could be extracted from POST')
  4970. # process the received text fields from the POST
  4971. if not fields.get('message') and \
  4972. not fields.get('imageDescription'):
  4973. return -1
  4974. if fields.get('submitPost'):
  4975. if fields['submitPost'] != 'Submit':
  4976. return -1
  4977. else:
  4978. return 2
  4979. if not fields.get('imageDescription'):
  4980. fields['imageDescription'] = None
  4981. if not fields.get('subject'):
  4982. fields['subject'] = None
  4983. if not fields.get('replyTo'):
  4984. fields['replyTo'] = None
  4985. if not fields.get('schedulePost'):
  4986. fields['schedulePost'] = False
  4987. else:
  4988. fields['schedulePost'] = True
  4989. print('DEBUG: shedulePost ' + str(fields['schedulePost']))
  4990. if not fields.get('eventDate'):
  4991. fields['eventDate'] = None
  4992. if not fields.get('eventTime'):
  4993. fields['eventTime'] = None
  4994. if not fields.get('location'):
  4995. fields['location'] = None
  4996. # Store a file which contains the time in seconds
  4997. # since epoch when an attempt to post something was made.
  4998. # This is then used for active monthly users counts
  4999. lastUsedFilename = \
  5000. self.server.baseDir + '/accounts/' + \
  5001. nickname + '@' + self.server.domain + '/.lastUsed'
  5002. try:
  5003. lastUsedFile = open(lastUsedFilename, 'w')
  5004. if lastUsedFile:
  5005. lastUsedFile.write(str(int(time.time())))
  5006. lastUsedFile.close()
  5007. except BaseException:
  5008. pass
  5009. mentionsStr = ''
  5010. if fields.get('mentions'):
  5011. mentionsStr = fields['mentions'].strip() + ' '
  5012. if postType == 'newpost':
  5013. messageJson = \
  5014. createPublicPost(self.server.baseDir,
  5015. nickname,
  5016. self.server.domain,
  5017. self.server.port,
  5018. self.server.httpPrefix,
  5019. mentionsStr + fields['message'],
  5020. False, False, False,
  5021. filename, attachmentMediaType,
  5022. fields['imageDescription'],
  5023. self.server.useBlurHash,
  5024. fields['replyTo'], fields['replyTo'],
  5025. fields['subject'], fields['schedulePost'],
  5026. fields['eventDate'], fields['eventTime'],
  5027. fields['location'])
  5028. if messageJson:
  5029. if fields['schedulePost']:
  5030. return 1
  5031. if self._postToOutbox(messageJson, __version__, nickname):
  5032. populateReplies(self.server.baseDir,
  5033. self.server.httpPrefix,
  5034. self.server.domainFull,
  5035. messageJson,
  5036. self.server.maxReplies,
  5037. self.server.debug)
  5038. return 1
  5039. else:
  5040. return -1
  5041. elif postType == 'newblog':
  5042. messageJson = \
  5043. createBlogPost(self.server.baseDir, nickname,
  5044. self.server.domain, self.server.port,
  5045. self.server.httpPrefix,
  5046. fields['message'],
  5047. False, False, False,
  5048. filename, attachmentMediaType,
  5049. fields['imageDescription'],
  5050. self.server.useBlurHash,
  5051. fields['replyTo'], fields['replyTo'],
  5052. fields['subject'], fields['schedulePost'],
  5053. fields['eventDate'], fields['eventTime'],
  5054. fields['location'])
  5055. if messageJson:
  5056. if fields['schedulePost']:
  5057. return 1
  5058. if self._postToOutbox(messageJson, __version__, nickname):
  5059. populateReplies(self.server.baseDir,
  5060. self.server.httpPrefix,
  5061. self.server.domainFull,
  5062. messageJson,
  5063. self.server.maxReplies,
  5064. self.server.debug)
  5065. return 1
  5066. else:
  5067. return -1
  5068. elif postType == 'editblogpost':
  5069. print('Edited blog post received')
  5070. postFilename = \
  5071. locatePost(self.server.baseDir,
  5072. nickname, self.server.domain,
  5073. fields['postUrl'])
  5074. if os.path.isfile(postFilename):
  5075. postJsonObject = loadJson(postFilename)
  5076. if postJsonObject:
  5077. cachedFilename = \
  5078. self.server.baseDir + '/accounts/' + \
  5079. nickname + '@' + self.server.domain + \
  5080. '/postcache/' + \
  5081. fields['postUrl'].replace('/', '#') + '.html'
  5082. if os.path.isfile(cachedFilename):
  5083. print('Edited blog post, removing cached html')
  5084. try:
  5085. os.remove(cachedFilename)
  5086. except BaseException:
  5087. pass
  5088. # remove from memory cache
  5089. removePostFromCache(postJsonObject,
  5090. self.server.recentPostsCache)
  5091. # change the blog post title
  5092. postJsonObject['object']['summary'] = fields['subject']
  5093. # format message
  5094. tags = []
  5095. hashtagsDict = {}
  5096. mentionedRecipients = []
  5097. fields['message'] = \
  5098. addHtmlTags(self.server.baseDir,
  5099. self.server.httpPrefix,
  5100. nickname, self.server.domain,
  5101. fields['message'],
  5102. mentionedRecipients,
  5103. hashtagsDict, True)
  5104. # replace emoji with unicode
  5105. tags = []
  5106. for tagName, tag in hashtagsDict.items():
  5107. tags.append(tag)
  5108. # get list of tags
  5109. fields['message'] = \
  5110. replaceEmojiFromTags(fields['message'],
  5111. tags, 'content')
  5112. postJsonObject['object']['content'] = fields['message']
  5113. imgDescription = ''
  5114. if fields.get('imageDescription'):
  5115. imgDescription = fields['imageDescription']
  5116. if filename:
  5117. postJsonObject['object'] = \
  5118. attachMedia(self.server.baseDir,
  5119. self.server.httpPrefix,
  5120. self.server.domain,
  5121. self.server.port,
  5122. postJsonObject['object'],
  5123. filename,
  5124. attachmentMediaType,
  5125. imgDescription,
  5126. self.server.useBlurHash)
  5127. replaceYouTube(postJsonObject)
  5128. saveJson(postJsonObject, postFilename)
  5129. print('Edited blog post, resaved ' + postFilename)
  5130. return 1
  5131. else:
  5132. print('Edited blog post, unable to load json for ' +
  5133. postFilename)
  5134. else:
  5135. print('Edited blog post not found ' +
  5136. str(fields['postUrl']))
  5137. return -1
  5138. elif postType == 'newunlisted':
  5139. messageJson = \
  5140. createUnlistedPost(self.server.baseDir,
  5141. nickname,
  5142. self.server.domain, self.server.port,
  5143. self.server.httpPrefix,
  5144. mentionsStr + fields['message'],
  5145. False, False, False,
  5146. filename, attachmentMediaType,
  5147. fields['imageDescription'],
  5148. self.server.useBlurHash,
  5149. fields['replyTo'],
  5150. fields['replyTo'],
  5151. fields['subject'],
  5152. fields['schedulePost'],
  5153. fields['eventDate'],
  5154. fields['eventTime'],
  5155. fields['location'])
  5156. if messageJson:
  5157. if fields['schedulePost']:
  5158. return 1
  5159. if self._postToOutbox(messageJson, __version__, nickname):
  5160. populateReplies(self.server.baseDir,
  5161. self.server.httpPrefix,
  5162. self.server.domain,
  5163. messageJson,
  5164. self.server.maxReplies,
  5165. self.server.debug)
  5166. return 1
  5167. else:
  5168. return -1
  5169. elif postType == 'newfollowers':
  5170. messageJson = \
  5171. createFollowersOnlyPost(self.server.baseDir,
  5172. nickname,
  5173. self.server.domain,
  5174. self.server.port,
  5175. self.server.httpPrefix,
  5176. mentionsStr + fields['message'],
  5177. True, False, False,
  5178. filename, attachmentMediaType,
  5179. fields['imageDescription'],
  5180. self.server.useBlurHash,
  5181. fields['replyTo'],
  5182. fields['replyTo'],
  5183. fields['subject'],
  5184. fields['schedulePost'],
  5185. fields['eventDate'],
  5186. fields['eventTime'],
  5187. fields['location'])
  5188. if messageJson:
  5189. if fields['schedulePost']:
  5190. return 1
  5191. if self._postToOutbox(messageJson, __version__, nickname):
  5192. populateReplies(self.server.baseDir,
  5193. self.server.httpPrefix,
  5194. self.server.domain,
  5195. messageJson,
  5196. self.server.maxReplies,
  5197. self.server.debug)
  5198. return 1
  5199. else:
  5200. return -1
  5201. elif postType == 'newdm':
  5202. messageJson = None
  5203. print('A DM was posted')
  5204. if '@' in mentionsStr:
  5205. messageJson = \
  5206. createDirectMessagePost(self.server.baseDir,
  5207. nickname,
  5208. self.server.domain,
  5209. self.server.port,
  5210. self.server.httpPrefix,
  5211. mentionsStr +
  5212. fields['message'],
  5213. True, False, False,
  5214. filename, attachmentMediaType,
  5215. fields['imageDescription'],
  5216. self.server.useBlurHash,
  5217. fields['replyTo'],
  5218. fields['replyTo'],
  5219. fields['subject'],
  5220. True, fields['schedulePost'],
  5221. fields['eventDate'],
  5222. fields['eventTime'],
  5223. fields['location'])
  5224. if messageJson:
  5225. if fields['schedulePost']:
  5226. return 1
  5227. # if self.server.debug:
  5228. print('DEBUG: new DM to ' +
  5229. str(messageJson['object']['to']))
  5230. if self._postToOutbox(messageJson, __version__, nickname):
  5231. populateReplies(self.server.baseDir,
  5232. self.server.httpPrefix,
  5233. self.server.domain,
  5234. messageJson,
  5235. self.server.maxReplies,
  5236. self.server.debug)
  5237. return 1
  5238. else:
  5239. return -1
  5240. elif postType == 'newreminder':
  5241. messageJson = None
  5242. handle = nickname + '@' + self.server.domainFull
  5243. print('A reminder was posted for ' + handle)
  5244. if '@' + handle not in mentionsStr:
  5245. mentionsStr = '@' + handle + ' ' + mentionsStr
  5246. messageJson = \
  5247. createDirectMessagePost(self.server.baseDir,
  5248. nickname,
  5249. self.server.domain,
  5250. self.server.port,
  5251. self.server.httpPrefix,
  5252. mentionsStr + fields['message'],
  5253. True, False, False,
  5254. filename, attachmentMediaType,
  5255. fields['imageDescription'],
  5256. self.server.useBlurHash,
  5257. None, None,
  5258. fields['subject'],
  5259. True, fields['schedulePost'],
  5260. fields['eventDate'],
  5261. fields['eventTime'],
  5262. fields['location'])
  5263. if messageJson:
  5264. if fields['schedulePost']:
  5265. return 1
  5266. print('DEBUG: new reminder to ' +
  5267. str(messageJson['object']['to']))
  5268. if self._postToOutbox(messageJson, __version__, nickname):
  5269. return 1
  5270. else:
  5271. return -1
  5272. elif postType == 'newreport':
  5273. if attachmentMediaType:
  5274. if attachmentMediaType != 'image':
  5275. return -1
  5276. # So as to be sure that this only goes to moderators
  5277. # and not accounts being reported we disable any
  5278. # included fediverse addresses by replacing '@' with '-at-'
  5279. fields['message'] = fields['message'].replace('@', '-at-')
  5280. messageJson = \
  5281. createReportPost(self.server.baseDir,
  5282. nickname,
  5283. self.server.domain, self.server.port,
  5284. self.server.httpPrefix,
  5285. mentionsStr + fields['message'],
  5286. True, False, False,
  5287. filename, attachmentMediaType,
  5288. fields['imageDescription'],
  5289. self.server.useBlurHash,
  5290. self.server.debug, fields['subject'])
  5291. if messageJson:
  5292. if self._postToOutbox(messageJson, __version__, nickname):
  5293. return 1
  5294. else:
  5295. return -1
  5296. elif postType == 'newquestion':
  5297. if not fields.get('duration'):
  5298. return -1
  5299. if not fields.get('message'):
  5300. return -1
  5301. # questionStr = fields['message']
  5302. qOptions = []
  5303. for questionCtr in range(8):
  5304. if fields.get('questionOption' + str(questionCtr)):
  5305. qOptions.append(fields['questionOption' +
  5306. str(questionCtr)])
  5307. if not qOptions:
  5308. return -1
  5309. messageJson = \
  5310. createQuestionPost(self.server.baseDir,
  5311. nickname,
  5312. self.server.domain,
  5313. self.server.port,
  5314. self.server.httpPrefix,
  5315. fields['message'], qOptions,
  5316. False, False, False,
  5317. filename, attachmentMediaType,
  5318. fields['imageDescription'],
  5319. self.server.useBlurHash,
  5320. fields['subject'],
  5321. int(fields['duration']))
  5322. if messageJson:
  5323. if self.server.debug:
  5324. print('DEBUG: new Question')
  5325. if self._postToOutbox(messageJson, __version__, nickname):
  5326. return 1
  5327. return -1
  5328. elif postType == 'newshare':
  5329. if not fields.get('itemType'):
  5330. return -1
  5331. if not fields.get('category'):
  5332. return -1
  5333. if not fields.get('location'):
  5334. return -1
  5335. if not fields.get('duration'):
  5336. return -1
  5337. if attachmentMediaType:
  5338. if attachmentMediaType != 'image':
  5339. return -1
  5340. durationStr = fields['duration']
  5341. if durationStr:
  5342. if ' ' not in durationStr:
  5343. durationStr = durationStr + ' days'
  5344. addShare(self.server.baseDir,
  5345. self.server.httpPrefix,
  5346. nickname,
  5347. self.server.domain, self.server.port,
  5348. fields['subject'],
  5349. fields['message'],
  5350. filename,
  5351. fields['itemType'],
  5352. fields['category'],
  5353. fields['location'],
  5354. durationStr,
  5355. self.server.debug)
  5356. if filename:
  5357. if os.path.isfile(filename):
  5358. os.remove(filename)
  5359. self.postToNickname = nickname
  5360. return 1
  5361. return -1
  5362. def _receiveNewPost(self, postType: str, path: str) -> int:
  5363. """A new post has been created
  5364. This creates a thread to send the new post
  5365. """
  5366. pageNumber = 1
  5367. if '/users/' not in path:
  5368. print('Not receiving new post for ' + path +
  5369. ' because /users/ not in path')
  5370. return None
  5371. if '?' + postType + '?' not in path:
  5372. print('Not receiving new post for ' + path +
  5373. ' because ?' + postType + '? not in path')
  5374. return None
  5375. print('New post begins: ' + postType + ' ' + path)
  5376. if '?page=' in path:
  5377. pageNumberStr = path.split('?page=')[1]
  5378. if '?' in pageNumberStr:
  5379. pageNumberStr = pageNumberStr.split('?')[0]
  5380. if '#' in pageNumberStr:
  5381. pageNumberStr = pageNumberStr.split('#')[0]
  5382. if pageNumberStr.isdigit():
  5383. pageNumber = int(pageNumberStr)
  5384. path = path.split('?page=')[0]
  5385. # get the username who posted
  5386. newPostThreadName = None
  5387. if '/users/' in path:
  5388. newPostThreadName = path.split('/users/')[1]
  5389. if '/' in newPostThreadName:
  5390. newPostThreadName = newPostThreadName.split('/')[0]
  5391. if not newPostThreadName:
  5392. newPostThreadName = '*'
  5393. if self.server.newPostThread.get(newPostThreadName):
  5394. print('Waiting for previous new post thread to end')
  5395. waitCtr = 0
  5396. while (self.server.newPostThread[newPostThreadName].isAlive() and
  5397. waitCtr < 8):
  5398. time.sleep(1)
  5399. waitCtr += 1
  5400. if waitCtr >= 8:
  5401. print('Killing previous new post thread for ' +
  5402. newPostThreadName)
  5403. self.server.newPostThread[newPostThreadName].kill()
  5404. # make a copy of self.headers
  5405. headers = {}
  5406. headersWithoutCookie = {}
  5407. for dictEntryName, headerLine in self.headers.items():
  5408. headers[dictEntryName] = headerLine
  5409. if dictEntryName.lower() != 'cookie':
  5410. headersWithoutCookie[dictEntryName] = headerLine
  5411. print('New post headers: ' + str(headersWithoutCookie))
  5412. length = int(headers['Content-Length'])
  5413. if length > self.server.maxPostLength:
  5414. print('POST size too large')
  5415. return None
  5416. if not headers.get('Content-Type'):
  5417. if headers.get('Content-type'):
  5418. headers['Content-Type'] = headers['Content-type']
  5419. elif headers.get('content-type'):
  5420. headers['Content-Type'] = headers['content-type']
  5421. if headers.get('Content-Type'):
  5422. if ' boundary=' in headers['Content-Type']:
  5423. boundary = headers['Content-Type'].split('boundary=')[1]
  5424. if ';' in boundary:
  5425. boundary = boundary.split(';')[0]
  5426. try:
  5427. postBytes = self.rfile.read(length)
  5428. except SocketError as e:
  5429. if e.errno == errno.ECONNRESET:
  5430. print('WARN: POST postBytes ' +
  5431. 'connection reset by peer')
  5432. else:
  5433. print('WARN: POST postBytes socket error')
  5434. return None
  5435. except ValueError as e:
  5436. print('ERROR: POST postBytes rfile.read failed')
  5437. print(e)
  5438. return None
  5439. # second length check from the bytes received
  5440. # since Content-Length could be untruthful
  5441. length = len(postBytes)
  5442. if length > self.server.maxPostLength:
  5443. print('POST size too large')
  5444. return None
  5445. # Note sending new posts needs to be synchronous,
  5446. # otherwise any attachments can get mangled if
  5447. # other events happen during their decoding
  5448. print('Creating new post from: ' + newPostThreadName)
  5449. self._receiveNewPostProcess(postType,
  5450. path, headers, length,
  5451. postBytes, boundary)
  5452. return pageNumber
  5453. def do_POST(self):
  5454. POSTstartTime = time.time()
  5455. POSTtimings = []
  5456. if not self.server.session:
  5457. print('Starting new session from POST')
  5458. self.server.session = \
  5459. createSession(self.server.proxyType)
  5460. if not self.server.session:
  5461. print('ERROR: POST failed to create session during POST')
  5462. self._404()
  5463. return
  5464. if self.server.debug:
  5465. print('DEBUG: POST to ' + self.server.baseDir +
  5466. ' path: ' + self.path + ' busy: ' +
  5467. str(self.server.POSTbusy))
  5468. if self.server.POSTbusy:
  5469. currTimePOST = int(time.time())
  5470. if currTimePOST - self.server.lastPOST == 0:
  5471. self.send_response(429)
  5472. self.end_headers()
  5473. return
  5474. self.server.lastPOST = currTimePOST
  5475. callingDomain = self.server.domainFull
  5476. if self.headers.get('Host'):
  5477. callingDomain = self.headers['Host']
  5478. if self.server.onionDomain:
  5479. if callingDomain != self.server.domain and \
  5480. callingDomain != self.server.domainFull and \
  5481. callingDomain != self.server.onionDomain:
  5482. print('POST domain blocked: ' + callingDomain)
  5483. self._400()
  5484. return
  5485. else:
  5486. if callingDomain != self.server.domain and \
  5487. callingDomain != self.server.domainFull:
  5488. print('POST domain blocked: ' + callingDomain)
  5489. self._400()
  5490. return
  5491. self.server.POSTbusy = True
  5492. if not self.headers.get('Content-type'):
  5493. print('Content-type header missing')
  5494. self._400()
  5495. self.server.POSTbusy = False
  5496. return
  5497. # remove any trailing slashes from the path
  5498. if not self.path.endswith('confirm'):
  5499. self.path = self.path.replace('/outbox/', '/outbox')
  5500. self.path = self.path.replace('/tlblogs/', '/tlblogs')
  5501. self.path = self.path.replace('/inbox/', '/inbox')
  5502. self.path = self.path.replace('/shares/', '/shares')
  5503. self.path = self.path.replace('/sharedInbox/', '/sharedInbox')
  5504. if self.path == '/inbox':
  5505. if not self.server.enableSharedInbox:
  5506. self._503()
  5507. self.server.POSTbusy = False
  5508. return
  5509. cookie = None
  5510. if self.headers.get('Cookie'):
  5511. cookie = self.headers['Cookie']
  5512. # check authorization
  5513. authorized = self._isAuthorized()
  5514. if self.server.debug:
  5515. if authorized:
  5516. print('POST Authorization granted')
  5517. else:
  5518. print('POST Not authorized')
  5519. print(str(self.headers))
  5520. # if this is a POST to the outbox then check authentication
  5521. self.outboxAuthenticated = False
  5522. self.postToNickname = None
  5523. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 1)
  5524. if self.path.startswith('/login'):
  5525. # get the contents of POST containing login credentials
  5526. length = int(self.headers['Content-length'])
  5527. if length > 512:
  5528. print('Login failed - credentials too long')
  5529. self.send_response(401)
  5530. self.end_headers()
  5531. self.server.POSTbusy = False
  5532. return
  5533. try:
  5534. loginParams = self.rfile.read(length).decode('utf-8')
  5535. except SocketError as e:
  5536. if e.errno == errno.ECONNRESET:
  5537. print('WARN: POST login read ' +
  5538. 'connection reset by peer')
  5539. else:
  5540. print('WARN: POST login read socket error')
  5541. self.send_response(400)
  5542. self.end_headers()
  5543. self.server.POSTbusy = False
  5544. return
  5545. except ValueError as e:
  5546. print('ERROR: POST login read failed')
  5547. print(e)
  5548. self.send_response(400)
  5549. self.end_headers()
  5550. self.server.POSTbusy = False
  5551. return
  5552. loginNickname, loginPassword, register = \
  5553. htmlGetLoginCredentials(loginParams, self.server.lastLoginTime)
  5554. if loginNickname:
  5555. self.server.lastLoginTime = int(time.time())
  5556. if register:
  5557. if not registerAccount(self.server.baseDir,
  5558. self.server.httpPrefix,
  5559. self.server.domain,
  5560. self.server.port,
  5561. loginNickname, loginPassword):
  5562. self.server.POSTbusy = False
  5563. if callingDomain.endswith('.onion') and \
  5564. self.server.onionDomain:
  5565. self._redirect_headers('http://' +
  5566. self.server.onionDomain +
  5567. '/login',
  5568. cookie, callingDomain)
  5569. elif (callingDomain.endswith('.i2p') and
  5570. self.server.i2pDomain):
  5571. self._redirect_headers('http://' +
  5572. self.server.i2pDomain +
  5573. '/login',
  5574. cookie, callingDomain)
  5575. else:
  5576. self._redirect_headers(self.server.httpPrefix +
  5577. '://' +
  5578. self.server.domainFull +
  5579. '/login',
  5580. cookie, callingDomain)
  5581. return
  5582. authHeader = createBasicAuthHeader(loginNickname,
  5583. loginPassword)
  5584. if not authorizeBasic(self.server.baseDir, '/users/' +
  5585. loginNickname + '/outbox',
  5586. authHeader, False):
  5587. print('Login failed: ' + loginNickname)
  5588. self._clearLoginDetails(loginNickname, callingDomain)
  5589. self.server.POSTbusy = False
  5590. return
  5591. else:
  5592. if isSuspended(self.server.baseDir, loginNickname):
  5593. msg = \
  5594. htmlSuspended(self.server.baseDir).encode('utf-8')
  5595. self._login_headers('text/html',
  5596. len(msg), callingDomain)
  5597. self._write(msg)
  5598. self.server.POSTbusy = False
  5599. return
  5600. # login success - redirect with authorization
  5601. print('Login success: ' + loginNickname)
  5602. # re-activate account if needed
  5603. activateAccount(self.server.baseDir, loginNickname,
  5604. self.server.domain)
  5605. # This produces a deterministic token based
  5606. # on nick+password+salt
  5607. saltFilename = \
  5608. self.server.baseDir+'/accounts/' + \
  5609. loginNickname + '@' + self.server.domain + '/.salt'
  5610. salt = createPassword(32)
  5611. if os.path.isfile(saltFilename):
  5612. try:
  5613. with open(saltFilename, 'r') as fp:
  5614. salt = fp.read()
  5615. except Exception as e:
  5616. print('WARN: Unable to read salt for ' +
  5617. loginNickname + ' ' + str(e))
  5618. else:
  5619. try:
  5620. with open(saltFilename, 'w') as fp:
  5621. fp.write(salt)
  5622. except Exception as e:
  5623. print('WARN: Unable to save salt for ' +
  5624. loginNickname + ' ' + str(e))
  5625. tokenText = loginNickname + loginPassword + salt
  5626. token = sha256(tokenText.encode('utf-8')).hexdigest()
  5627. self.server.tokens[loginNickname] = token
  5628. loginHandle = loginNickname + '@' + self.server.domain
  5629. tokenFilename = \
  5630. self.server.baseDir+'/accounts/' + \
  5631. loginHandle + '/.token'
  5632. try:
  5633. with open(tokenFilename, 'w') as fp:
  5634. fp.write(token)
  5635. except Exception as e:
  5636. print('WARN: Unable to save token for ' +
  5637. loginNickname + ' ' + str(e))
  5638. personUpgradeActor(self.server.baseDir, None, loginHandle,
  5639. self.server.baseDir + '/accounts/' +
  5640. loginHandle + '.json')
  5641. index = self.server.tokens[loginNickname]
  5642. self.server.tokensLookup[index] = loginNickname
  5643. cookieStr = 'SET:epicyon=' + \
  5644. self.server.tokens[loginNickname] + '; SameSite=Strict'
  5645. if callingDomain.endswith('.onion') and \
  5646. self.server.onionDomain:
  5647. self._redirect_headers('http://' +
  5648. self.server.onionDomain +
  5649. '/users/' +
  5650. loginNickname + '/' +
  5651. self.server.defaultTimeline,
  5652. cookieStr, callingDomain)
  5653. elif (callingDomain.endswith('.i2p') and
  5654. self.server.i2pDomain):
  5655. self._redirect_headers('http://' +
  5656. self.server.i2pDomain +
  5657. '/users/' +
  5658. loginNickname + '/' +
  5659. self.server.defaultTimeline,
  5660. cookieStr, callingDomain)
  5661. else:
  5662. self._redirect_headers(self.server.httpPrefix+'://' +
  5663. self.server.domainFull +
  5664. '/users/' +
  5665. loginNickname + '/' +
  5666. self.server.defaultTimeline,
  5667. cookieStr, callingDomain)
  5668. self.server.POSTbusy = False
  5669. return
  5670. self._200()
  5671. self.server.POSTbusy = False
  5672. return
  5673. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 2)
  5674. # update of profile/avatar from web interface
  5675. if authorized and self.path.endswith('/profiledata'):
  5676. usersPath = self.path.replace('/profiledata', '')
  5677. usersPath = usersPath.replace('/editprofile', '')
  5678. actorStr = \
  5679. self.server.httpPrefix + '://' + \
  5680. self.server.domainFull + usersPath
  5681. if ' boundary=' in self.headers['Content-type']:
  5682. boundary = self.headers['Content-type'].split('boundary=')[1]
  5683. if ';' in boundary:
  5684. boundary = boundary.split(';')[0]
  5685. nickname = getNicknameFromActor(actorStr)
  5686. if not nickname:
  5687. if callingDomain.endswith('.onion') and \
  5688. self.server.onionDomain:
  5689. actorStr = \
  5690. 'http://' + self.server.onionDomain + usersPath
  5691. elif (callingDomain.endswith('.i2p') and
  5692. self.server.i2pDomain):
  5693. actorStr = \
  5694. 'http://' + self.server.i2pDomain + usersPath
  5695. print('WARN: nickname not found in ' + actorStr)
  5696. self._redirect_headers(actorStr, cookie, callingDomain)
  5697. self.server.POSTbusy = False
  5698. return
  5699. length = int(self.headers['Content-length'])
  5700. if length > self.server.maxPostLength:
  5701. if callingDomain.endswith('.onion') and \
  5702. self.server.onionDomain:
  5703. actorStr = \
  5704. 'http://' + self.server.onionDomain + usersPath
  5705. elif (callingDomain.endswith('.i2p') and
  5706. self.server.i2pDomain):
  5707. actorStr = \
  5708. 'http://' + self.server.i2pDomain + usersPath
  5709. print('Maximum profile data length exceeded ' +
  5710. str(length))
  5711. self._redirect_headers(actorStr, cookie, callingDomain)
  5712. self.server.POSTbusy = False
  5713. return
  5714. try:
  5715. # read the bytes of the http form POST
  5716. postBytes = self.rfile.read(length)
  5717. except SocketError as e:
  5718. if e.errno == errno.ECONNRESET:
  5719. print('WARN: connection was reset while ' +
  5720. 'reading bytes from http form POST')
  5721. else:
  5722. print('WARN: error while reading bytes ' +
  5723. 'from http form POST')
  5724. self.send_response(400)
  5725. self.end_headers()
  5726. self.server.POSTbusy = False
  5727. return
  5728. except ValueError as e:
  5729. print('ERROR: failed to read bytes for POST')
  5730. print(e)
  5731. self.send_response(400)
  5732. self.end_headers()
  5733. self.server.POSTbusy = False
  5734. return
  5735. # extract each image type
  5736. actorChanged = True
  5737. profileMediaTypes = ('avatar', 'image',
  5738. 'banner', 'search_banner',
  5739. 'instanceLogo')
  5740. profileMediaTypesUploaded = {}
  5741. for mType in profileMediaTypes:
  5742. if self.server.debug:
  5743. print('DEBUG: profile update extracting ' + mType +
  5744. ' image or font from POST')
  5745. mediaBytes, postBytes = \
  5746. extractMediaInFormPOST(postBytes, boundary, mType)
  5747. if mediaBytes:
  5748. if self.server.debug:
  5749. print('DEBUG: profile update ' + mType +
  5750. ' image or font was found. ' +
  5751. str(len(mediaBytes)) + ' bytes')
  5752. else:
  5753. if self.server.debug:
  5754. print('DEBUG: profile update, no ' + mType +
  5755. ' image or font was found in POST')
  5756. continue
  5757. # Note: a .temp extension is used here so that at no
  5758. # time is an image with metadata publicly exposed,
  5759. # even for a few mS
  5760. if mType == 'instanceLogo':
  5761. filenameBase = \
  5762. self.server.baseDir + '/accounts/login.temp'
  5763. else:
  5764. filenameBase = \
  5765. self.server.baseDir + '/accounts/' + \
  5766. nickname + '@' + self.server.domain + \
  5767. '/' + mType + '.temp'
  5768. filename, attachmentMediaType = \
  5769. saveMediaInFormPOST(mediaBytes, self.server.debug,
  5770. filenameBase)
  5771. if filename:
  5772. print('Profile update POST ' + mType +
  5773. ' media or font filename is ' + filename)
  5774. else:
  5775. print('Profile update, no ' + mType +
  5776. ' media or font filename in POST')
  5777. continue
  5778. postImageFilename = filename.replace('.temp', '')
  5779. if self.server.debug:
  5780. print('DEBUG: POST ' + mType +
  5781. ' media removing metadata')
  5782. # remove existing etag
  5783. if os.path.isfile(postImageFilename + '.etag'):
  5784. try:
  5785. os.remove(postImageFilename + '.etag')
  5786. except BaseException:
  5787. pass
  5788. removeMetaData(filename, postImageFilename)
  5789. if os.path.isfile(postImageFilename):
  5790. print('profile update POST ' + mType +
  5791. ' image or font saved to ' + postImageFilename)
  5792. if mType != 'instanceLogo':
  5793. lastPartOfImageFilename = \
  5794. postImageFilename.split('/')[-1]
  5795. profileMediaTypesUploaded[mType] = \
  5796. lastPartOfImageFilename
  5797. actorChanged = True
  5798. else:
  5799. print('ERROR: profile update POST ' + mType +
  5800. ' image or font could not be saved to ' +
  5801. postImageFilename)
  5802. fields = \
  5803. extractTextFieldsInPOST(postBytes, boundary,
  5804. self.server.debug)
  5805. if self.server.debug:
  5806. if fields:
  5807. print('DEBUG: profile update text ' +
  5808. 'field extracted from POST ' + str(fields))
  5809. else:
  5810. print('WARN: profile update, no text ' +
  5811. 'fields could be extracted from POST')
  5812. actorFilename = \
  5813. self.server.baseDir + '/accounts/' + \
  5814. nickname + '@' + self.server.domain + '.json'
  5815. if os.path.isfile(actorFilename):
  5816. actorJson = loadJson(actorFilename)
  5817. if actorJson:
  5818. # update the avatar/image url file extension
  5819. uploads = profileMediaTypesUploaded.items()
  5820. for mType, lastPart in uploads:
  5821. repStr = '/' + lastPart
  5822. if mType == 'avatar':
  5823. lastPartOfUrl = \
  5824. actorJson['icon']['url'].split('/')[-1]
  5825. srchStr = '/' + lastPartOfUrl
  5826. actorJson['icon']['url'] = \
  5827. actorJson['icon']['url'].replace(srchStr,
  5828. repStr)
  5829. elif mType == 'image':
  5830. lastPartOfUrl = \
  5831. actorJson['image']['url'].split('/')[-1]
  5832. srchStr = '/' + lastPartOfUrl
  5833. actorJson['image']['url'] = \
  5834. actorJson['image']['url'].replace(srchStr,
  5835. repStr)
  5836. skillCtr = 1
  5837. newSkills = {}
  5838. while skillCtr < 10:
  5839. skillName = \
  5840. fields.get('skillName' + str(skillCtr))
  5841. if not skillName:
  5842. skillCtr += 1
  5843. continue
  5844. skillValue = \
  5845. fields.get('skillValue' + str(skillCtr))
  5846. if not skillValue:
  5847. skillCtr += 1
  5848. continue
  5849. if not actorJson['skills'].get(skillName):
  5850. actorChanged = True
  5851. else:
  5852. if actorJson['skills'][skillName] != \
  5853. int(skillValue):
  5854. actorChanged = True
  5855. newSkills[skillName] = int(skillValue)
  5856. skillCtr += 1
  5857. if len(actorJson['skills'].items()) != \
  5858. len(newSkills.items()):
  5859. actorChanged = True
  5860. actorJson['skills'] = newSkills
  5861. if fields.get('password'):
  5862. if fields.get('passwordconfirm'):
  5863. if actorJson['password'] == \
  5864. fields['passwordconfirm']:
  5865. if len(actorJson['password']) > 2:
  5866. # set password
  5867. baseDir = self.server.baseDir
  5868. pwd = actorJson['password']
  5869. storeBasicCredentials(baseDir,
  5870. nickname,
  5871. pwd)
  5872. if fields.get('displayNickname'):
  5873. if fields['displayNickname'] != actorJson['name']:
  5874. actorJson['name'] = fields['displayNickname']
  5875. actorChanged = True
  5876. if fields.get('themeDropdown'):
  5877. setTheme(self.server.baseDir,
  5878. fields['themeDropdown'])
  5879. # self.server.iconsCache={}
  5880. currentEmailAddress = getEmailAddress(actorJson)
  5881. if fields.get('email'):
  5882. if fields['email'] != currentEmailAddress:
  5883. setEmailAddress(actorJson, fields['email'])
  5884. actorChanged = True
  5885. else:
  5886. if currentEmailAddress:
  5887. setEmailAddress(actorJson, '')
  5888. actorChanged = True
  5889. currentXmppAddress = getXmppAddress(actorJson)
  5890. if fields.get('xmppAddress'):
  5891. if fields['xmppAddress'] != currentXmppAddress:
  5892. setXmppAddress(actorJson,
  5893. fields['xmppAddress'])
  5894. actorChanged = True
  5895. else:
  5896. if currentXmppAddress:
  5897. setXmppAddress(actorJson, '')
  5898. actorChanged = True
  5899. currentMatrixAddress = getMatrixAddress(actorJson)
  5900. if fields.get('matrixAddress'):
  5901. if fields['matrixAddress'] != currentMatrixAddress:
  5902. setMatrixAddress(actorJson,
  5903. fields['matrixAddress'])
  5904. actorChanged = True
  5905. else:
  5906. if currentMatrixAddress:
  5907. setMatrixAddress(actorJson, '')
  5908. actorChanged = True
  5909. currentSSBAddress = getSSBAddress(actorJson)
  5910. if fields.get('ssbAddress'):
  5911. if fields['ssbAddress'] != currentSSBAddress:
  5912. setSSBAddress(actorJson,
  5913. fields['ssbAddress'])
  5914. actorChanged = True
  5915. else:
  5916. if currentSSBAddress:
  5917. setSSBAddress(actorJson, '')
  5918. actorChanged = True
  5919. currentBlogAddress = getBlogAddress(actorJson)
  5920. if fields.get('blogAddress'):
  5921. if fields['blogAddress'] != currentBlogAddress:
  5922. setBlogAddress(actorJson,
  5923. fields['blogAddress'])
  5924. actorChanged = True
  5925. else:
  5926. if currentBlogAddress:
  5927. setBlogAddress(actorJson, '')
  5928. actorChanged = True
  5929. currentToxAddress = getToxAddress(actorJson)
  5930. if fields.get('toxAddress'):
  5931. if fields['toxAddress'] != currentToxAddress:
  5932. setToxAddress(actorJson,
  5933. fields['toxAddress'])
  5934. actorChanged = True
  5935. else:
  5936. if currentToxAddress:
  5937. setToxAddress(actorJson, '')
  5938. actorChanged = True
  5939. currentPGPpubKey = getPGPpubKey(actorJson)
  5940. if fields.get('pgp'):
  5941. if fields['pgp'] != currentPGPpubKey:
  5942. setPGPpubKey(actorJson,
  5943. fields['pgp'])
  5944. actorChanged = True
  5945. else:
  5946. if currentPGPpubKey:
  5947. setPGPpubKey(actorJson, '')
  5948. actorChanged = True
  5949. currentPGPfingerprint = getPGPfingerprint(actorJson)
  5950. if fields.get('openpgp'):
  5951. if fields['openpgp'] != currentPGPfingerprint:
  5952. setPGPfingerprint(actorJson,
  5953. fields['openpgp'])
  5954. actorChanged = True
  5955. else:
  5956. if currentPGPfingerprint:
  5957. setPGPfingerprint(actorJson, '')
  5958. actorChanged = True
  5959. currentDonateUrl = getDonationUrl(actorJson)
  5960. if fields.get('donateUrl'):
  5961. if fields['donateUrl'] != currentDonateUrl:
  5962. setDonationUrl(actorJson,
  5963. fields['donateUrl'])
  5964. actorChanged = True
  5965. else:
  5966. if currentDonateUrl:
  5967. setDonationUrl(actorJson, '')
  5968. actorChanged = True
  5969. if fields.get('instanceTitle'):
  5970. currInstanceTitle = \
  5971. getConfigParam(self.server.baseDir,
  5972. 'instanceTitle')
  5973. if fields['instanceTitle'] != currInstanceTitle:
  5974. setConfigParam(self.server.baseDir,
  5975. 'instanceTitle',
  5976. fields['instanceTitle'])
  5977. currInstanceDescriptionShort = \
  5978. getConfigParam(self.server.baseDir,
  5979. 'instanceDescriptionShort')
  5980. if fields.get('instanceDescriptionShort'):
  5981. if fields['instanceDescriptionShort'] != \
  5982. currInstanceDescriptionShort:
  5983. iDesc = fields['instanceDescriptionShort']
  5984. setConfigParam(self.server.baseDir,
  5985. 'instanceDescriptionShort',
  5986. iDesc)
  5987. else:
  5988. if currInstanceDescriptionShort:
  5989. setConfigParam(self.server.baseDir,
  5990. 'instanceDescriptionShort', '')
  5991. currInstanceDescription = \
  5992. getConfigParam(self.server.baseDir,
  5993. 'instanceDescription')
  5994. if fields.get('instanceDescription'):
  5995. if fields['instanceDescription'] != \
  5996. currInstanceDescription:
  5997. setConfigParam(self.server.baseDir,
  5998. 'instanceDescription',
  5999. fields['instanceDescription'])
  6000. else:
  6001. if currInstanceDescription:
  6002. setConfigParam(self.server.baseDir,
  6003. 'instanceDescription', '')
  6004. if fields.get('bio'):
  6005. if fields['bio'] != actorJson['summary']:
  6006. actorTags = {}
  6007. actorJson['summary'] = \
  6008. addHtmlTags(self.server.baseDir,
  6009. self.server.httpPrefix,
  6010. nickname,
  6011. self.server.domainFull,
  6012. fields['bio'], [], actorTags)
  6013. if actorTags:
  6014. actorJson['tag'] = []
  6015. for tagName, tag in actorTags.items():
  6016. actorJson['tag'].append(tag)
  6017. actorChanged = True
  6018. else:
  6019. if actorJson['summary']:
  6020. actorJson['summary'] = ''
  6021. actorChanged = True
  6022. if fields.get('moderators'):
  6023. adminNickname = \
  6024. getConfigParam(self.server.baseDir, 'admin')
  6025. if self.path.startswith('/users/' +
  6026. adminNickname + '/'):
  6027. moderatorsFile = \
  6028. self.server.baseDir + \
  6029. '/accounts/moderators.txt'
  6030. clearModeratorStatus(self.server.baseDir)
  6031. if ',' in fields['moderators']:
  6032. # if the list was given as comma separated
  6033. modFile = open(moderatorsFile, "w+")
  6034. mods = fields['moderators'].split(',')
  6035. for modNick in mods:
  6036. modNick = modNick.strip()
  6037. modDir = self.server.baseDir + \
  6038. '/accounts/' + modNick + \
  6039. '@' + self.server.domain
  6040. if os.path.isdir(modDir):
  6041. modFile.write(modNick + '\n')
  6042. modFile.close()
  6043. mods = fields['moderators'].split(',')
  6044. for modNick in mods:
  6045. modNick = modNick.strip()
  6046. modDir = self.server.baseDir + \
  6047. '/accounts/' + modNick + \
  6048. '@' + self.server.domain
  6049. if os.path.isdir(modDir):
  6050. setRole(self.server.baseDir,
  6051. modNick,
  6052. self.server.domain,
  6053. 'instance', 'moderator')
  6054. else:
  6055. # nicknames on separate lines
  6056. modFile = open(moderatorsFile, "w+")
  6057. mods = fields['moderators'].split('\n')
  6058. for modNick in mods:
  6059. modNick = modNick.strip()
  6060. modDir = \
  6061. self.server.baseDir + \
  6062. '/accounts/' + modNick + \
  6063. '@' + self.server.domain
  6064. if os.path.isdir(modDir):
  6065. modFile.write(modNick + '\n')
  6066. modFile.close()
  6067. mods = fields['moderators'].split('\n')
  6068. for modNick in mods:
  6069. modNick = modNick.strip()
  6070. modDir = \
  6071. self.server.baseDir + \
  6072. '/accounts/' + \
  6073. modNick + '@' + \
  6074. self.server.domain
  6075. if os.path.isdir(modDir):
  6076. setRole(self.server.baseDir,
  6077. modNick,
  6078. self.server.domain,
  6079. 'instance',
  6080. 'moderator')
  6081. if fields.get('removeScheduledPosts'):
  6082. if fields['removeScheduledPosts'] == 'on':
  6083. removeScheduledPosts(self.server.baseDir,
  6084. nickname,
  6085. self.server.domain)
  6086. approveFollowers = False
  6087. if fields.get('approveFollowers'):
  6088. if fields['approveFollowers'] == 'on':
  6089. approveFollowers = True
  6090. if approveFollowers != \
  6091. actorJson['manuallyApprovesFollowers']:
  6092. actorJson['manuallyApprovesFollowers'] = \
  6093. approveFollowers
  6094. actorChanged = True
  6095. if fields.get('removeCustomFont'):
  6096. if fields['removeCustomFont'] == 'on':
  6097. fontExt = ('woff', 'woff2', 'otf', 'ttf')
  6098. for ext in fontExt:
  6099. if os.path.isfile(self.server.baseDir +
  6100. '/fonts/custom.' + ext):
  6101. os.remove(self.server.baseDir +
  6102. '/fonts/custom.' + ext)
  6103. if os.path.isfile(self.server.baseDir +
  6104. '/fonts/custom.' + ext +
  6105. '.etag'):
  6106. os.remove(self.server.baseDir +
  6107. '/fonts/custom.' + ext +
  6108. '.etag')
  6109. currTheme = getTheme(self.server.baseDir)
  6110. if currTheme:
  6111. setTheme(self.server.baseDir, currTheme)
  6112. if fields.get('mediaInstance'):
  6113. self.server.mediaInstance = False
  6114. self.server.defaultTimeline = 'inbox'
  6115. if fields['mediaInstance'] == 'on':
  6116. self.server.mediaInstance = True
  6117. self.server.defaultTimeline = 'tlmedia'
  6118. setConfigParam(self.server.baseDir,
  6119. "mediaInstance",
  6120. self.server.mediaInstance)
  6121. else:
  6122. if self.server.mediaInstance:
  6123. self.server.mediaInstance = False
  6124. self.server.defaultTimeline = 'inbox'
  6125. setConfigParam(self.server.baseDir,
  6126. "mediaInstance",
  6127. self.server.mediaInstance)
  6128. if fields.get('blogsInstance'):
  6129. self.server.blogsInstance = False
  6130. self.server.defaultTimeline = 'inbox'
  6131. if fields['blogsInstance'] == 'on':
  6132. self.server.blogsInstance = True
  6133. self.server.defaultTimeline = 'tlblogs'
  6134. setConfigParam(self.server.baseDir,
  6135. "blogsInstance",
  6136. self.server.blogsInstance)
  6137. else:
  6138. if self.server.blogsInstance:
  6139. self.server.blogsInstance = False
  6140. self.server.defaultTimeline = 'inbox'
  6141. setConfigParam(self.server.baseDir,
  6142. "blogsInstance",
  6143. self.server.blogsInstance)
  6144. # only receive DMs from accounts you follow
  6145. followDMsFilename = \
  6146. self.server.baseDir + '/accounts/' + \
  6147. nickname + '@' + self.server.domain + \
  6148. '/.followDMs'
  6149. followDMsActive = False
  6150. if fields.get('followDMs'):
  6151. if fields['followDMs'] == 'on':
  6152. followDMsActive = True
  6153. with open(followDMsFilename, "w") as fFile:
  6154. fFile.write('\n')
  6155. if not followDMsActive:
  6156. if os.path.isfile(followDMsFilename):
  6157. os.remove(followDMsFilename)
  6158. # remove Twitter retweets
  6159. removeTwitterFilename = \
  6160. self.server.baseDir + '/accounts/' + \
  6161. nickname + '@' + self.server.domain + \
  6162. '/.removeTwitter'
  6163. removeTwitterActive = False
  6164. if fields.get('removeTwitter'):
  6165. if fields['removeTwitter'] == 'on':
  6166. removeTwitterActive = True
  6167. with open(removeTwitterFilename, "w") as rFile:
  6168. rFile.write('\n')
  6169. if not removeTwitterActive:
  6170. if os.path.isfile(removeTwitterFilename):
  6171. os.remove(removeTwitterFilename)
  6172. # this account is a bot
  6173. if fields.get('isBot'):
  6174. if fields['isBot'] == 'on':
  6175. if actorJson['type'] != 'Service':
  6176. actorJson['type'] = 'Service'
  6177. actorChanged = True
  6178. else:
  6179. # this account is a group
  6180. if fields.get('isGroup'):
  6181. if fields['isGroup'] == 'on':
  6182. if actorJson['type'] != 'Group':
  6183. actorJson['type'] = 'Group'
  6184. actorChanged = True
  6185. else:
  6186. # this account is a person (default)
  6187. if actorJson['type'] != 'Person':
  6188. actorJson['type'] = 'Person'
  6189. actorChanged = True
  6190. # save filtered words list
  6191. filterFilename = \
  6192. self.server.baseDir + '/accounts/' + \
  6193. nickname + '@' + self.server.domain + \
  6194. '/filters.txt'
  6195. if fields.get('filteredWords'):
  6196. with open(filterFilename, "w") as filterfile:
  6197. filterfile.write(fields['filteredWords'])
  6198. else:
  6199. if os.path.isfile(filterFilename):
  6200. os.remove(filterFilename)
  6201. # word replacements
  6202. switchFilename = \
  6203. self.server.baseDir + '/accounts/' + \
  6204. nickname + '@' + self.server.domain + \
  6205. '/replacewords.txt'
  6206. if fields.get('switchWords'):
  6207. with open(switchFilename, "w") as switchfile:
  6208. switchfile.write(fields['switchWords'])
  6209. else:
  6210. if os.path.isfile(switchFilename):
  6211. os.remove(switchFilename)
  6212. # save blocked accounts list
  6213. blockedFilename = \
  6214. self.server.baseDir + '/accounts/' + \
  6215. nickname + '@' + self.server.domain + \
  6216. '/blocking.txt'
  6217. if fields.get('blocked'):
  6218. with open(blockedFilename, "w") as blockedfile:
  6219. blockedfile.write(fields['blocked'])
  6220. else:
  6221. if os.path.isfile(blockedFilename):
  6222. os.remove(blockedFilename)
  6223. # save allowed instances list
  6224. allowedInstancesFilename = \
  6225. self.server.baseDir + '/accounts/' + \
  6226. nickname + '@' + self.server.domain + \
  6227. '/allowedinstances.txt'
  6228. if fields.get('allowedInstances'):
  6229. with open(allowedInstancesFilename, "w") as aFile:
  6230. aFile.write(fields['allowedInstances'])
  6231. else:
  6232. if os.path.isfile(allowedInstancesFilename):
  6233. os.remove(allowedInstancesFilename)
  6234. # save git project names list
  6235. gitProjectsFilename = \
  6236. self.server.baseDir + '/accounts/' + \
  6237. nickname + '@' + self.server.domain + \
  6238. '/gitprojects.txt'
  6239. if fields.get('gitProjects'):
  6240. with open(gitProjectsFilename, "w") as aFile:
  6241. aFile.write(fields['gitProjects'].lower())
  6242. else:
  6243. if os.path.isfile(gitProjectsFilename):
  6244. os.remove(gitProjectsFilename)
  6245. # save actor json file within accounts
  6246. if actorChanged:
  6247. randomizeActorImages(actorJson)
  6248. saveJson(actorJson, actorFilename)
  6249. webfingerUpdate(self.server.baseDir,
  6250. nickname,
  6251. self.server.domain,
  6252. self.server.onionDomain,
  6253. self.server.cachedWebfingers)
  6254. # also copy to the actors cache and
  6255. # personCache in memory
  6256. storePersonInCache(self.server.baseDir,
  6257. actorJson['id'], actorJson,
  6258. self.server.personCache)
  6259. # clear any cached images for this actor
  6260. idStr = actorJson['id'].replace('/', '-')
  6261. removeAvatarFromCache(self.server.baseDir, idStr)
  6262. # save the actor to the cache
  6263. actorCacheFilename = \
  6264. self.server.baseDir + '/cache/actors/' + \
  6265. actorJson['id'].replace('/', '#') + '.json'
  6266. saveJson(actorJson, actorCacheFilename)
  6267. # send profile update to followers
  6268. ccStr = 'https://www.w3.org/ns/' + \
  6269. 'activitystreams#Public'
  6270. updateActorJson = {
  6271. 'type': 'Update',
  6272. 'actor': actorJson['id'],
  6273. 'to': [actorJson['id'] + '/followers'],
  6274. 'cc': [ccStr],
  6275. 'object': actorJson
  6276. }
  6277. self._postToOutbox(updateActorJson,
  6278. __version__, nickname)
  6279. if fields.get('deactivateThisAccount'):
  6280. if fields['deactivateThisAccount'] == 'on':
  6281. deactivateAccount(self.server.baseDir,
  6282. nickname,
  6283. self.server.domain)
  6284. self._clearLoginDetails(nickname,
  6285. callingDomain)
  6286. self.server.POSTbusy = False
  6287. return
  6288. if callingDomain.endswith('.onion') and \
  6289. self.server.onionDomain:
  6290. actorStr = \
  6291. 'http://' + self.server.onionDomain + usersPath
  6292. elif (callingDomain.endswith('.i2p') and
  6293. self.server.i2pDomain):
  6294. actorStr = \
  6295. 'http://' + self.server.i2pDomain + usersPath
  6296. self._redirect_headers(actorStr, cookie, callingDomain)
  6297. self.server.POSTbusy = False
  6298. return
  6299. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 3)
  6300. # moderator action buttons
  6301. if authorized and '/users/' in self.path and \
  6302. self.path.endswith('/moderationaction'):
  6303. usersPath = self.path.replace('/moderationaction', '')
  6304. actorStr = \
  6305. self.server.httpPrefix + '://' + \
  6306. self.server.domainFull + usersPath
  6307. length = int(self.headers['Content-length'])
  6308. try:
  6309. moderationParams = self.rfile.read(length).decode('utf-8')
  6310. except SocketError as e:
  6311. if e.errno == errno.ECONNRESET:
  6312. print('WARN: POST moderationParams connection was reset')
  6313. else:
  6314. print('WARN: POST moderationParams ' +
  6315. 'rfile.read socket error')
  6316. self.send_response(400)
  6317. self.end_headers()
  6318. self.server.POSTbusy = False
  6319. return
  6320. except ValueError as e:
  6321. print('ERROR: POST moderationParams rfile.read failed')
  6322. print(e)
  6323. self.send_response(400)
  6324. self.end_headers()
  6325. self.server.POSTbusy = False
  6326. return
  6327. if '&' in moderationParams:
  6328. moderationText = None
  6329. moderationButton = None
  6330. for moderationStr in moderationParams.split('&'):
  6331. if moderationStr.startswith('moderationAction'):
  6332. if '=' in moderationStr:
  6333. moderationText = \
  6334. moderationStr.split('=')[1].strip()
  6335. moderationText = moderationText.replace('+', ' ')
  6336. moderationText = \
  6337. urllib.parse.unquote(moderationText.strip())
  6338. elif moderationStr.startswith('submitInfo'):
  6339. msg = htmlModerationInfo(self.server.translate,
  6340. self.server.baseDir,
  6341. self.server.httpPrefix)
  6342. msg = msg.encode('utf-8')
  6343. self._login_headers('text/html',
  6344. len(msg), callingDomain)
  6345. self._write(msg)
  6346. self.server.POSTbusy = False
  6347. return
  6348. elif moderationStr.startswith('submitBlock'):
  6349. moderationButton = 'block'
  6350. elif moderationStr.startswith('submitUnblock'):
  6351. moderationButton = 'unblock'
  6352. elif moderationStr.startswith('submitSuspend'):
  6353. moderationButton = 'suspend'
  6354. elif moderationStr.startswith('submitUnsuspend'):
  6355. moderationButton = 'unsuspend'
  6356. elif moderationStr.startswith('submitRemove'):
  6357. moderationButton = 'remove'
  6358. if moderationButton and moderationText:
  6359. if self.server.debug:
  6360. print('moderationButton: ' + moderationButton)
  6361. print('moderationText: ' + moderationText)
  6362. nickname = moderationText
  6363. if nickname.startswith('http') or \
  6364. nickname.startswith('dat'):
  6365. nickname = getNicknameFromActor(nickname)
  6366. if '@' in nickname:
  6367. nickname = nickname.split('@')[0]
  6368. if moderationButton == 'suspend':
  6369. suspendAccount(self.server.baseDir, nickname,
  6370. self.server.domain)
  6371. if moderationButton == 'unsuspend':
  6372. unsuspendAccount(self.server.baseDir, nickname)
  6373. if moderationButton == 'block':
  6374. fullBlockDomain = None
  6375. if moderationText.startswith('http') or \
  6376. moderationText.startswith('dat'):
  6377. blockDomain, blockPort = \
  6378. getDomainFromActor(moderationText)
  6379. fullBlockDomain = blockDomain
  6380. if blockPort:
  6381. if blockPort != 80 and blockPort != 443:
  6382. if ':' not in blockDomain:
  6383. fullBlockDomain = \
  6384. blockDomain + ':' + str(blockPort)
  6385. if '@' in moderationText:
  6386. fullBlockDomain = moderationText.split('@')[1]
  6387. if fullBlockDomain or nickname.startswith('#'):
  6388. addGlobalBlock(self.server.baseDir,
  6389. nickname, fullBlockDomain)
  6390. if moderationButton == 'unblock':
  6391. fullBlockDomain = None
  6392. if moderationText.startswith('http') or \
  6393. moderationText.startswith('dat'):
  6394. blockDomain, blockPort = \
  6395. getDomainFromActor(moderationText)
  6396. fullBlockDomain = blockDomain
  6397. if blockPort:
  6398. if blockPort != 80 and blockPort != 443:
  6399. if ':' not in blockDomain:
  6400. fullBlockDomain = \
  6401. blockDomain + ':' + str(blockPort)
  6402. if '@' in moderationText:
  6403. fullBlockDomain = moderationText.split('@')[1]
  6404. if fullBlockDomain or nickname.startswith('#'):
  6405. removeGlobalBlock(self.server.baseDir,
  6406. nickname, fullBlockDomain)
  6407. if moderationButton == 'remove':
  6408. if '/statuses/' not in moderationText:
  6409. removeAccount(self.server.baseDir,
  6410. nickname,
  6411. self.server.domain,
  6412. self.server.port)
  6413. else:
  6414. # remove a post or thread
  6415. postFilename = \
  6416. locatePost(self.server.baseDir,
  6417. nickname, self.server.domain,
  6418. moderationText)
  6419. if postFilename:
  6420. if canRemovePost(self.server.baseDir,
  6421. nickname,
  6422. self.server.domain,
  6423. self.server.port,
  6424. moderationText):
  6425. deletePost(self.server.baseDir,
  6426. self.server.httpPrefix,
  6427. nickname, self.server.domain,
  6428. postFilename,
  6429. self.server.debug,
  6430. self.server.recentPostsCache)
  6431. if callingDomain.endswith('.onion') and \
  6432. self.server.onionDomain:
  6433. actorStr = \
  6434. 'http://' + self.server.onionDomain + usersPath
  6435. elif (callingDomain.endswith('.i2p') and
  6436. self.server.i2pDomain):
  6437. actorStr = \
  6438. 'http://' + self.server.i2pDomain + usersPath
  6439. self._redirect_headers(actorStr + '/moderation',
  6440. cookie, callingDomain)
  6441. self.server.POSTbusy = False
  6442. return
  6443. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 4)
  6444. searchForEmoji = False
  6445. if self.path.endswith('/searchhandleemoji'):
  6446. searchForEmoji = True
  6447. self.path = self.path.replace('/searchhandleemoji',
  6448. '/searchhandle')
  6449. if self.server.debug:
  6450. print('DEBUG: searching for emoji')
  6451. print('authorized: ' + str(authorized))
  6452. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 5)
  6453. # a vote/question/poll is posted
  6454. if (authorized and
  6455. (self.path.endswith('/question') or
  6456. '/question?page=' in self.path)):
  6457. pageNumber = 1
  6458. if '?page=' in self.path:
  6459. pageNumberStr = self.path.split('?page=')[1]
  6460. if '#' in pageNumberStr:
  6461. pageNumberStr = pageNumberStr.split('#')[0]
  6462. if pageNumberStr.isdigit():
  6463. pageNumber = int(pageNumberStr)
  6464. self.path = self.path.split('?page=')[0]
  6465. # the actor who votes
  6466. usersPath = self.path.replace('/question', '')
  6467. actor = \
  6468. self.server.httpPrefix + '://' + \
  6469. self.server.domainFull + usersPath
  6470. nickname = getNicknameFromActor(actor)
  6471. if not nickname:
  6472. if callingDomain.endswith('.onion') and \
  6473. self.server.onionDomain:
  6474. actor = 'http://' + self.server.onionDomain + usersPath
  6475. elif (callingDomain.endswith('.i2p') and
  6476. self.server.i2pDomain):
  6477. actor = 'http://' + self.server.i2pDomain + usersPath
  6478. self._redirect_headers(actor + '/' +
  6479. self.server.defaultTimeline +
  6480. '?page=' + str(pageNumber),
  6481. cookie, callingDomain)
  6482. self.server.POSTbusy = False
  6483. return
  6484. # get the parameters
  6485. length = int(self.headers['Content-length'])
  6486. try:
  6487. questionParams = self.rfile.read(length).decode('utf-8')
  6488. except SocketError as e:
  6489. if e.errno == errno.ECONNRESET:
  6490. print('WARN: POST questionParams connection was reset')
  6491. else:
  6492. print('WARN: POST questionParams socket error')
  6493. self.send_response(400)
  6494. self.end_headers()
  6495. self.server.POSTbusy = False
  6496. return
  6497. except ValueError as e:
  6498. print('ERROR: POST questionParams rfile.read failed')
  6499. print(e)
  6500. self.send_response(400)
  6501. self.end_headers()
  6502. self.server.POSTbusy = False
  6503. return
  6504. questionParams = questionParams.replace('+', ' ')
  6505. questionParams = questionParams.replace('%3F', '')
  6506. questionParams = \
  6507. urllib.parse.unquote(questionParams.strip())
  6508. # post being voted on
  6509. messageId = None
  6510. if 'messageId=' in questionParams:
  6511. messageId = questionParams.split('messageId=')[1]
  6512. if '&' in messageId:
  6513. messageId = messageId.split('&')[0]
  6514. answer = None
  6515. if 'answer=' in questionParams:
  6516. answer = questionParams.split('answer=')[1]
  6517. if '&' in answer:
  6518. answer = answer.split('&')[0]
  6519. self._sendReplyToQuestion(nickname, messageId, answer)
  6520. if callingDomain.endswith('.onion') and \
  6521. self.server.onionDomain:
  6522. actor = 'http://' + self.server.onionDomain + usersPath
  6523. elif (callingDomain.endswith('.i2p') and
  6524. self.server.i2pDomain):
  6525. actor = 'http://' + self.server.i2pDomain + usersPath
  6526. self._redirect_headers(actor + '/' +
  6527. self.server.defaultTimeline +
  6528. '?page=' + str(pageNumber), cookie,
  6529. callingDomain)
  6530. self.server.POSTbusy = False
  6531. return
  6532. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 6)
  6533. # a search was made
  6534. if ((authorized or searchForEmoji) and
  6535. (self.path.endswith('/searchhandle') or
  6536. '/searchhandle?page=' in self.path)):
  6537. # get the page number
  6538. pageNumber = 1
  6539. if '/searchhandle?page=' in self.path:
  6540. pageNumberStr = self.path.split('/searchhandle?page=')[1]
  6541. if '#' in pageNumberStr:
  6542. pageNumberStr = pageNumberStr.split('#')[0]
  6543. if pageNumberStr.isdigit():
  6544. pageNumber = int(pageNumberStr)
  6545. self.path = self.path.split('?page=')[0]
  6546. usersPath = self.path.replace('/searchhandle', '')
  6547. actorStr = \
  6548. self.server.httpPrefix + '://' + \
  6549. self.server.domainFull + usersPath
  6550. length = int(self.headers['Content-length'])
  6551. try:
  6552. searchParams = self.rfile.read(length).decode('utf-8')
  6553. except SocketError as e:
  6554. if e.errno == errno.ECONNRESET:
  6555. print('WARN: POST searchParams connection was reset')
  6556. else:
  6557. print('WARN: POST searchParams socket error')
  6558. self.send_response(400)
  6559. self.end_headers()
  6560. self.server.POSTbusy = False
  6561. return
  6562. except ValueError as e:
  6563. print('ERROR: POST searchParams rfile.read failed')
  6564. print(e)
  6565. self.send_response(400)
  6566. self.end_headers()
  6567. self.server.POSTbusy = False
  6568. return
  6569. if 'submitBack=' in searchParams:
  6570. # go back on search screen
  6571. if callingDomain.endswith('.onion') and \
  6572. self.server.onionDomain:
  6573. actorStr = 'http://' + self.server.onionDomain + usersPath
  6574. elif (callingDomain.endswith('.i2p') and
  6575. self.server.i2pDomain):
  6576. actorStr = 'http://' + self.server.i2pDomain + usersPath
  6577. self._redirect_headers(actorStr + '/' +
  6578. self.server.defaultTimeline,
  6579. cookie, callingDomain)
  6580. self.server.POSTbusy = False
  6581. return
  6582. if 'searchtext=' in searchParams:
  6583. searchStr = searchParams.split('searchtext=')[1]
  6584. if '&' in searchStr:
  6585. searchStr = searchStr.split('&')[0]
  6586. searchStr = searchStr.replace('+', ' ')
  6587. searchStr = \
  6588. urllib.parse.unquote(searchStr.strip())
  6589. searchStr2 = searchStr.lower().strip('\n').strip('\r')
  6590. print('searchStr: ' + searchStr)
  6591. if searchForEmoji:
  6592. searchStr = ':' + searchStr + ':'
  6593. if searchStr.startswith('#'):
  6594. nickname = getNicknameFromActor(actorStr)
  6595. # hashtag search
  6596. hashtagStr = \
  6597. htmlHashtagSearch(nickname,
  6598. self.server.domain,
  6599. self.server.port,
  6600. self.server.recentPostsCache,
  6601. self.server.maxRecentPosts,
  6602. self.server.translate,
  6603. self.server.baseDir,
  6604. searchStr[1:], 1,
  6605. maxPostsInFeed,
  6606. self.server.session,
  6607. self.server.cachedWebfingers,
  6608. self.server.personCache,
  6609. self.server.httpPrefix,
  6610. self.server.projectVersion)
  6611. if hashtagStr:
  6612. msg = hashtagStr.encode('utf-8')
  6613. self._login_headers('text/html',
  6614. len(msg), callingDomain)
  6615. self._write(msg)
  6616. self.server.POSTbusy = False
  6617. return
  6618. elif searchStr.startswith('*'):
  6619. # skill search
  6620. searchStr = searchStr.replace('*', '').strip()
  6621. skillStr = \
  6622. htmlSkillsSearch(self.server.translate,
  6623. self.server.baseDir,
  6624. self.server.httpPrefix,
  6625. searchStr,
  6626. self.server.instanceOnlySkillsSearch,
  6627. 64)
  6628. if skillStr:
  6629. msg = skillStr.encode('utf-8')
  6630. self._login_headers('text/html',
  6631. len(msg), callingDomain)
  6632. self._write(msg)
  6633. self.server.POSTbusy = False
  6634. return
  6635. elif searchStr.startswith('!'):
  6636. # your post history search
  6637. nickname = getNicknameFromActor(actorStr)
  6638. searchStr = searchStr.replace('!', '').strip()
  6639. historyStr = \
  6640. htmlHistorySearch(self.server.translate,
  6641. self.server.baseDir,
  6642. self.server.httpPrefix,
  6643. nickname,
  6644. self.server.domain,
  6645. searchStr,
  6646. maxPostsInFeed,
  6647. pageNumber,
  6648. self.server.projectVersion,
  6649. self.server.recentPostsCache,
  6650. self.server.maxRecentPosts,
  6651. self.server.session,
  6652. self.server.cachedWebfingers,
  6653. self.server.personCache,
  6654. self.server.port)
  6655. if historyStr:
  6656. msg = historyStr.encode('utf-8')
  6657. self._login_headers('text/html',
  6658. len(msg), callingDomain)
  6659. self._write(msg)
  6660. self.server.POSTbusy = False
  6661. return
  6662. elif '@' in searchStr:
  6663. # profile search
  6664. nickname = getNicknameFromActor(actorStr)
  6665. if not self.server.session:
  6666. print('Starting new session during handle search')
  6667. self.server.session = \
  6668. createSession(self.server.proxyType)
  6669. if not self.server.session:
  6670. print('ERROR: POST failed to create session ' +
  6671. 'during handle search')
  6672. self._404()
  6673. self.server.POSTbusy = False
  6674. return
  6675. profilePathStr = self.path.replace('/searchhandle', '')
  6676. profileStr = \
  6677. htmlProfileAfterSearch(self.server.recentPostsCache,
  6678. self.server.maxRecentPosts,
  6679. self.server.translate,
  6680. self.server.baseDir,
  6681. profilePathStr,
  6682. self.server.httpPrefix,
  6683. nickname,
  6684. self.server.domain,
  6685. self.server.port,
  6686. searchStr,
  6687. self.server.session,
  6688. self.server.cachedWebfingers,
  6689. self.server.personCache,
  6690. self.server.debug,
  6691. self.server.projectVersion)
  6692. if profileStr:
  6693. msg = profileStr.encode('utf-8')
  6694. self._login_headers('text/html',
  6695. len(msg), callingDomain)
  6696. self._write(msg)
  6697. self.server.POSTbusy = False
  6698. return
  6699. else:
  6700. if callingDomain.endswith('.onion') and \
  6701. self.server.onionDomain:
  6702. actorStr = 'http://' + self.server.onionDomain + \
  6703. usersPath
  6704. elif (callingDomain.endswith('.i2p') and
  6705. self.server.i2pDomain):
  6706. actorStr = 'http://' + self.server.i2pDomain + \
  6707. usersPath
  6708. self._redirect_headers(actorStr + '/search',
  6709. cookie, callingDomain)
  6710. self.server.POSTbusy = False
  6711. return
  6712. elif (searchStr.startswith(':') or
  6713. searchStr2.endswith(' emoji')):
  6714. # eg. "cat emoji"
  6715. if searchStr2.endswith(' emoji'):
  6716. searchStr = \
  6717. searchStr2.replace(' emoji', '')
  6718. # emoji search
  6719. emojiStr = \
  6720. htmlSearchEmoji(self.server.translate,
  6721. self.server.baseDir,
  6722. self.server.httpPrefix,
  6723. searchStr)
  6724. if emojiStr:
  6725. msg = emojiStr.encode('utf-8')
  6726. self._login_headers('text/html',
  6727. len(msg), callingDomain)
  6728. self._write(msg)
  6729. self.server.POSTbusy = False
  6730. return
  6731. else:
  6732. # shared items search
  6733. sharedItemsStr = \
  6734. htmlSearchSharedItems(self.server.translate,
  6735. self.server.baseDir,
  6736. searchStr, pageNumber,
  6737. maxPostsInFeed,
  6738. self.server.httpPrefix,
  6739. self.server.domainFull,
  6740. actorStr)
  6741. if sharedItemsStr:
  6742. msg = sharedItemsStr.encode('utf-8')
  6743. self._login_headers('text/html',
  6744. len(msg), callingDomain)
  6745. self._write(msg)
  6746. self.server.POSTbusy = False
  6747. return
  6748. if callingDomain.endswith('.onion') and self.server.onionDomain:
  6749. actorStr = 'http://' + self.server.onionDomain + usersPath
  6750. elif callingDomain.endswith('.i2p') and self.server.i2pDomain:
  6751. actorStr = 'http://' + self.server.i2pDomain + usersPath
  6752. self._redirect_headers(actorStr + '/' +
  6753. self.server.defaultTimeline,
  6754. cookie, callingDomain)
  6755. self.server.POSTbusy = False
  6756. return
  6757. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 7)
  6758. # removes a shared item
  6759. if authorized and self.path.endswith('/rmshare'):
  6760. usersPath = self.path.split('/rmshare')[0]
  6761. originPathStr = \
  6762. self.server.httpPrefix + '://' + \
  6763. self.server.domainFull + usersPath
  6764. length = int(self.headers['Content-length'])
  6765. try:
  6766. removeShareConfirmParams = \
  6767. self.rfile.read(length).decode('utf-8')
  6768. except SocketError as e:
  6769. if e.errno == errno.ECONNRESET:
  6770. print('WARN: POST removeShareConfirmParams ' +
  6771. 'connection was reset')
  6772. else:
  6773. print('WARN: POST removeShareConfirmParams socket error')
  6774. self.send_response(400)
  6775. self.end_headers()
  6776. self.server.POSTbusy = False
  6777. return
  6778. except ValueError as e:
  6779. print('ERROR: POST removeShareConfirmParams rfile.read failed')
  6780. print(e)
  6781. self.send_response(400)
  6782. self.end_headers()
  6783. self.server.POSTbusy = False
  6784. return
  6785. if '&submitYes=' in removeShareConfirmParams:
  6786. removeShareConfirmParams = \
  6787. removeShareConfirmParams.replace('+', ' ').strip()
  6788. removeShareConfirmParams = \
  6789. urllib.parse.unquote(removeShareConfirmParams)
  6790. shareActor = removeShareConfirmParams.split('actor=')[1]
  6791. if '&' in shareActor:
  6792. shareActor = shareActor.split('&')[0]
  6793. shareName = removeShareConfirmParams.split('shareName=')[1]
  6794. if '&' in shareName:
  6795. shareName = shareName.split('&')[0]
  6796. shareNickname = getNicknameFromActor(shareActor)
  6797. if shareNickname:
  6798. shareDomain, sharePort = getDomainFromActor(shareActor)
  6799. removeShare(self.server.baseDir,
  6800. shareNickname, shareDomain, shareName)
  6801. if callingDomain.endswith('.onion') and \
  6802. self.server.onionDomain:
  6803. originPathStr = \
  6804. 'http://' + self.server.onionDomain + usersPath
  6805. elif (callingDomain.endswith('.i2p') and
  6806. self.server.i2pDomain):
  6807. originPathStr = \
  6808. 'http://' + self.server.i2pDomain + usersPath
  6809. self._redirect_headers(originPathStr + '/tlshares',
  6810. cookie, callingDomain)
  6811. self.server.POSTbusy = False
  6812. return
  6813. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 8)
  6814. # removes a post
  6815. if authorized and self.path.endswith('/rmpost'):
  6816. pageNumber = 1
  6817. usersPath = self.path.split('/rmpost')[0]
  6818. originPathStr = \
  6819. self.server.httpPrefix + '://' + \
  6820. self.server.domainFull + usersPath
  6821. length = int(self.headers['Content-length'])
  6822. try:
  6823. removePostConfirmParams = \
  6824. self.rfile.read(length).decode('utf-8')
  6825. except SocketError as e:
  6826. if e.errno == errno.ECONNRESET:
  6827. print('WARN: POST removePostConfirmParams ' +
  6828. 'connection was reset')
  6829. else:
  6830. print('WARN: POST removePostConfirmParams socket error')
  6831. self.send_response(400)
  6832. self.end_headers()
  6833. self.server.POSTbusy = False
  6834. return
  6835. except ValueError as e:
  6836. print('ERROR: POST removePostConfirmParams rfile.read failed')
  6837. print(e)
  6838. self.send_response(400)
  6839. self.end_headers()
  6840. self.server.POSTbusy = False
  6841. return
  6842. if '&submitYes=' in removePostConfirmParams:
  6843. removePostConfirmParams = \
  6844. urllib.parse.unquote(removePostConfirmParams)
  6845. removeMessageId = \
  6846. removePostConfirmParams.split('messageId=')[1]
  6847. if '&' in removeMessageId:
  6848. removeMessageId = removeMessageId.split('&')[0]
  6849. if 'pageNumber=' in removePostConfirmParams:
  6850. pageNumberStr = \
  6851. removePostConfirmParams.split('pageNumber=')[1]
  6852. if '&' in pageNumberStr:
  6853. pageNumberStr = pageNumberStr.split('&')[0]
  6854. if pageNumberStr.isdigit():
  6855. pageNumber = int(pageNumberStr)
  6856. yearStr = None
  6857. if 'year=' in removePostConfirmParams:
  6858. yearStr = removePostConfirmParams.split('year=')[1]
  6859. if '&' in yearStr:
  6860. yearStr = yearStr.split('&')[0]
  6861. monthStr = None
  6862. if 'month=' in removePostConfirmParams:
  6863. monthStr = removePostConfirmParams.split('month=')[1]
  6864. if '&' in monthStr:
  6865. monthStr = monthStr.split('&')[0]
  6866. if '/statuses/' in removeMessageId:
  6867. removePostActor = removeMessageId.split('/statuses/')[0]
  6868. if originPathStr in removePostActor:
  6869. toList = ['https://www.w3.org/ns/activitystreams#Public',
  6870. removePostActor]
  6871. deleteJson = {
  6872. "@context": "https://www.w3.org/ns/activitystreams",
  6873. 'actor': removePostActor,
  6874. 'object': removeMessageId,
  6875. 'to': toList,
  6876. 'cc': [removePostActor+'/followers'],
  6877. 'type': 'Delete'
  6878. }
  6879. self.postToNickname = getNicknameFromActor(removePostActor)
  6880. if self.postToNickname:
  6881. if monthStr and yearStr:
  6882. if monthStr.isdigit() and yearStr.isdigit():
  6883. removeCalendarEvent(self.server.baseDir,
  6884. self.postToNickname,
  6885. self.server.domain,
  6886. int(yearStr),
  6887. int(monthStr),
  6888. removeMessageId)
  6889. self._postToOutboxThread(deleteJson)
  6890. if callingDomain.endswith('.onion') and \
  6891. self.server.onionDomain:
  6892. originPathStr = 'http://' + self.server.onionDomain + usersPath
  6893. elif (callingDomain.endswith('.i2p') and
  6894. self.server.i2pDomain):
  6895. originPathStr = 'http://' + self.server.i2pDomain + usersPath
  6896. if pageNumber == 1:
  6897. self._redirect_headers(originPathStr + '/outbox', cookie,
  6898. callingDomain)
  6899. else:
  6900. self._redirect_headers(originPathStr + '/outbox?page=' +
  6901. str(pageNumber),
  6902. cookie, callingDomain)
  6903. self.server.POSTbusy = False
  6904. return
  6905. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 9)
  6906. # decision to follow in the web interface is confirmed
  6907. if authorized and self.path.endswith('/followconfirm'):
  6908. usersPath = self.path.split('/followconfirm')[0]
  6909. originPathStr = self.server.httpPrefix + '://' + \
  6910. self.server.domainFull + usersPath
  6911. followerNickname = getNicknameFromActor(originPathStr)
  6912. length = int(self.headers['Content-length'])
  6913. try:
  6914. followConfirmParams = self.rfile.read(length).decode('utf-8')
  6915. except SocketError as e:
  6916. if e.errno == errno.ECONNRESET:
  6917. print('WARN: POST followConfirmParams ' +
  6918. 'connection was reset')
  6919. else:
  6920. print('WARN: POST followConfirmParams socket error')
  6921. self.send_response(400)
  6922. self.end_headers()
  6923. self.server.POSTbusy = False
  6924. return
  6925. except ValueError as e:
  6926. print('ERROR: POST followConfirmParams rfile.read failed')
  6927. print(e)
  6928. self.send_response(400)
  6929. self.end_headers()
  6930. self.server.POSTbusy = False
  6931. return
  6932. if '&submitView=' in followConfirmParams:
  6933. followingActor = \
  6934. urllib.parse.unquote(followConfirmParams)
  6935. followingActor = followingActor.split('actor=')[1]
  6936. if '&' in followingActor:
  6937. followingActor = followingActor.split('&')[0]
  6938. self._redirect_headers(followingActor, cookie, callingDomain)
  6939. self.server.POSTbusy = False
  6940. return
  6941. if '&submitYes=' in followConfirmParams:
  6942. followingActor = \
  6943. urllib.parse.unquote(followConfirmParams)
  6944. followingActor = followingActor.split('actor=')[1]
  6945. if '&' in followingActor:
  6946. followingActor = followingActor.split('&')[0]
  6947. followingNickname = getNicknameFromActor(followingActor)
  6948. followingDomain, followingPort = \
  6949. getDomainFromActor(followingActor)
  6950. if followerNickname == followingNickname and \
  6951. followingDomain == self.server.domain and \
  6952. followingPort == self.server.port:
  6953. if self.server.debug:
  6954. print('You cannot follow yourself!')
  6955. else:
  6956. if self.server.debug:
  6957. print('Sending follow request from ' +
  6958. followerNickname + ' to ' + followingActor)
  6959. sendFollowRequest(self.server.session,
  6960. self.server.baseDir,
  6961. followerNickname,
  6962. self.server.domain, self.server.port,
  6963. self.server.httpPrefix,
  6964. followingNickname,
  6965. followingDomain,
  6966. followingPort, self.server.httpPrefix,
  6967. False, self.server.federationList,
  6968. self.server.sendThreads,
  6969. self.server.postLog,
  6970. self.server.cachedWebfingers,
  6971. self.server.personCache,
  6972. self.server.debug,
  6973. self.server.projectVersion)
  6974. if callingDomain.endswith('.onion') and \
  6975. self.server.onionDomain:
  6976. originPathStr = \
  6977. 'http://' + self.server.onionDomain + usersPath
  6978. elif (callingDomain.endswith('.i2p') and
  6979. self.server.i2pDomain):
  6980. originPathStr = \
  6981. 'http://' + self.server.i2pDomain + usersPath
  6982. self._redirect_headers(originPathStr, cookie, callingDomain)
  6983. self.server.POSTbusy = False
  6984. return
  6985. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 10)
  6986. # decision to unfollow in the web interface is confirmed
  6987. if authorized and self.path.endswith('/unfollowconfirm'):
  6988. usersPath = self.path.split('/unfollowconfirm')[0]
  6989. originPathStr = self.server.httpPrefix + '://' + \
  6990. self.server.domainFull + usersPath
  6991. followerNickname = getNicknameFromActor(originPathStr)
  6992. length = int(self.headers['Content-length'])
  6993. try:
  6994. followConfirmParams = self.rfile.read(length).decode('utf-8')
  6995. except SocketError as e:
  6996. if e.errno == errno.ECONNRESET:
  6997. print('WARN: POST followConfirmParams ' +
  6998. 'connection was reset')
  6999. else:
  7000. print('WARN: POST followConfirmParams socket error')
  7001. self.send_response(400)
  7002. self.end_headers()
  7003. self.server.POSTbusy = False
  7004. return
  7005. except ValueError as e:
  7006. print('ERROR: POST followConfirmParams rfile.read failed')
  7007. print(e)
  7008. self.send_response(400)
  7009. self.end_headers()
  7010. self.server.POSTbusy = False
  7011. return
  7012. if '&submitYes=' in followConfirmParams:
  7013. followingActor = \
  7014. urllib.parse.unquote(followConfirmParams)
  7015. followingActor = followingActor.split('actor=')[1]
  7016. if '&' in followingActor:
  7017. followingActor = followingActor.split('&')[0]
  7018. followingNickname = getNicknameFromActor(followingActor)
  7019. followingDomain, followingPort = \
  7020. getDomainFromActor(followingActor)
  7021. if followerNickname == followingNickname and \
  7022. followingDomain == self.server.domain and \
  7023. followingPort == self.server.port:
  7024. if self.server.debug:
  7025. print('You cannot unfollow yourself!')
  7026. else:
  7027. if self.server.debug:
  7028. print(followerNickname + ' stops following ' +
  7029. followingActor)
  7030. followActor = \
  7031. self.server.httpPrefix + '://' + \
  7032. self.server.domainFull + \
  7033. '/users/' + followerNickname
  7034. statusNumber, published = getStatusNumber()
  7035. followId = followActor + '/statuses/' + str(statusNumber)
  7036. unfollowJson = {
  7037. '@context': 'https://www.w3.org/ns/activitystreams',
  7038. 'id': followId+'/undo',
  7039. 'type': 'Undo',
  7040. 'actor': followActor,
  7041. 'object': {
  7042. 'id': followId,
  7043. 'type': 'Follow',
  7044. 'actor': followActor,
  7045. 'object': followingActor
  7046. }
  7047. }
  7048. pathUsersSection = self.path.split('/users/')[1]
  7049. self.postToNickname = pathUsersSection.split('/')[0]
  7050. self._postToOutboxThread(unfollowJson)
  7051. if callingDomain.endswith('.onion') and \
  7052. self.server.onionDomain:
  7053. originPathStr = \
  7054. 'http://' + self.server.onionDomain + usersPath
  7055. elif (callingDomain.endswith('.i2p') and
  7056. self.server.i2pDomain):
  7057. originPathStr = \
  7058. 'http://' + self.server.i2pDomain + usersPath
  7059. self._redirect_headers(originPathStr, cookie, callingDomain)
  7060. self.server.POSTbusy = False
  7061. return
  7062. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 11)
  7063. # decision to unblock in the web interface is confirmed
  7064. if authorized and self.path.endswith('/unblockconfirm'):
  7065. usersPath = self.path.split('/unblockconfirm')[0]
  7066. originPathStr = \
  7067. self.server.httpPrefix + '://' + \
  7068. self.server.domainFull + usersPath
  7069. blockerNickname = getNicknameFromActor(originPathStr)
  7070. if not blockerNickname:
  7071. if callingDomain.endswith('.onion') and \
  7072. self.server.onionDomain:
  7073. originPathStr = \
  7074. 'http://' + self.server.onionDomain + usersPath
  7075. elif (callingDomain.endswith('.i2p') and
  7076. self.server.i2pDomain):
  7077. originPathStr = \
  7078. 'http://' + self.server.i2pDomain + usersPath
  7079. print('WARN: unable to find nickname in ' + originPathStr)
  7080. self._redirect_headers(originPathStr,
  7081. cookie, callingDomain)
  7082. self.server.POSTbusy = False
  7083. return
  7084. length = int(self.headers['Content-length'])
  7085. try:
  7086. blockConfirmParams = self.rfile.read(length).decode('utf-8')
  7087. except SocketError as e:
  7088. if e.errno == errno.ECONNRESET:
  7089. print('WARN: POST blockConfirmParams ' +
  7090. 'connection was reset')
  7091. else:
  7092. print('WARN: POST blockConfirmParams socket error')
  7093. self.send_response(400)
  7094. self.end_headers()
  7095. self.server.POSTbusy = False
  7096. return
  7097. except ValueError as e:
  7098. print('ERROR: POST blockConfirmParams rfile.read failed')
  7099. print(e)
  7100. self.send_response(400)
  7101. self.end_headers()
  7102. self.server.POSTbusy = False
  7103. return
  7104. if '&submitYes=' in blockConfirmParams:
  7105. blockingActor = \
  7106. urllib.parse.unquote(blockConfirmParams)
  7107. blockingActor = blockingActor.split('actor=')[1]
  7108. if '&' in blockingActor:
  7109. blockingActor = blockingActor.split('&')[0]
  7110. blockingNickname = getNicknameFromActor(blockingActor)
  7111. if not blockingNickname:
  7112. if callingDomain.endswith('.onion') and \
  7113. self.server.onionDomain:
  7114. originPathStr = \
  7115. 'http://' + self.server.onionDomain + usersPath
  7116. elif (callingDomain.endswith('.i2p') and
  7117. self.server.i2pDomain):
  7118. originPathStr = \
  7119. 'http://' + self.server.i2pDomain + usersPath
  7120. print('WARN: unable to find nickname in ' + blockingActor)
  7121. self._redirect_headers(originPathStr,
  7122. cookie, callingDomain)
  7123. self.server.POSTbusy = False
  7124. return
  7125. blockingDomain, blockingPort = \
  7126. getDomainFromActor(blockingActor)
  7127. blockingDomainFull = blockingDomain
  7128. if blockingPort:
  7129. if blockingPort != 80 and blockingPort != 443:
  7130. if ':' not in blockingDomain:
  7131. blockingDomainFull = \
  7132. blockingDomain + ':' + str(blockingPort)
  7133. if blockerNickname == blockingNickname and \
  7134. blockingDomain == self.server.domain and \
  7135. blockingPort == self.server.port:
  7136. if self.server.debug:
  7137. print('You cannot unblock yourself!')
  7138. else:
  7139. if self.server.debug:
  7140. print(blockerNickname + ' stops blocking ' +
  7141. blockingActor)
  7142. removeBlock(self.server.baseDir,
  7143. blockerNickname, self.server.domain,
  7144. blockingNickname, blockingDomainFull)
  7145. if callingDomain.endswith('.onion') and \
  7146. self.server.onionDomain:
  7147. originPathStr = \
  7148. 'http://' + self.server.onionDomain + usersPath
  7149. elif (callingDomain.endswith('.i2p') and
  7150. self.server.i2pDomain):
  7151. originPathStr = \
  7152. 'http://' + self.server.i2pDomain + usersPath
  7153. self._redirect_headers(originPathStr,
  7154. cookie, callingDomain)
  7155. self.server.POSTbusy = False
  7156. return
  7157. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 12)
  7158. # decision to block in the web interface is confirmed
  7159. if authorized and self.path.endswith('/blockconfirm'):
  7160. usersPath = self.path.split('/blockconfirm')[0]
  7161. originPathStr = \
  7162. self.server.httpPrefix + '://' + \
  7163. self.server.domainFull + usersPath
  7164. blockerNickname = getNicknameFromActor(originPathStr)
  7165. if not blockerNickname:
  7166. if callingDomain.endswith('.onion') and \
  7167. self.server.onionDomain:
  7168. originPathStr = \
  7169. 'http://' + self.server.onionDomain + usersPath
  7170. elif (callingDomain.endswith('.i2p') and
  7171. self.server.i2pDomain):
  7172. originPathStr = \
  7173. 'http://' + self.server.i2pDomain + usersPath
  7174. print('WARN: unable to find nickname in ' + originPathStr)
  7175. self._redirect_headers(originPathStr,
  7176. cookie, callingDomain)
  7177. self.server.POSTbusy = False
  7178. return
  7179. length = int(self.headers['Content-length'])
  7180. try:
  7181. blockConfirmParams = self.rfile.read(length).decode('utf-8')
  7182. except SocketError as e:
  7183. if e.errno == errno.ECONNRESET:
  7184. print('WARN: POST blockConfirmParams ' +
  7185. 'connection was reset')
  7186. else:
  7187. print('WARN: POST blockConfirmParams socket error')
  7188. self.send_response(400)
  7189. self.end_headers()
  7190. self.server.POSTbusy = False
  7191. return
  7192. except ValueError as e:
  7193. print('ERROR: POST blockConfirmParams rfile.read failed')
  7194. print(e)
  7195. self.send_response(400)
  7196. self.end_headers()
  7197. self.server.POSTbusy = False
  7198. return
  7199. if '&submitYes=' in blockConfirmParams:
  7200. blockingActor = \
  7201. urllib.parse.unquote(blockConfirmParams)
  7202. blockingActor = blockingActor.split('actor=')[1]
  7203. if '&' in blockingActor:
  7204. blockingActor = blockingActor.split('&')[0]
  7205. blockingNickname = getNicknameFromActor(blockingActor)
  7206. if not blockingNickname:
  7207. if callingDomain.endswith('.onion') and \
  7208. self.server.onionDomain:
  7209. originPathStr = \
  7210. 'http://' + self.server.onionDomain + usersPath
  7211. elif (callingDomain.endswith('.i2p') and
  7212. self.server.i2pDomain):
  7213. originPathStr = \
  7214. 'http://' + self.server.i2pDomain + usersPath
  7215. print('WARN: unable to find nickname in ' + blockingActor)
  7216. self._redirect_headers(originPathStr,
  7217. cookie, callingDomain)
  7218. self.server.POSTbusy = False
  7219. return
  7220. blockingDomain, blockingPort = \
  7221. getDomainFromActor(blockingActor)
  7222. blockingDomainFull = blockingDomain
  7223. if blockingPort:
  7224. if blockingPort != 80 and blockingPort != 443:
  7225. if ':' not in blockingDomain:
  7226. blockingDomainFull = \
  7227. blockingDomain + ':' + str(blockingPort)
  7228. if blockerNickname == blockingNickname and \
  7229. blockingDomain == self.server.domain and \
  7230. blockingPort == self.server.port:
  7231. if self.server.debug:
  7232. print('You cannot block yourself!')
  7233. else:
  7234. if self.server.debug:
  7235. print('Adding block by ' + blockerNickname +
  7236. ' of ' + blockingActor)
  7237. addBlock(self.server.baseDir, blockerNickname,
  7238. self.server.domain,
  7239. blockingNickname,
  7240. blockingDomainFull)
  7241. if callingDomain.endswith('.onion') and \
  7242. self.server.onionDomain:
  7243. originPathStr = \
  7244. 'http://' + self.server.onionDomain + usersPath
  7245. elif (callingDomain.endswith('.i2p') and
  7246. self.server.i2pDomain):
  7247. originPathStr = \
  7248. 'http://' + self.server.i2pDomain + usersPath
  7249. self._redirect_headers(originPathStr, cookie, callingDomain)
  7250. self.server.POSTbusy = False
  7251. return
  7252. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 13)
  7253. # an option was chosen from person options screen
  7254. # view/follow/block/report
  7255. if authorized and self.path.endswith('/personoptions'):
  7256. pageNumber = 1
  7257. usersPath = self.path.split('/personoptions')[0]
  7258. originPathStr = \
  7259. self.server.httpPrefix + '://' + \
  7260. self.server.domainFull + usersPath
  7261. chooserNickname = getNicknameFromActor(originPathStr)
  7262. if not chooserNickname:
  7263. if callingDomain.endswith('.onion') and \
  7264. self.server.onionDomain:
  7265. originPathStr = \
  7266. 'http://' + self.server.onionDomain + usersPath
  7267. elif (callingDomain.endswith('.i2p') and
  7268. self.server.i2pDomain):
  7269. originPathStr = \
  7270. 'http://' + self.server.i2pDomain + usersPath
  7271. print('WARN: unable to find nickname in ' + originPathStr)
  7272. self._redirect_headers(originPathStr, cookie, callingDomain)
  7273. self.server.POSTbusy = False
  7274. return
  7275. length = int(self.headers['Content-length'])
  7276. try:
  7277. optionsConfirmParams = self.rfile.read(length).decode('utf-8')
  7278. except SocketError as e:
  7279. if e.errno == errno.ECONNRESET:
  7280. print('WARN: POST optionsConfirmParams ' +
  7281. 'connection reset by peer')
  7282. else:
  7283. print('WARN: POST optionsConfirmParams socket error')
  7284. self.send_response(400)
  7285. self.end_headers()
  7286. self.server.POSTbusy = False
  7287. return
  7288. except ValueError as e:
  7289. print('ERROR: POST optionsConfirmParams rfile.read failed')
  7290. print(e)
  7291. self.send_response(400)
  7292. self.end_headers()
  7293. self.server.POSTbusy = False
  7294. return
  7295. optionsConfirmParams = \
  7296. urllib.parse.unquote(optionsConfirmParams)
  7297. # page number to return to
  7298. if 'pageNumber=' in optionsConfirmParams:
  7299. pageNumberStr = optionsConfirmParams.split('pageNumber=')[1]
  7300. if '&' in pageNumberStr:
  7301. pageNumberStr = pageNumberStr.split('&')[0]
  7302. if pageNumberStr.isdigit():
  7303. pageNumber = int(pageNumberStr)
  7304. # actor for the person
  7305. optionsActor = optionsConfirmParams.split('actor=')[1]
  7306. if '&' in optionsActor:
  7307. optionsActor = optionsActor.split('&')[0]
  7308. # url of the avatar
  7309. optionsAvatarUrl = optionsConfirmParams.split('avatarUrl=')[1]
  7310. if '&' in optionsAvatarUrl:
  7311. optionsAvatarUrl = optionsAvatarUrl.split('&')[0]
  7312. # link to a post, which can then be included in reports
  7313. postUrl = None
  7314. if 'postUrl' in optionsConfirmParams:
  7315. postUrl = optionsConfirmParams.split('postUrl=')[1]
  7316. if '&' in postUrl:
  7317. postUrl = postUrl.split('&')[0]
  7318. petname = None
  7319. if 'optionpetname' in optionsConfirmParams:
  7320. petname = optionsConfirmParams.split('optionpetname=')[1]
  7321. if '&' in petname:
  7322. petname = petname.split('&')[0]
  7323. # Limit the length of the petname
  7324. if len(petname) > 20 or \
  7325. ' ' in petname or '/' in petname or \
  7326. '?' in petname or '#' in petname:
  7327. petname = None
  7328. optionsNickname = getNicknameFromActor(optionsActor)
  7329. if not optionsNickname:
  7330. if callingDomain.endswith('.onion') and \
  7331. self.server.onionDomain:
  7332. originPathStr = \
  7333. 'http://' + self.server.onionDomain + usersPath
  7334. elif (callingDomain.endswith('.i2p') and
  7335. self.server.i2pDomain):
  7336. originPathStr = \
  7337. 'http://' + self.server.i2pDomain + usersPath
  7338. print('WARN: unable to find nickname in ' + optionsActor)
  7339. self._redirect_headers(originPathStr, cookie, callingDomain)
  7340. self.server.POSTbusy = False
  7341. return
  7342. optionsDomain, optionsPort = getDomainFromActor(optionsActor)
  7343. optionsDomainFull = optionsDomain
  7344. if optionsPort:
  7345. if optionsPort != 80 and optionsPort != 443:
  7346. if ':' not in optionsDomain:
  7347. optionsDomainFull = optionsDomain + ':' + \
  7348. str(optionsPort)
  7349. if chooserNickname == optionsNickname and \
  7350. optionsDomain == self.server.domain and \
  7351. optionsPort == self.server.port:
  7352. if self.server.debug:
  7353. print('You cannot perform an option action on yourself')
  7354. if '&submitView=' in optionsConfirmParams:
  7355. if self.server.debug:
  7356. print('Viewing ' + optionsActor)
  7357. self._redirect_headers(optionsActor,
  7358. cookie, callingDomain)
  7359. self.server.POSTbusy = False
  7360. return
  7361. if '&submitPetname=' in optionsConfirmParams and petname:
  7362. if self.server.debug:
  7363. print('Change petname to ' + petname)
  7364. handle = optionsNickname + '@' + optionsDomainFull
  7365. setPetName(self.server.baseDir,
  7366. chooserNickname,
  7367. self.server.domain,
  7368. handle, petname)
  7369. self._redirect_headers(originPathStr + '/' +
  7370. self.server.defaultTimeline +
  7371. '?page='+str(pageNumber), cookie,
  7372. callingDomain)
  7373. self.server.POSTbusy = False
  7374. return
  7375. if '&submitOnCalendar=' in optionsConfirmParams:
  7376. onCalendar = None
  7377. if 'onCalendar=' in optionsConfirmParams:
  7378. onCalendar = optionsConfirmParams.split('onCalendar=')[1]
  7379. if '&' in onCalendar:
  7380. onCalendar = onCalendar.split('&')[0]
  7381. if onCalendar == 'on':
  7382. addPersonToCalendar(self.server.baseDir,
  7383. chooserNickname,
  7384. self.server.domain,
  7385. optionsNickname,
  7386. optionsDomainFull)
  7387. else:
  7388. removePersonFromCalendar(self.server.baseDir,
  7389. chooserNickname,
  7390. self.server.domain,
  7391. optionsNickname,
  7392. optionsDomainFull)
  7393. self._redirect_headers(originPathStr + '/' +
  7394. self.server.defaultTimeline +
  7395. '?page='+str(pageNumber), cookie,
  7396. callingDomain)
  7397. self.server.POSTbusy = False
  7398. return
  7399. if '&submitBlock=' in optionsConfirmParams:
  7400. if self.server.debug:
  7401. print('Adding block by ' + chooserNickname +
  7402. ' of ' + optionsActor)
  7403. addBlock(self.server.baseDir, chooserNickname,
  7404. self.server.domain,
  7405. optionsNickname, optionsDomainFull)
  7406. if '&submitUnblock=' in optionsConfirmParams:
  7407. if self.server.debug:
  7408. print('Unblocking ' + optionsActor)
  7409. msg = \
  7410. htmlUnblockConfirm(self.server.translate,
  7411. self.server.baseDir,
  7412. originPathStr,
  7413. optionsActor,
  7414. optionsAvatarUrl).encode('utf-8')
  7415. self._set_headers('text/html', len(msg),
  7416. cookie, callingDomain)
  7417. self._write(msg)
  7418. self.server.POSTbusy = False
  7419. return
  7420. if '&submitFollow=' in optionsConfirmParams:
  7421. if self.server.debug:
  7422. print('Following ' + optionsActor)
  7423. msg = \
  7424. htmlFollowConfirm(self.server.translate,
  7425. self.server.baseDir,
  7426. originPathStr,
  7427. optionsActor,
  7428. optionsAvatarUrl).encode('utf-8')
  7429. self._set_headers('text/html', len(msg),
  7430. cookie, callingDomain)
  7431. self._write(msg)
  7432. self.server.POSTbusy = False
  7433. return
  7434. if '&submitUnfollow=' in optionsConfirmParams:
  7435. if self.server.debug:
  7436. print('Unfollowing ' + optionsActor)
  7437. msg = \
  7438. htmlUnfollowConfirm(self.server.translate,
  7439. self.server.baseDir,
  7440. originPathStr,
  7441. optionsActor,
  7442. optionsAvatarUrl).encode('utf-8')
  7443. self._set_headers('text/html', len(msg),
  7444. cookie, callingDomain)
  7445. self._write(msg)
  7446. self.server.POSTbusy = False
  7447. return
  7448. if '&submitDM=' in optionsConfirmParams:
  7449. if self.server.debug:
  7450. print('Sending DM to ' + optionsActor)
  7451. reportPath = self.path.replace('/personoptions', '') + '/newdm'
  7452. msg = htmlNewPost(False, self.server.translate,
  7453. self.server.baseDir,
  7454. self.server.httpPrefix,
  7455. reportPath, None,
  7456. [optionsActor], None,
  7457. pageNumber,
  7458. chooserNickname,
  7459. self.server.domain,
  7460. self.server.domainFull).encode('utf-8')
  7461. self._set_headers('text/html', len(msg),
  7462. cookie, callingDomain)
  7463. self._write(msg)
  7464. self.server.POSTbusy = False
  7465. return
  7466. if '&submitSnooze=' in optionsConfirmParams:
  7467. usersPath = self.path.split('/personoptions')[0]
  7468. thisActor = \
  7469. self.server.httpPrefix + '://' + \
  7470. self.server.domainFull+usersPath
  7471. if self.server.debug:
  7472. print('Snoozing ' + optionsActor + ' ' + thisActor)
  7473. if '/users/' in thisActor:
  7474. nickname = thisActor.split('/users/')[1]
  7475. personSnooze(self.server.baseDir, nickname,
  7476. self.server.domain, optionsActor)
  7477. if callingDomain.endswith('.onion') and \
  7478. self.server.onionDomain:
  7479. thisActor = \
  7480. 'http://' + self.server.onionDomain + usersPath
  7481. elif (callingDomain.endswith('.i2p') and
  7482. self.server.i2pDomain):
  7483. thisActor = \
  7484. 'http://' + self.server.i2pDomain + usersPath
  7485. self._redirect_headers(thisActor + '/' +
  7486. self.server.defaultTimeline +
  7487. '?page='+str(pageNumber), cookie,
  7488. callingDomain)
  7489. self.server.POSTbusy = False
  7490. return
  7491. if '&submitUnSnooze=' in optionsConfirmParams:
  7492. usersPath = self.path.split('/personoptions')[0]
  7493. thisActor = \
  7494. self.server.httpPrefix + '://' + \
  7495. self.server.domainFull + usersPath
  7496. if self.server.debug:
  7497. print('Unsnoozing ' + optionsActor + ' ' + thisActor)
  7498. if '/users/' in thisActor:
  7499. nickname = thisActor.split('/users/')[1]
  7500. personUnsnooze(self.server.baseDir, nickname,
  7501. self.server.domain, optionsActor)
  7502. if callingDomain.endswith('.onion') and \
  7503. self.server.onionDomain:
  7504. thisActor = \
  7505. 'http://' + self.server.onionDomain + usersPath
  7506. elif (callingDomain.endswith('.i2p') and
  7507. self.server.i2pDomain):
  7508. thisActor = \
  7509. 'http://' + self.server.i2pDomain + usersPath
  7510. self._redirect_headers(thisActor + '/' +
  7511. self.server.defaultTimeline +
  7512. '?page=' + str(pageNumber), cookie,
  7513. callingDomain)
  7514. self.server.POSTbusy = False
  7515. return
  7516. if '&submitReport=' in optionsConfirmParams:
  7517. if self.server.debug:
  7518. print('Reporting ' + optionsActor)
  7519. reportPath = \
  7520. self.path.replace('/personoptions', '') + '/newreport'
  7521. msg = htmlNewPost(False, self.server.translate,
  7522. self.server.baseDir,
  7523. self.server.httpPrefix,
  7524. reportPath, None, [],
  7525. postUrl, pageNumber,
  7526. chooserNickname,
  7527. self.server.domain,
  7528. self.server.domainFull).encode('utf-8')
  7529. self._set_headers('text/html', len(msg),
  7530. cookie, callingDomain)
  7531. self._write(msg)
  7532. self.server.POSTbusy = False
  7533. return
  7534. if callingDomain.endswith('.onion') and self.server.onionDomain:
  7535. originPathStr = \
  7536. 'http://' + self.server.onionDomain + usersPath
  7537. elif callingDomain.endswith('.i2p') and self.server.i2pDomain:
  7538. originPathStr = \
  7539. 'http://' + self.server.i2pDomain + usersPath
  7540. self._redirect_headers(originPathStr, cookie, callingDomain)
  7541. self.server.POSTbusy = False
  7542. return
  7543. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 14)
  7544. # receive different types of post created by htmlNewPost
  7545. postTypes = ("newpost", "newblog", "newunlisted", "newfollowers",
  7546. "newdm", "newreport", "newshare", "newquestion",
  7547. "editblogpost", "newreminder")
  7548. for currPostType in postTypes:
  7549. if not authorized:
  7550. break
  7551. if currPostType != 'newshare':
  7552. postRedirect = self.server.defaultTimeline
  7553. else:
  7554. postRedirect = 'shares'
  7555. pageNumber = self._receiveNewPost(currPostType, self.path)
  7556. if pageNumber:
  7557. nickname = self.path.split('/users/')[1]
  7558. if '/' in nickname:
  7559. nickname = nickname.split('/')[0]
  7560. if callingDomain.endswith('.onion') and \
  7561. self.server.onionDomain:
  7562. self._redirect_headers('http://' +
  7563. self.server.onionDomain +
  7564. '/users/' + nickname +
  7565. '/' + postRedirect +
  7566. '?page=' + str(pageNumber), cookie,
  7567. callingDomain)
  7568. elif (callingDomain.endswith('.i2p') and
  7569. self.server.i2pDomain):
  7570. self._redirect_headers('http://' +
  7571. self.server.i2pDomain +
  7572. '/users/' + nickname +
  7573. '/' + postRedirect +
  7574. '?page=' + str(pageNumber), cookie,
  7575. callingDomain)
  7576. else:
  7577. self._redirect_headers(self.server.httpPrefix + '://' +
  7578. self.server.domainFull +
  7579. '/users/' + nickname +
  7580. '/' + postRedirect +
  7581. '?page=' + str(pageNumber), cookie,
  7582. callingDomain)
  7583. self.server.POSTbusy = False
  7584. return
  7585. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 15)
  7586. if self.path.endswith('/outbox') or self.path.endswith('/shares'):
  7587. if '/users/' in self.path:
  7588. if authorized:
  7589. self.outboxAuthenticated = True
  7590. pathUsersSection = self.path.split('/users/')[1]
  7591. self.postToNickname = pathUsersSection.split('/')[0]
  7592. if not self.outboxAuthenticated:
  7593. self.send_response(405)
  7594. self.end_headers()
  7595. self.server.POSTbusy = False
  7596. return
  7597. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 16)
  7598. # check that the post is to an expected path
  7599. if not (self.path.endswith('/outbox') or
  7600. self.path.endswith('/inbox') or
  7601. self.path.endswith('/shares') or
  7602. self.path.endswith('/moderationaction') or
  7603. self.path.endswith('/caps/new') or
  7604. self.path == '/sharedInbox'):
  7605. print('Attempt to POST to invalid path ' + self.path)
  7606. self._400()
  7607. self.server.POSTbusy = False
  7608. return
  7609. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 17)
  7610. # read the message and convert it into a python dictionary
  7611. length = int(self.headers['Content-length'])
  7612. if self.server.debug:
  7613. print('DEBUG: content-length: ' + str(length))
  7614. if not self.headers['Content-type'].startswith('image/') and \
  7615. not self.headers['Content-type'].startswith('video/') and \
  7616. not self.headers['Content-type'].startswith('audio/'):
  7617. if length > self.server.maxMessageLength:
  7618. print('Maximum message length exceeded ' + str(length))
  7619. self._400()
  7620. self.server.POSTbusy = False
  7621. return
  7622. else:
  7623. if length > self.server.maxMediaSize:
  7624. print('Maximum media size exceeded ' + str(length))
  7625. self._400()
  7626. self.server.POSTbusy = False
  7627. return
  7628. # receive images to the outbox
  7629. if self.headers['Content-type'].startswith('image/') and \
  7630. '/users/' in self.path:
  7631. if not self.outboxAuthenticated:
  7632. if self.server.debug:
  7633. print('DEBUG: unauthenticated attempt to ' +
  7634. 'post image to outbox')
  7635. self.send_response(403)
  7636. self.end_headers()
  7637. self.server.POSTbusy = False
  7638. return
  7639. pathUsersSection = self.path.split('/users/')[1]
  7640. if '/' not in pathUsersSection:
  7641. self._404()
  7642. self.server.POSTbusy = False
  7643. return
  7644. self.postFromNickname = pathUsersSection.split('/')[0]
  7645. accountsDir = \
  7646. self.server.baseDir + '/accounts/' + \
  7647. self.postFromNickname + '@' + self.server.domain
  7648. if not os.path.isdir(accountsDir):
  7649. self._404()
  7650. self.server.POSTbusy = False
  7651. return
  7652. try:
  7653. mediaBytes = self.rfile.read(length)
  7654. except SocketError as e:
  7655. if e.errno == errno.ECONNRESET:
  7656. print('WARN: POST mediaBytes ' +
  7657. 'connection reset by peer')
  7658. else:
  7659. print('WARN: POST mediaBytes socket error')
  7660. self.send_response(400)
  7661. self.end_headers()
  7662. self.server.POSTbusy = False
  7663. return
  7664. except ValueError as e:
  7665. print('ERROR: POST mediaBytes rfile.read failed')
  7666. print(e)
  7667. self.send_response(400)
  7668. self.end_headers()
  7669. self.server.POSTbusy = False
  7670. return
  7671. mediaFilenameBase = accountsDir + '/upload'
  7672. mediaFilename = mediaFilenameBase + '.png'
  7673. if self.headers['Content-type'].endswith('jpeg'):
  7674. mediaFilename = mediaFilenameBase + '.jpg'
  7675. if self.headers['Content-type'].endswith('gif'):
  7676. mediaFilename = mediaFilenameBase + '.gif'
  7677. if self.headers['Content-type'].endswith('webp'):
  7678. mediaFilename = mediaFilenameBase + '.webp'
  7679. with open(mediaFilename, 'wb') as avFile:
  7680. avFile.write(mediaBytes)
  7681. if self.server.debug:
  7682. print('DEBUG: image saved to ' + mediaFilename)
  7683. self.send_response(201)
  7684. self.end_headers()
  7685. self.server.POSTbusy = False
  7686. return
  7687. # refuse to receive non-json content
  7688. if self.headers['Content-type'] != 'application/json' and \
  7689. self.headers['Content-type'] != 'application/activity+json':
  7690. print("POST is not json: " + self.headers['Content-type'])
  7691. if self.server.debug:
  7692. print(str(self.headers))
  7693. length = int(self.headers['Content-length'])
  7694. if length < self.server.maxPostLength:
  7695. try:
  7696. unknownPost = self.rfile.read(length).decode('utf-8')
  7697. except SocketError as e:
  7698. if e.errno == errno.ECONNRESET:
  7699. print('WARN: POST unknownPost ' +
  7700. 'connection reset by peer')
  7701. else:
  7702. print('WARN: POST unknownPost socket error')
  7703. self.send_response(400)
  7704. self.end_headers()
  7705. self.server.POSTbusy = False
  7706. return
  7707. except ValueError as e:
  7708. print('ERROR: POST unknownPost rfile.read failed')
  7709. print(e)
  7710. self.send_response(400)
  7711. self.end_headers()
  7712. self.server.POSTbusy = False
  7713. return
  7714. print(str(unknownPost))
  7715. self._400()
  7716. self.server.POSTbusy = False
  7717. return
  7718. if self.server.debug:
  7719. print('DEBUG: Reading message')
  7720. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 18)
  7721. # check content length before reading bytes
  7722. if self.path == '/sharedInbox' or self.path == '/inbox':
  7723. length = 0
  7724. if self.headers.get('Content-length'):
  7725. length = int(self.headers['Content-length'])
  7726. elif self.headers.get('Content-Length'):
  7727. length = int(self.headers['Content-Length'])
  7728. elif self.headers.get('content-length'):
  7729. length = int(self.headers['content-length'])
  7730. if length > 10240:
  7731. print('WARN: post to shared inbox is too long ' +
  7732. str(length) + ' bytes')
  7733. self._400()
  7734. self.server.POSTbusy = False
  7735. return
  7736. try:
  7737. messageBytes = self.rfile.read(length)
  7738. except SocketError as e:
  7739. if e.errno == errno.ECONNRESET:
  7740. print('WARN: POST messageBytes ' +
  7741. 'connection reset by peer')
  7742. else:
  7743. print('WARN: POST messageBytes socket error')
  7744. self.send_response(400)
  7745. self.end_headers()
  7746. self.server.POSTbusy = False
  7747. return
  7748. except ValueError as e:
  7749. print('ERROR: POST messageBytes rfile.read failed')
  7750. print(e)
  7751. self.send_response(400)
  7752. self.end_headers()
  7753. self.server.POSTbusy = False
  7754. return
  7755. # check content length after reading bytes
  7756. if self.path == '/sharedInbox' or self.path == '/inbox':
  7757. lenMessage = len(messageBytes)
  7758. if lenMessage > 10240:
  7759. print('WARN: post to shared inbox is too long ' +
  7760. str(lenMessage) + ' bytes')
  7761. self._400()
  7762. self.server.POSTbusy = False
  7763. return
  7764. # convert the raw bytes to json
  7765. messageJson = json.loads(messageBytes)
  7766. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 19)
  7767. # https://www.w3.org/TR/activitypub/#object-without-create
  7768. if self.outboxAuthenticated:
  7769. if self._postToOutbox(messageJson, __version__):
  7770. if messageJson.get('id'):
  7771. locnStr = messageJson['id'].replace('/activity', '')
  7772. locnStr = locnStr.replace('/undo', '')
  7773. self.headers['Location'] = locnStr
  7774. self.send_response(201)
  7775. self.end_headers()
  7776. self.server.POSTbusy = False
  7777. return
  7778. else:
  7779. if self.server.debug:
  7780. print('Failed to post to outbox')
  7781. self.send_response(403)
  7782. self.end_headers()
  7783. self.server.POSTbusy = False
  7784. return
  7785. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 20)
  7786. # check the necessary properties are available
  7787. if self.server.debug:
  7788. print('DEBUG: Check message has params')
  7789. if self.path.endswith('/inbox') or \
  7790. self.path == '/sharedInbox':
  7791. if not inboxMessageHasParams(messageJson):
  7792. if self.server.debug:
  7793. print("DEBUG: inbox message doesn't have the " +
  7794. "required parameters")
  7795. self.send_response(403)
  7796. self.end_headers()
  7797. self.server.POSTbusy = False
  7798. return
  7799. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 21)
  7800. if not self.headers.get('signature'):
  7801. if 'keyId=' not in self.headers['signature']:
  7802. if self.server.debug:
  7803. print('DEBUG: POST to inbox has no keyId in ' +
  7804. 'header signature parameter')
  7805. self.send_response(403)
  7806. self.end_headers()
  7807. self.server.POSTbusy = False
  7808. return
  7809. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 22)
  7810. if not inboxPermittedMessage(self.server.domain,
  7811. messageJson,
  7812. self.server.federationList):
  7813. if self.server.debug:
  7814. # https://www.youtube.com/watch?v=K3PrSj9XEu4
  7815. print('DEBUG: Ah Ah Ah')
  7816. self.send_response(403)
  7817. self.end_headers()
  7818. self.server.POSTbusy = False
  7819. return
  7820. self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 23)
  7821. if self.server.debug:
  7822. print('DEBUG: POST saving to inbox queue')
  7823. if '/users/' in self.path:
  7824. pathUsersSection = self.path.split('/users/')[1]
  7825. if '/' not in pathUsersSection:
  7826. if self.server.debug:
  7827. print('DEBUG: This is not a users endpoint')
  7828. else:
  7829. self.postToNickname = pathUsersSection.split('/')[0]
  7830. if self.postToNickname:
  7831. queueStatus = \
  7832. self._updateInboxQueue(self.postToNickname,
  7833. messageJson, messageBytes)
  7834. if queueStatus >= 0 and queueStatus <= 3:
  7835. return
  7836. if self.server.debug:
  7837. print('_updateInboxQueue exited ' +
  7838. 'without doing anything')
  7839. else:
  7840. if self.server.debug:
  7841. print('self.postToNickname is None')
  7842. self.send_response(403)
  7843. self.end_headers()
  7844. self.server.POSTbusy = False
  7845. return
  7846. else:
  7847. if self.path == '/sharedInbox' or self.path == '/inbox':
  7848. print('DEBUG: POST to shared inbox')
  7849. queueStatus = \
  7850. self._updateInboxQueue('inbox', messageJson, messageBytes)
  7851. if queueStatus >= 0 and queueStatus <= 3:
  7852. return
  7853. self._200()
  7854. self.server.POSTbusy = False
  7855. class PubServerUnitTest(PubServer):
  7856. protocol_version = 'HTTP/1.0'
  7857. def runPostsQueue(baseDir: str, sendThreads: [], debug: bool) -> None:
  7858. """Manages the threads used to send posts
  7859. """
  7860. while True:
  7861. time.sleep(1)
  7862. removeDormantThreads(baseDir, sendThreads, debug)
  7863. def runSharesExpire(versionNumber: str, baseDir: str) -> None:
  7864. """Expires shares as needed
  7865. """
  7866. while True:
  7867. time.sleep(120)
  7868. expireShares(baseDir)
  7869. def runPostsWatchdog(projectVersion: str, httpd) -> None:
  7870. """This tries to keep the posts thread running even if it dies
  7871. """
  7872. print('Starting posts queue watchdog')
  7873. postsQueueOriginal = httpd.thrPostsQueue.clone(runPostsQueue)
  7874. httpd.thrPostsQueue.start()
  7875. while True:
  7876. time.sleep(20)
  7877. if not httpd.thrPostsQueue.isAlive():
  7878. httpd.thrPostsQueue.kill()
  7879. httpd.thrPostsQueue = postsQueueOriginal.clone(runPostsQueue)
  7880. httpd.thrPostsQueue.start()
  7881. print('Restarting posts queue...')
  7882. def runSharesExpireWatchdog(projectVersion: str, httpd) -> None:
  7883. """This tries to keep the shares expiry thread running even if it dies
  7884. """
  7885. print('Starting shares expiry watchdog')
  7886. sharesExpireOriginal = httpd.thrSharesExpire.clone(runSharesExpire)
  7887. httpd.thrSharesExpire.start()
  7888. while True:
  7889. time.sleep(20)
  7890. if not httpd.thrSharesExpire.isAlive():
  7891. httpd.thrSharesExpire.kill()
  7892. httpd.thrSharesExpire = sharesExpireOriginal.clone(runSharesExpire)
  7893. httpd.thrSharesExpire.start()
  7894. print('Restarting shares expiry...')
  7895. def loadTokens(baseDir: str, tokensDict: {}, tokensLookup: {}) -> None:
  7896. for subdir, dirs, files in os.walk(baseDir + '/accounts'):
  7897. for handle in dirs:
  7898. if '@' in handle:
  7899. tokenFilename = baseDir + '/accounts/' + handle + '/.token'
  7900. if not os.path.isfile(tokenFilename):
  7901. continue
  7902. nickname = handle.split('@')[0]
  7903. token = None
  7904. try:
  7905. with open(tokenFilename, 'r') as fp:
  7906. token = fp.read()
  7907. except Exception as e:
  7908. print('WARN: Unable to read token for ' +
  7909. nickname + ' ' + str(e))
  7910. if not token:
  7911. continue
  7912. tokensDict[nickname] = token
  7913. tokensLookup[token] = nickname
  7914. def runDaemon(blogsInstance: bool, mediaInstance: bool,
  7915. maxRecentPosts: int,
  7916. enableSharedInbox: bool, registration: bool,
  7917. language: str, projectVersion: str,
  7918. instanceId: str, clientToServer: bool,
  7919. baseDir: str, domain: str,
  7920. onionDomain: str, i2pDomain: str,
  7921. port=80, proxyPort=80, httpPrefix='https',
  7922. fedList=[], maxMentions=10, maxEmoji=10,
  7923. authenticatedFetch=False,
  7924. noreply=False, nolike=False, nopics=False,
  7925. noannounce=False, cw=False, ocapAlways=False,
  7926. proxyType=None, maxReplies=64,
  7927. domainMaxPostsPerDay=8640, accountMaxPostsPerDay=864,
  7928. allowDeletion=False, debug=False, unitTest=False,
  7929. instanceOnlySkillsSearch=False, sendThreads=[],
  7930. useBlurHash=False) -> None:
  7931. if len(domain) == 0:
  7932. domain = 'localhost'
  7933. if '.' not in domain:
  7934. if domain != 'localhost':
  7935. print('Invalid domain: ' + domain)
  7936. return
  7937. if unitTest:
  7938. serverAddress = (domain, proxyPort)
  7939. pubHandler = partial(PubServerUnitTest)
  7940. else:
  7941. serverAddress = ('', proxyPort)
  7942. pubHandler = partial(PubServer)
  7943. try:
  7944. httpd = ThreadingHTTPServer(serverAddress, pubHandler)
  7945. except Exception as e:
  7946. if e.errno == 98:
  7947. print('ERROR: HTTP server address is already in use. ' +
  7948. str(serverAddress))
  7949. return False
  7950. print('ERROR: HTTP server failed to start. ' + str(e))
  7951. return False
  7952. # This counter is used to update the list of blocked domains in memory.
  7953. # It helps to avoid touching the disk and so improves flooding resistance
  7954. httpd.blocklistUpdateCtr = 0
  7955. httpd.blocklistUpdateInterval = 100
  7956. httpd.domainBlocklist = getDomainBlocklist(baseDir)
  7957. httpd.onionDomain = onionDomain
  7958. httpd.i2pDomain = i2pDomain
  7959. httpd.useBlurHash = useBlurHash
  7960. httpd.mediaInstance = mediaInstance
  7961. httpd.blogsInstance = blogsInstance
  7962. httpd.defaultTimeline = 'inbox'
  7963. if mediaInstance:
  7964. httpd.defaultTimeline = 'tlmedia'
  7965. if blogsInstance:
  7966. httpd.defaultTimeline = 'tlblogs'
  7967. # load translations dictionary
  7968. httpd.translate = {}
  7969. httpd.systemLanguage = 'en'
  7970. if not unitTest:
  7971. if not os.path.isdir(baseDir + '/translations'):
  7972. print('ERROR: translations directory not found')
  7973. return
  7974. if not language:
  7975. systemLanguage = locale.getdefaultlocale()[0]
  7976. else:
  7977. systemLanguage = language
  7978. if not systemLanguage:
  7979. systemLanguage = 'en'
  7980. if '_' in systemLanguage:
  7981. systemLanguage = systemLanguage.split('_')[0]
  7982. while '/' in systemLanguage:
  7983. systemLanguage = systemLanguage.split('/')[1]
  7984. if '.' in systemLanguage:
  7985. systemLanguage = systemLanguage.split('.')[0]
  7986. translationsFile = baseDir + '/translations/' + \
  7987. systemLanguage + '.json'
  7988. if not os.path.isfile(translationsFile):
  7989. systemLanguage = 'en'
  7990. translationsFile = baseDir + '/translations/' + \
  7991. systemLanguage + '.json'
  7992. print('System language: ' + systemLanguage)
  7993. httpd.systemLanguage = systemLanguage
  7994. httpd.translate = loadJson(translationsFile)
  7995. if not httpd.translate:
  7996. print('ERROR: no translations loaded from ' + translationsFile)
  7997. sys.exit()
  7998. if registration == 'open':
  7999. httpd.registration = True
  8000. else:
  8001. httpd.registration = False
  8002. httpd.enableSharedInbox = enableSharedInbox
  8003. httpd.outboxThread = {}
  8004. httpd.newPostThread = {}
  8005. httpd.projectVersion = projectVersion
  8006. httpd.authenticatedFetch = authenticatedFetch
  8007. # max POST size of 30M
  8008. httpd.maxPostLength = 1024 * 1024 * 30
  8009. httpd.maxMediaSize = httpd.maxPostLength
  8010. # Maximum text length is 32K - enough for a blog post
  8011. httpd.maxMessageLength = 32000
  8012. # Maximum overall number of posts per box
  8013. httpd.maxPostsInBox = 32000
  8014. httpd.domain = domain
  8015. httpd.port = port
  8016. httpd.domainFull = domain
  8017. if port:
  8018. if port != 80 and port != 443:
  8019. if ':' not in domain:
  8020. httpd.domainFull = domain + ':' + str(port)
  8021. saveDomainQrcode(baseDir, httpPrefix, httpd.domainFull)
  8022. httpd.httpPrefix = httpPrefix
  8023. httpd.debug = debug
  8024. httpd.federationList = fedList.copy()
  8025. httpd.baseDir = baseDir
  8026. httpd.instanceId = instanceId
  8027. httpd.personCache = {}
  8028. httpd.cachedWebfingers = {}
  8029. httpd.proxyType = proxyType
  8030. httpd.session = None
  8031. httpd.sessionLastUpdate = 0
  8032. httpd.lastGET = 0
  8033. httpd.lastPOST = 0
  8034. httpd.GETbusy = False
  8035. httpd.POSTbusy = False
  8036. httpd.receivedMessage = False
  8037. httpd.inboxQueue = []
  8038. httpd.sendThreads = sendThreads
  8039. httpd.postLog = []
  8040. httpd.maxQueueLength = 64
  8041. httpd.ocapAlways = ocapAlways
  8042. httpd.allowDeletion = allowDeletion
  8043. httpd.lastLoginTime = 0
  8044. httpd.maxReplies = maxReplies
  8045. httpd.tokens = {}
  8046. httpd.tokensLookup = {}
  8047. loadTokens(baseDir, httpd.tokens, httpd.tokensLookup)
  8048. httpd.instanceOnlySkillsSearch = instanceOnlySkillsSearch
  8049. httpd.acceptedCaps = ["inbox:write", "objects:read"]
  8050. # contains threads used to send posts to followers
  8051. httpd.followersThreads = []
  8052. if noreply:
  8053. httpd.acceptedCaps.append('inbox:noreply')
  8054. if nolike:
  8055. httpd.acceptedCaps.append('inbox:nolike')
  8056. if nopics:
  8057. httpd.acceptedCaps.append('inbox:nopics')
  8058. if noannounce:
  8059. httpd.acceptedCaps.append('inbox:noannounce')
  8060. if cw:
  8061. httpd.acceptedCaps.append('inbox:cw')
  8062. if not os.path.isdir(baseDir + '/accounts/inbox@' + domain):
  8063. print('Creating shared inbox: inbox@' + domain)
  8064. createSharedInbox(baseDir, 'inbox', domain, port, httpPrefix)
  8065. if not os.path.isdir(baseDir + '/cache'):
  8066. os.mkdir(baseDir + '/cache')
  8067. if not os.path.isdir(baseDir + '/cache/actors'):
  8068. print('Creating actors cache')
  8069. os.mkdir(baseDir + '/cache/actors')
  8070. if not os.path.isdir(baseDir + '/cache/announce'):
  8071. print('Creating announce cache')
  8072. os.mkdir(baseDir + '/cache/announce')
  8073. if not os.path.isdir(baseDir + '/cache/avatars'):
  8074. print('Creating avatars cache')
  8075. os.mkdir(baseDir + '/cache/avatars')
  8076. archiveDir = baseDir + '/archive'
  8077. if not os.path.isdir(archiveDir):
  8078. print('Creating archive')
  8079. os.mkdir(archiveDir)
  8080. print('Creating cache expiry thread')
  8081. httpd.thrCache = \
  8082. threadWithTrace(target=expireCache,
  8083. args=(baseDir, httpd.personCache,
  8084. httpd.httpPrefix,
  8085. archiveDir,
  8086. httpd.maxPostsInBox), daemon=True)
  8087. httpd.thrCache.start()
  8088. print('Creating posts queue')
  8089. httpd.thrPostsQueue = \
  8090. threadWithTrace(target=runPostsQueue,
  8091. args=(baseDir, httpd.sendThreads, debug), daemon=True)
  8092. if not unitTest:
  8093. httpd.thrPostsWatchdog = \
  8094. threadWithTrace(target=runPostsWatchdog,
  8095. args=(projectVersion, httpd), daemon=True)
  8096. httpd.thrPostsWatchdog.start()
  8097. else:
  8098. httpd.thrPostsQueue.start()
  8099. print('Creating expire thread for shared items')
  8100. httpd.thrSharesExpire = \
  8101. threadWithTrace(target=runSharesExpire,
  8102. args=(__version__, baseDir), daemon=True)
  8103. if not unitTest:
  8104. httpd.thrSharesExpireWatchdog = \
  8105. threadWithTrace(target=runSharesExpireWatchdog,
  8106. args=(projectVersion, httpd), daemon=True)
  8107. httpd.thrSharesExpireWatchdog.start()
  8108. else:
  8109. httpd.thrSharesExpire.start()
  8110. httpd.recentPostsCache = {}
  8111. httpd.maxRecentPosts = maxRecentPosts
  8112. httpd.iconsCache = {}
  8113. httpd.fontsCache = {}
  8114. print('Creating inbox queue')
  8115. httpd.thrInboxQueue = \
  8116. threadWithTrace(target=runInboxQueue,
  8117. args=(httpd.recentPostsCache, httpd.maxRecentPosts,
  8118. projectVersion,
  8119. baseDir, httpPrefix, httpd.sendThreads,
  8120. httpd.postLog, httpd.cachedWebfingers,
  8121. httpd.personCache, httpd.inboxQueue,
  8122. domain, onionDomain, i2pDomain, port, proxyType,
  8123. httpd.federationList,
  8124. httpd.ocapAlways, maxReplies,
  8125. domainMaxPostsPerDay, accountMaxPostsPerDay,
  8126. allowDeletion, debug, maxMentions, maxEmoji,
  8127. httpd.translate,
  8128. unitTest, httpd.acceptedCaps), daemon=True)
  8129. print('Creating scheduled post thread')
  8130. httpd.thrPostSchedule = \
  8131. threadWithTrace(target=runPostSchedule,
  8132. args=(baseDir, httpd, 20), daemon=True)
  8133. # flags used when restarting the inbox queue
  8134. httpd.restartInboxQueueInProgress = False
  8135. httpd.restartInboxQueue = False
  8136. if not unitTest:
  8137. print('Creating inbox queue watchdog')
  8138. httpd.thrWatchdog = \
  8139. threadWithTrace(target=runInboxQueueWatchdog,
  8140. args=(projectVersion, httpd), daemon=True)
  8141. httpd.thrWatchdog.start()
  8142. print('Creating scheduled post watchdog')
  8143. httpd.thrWatchdogSchedule = \
  8144. threadWithTrace(target=runPostScheduleWatchdog,
  8145. args=(projectVersion, httpd), daemon=True)
  8146. httpd.thrWatchdogSchedule.start()
  8147. else:
  8148. httpd.thrInboxQueue.start()
  8149. httpd.thrPostSchedule.start()
  8150. if clientToServer:
  8151. print('Running ActivityPub client on ' +
  8152. domain + ' port ' + str(proxyPort))
  8153. else:
  8154. print('Running ActivityPub server on ' +
  8155. domain + ' port ' + str(proxyPort))
  8156. httpd.serve_forever()