12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814 |
- /* Copyright 2014 Google Inc. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- #if !defined(__has_feature) || !__has_feature(objc_arc)
- #error "This file requires ARC support."
- #endif
- #import "GTMSessionFetcher/GTMSessionFetcher.h"
- #import "GTMSessionFetcher/GTMSessionFetcherService.h"
- #import "GTMSessionFetcherService+Internal.h"
- #if TARGET_OS_OSX && GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH
- // To reconnect background sessions on Mac outside +load requires importing and linking
- // AppKit to access the NSApplicationDidFinishLaunching symbol.
- #import <AppKit/AppKit.h>
- #endif
- #include <sys/utsname.h>
- #ifndef STRIP_GTM_FETCH_LOGGING
- #error GTMSessionFetcher headers should have defaulted this if it wasn't already defined.
- #endif
- NS_ASSUME_NONNULL_BEGIN
- NSString *const kGTMSessionFetcherStartedNotification = @"kGTMSessionFetcherStartedNotification";
- NSString *const kGTMSessionFetcherStoppedNotification = @"kGTMSessionFetcherStoppedNotification";
- NSString *const kGTMSessionFetcherRetryDelayStartedNotification =
- @"kGTMSessionFetcherRetryDelayStartedNotification";
- NSString *const kGTMSessionFetcherRetryDelayStoppedNotification =
- @"kGTMSessionFetcherRetryDelayStoppedNotification";
- NSString *const kGTMSessionFetcherCompletionInvokedNotification =
- @"kGTMSessionFetcherCompletionInvokedNotification";
- NSString *const kGTMSessionFetcherCompletionDataKey = @"data";
- NSString *const kGTMSessionFetcherCompletionErrorKey = @"error";
- NSString *const kGTMSessionFetcherErrorDomain = @"com.google.GTMSessionFetcher";
- NSString *const kGTMSessionFetcherStatusDomain = @"com.google.HTTPStatus";
- NSString *const kGTMSessionFetcherStatusDataKey =
- @"data"; // data returned with a kGTMSessionFetcherStatusDomain error
- NSString *const kGTMSessionFetcherStatusDataContentTypeKey = @"data_content_type";
- NSString *const kGTMSessionFetcherNumberOfRetriesDoneKey =
- @"kGTMSessionFetcherNumberOfRetriesDoneKey";
- NSString *const kGTMSessionFetcherElapsedIntervalWithRetriesKey =
- @"kGTMSessionFetcherElapsedIntervalWithRetriesKey";
- static NSString *const kGTMSessionIdentifierPrefix = @"com.google.GTMSessionFetcher";
- static NSString *const kGTMSessionIdentifierDestinationFileURLMetadataKey = @"_destURL";
- static NSString *const kGTMSessionIdentifierBodyFileURLMetadataKey = @"_bodyURL";
- static NSString *const kGTMSessionIdentifierClientReconnectMetadataKey = @"_clientWillReconnect";
- // The default max retry interview is 10 minutes for uploads (POST/PUT/PATCH),
- // 1 minute for downloads.
- static const NSTimeInterval kUnsetMaxRetryInterval = -1.0;
- static const NSTimeInterval kDefaultMaxDownloadRetryInterval = 60.0;
- static const NSTimeInterval kDefaultMaxUploadRetryInterval = 60.0 * 10.;
- // The maximum data length that can be loaded to the error userInfo
- static const int64_t kMaximumDownloadErrorDataLength = 20000;
- #ifdef GTMSESSION_PERSISTED_DESTINATION_KEY
- // Projects using unique class names should also define a unique persisted destination key.
- static NSString *const kGTMSessionFetcherPersistedDestinationKey =
- GTMSESSION_PERSISTED_DESTINATION_KEY;
- #else
- static NSString *const kGTMSessionFetcherPersistedDestinationKey =
- @"com.google.GTMSessionFetcher.downloads";
- #endif
- NS_ASSUME_NONNULL_END
- //
- // GTMSessionFetcher
- //
- #if 0
- #define GTM_LOG_BACKGROUND_SESSION(...) GTMSESSION_LOG_DEBUG(__VA_ARGS__)
- #else
- #define GTM_LOG_BACKGROUND_SESSION(...)
- #endif
- #ifndef GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY
- #define GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY 1
- #endif
- #if ((defined(TARGET_OS_MACCATALYST) && TARGET_OS_MACCATALYST) || \
- (TARGET_OS_OSX && defined(__MAC_10_15) && __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_15) || \
- (TARGET_OS_IOS && defined(__IPHONE_13_0) && \
- __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_13_0) || \
- (TARGET_OS_WATCH && defined(__WATCHOS_6_0) && \
- __WATCH_OS_VERSION_MIN_REQUIRED >= __WATCHOS_6_0) || \
- (TARGET_OS_TV && defined(__TVOS_13_0) && __TVOS_VERSION_MIN_REQUIRED >= __TVOS_13_0))
- #define GTM_SDK_REQUIRES_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 1
- #define GTM_SDK_SUPPORTS_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 1
- #elif ((TARGET_OS_OSX && defined(__MAC_10_15) && __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_15) || \
- (TARGET_OS_IOS && defined(__IPHONE_13_0) && \
- __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0) || \
- (TARGET_OS_WATCH && defined(__WATCHOS_6_0) && \
- __WATCH_OS_VERSION_MAX_ALLOWED >= __WATCHOS_6_0) || \
- (TARGET_OS_TV && defined(__TVOS_13_0) && __TVOS_VERSION_MAX_ALLOWED >= __TVOS_13_0))
- #define GTM_SDK_REQUIRES_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 0
- #define GTM_SDK_SUPPORTS_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 1
- #else
- #define GTM_SDK_REQUIRES_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 0
- #define GTM_SDK_SUPPORTS_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 0
- #endif
- #if ((defined(TARGET_OS_MACCATALYST) && TARGET_OS_MACCATALYST) || \
- (TARGET_OS_OSX && defined(__MAC_10_15) && __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_15) || \
- (TARGET_OS_IOS && defined(__IPHONE_13_0) && \
- __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_13_0) || \
- (TARGET_OS_WATCH && defined(__WATCHOS_6_0) && \
- __WATCH_OS_VERSION_MIN_REQUIRED >= __WATCHOS_6_0) || \
- (TARGET_OS_TV && defined(__TVOS_13_0) && __TVOS_VERSION_MIN_REQUIRED >= __TVOS_13_0))
- #define GTM_SDK_REQUIRES_SECTRUSTEVALUATEWITHERROR 1
- #else
- #define GTM_SDK_REQUIRES_SECTRUSTEVALUATEWITHERROR 0
- #endif
- #if __has_attribute(swift_async)
- // Once Clang 13/Xcode 13 can be assumed, can switch to NS_SWIFT_DISABLE_ASYNC.
- #define GTM_SWIFT_DISABLE_ASYNC __attribute__((swift_async(none)))
- #else
- #define GTM_SWIFT_DISABLE_ASYNC
- #endif
- @interface GTMSessionFetcher ()
- @property(atomic, strong, readwrite, nullable) NSData *downloadedData;
- @property(atomic, strong, readwrite, nullable) NSData *downloadResumeData;
- #if GTM_BACKGROUND_TASK_FETCHING
- // Should always be accessed within an @synchronized(self).
- @property(assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskIdentifier;
- #endif
- @property(atomic, readwrite, getter=isUsingBackgroundSession) BOOL usingBackgroundSession;
- @end
- #if !GTMSESSION_BUILD_COMBINED_SOURCES
- @interface GTMSessionFetcher (GTMSessionFetcherLoggingInternal)
- - (void)logFetchWithError:(NSError *)error;
- - (void)logNowWithError:(nullable NSError *)error;
- - (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream;
- - (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider:
- (GTMSessionFetcherBodyStreamProvider)streamProvider;
- @end
- #endif // !GTMSESSION_BUILD_COMBINED_SOURCES
- NS_ASSUME_NONNULL_BEGIN
- static NSTimeInterval InitialMinRetryInterval(void) {
- return 1.0 + ((double)(arc4random_uniform(0x0FFFF)) / (double)0x0FFFF);
- }
- static BOOL IsLocalhost(NSString *_Nullable host) {
- // We check if there's host, and then make the comparisons.
- if (host == nil) return NO;
- return ([host caseInsensitiveCompare:@"localhost"] == NSOrderedSame || [host isEqual:@"::1"] ||
- [host isEqual:@"127.0.0.1"]);
- }
- static NSDictionary *_Nullable GTMErrorUserInfoForData(NSData *_Nullable data,
- NSDictionary *_Nullable responseHeaders) {
- NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
- if (data.length > 0) {
- userInfo[kGTMSessionFetcherStatusDataKey] = data;
- NSString *contentType = responseHeaders[@"Content-Type"];
- if (contentType) {
- userInfo[kGTMSessionFetcherStatusDataContentTypeKey] = contentType;
- }
- }
- return userInfo.count > 0 ? userInfo : nil;
- }
- static GTMSessionFetcherTestBlock _Nullable gGlobalTestBlock;
- @implementation GTMSessionFetcher {
- NSMutableURLRequest *_request; // after beginFetch, changed only in delegate callbacks
- BOOL _useUploadTask; // immutable after beginFetch
- NSURL *_bodyFileURL; // immutable after beginFetch
- GTMSessionFetcherBodyStreamProvider _bodyStreamProvider; // immutable after beginFetch
- NSURLSession *_session;
- BOOL _shouldInvalidateSession; // immutable after beginFetch
- NSURLSession *_sessionNeedingInvalidation;
- NSURLSessionConfiguration *_configuration;
- NSURLSessionTask *_sessionTask;
- NSString *_taskDescription;
- float _taskPriority;
- NSURLResponse *_response;
- NSString *_sessionIdentifier;
- BOOL _wasCreatedFromBackgroundSession;
- BOOL _clientWillReconnectBackgroundSession;
- BOOL _didCreateSessionIdentifier;
- NSString *_sessionIdentifierUUID;
- BOOL _userRequestedBackgroundSession;
- BOOL _usingBackgroundSession;
- NSMutableData *_Nullable _downloadedData;
- NSError *_downloadFinishedError;
- NSData *_downloadResumeData; // immutable after construction
- NSData *_Nullable _downloadTaskErrorData; // Data for when download task fails
- NSURL *_destinationFileURL;
- int64_t _downloadedLength;
- NSURLCredential *_credential; // username & password
- NSURLCredential *_proxyCredential; // credential supplied to proxy servers
- BOOL _isStopNotificationNeeded; // set when start notification has been sent
- BOOL _isUsingTestBlock; // set when a test block was provided (remains set when the block is
- // released)
- id _userData; // retained, if set by caller
- NSMutableDictionary *_properties; // more data retained for caller
- dispatch_queue_t _callbackQueue;
- dispatch_group_t _callbackGroup; // read-only after creation
- NSOperationQueue *_delegateQueue; // immutable after beginFetch
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated"
- id<GTMFetcherAuthorizationProtocol> _authorizer; // immutable after beginFetch
- #pragma clang diagnostic pop
- // The service object that created and monitors this fetcher, if any.
- GTMSessionFetcherService *_service; // immutable; set by the fetcher service upon creation
- NSString *_serviceHost;
- NSInteger _servicePriority; // immutable after beginFetch
- BOOL _hasStoppedFetching; // counterpart to _initialBeginFetchDate
- BOOL _userStoppedFetching;
- BOOL _isRetryEnabled; // user wants auto-retry
- NSTimer *_retryTimer;
- NSUInteger _retryCount;
- NSTimeInterval _maxRetryInterval; // default 60 (download) or 600 (upload) seconds
- NSTimeInterval _minRetryInterval; // random between 1 and 2 seconds
- NSTimeInterval _retryFactor; // default interval multiplier is 2
- NSTimeInterval _lastRetryInterval;
- NSDate *_initialBeginFetchDate; // date that beginFetch was first invoked; immutable after
- // initial beginFetch
- NSDate *_initialRequestDate; // date of first request to the target server (ignoring auth)
- BOOL _hasAttemptedAuthRefresh; // accessed only in shouldRetryNowForStatus:
- NSString *_comment; // comment for log
- NSString *_log;
- #if !STRIP_GTM_FETCH_LOGGING
- NSMutableData *_loggedStreamData;
- NSURL *_redirectedFromURL;
- NSString *_logRequestBody;
- NSString *_logResponseBody;
- BOOL _hasLoggedError;
- BOOL _deferResponseBodyLogging;
- #endif
- }
- #if !GTMSESSION_UNIT_TESTING
- + (void)load {
- #if GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH && TARGET_OS_IPHONE
- NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
- [nc addObserver:self
- selector:@selector(reconnectFetchersForBackgroundSessionsOnAppLaunch:)
- name:UIApplicationDidFinishLaunchingNotification
- object:nil];
- #elif GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH && TARGET_OS_OSX
- NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
- [nc addObserver:self
- selector:@selector(reconnectFetchersForBackgroundSessionsOnAppLaunch:)
- name:NSApplicationDidFinishLaunchingNotification
- object:nil];
- #else
- [self fetchersForBackgroundSessions];
- #endif
- }
- + (void)reconnectFetchersForBackgroundSessionsOnAppLaunch:(NSNotification *)notification {
- // Give all other app-did-launch handlers a chance to complete before
- // reconnecting the fetchers. Not doing this may lead to reconnecting
- // before the app delegate has a chance to run.
- dispatch_async(dispatch_get_main_queue(), ^{
- [self fetchersForBackgroundSessions];
- });
- }
- #endif // !GTMSESSION_UNIT_TESTING
- + (instancetype)fetcherWithRequest:(nullable NSURLRequest *)request {
- return [[self alloc] initWithRequest:request configuration:nil];
- }
- + (instancetype)fetcherWithURL:(NSURL *)requestURL {
- return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]];
- }
- + (instancetype)fetcherWithURLString:(NSString *)requestURLString {
- return [self fetcherWithURL:(NSURL *)[NSURL URLWithString:requestURLString]];
- }
- + (instancetype)fetcherWithDownloadResumeData:(NSData *)resumeData {
- GTMSessionFetcher *fetcher = [self fetcherWithRequest:nil];
- fetcher.comment = @"Resuming download";
- fetcher.downloadResumeData = resumeData;
- return fetcher;
- }
- + (nullable instancetype)fetcherWithSessionIdentifier:(NSString *)sessionIdentifier {
- GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier");
- NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap];
- GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier];
- if (!fetcher && [sessionIdentifier hasPrefix:kGTMSessionIdentifierPrefix]) {
- fetcher = [self fetcherWithRequest:nil];
- [fetcher setSessionIdentifier:sessionIdentifier];
- [sessionIdentifierToFetcherMap setObject:fetcher forKey:sessionIdentifier];
- fetcher->_wasCreatedFromBackgroundSession = YES;
- [fetcher setCommentWithFormat:@"Resuming %@", fetcher && fetcher->_sessionIdentifierUUID
- ? fetcher->_sessionIdentifierUUID
- : @"?"];
- }
- return fetcher;
- }
- + (NSMapTable *)sessionIdentifierToFetcherMap {
- // TODO: What if a service is involved in creating the fetcher? Currently, when re-creating
- // fetchers, if a service was involved, it is not re-created. Should the service maintain a map?
- static NSMapTable *gSessionIdentifierToFetcherMap = nil;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- gSessionIdentifierToFetcherMap = [NSMapTable strongToWeakObjectsMapTable];
- });
- return gSessionIdentifierToFetcherMap;
- }
- #if !GTM_ALLOW_INSECURE_REQUESTS
- + (BOOL)appAllowsInsecureRequests {
- // If the main bundle Info.plist key NSAppTransportSecurity is present, and it specifies
- // NSAllowsArbitraryLoads, then we need to explicitly enforce secure schemes.
- #if GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY
- static BOOL allowsInsecureRequests;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- NSBundle *mainBundle = [NSBundle mainBundle];
- NSDictionary *appTransportSecurity =
- [mainBundle objectForInfoDictionaryKey:@"NSAppTransportSecurity"];
- allowsInsecureRequests =
- [[appTransportSecurity objectForKey:@"NSAllowsArbitraryLoads"] boolValue];
- });
- return allowsInsecureRequests;
- #else
- // For builds targeting iOS 8 or 10.10 and earlier, we want to require fetcher
- // security checks.
- return YES;
- #endif // GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY
- }
- #else // GTM_ALLOW_INSECURE_REQUESTS
- + (BOOL)appAllowsInsecureRequests {
- return YES;
- }
- #endif // !GTM_ALLOW_INSECURE_REQUESTS
- - (instancetype)init {
- return [self initWithRequest:nil configuration:nil];
- }
- - (instancetype)initWithRequest:(NSURLRequest *)request {
- return [self initWithRequest:request configuration:nil];
- }
- - (instancetype)initWithRequest:(nullable NSURLRequest *)request
- configuration:(nullable NSURLSessionConfiguration *)configuration {
- self = [super init];
- if (self) {
- #if GTM_BACKGROUND_TASK_FETCHING
- _backgroundTaskIdentifier = UIBackgroundTaskInvalid;
- #endif
- _request = [request mutableCopy];
- _configuration = configuration;
- NSData *bodyData = request.HTTPBody;
- if (bodyData) {
- _bodyLength = (int64_t)bodyData.length;
- } else {
- _bodyLength = NSURLSessionTransferSizeUnknown;
- }
- _callbackQueue = dispatch_get_main_queue();
- _callbackGroup = dispatch_group_create();
- _delegateQueue = [NSOperationQueue mainQueue];
- _minRetryInterval = InitialMinRetryInterval();
- _maxRetryInterval = kUnsetMaxRetryInterval;
- _taskPriority = -1.0f; // Valid values if set are 0.0...1.0.
- _testBlockAccumulateDataChunkCount = 1;
- #if !STRIP_GTM_FETCH_LOGGING
- // Encourage developers to set the comment property or use
- // setCommentWithFormat: by providing a default string.
- _comment = @"(No fetcher comment set)";
- #endif
- }
- return self;
- }
- - (id)copyWithZone:(NSZone *)zone {
- // disallow use of fetchers in a copy property
- [self doesNotRecognizeSelector:_cmd];
- return nil;
- }
- - (NSString *)description {
- NSString *requestStr = self.request.URL.description;
- if (requestStr.length == 0) {
- if (self.downloadResumeData.length > 0) {
- requestStr = @"<download resume data>";
- } else if (_wasCreatedFromBackgroundSession) {
- requestStr = @"<from bg session>";
- } else {
- requestStr = @"<no request>";
- }
- }
- return [NSString stringWithFormat:@"%@ %p (%@)", [self class], self, requestStr];
- }
- - (void)dealloc {
- GTMSESSION_ASSERT_DEBUG(!_isStopNotificationNeeded, @"unbalanced fetcher notification for %@",
- _request.URL);
- [self forgetSessionIdentifierForFetcherWithoutSyncCheck];
- // Note: if a session task or a retry timer was pending, then this instance
- // would be retained by those so it wouldn't be getting dealloc'd,
- // hence we don't need to stopFetch here
- }
- #pragma mark -
- // Begin fetching the URL (or begin a retry fetch). The delegate is retained
- // for the duration of the fetch connection.
- - (void)beginFetchWithCompletionHandler:(nullable GTMSessionFetcherCompletionHandler)handler {
- GTMSessionCheckNotSynchronized(self);
- _completionHandler = [handler copy];
- // The user may have called setDelegate: earlier if they want to use other
- // delegate-style callbacks during the fetch; otherwise, the delegate is nil,
- // which is fine.
- [self beginFetchMayDelay:YES mayAuthorize:YES mayDecorate:YES];
- }
- // Begin fetching the URL for a retry fetch. The delegate and completion handler
- // are already provided, and do not need to be copied.
- - (void)beginFetchForRetry {
- GTMSessionCheckNotSynchronized(self);
- [self beginFetchMayDelay:YES mayAuthorize:YES mayDecorate:YES];
- }
- - (GTMSessionFetcherCompletionHandler)completionHandlerWithTarget:(nullable id)target
- didFinishSelector:(nullable SEL)finishedSelector {
- GTMSessionFetcherAssertValidSelector(target, finishedSelector, @encode(GTMSessionFetcher *),
- @encode(NSData *), @encode(NSError *), 0);
- GTMSessionFetcherCompletionHandler completionHandler = ^(NSData *data, NSError *error) {
- if (target && finishedSelector) {
- id selfArg = self; // Placate ARC.
- NSMethodSignature *sig = [target methodSignatureForSelector:finishedSelector];
- NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
- [invocation setSelector:(SEL)finishedSelector];
- [invocation setTarget:target];
- [invocation setArgument:&selfArg atIndex:2];
- [invocation setArgument:&data atIndex:3];
- [invocation setArgument:&error atIndex:4];
- [invocation invoke];
- }
- };
- return completionHandler;
- }
- - (void)beginFetchWithDelegate:(nullable id)target
- didFinishSelector:(nullable SEL)finishedSelector {
- GTMSessionCheckNotSynchronized(self);
- GTMSessionFetcherCompletionHandler handler = [self completionHandlerWithTarget:target
- didFinishSelector:finishedSelector];
- [self beginFetchWithCompletionHandler:handler];
- }
- - (void)beginFetchMayDelay:(BOOL)mayDelay
- mayAuthorize:(BOOL)mayAuthorize
- mayDecorate:(BOOL)mayDecorate {
- // This is the internal entry point for re-starting fetches.
- GTMSessionCheckNotSynchronized(self);
- NSMutableURLRequest *fetchRequest =
- _request; // The request property is now externally immutable.
- NSURL *fetchRequestURL = fetchRequest.URL;
- NSString *priorSessionIdentifier = self.sessionIdentifier;
- GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URL:%@ beginFetchMayDelay:%d mayAuthorize:%d mayDecorate:%d",
- [self class], self, _request.URL, mayDelay, mayAuthorize,
- mayDecorate);
- // A utility block for creating error objects when we fail to start the fetch.
- NSError * (^beginFailureError)(NSInteger) = ^(NSInteger code) {
- NSString *urlString = fetchRequestURL.absoluteString;
- NSDictionary *userInfo =
- @{NSURLErrorFailingURLStringErrorKey : (urlString ? urlString : @"(missing URL)")};
- return [NSError errorWithDomain:kGTMSessionFetcherErrorDomain code:code userInfo:userInfo];
- };
- // Catch delegate queue maxConcurrentOperationCount values other than 1, particularly
- // NSOperationQueueDefaultMaxConcurrentOperationCount (-1), to avoid the additional complexity
- // of simultaneous or out-of-order delegate callbacks.
- GTMSESSION_ASSERT_DEBUG(_delegateQueue.maxConcurrentOperationCount == 1,
- @"delegate queue %@ should support one concurrent operation, not %ld",
- _delegateQueue.name, (long)_delegateQueue.maxConcurrentOperationCount);
- if (!_initialBeginFetchDate) {
- // This ivar is set only here on the initial beginFetch so need not be synchronized.
- _initialBeginFetchDate = [[NSDate alloc] init];
- }
- if (self.sessionTask != nil) {
- // If cached fetcher returned through fetcherWithSessionIdentifier:, then it's
- // already begun, but don't consider this a failure, since the user need not know this.
- if (self.sessionIdentifier != nil) {
- return;
- }
- GTMSESSION_ASSERT_DEBUG(NO, @"Fetch object %@ being reused; this should never happen", self);
- [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorDownloadFailed)];
- return;
- }
- if (fetchRequestURL == nil && !_downloadResumeData && !priorSessionIdentifier) {
- GTMSESSION_ASSERT_DEBUG(NO, @"Beginning a fetch requires a request with a URL");
- [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorDownloadFailed)];
- return;
- }
- // We'll respect the user's request for a background session (unless this is
- // an upload fetcher, which does its initial request foreground.)
- self.usingBackgroundSession = self.useBackgroundSession && [self canFetchWithBackgroundSession];
- NSURL *bodyFileURL = self.bodyFileURL;
- if (bodyFileURL) {
- NSError *fileCheckError;
- if (![bodyFileURL checkResourceIsReachableAndReturnError:&fileCheckError]) {
- // This assert fires when the file being uploaded no longer exists once
- // the fetcher is ready to start the upload.
- GTMSESSION_ASSERT_DEBUG_OR_LOG(0, @"Body file is unreachable: %@\n %@", bodyFileURL.path,
- fileCheckError);
- [self failToBeginFetchWithError:fileCheckError];
- return;
- }
- }
- NSString *requestScheme = fetchRequestURL.scheme;
- BOOL isDataRequest = [requestScheme isEqual:@"data"];
- if (isDataRequest) {
- // NSURLSession does not support data URLs in background sessions.
- #if DEBUG
- if (priorSessionIdentifier || self.sessionIdentifier) {
- GTMSESSION_LOG_DEBUG(@"Converting background to foreground session for %@", fetchRequest);
- }
- #endif
- // If priorSessionIdentifier is allowed to stay non-nil, a background session can
- // still be created.
- priorSessionIdentifier = nil;
- [self setSessionIdentifierInternal:nil];
- self.usingBackgroundSession = NO;
- }
- #if GTM_ALLOW_INSECURE_REQUESTS
- BOOL shouldCheckSecurity = NO;
- #else
- BOOL shouldCheckSecurity =
- (fetchRequestURL != nil && !isDataRequest && [[self class] appAllowsInsecureRequests]);
- #endif
- if (shouldCheckSecurity) {
- // Allow https only for requests, unless overridden by the client.
- //
- // Non-https requests may too easily be snooped, so we disallow them by default.
- //
- // file: and data: schemes are usually safe if they are hardcoded in the client or provided
- // by a trusted source, but since it's fairly rare to need them, it's safest to make clients
- // explicitly allow them.
- BOOL isSecure =
- requestScheme != nil && [requestScheme caseInsensitiveCompare:@"https"] == NSOrderedSame;
- if (!isSecure) {
- BOOL allowRequest = NO;
- NSString *host = fetchRequestURL.host;
- // Check schemes first. A file scheme request may be allowed here, or as a localhost request.
- for (NSString *allowedScheme in _allowedInsecureSchemes) {
- if (requestScheme != nil &&
- [requestScheme caseInsensitiveCompare:allowedScheme] == NSOrderedSame) {
- allowRequest = YES;
- break;
- }
- }
- if (!allowRequest) {
- // Check for localhost requests. Security checks only occur for non-https requests, so
- // this check won't happen for an https request to localhost.
- BOOL isLocalhostRequest =
- (host.length == 0 && [fetchRequestURL isFileURL]) || IsLocalhost(host);
- if (isLocalhostRequest) {
- if (self.allowLocalhostRequest) {
- allowRequest = YES;
- } else {
- GTMSESSION_ASSERT_DEBUG(NO,
- @"Fetch request for localhost but fetcher"
- @" allowLocalhostRequest is not set: %@",
- fetchRequestURL);
- }
- } else {
- GTMSESSION_ASSERT_DEBUG(NO,
- @"Insecure fetch request has a scheme (%@)"
- @" not found in fetcher allowedInsecureSchemes (%@): %@",
- requestScheme, _allowedInsecureSchemes ?: @" @[] ",
- fetchRequestURL);
- }
- }
- if (!allowRequest) {
- #if !DEBUG
- NSLog(@"Insecure fetch disallowed for %@",
- fetchRequestURL.description ?: @"nil request URL");
- #endif
- [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorInsecureRequest)];
- return;
- }
- } // !isSecure
- } // (requestURL != nil) && !isDataRequest
- if (self.cookieStorage == nil) {
- self.cookieStorage = [[self class] staticCookieStorage];
- }
- BOOL isRecreatingSession = (self.sessionIdentifier != nil) && (fetchRequest == nil);
- self.canShareSession = (_service != nil) && !isRecreatingSession && !self.usingBackgroundSession;
- if (!self.session) {
- if (self.canShareSession) {
- self.session = [_service
- sessionWithCreationBlock:^NSURLSession *(id<NSURLSessionDelegate> sessionDelegate) {
- return [self createSessionWithDelegate:sessionDelegate
- sessionIdentifier:priorSessionIdentifier];
- }];
- } else {
- self.session = [self createSessionWithDelegate:self sessionIdentifier:priorSessionIdentifier];
- }
- }
- if (isRecreatingSession) {
- _shouldInvalidateSession = YES;
- // Let's make sure there are tasks still running or if not that we get a callback from a
- // completed one; otherwise, we assume the tasks failed.
- // This is the observed behavior perhaps 25% of the time within the Simulator running 7.0.3 on
- // exiting the app after starting an upload and relaunching the app if we manage to relaunch
- // after the task has completed, but before the system relaunches us in the background.
- [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks,
- NSArray *downloadTasks) {
- if (dataTasks.count == 0 && uploadTasks.count == 0 && downloadTasks.count == 0) {
- double const kDelayInSeconds = 1.0; // We should get progress indication or completion soon
- dispatch_time_t checkForFeedbackDelay =
- dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kDelayInSeconds * NSEC_PER_SEC));
- dispatch_after(checkForFeedbackDelay, dispatch_get_main_queue(), ^{
- if (!self.sessionTask && !fetchRequest) {
- // If our task and/or request haven't been restored, then we assume task feedback lost.
- [self removePersistedBackgroundSessionFromDefaults];
- NSError *sessionError =
- [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
- code:GTMSessionFetcherErrorBackgroundFetchFailed
- userInfo:nil];
- [self failToBeginFetchWithError:sessionError];
- }
- });
- }
- }];
- return;
- }
- self.downloadedData = nil;
- self.downloadedLength = 0;
- if (_servicePriority == NSIntegerMin) {
- mayDelay = NO;
- }
- if (mayDelay && _service) {
- BOOL shouldFetchNow = [_service fetcherShouldBeginFetching:self];
- if (!shouldFetchNow) {
- // The fetch is deferred, but will happen later.
- //
- // If this session is held by the fetcher service, clear the session now so that we don't
- // assume it's still valid after the fetcher is restarted.
- if (self.canShareSession) {
- self.session = nil;
- }
- return;
- }
- }
- NSString *effectiveHTTPMethod = [fetchRequest valueForHTTPHeaderField:@"X-HTTP-Method-Override"];
- if (effectiveHTTPMethod == nil) {
- effectiveHTTPMethod = fetchRequest.HTTPMethod;
- }
- BOOL isEffectiveHTTPGet = (effectiveHTTPMethod == nil || [effectiveHTTPMethod isEqual:@"GET"]);
- BOOL needsUploadTask = (self.useUploadTask || self.bodyFileURL || self.bodyStreamProvider);
- if (_bodyData || self.bodyStreamProvider || fetchRequest.HTTPBodyStream) {
- if (isEffectiveHTTPGet) {
- fetchRequest.HTTPMethod = @"POST";
- isEffectiveHTTPGet = NO;
- }
- if (_bodyData) {
- if (!needsUploadTask) {
- fetchRequest.HTTPBody = _bodyData;
- }
- #if !STRIP_GTM_FETCH_LOGGING
- } else if (fetchRequest.HTTPBodyStream) {
- if ([self respondsToSelector:@selector(loggedInputStreamForInputStream:)]) {
- fetchRequest.HTTPBodyStream =
- [self performSelector:@selector(loggedInputStreamForInputStream:)
- withObject:fetchRequest.HTTPBodyStream];
- }
- #endif
- }
- }
- // We authorize after setting up the http method and body in the request
- // because OAuth 1 may need to sign the request body
- if (mayAuthorize && _authorizer && !isDataRequest) {
- BOOL isAuthorized = [_authorizer isAuthorizedRequest:fetchRequest];
- if (!isAuthorized) {
- // Authorization needed.
- //
- // If this session is held by the fetcher service, clear the session now so that we don't
- // assume it's still valid after authorization completes.
- if (self.canShareSession) {
- self.session = nil;
- }
- // Authorizing the request will recursively call this beginFetch:mayDelay:
- // or failToBeginFetchWithError:.
- [self authorizeRequest];
- return;
- }
- }
- if (mayDecorate && [_service respondsToSelector:@selector(decorators)]) {
- NSArray<id<GTMFetcherDecoratorProtocol>> *decorators = _service.decorators;
- if (decorators.count) {
- // If this session is held by the fetcher service, clear the session now so that we don't
- // assume it's still valid after decoration completes.
- //
- // The service will still hold on to the session, so as long as decoration doesn't take more
- // than 30 seconds since the last request, the service's session will be re-used when the
- // fetch actually starts.
- if (self.canShareSession) {
- self.session = nil;
- }
- [self applyDecoratorsAtRequestWillStart:decorators startingAtIndex:0];
- return;
- }
- }
- // set the default upload or download retry interval, if necessary
- if ([self isRetryEnabled] && self.maxRetryInterval <= 0) {
- if (isEffectiveHTTPGet || [effectiveHTTPMethod isEqual:@"HEAD"]) {
- [self setMaxRetryInterval:kDefaultMaxDownloadRetryInterval];
- } else {
- [self setMaxRetryInterval:kDefaultMaxUploadRetryInterval];
- }
- }
- // finally, start the connection
- NSURLSessionTask *newSessionTask;
- BOOL needsDataAccumulator = NO;
- if (_downloadResumeData) {
- newSessionTask = [_session downloadTaskWithResumeData:_downloadResumeData];
- GTMSESSION_ASSERT_DEBUG_OR_LOG(
- newSessionTask, @"Failed downloadTaskWithResumeData for %@, resume data %lu bytes",
- _session, (unsigned long)_downloadResumeData.length);
- } else if (_destinationFileURL && !isDataRequest) {
- newSessionTask = [_session downloadTaskWithRequest:fetchRequest];
- GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, @"Failed downloadTaskWithRequest for %@, %@",
- _session, fetchRequest);
- } else if (needsUploadTask) {
- if (bodyFileURL) {
- newSessionTask = [_session uploadTaskWithRequest:fetchRequest fromFile:bodyFileURL];
- GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask,
- @"Failed uploadTaskWithRequest for %@, %@, file %@", _session,
- fetchRequest, bodyFileURL.path);
- } else if (self.bodyStreamProvider) {
- newSessionTask = [_session uploadTaskWithStreamedRequest:fetchRequest];
- GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask,
- @"Failed uploadTaskWithStreamedRequest for %@, %@", _session,
- fetchRequest);
- } else {
- GTMSESSION_ASSERT_DEBUG_OR_LOG(_bodyData != nil, @"Upload task needs body data, %@",
- fetchRequest);
- newSessionTask = [_session uploadTaskWithRequest:fetchRequest
- fromData:(NSData *_Nonnull)_bodyData];
- GTMSESSION_ASSERT_DEBUG_OR_LOG(
- newSessionTask, @"Failed uploadTaskWithRequest for %@, %@, body data %lu bytes", _session,
- fetchRequest, (unsigned long)_bodyData.length);
- }
- needsDataAccumulator = YES;
- } else {
- newSessionTask = [_session dataTaskWithRequest:fetchRequest];
- needsDataAccumulator = YES;
- GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, @"Failed dataTaskWithRequest for %@, %@",
- _session, fetchRequest);
- }
- self.sessionTask = newSessionTask;
- if (!newSessionTask) {
- // We shouldn't get here; if we're here, an earlier assertion should have fired to explain
- // which session task creation failed.
- [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorTaskCreationFailed)];
- return;
- }
- if (needsDataAccumulator && _accumulateDataBlock == nil) {
- self.downloadedData = [NSMutableData data];
- }
- if (_taskDescription) {
- newSessionTask.taskDescription = _taskDescription;
- }
- if (_taskPriority >= 0) {
- newSessionTask.priority = _taskPriority;
- }
- #if GTM_DISABLE_FETCHER_TEST_BLOCK
- GTMSESSION_ASSERT_DEBUG(_testBlock == nil && gGlobalTestBlock == nil, @"test blocks disabled");
- _testBlock = nil;
- #else
- if (!_testBlock) {
- if (gGlobalTestBlock) {
- // Note that the test block may pass nil for all of its response parameters,
- // indicating that the fetch should actually proceed. This is useful when the
- // global test block has been set, and the app is only testing a specific
- // fetcher. The block simulation code will then resume the task.
- _testBlock = gGlobalTestBlock;
- }
- }
- _isUsingTestBlock = (_testBlock != nil);
- #endif // GTM_DISABLE_FETCHER_TEST_BLOCK
- #if GTM_BACKGROUND_TASK_FETCHING
- id<GTMUIApplicationProtocol> app = [[self class] fetcherUIApplication];
- // Background tasks seem to interfere with out-of-process uploads and downloads.
- if (app && !self.skipBackgroundTask && !self.usingBackgroundSession) {
- // Tell UIApplication that we want to continue even when the app is in the
- // background.
- #if DEBUG
- NSString *bgTaskName =
- [NSString stringWithFormat:@"%@-%@", [self class], fetchRequest.URL.host];
- #else
- NSString *bgTaskName = @"GTMSessionFetcher";
- #endif
- // Since a request can be started from any thread, we also have to ensure the
- // variable for accessing it is safe across the initial thread and the handler
- // (incase it gets failed immediately from the app already heading into the
- // background).
- __block UIBackgroundTaskIdentifier guardedTaskID = UIBackgroundTaskInvalid;
- UIBackgroundTaskIdentifier returnedTaskID =
- [app beginBackgroundTaskWithName:bgTaskName
- expirationHandler:^{
- // Background task expiration callback - this block is always invoked by
- // UIApplication on the main thread.
- UIBackgroundTaskIdentifier localTaskID;
- @synchronized(self) {
- localTaskID = guardedTaskID;
- }
- if (localTaskID != UIBackgroundTaskInvalid) {
- @synchronized(self) {
- if (localTaskID == self.backgroundTaskIdentifier) {
- self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
- }
- }
- [app endBackgroundTask:localTaskID];
- }
- }];
- @synchronized(self) {
- guardedTaskID = returnedTaskID;
- self.backgroundTaskIdentifier = returnedTaskID;
- }
- }
- #endif
- if (!_initialRequestDate) {
- _initialRequestDate = [[NSDate alloc] init];
- }
- // We don't expect to reach here even on retry or auth until a stop notification has been sent
- // for the previous task, but we should ensure that we don't unbalance that.
- GTMSESSION_ASSERT_DEBUG(!_isStopNotificationNeeded, @"Start notification without a prior stop");
- [self sendStopNotificationIfNeeded];
- [self addPersistedBackgroundSessionToDefaults];
- [self setStopNotificationNeeded:YES];
- [self postNotificationOnMainThreadWithName:kGTMSessionFetcherStartedNotification
- userInfo:nil
- requireAsync:NO];
- // The service needs to know our task if it is serving as NSURLSession delegate.
- [_service fetcherDidBeginFetching:self];
- if (_testBlock) {
- #if !GTM_DISABLE_FETCHER_TEST_BLOCK
- [self simulateFetchForTestBlock];
- #endif
- } else {
- // We resume the session task after posting the notification since the
- // delegate callbacks may happen immediately if the fetch is started off
- // the main thread or the session delegate queue is on a background thread,
- // and we don't want to post a start notification after a premature finish
- // of the session task.
- [newSessionTask resume];
- }
- }
- // Helper method to create a new NSURLSession for this fetcher. Because the original
- // implementation had this code inline, marking direct to avoid any danger of subclasses
- // overriding the behavior.
- - (NSURLSession *)createSessionWithDelegate:(id<NSURLSessionDelegate>)sessionDelegate
- sessionIdentifier:(nullable NSString *)priorSessionIdentifier
- __attribute__((objc_direct)) {
- // Create a session.
- if (!_configuration) {
- if (priorSessionIdentifier || self.usingBackgroundSession) {
- NSString *sessionIdentifier = priorSessionIdentifier;
- if (!sessionIdentifier) {
- sessionIdentifier = [self createSessionIdentifierWithMetadata:nil];
- }
- NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIdentifierToFetcherMap];
- [sessionIdentifierToFetcherMap setObject:self forKey:self.sessionIdentifier];
- _configuration = [NSURLSessionConfiguration
- backgroundSessionConfigurationWithIdentifier:sessionIdentifier];
- self.usingBackgroundSession = YES;
- self.canShareSession = NO;
- } else {
- _configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
- }
- #if !GTM_ALLOW_INSECURE_REQUESTS
- #if GTM_SDK_REQUIRES_TLSMINIMUMSUPPORTEDPROTOCOLVERSION
- _configuration.TLSMinimumSupportedProtocolVersion = tls_protocol_version_TLSv12;
- #elif GTM_SDK_SUPPORTS_TLSMINIMUMSUPPORTEDPROTOCOLVERSION
- if (@available(iOS 13, tvOS 13, macOS 10.15, *)) {
- _configuration.TLSMinimumSupportedProtocolVersion = tls_protocol_version_TLSv12;
- } else {
- _configuration.TLSMinimumSupportedProtocol = kTLSProtocol12;
- }
- #else
- _configuration.TLSMinimumSupportedProtocol = kTLSProtocol12;
- #endif // GTM_SDK_REQUIRES_TLSMINIMUMSUPPORTEDPROTOCOLVERSION
- #endif
- } // !_configuration
- _configuration.HTTPCookieStorage = self.cookieStorage;
- if (_configurationBlock) {
- _configurationBlock(self, _configuration);
- }
- id<NSURLSessionDelegate> delegate = sessionDelegate;
- if (!delegate || !self.canShareSession) {
- delegate = self;
- }
- NSURLSession *session = [NSURLSession sessionWithConfiguration:_configuration
- delegate:delegate
- delegateQueue:self.sessionDelegateQueue];
- GTMSESSION_ASSERT_DEBUG(session, @"Couldn't create session");
- // If this assertion fires, the client probably tried to use a session identifier that was
- // already used. The solution is to make the client use a unique identifier (or better yet let
- // the session fetcher assign the identifier).
- GTMSESSION_ASSERT_DEBUG(session.delegate == delegate, @"Couldn't assign delegate.");
- if (session) {
- BOOL isUsingSharedDelegate = (delegate != self);
- if (!isUsingSharedDelegate) {
- _shouldInvalidateSession = YES;
- }
- }
- return session;
- }
- NSData *_Nullable GTMDataFromInputStream(NSInputStream *inputStream, NSError **outError) {
- NSMutableData *data = [NSMutableData data];
- [inputStream open];
- NSInteger numberOfBytesRead = 0;
- while ([inputStream hasBytesAvailable]) {
- uint8_t buffer[512];
- numberOfBytesRead = [inputStream read:buffer maxLength:sizeof(buffer)];
- if (numberOfBytesRead > 0) {
- [data appendBytes:buffer length:(NSUInteger)numberOfBytesRead];
- } else {
- break;
- }
- }
- [inputStream close];
- NSError *streamError = inputStream.streamError;
- if (streamError) {
- data = nil;
- }
- if (outError) {
- *outError = streamError;
- }
- return data;
- }
- #if !GTM_DISABLE_FETCHER_TEST_BLOCK
- - (void)simulateFetchForTestBlock {
- // This is invoked on the same thread as the beginFetch method was.
- //
- // Callbacks will all occur on the callback queue.
- _testBlock(self, ^(NSURLResponse *response, NSData *responseData, NSError *error) {
- // Callback from test block.
- if (response == nil && responseData == nil && error == nil) {
- // Assume the fetcher should execute rather than be tested.
- self->_testBlock = nil;
- self->_isUsingTestBlock = NO;
- [self->_sessionTask resume];
- return;
- }
- GTMSessionFetcherBodyStreamProvider bodyStreamProvider = self.bodyStreamProvider;
- if (bodyStreamProvider) {
- bodyStreamProvider(^(NSInputStream *bodyStream) {
- // Read from the input stream into an NSData buffer. We'll drain the stream
- // explicitly on a background queue.
- [self
- invokeOnCallbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
- afterUserStopped:NO
- block:^{
- NSError *streamError;
- NSData *streamedData =
- GTMDataFromInputStream(bodyStream, &streamError);
- dispatch_async(dispatch_get_main_queue(), ^{
- // Continue callbacks on the main thread, since serial behavior
- // is more reliable for tests.
- [self
- simulateDataCallbacksForTestBlockWithBodyData:streamedData
- response:response
- responseData:responseData
- error:
- (error
- ?: streamError)];
- });
- }];
- });
- } else {
- // No input stream; use the supplied data or file URL.
- NSURL *bodyFileURL = self.bodyFileURL;
- if (bodyFileURL) {
- NSError *readError;
- self->_bodyData = [NSData dataWithContentsOfURL:bodyFileURL
- options:NSDataReadingMappedIfSafe
- error:&readError];
- error = readError;
- }
- // No stream provider.
- // In real fetches, nothing happens until the run loop spins, so apps have leeway to
- // set callbacks after they call beginFetch. We'll mirror that fetcher behavior by
- // delaying callbacks here at least to the next spin of the run loop. That keeps
- // immediate, synchronous setting of callback blocks after beginFetch working in tests.
- dispatch_async(dispatch_get_main_queue(), ^{
- [self simulateDataCallbacksForTestBlockWithBodyData:self->_bodyData
- response:response
- responseData:responseData
- error:error];
- });
- }
- });
- }
- - (void)simulateByteTransferReportWithDataLength:(int64_t)totalDataLength
- block:(GTMSessionFetcherSendProgressBlock)block {
- // This utility method simulates transfer progress with up to three callbacks.
- // It is used to call back to any of the progress blocks.
- int64_t sendReportSize = totalDataLength / 3 + 1;
- int64_t totalSent = 0;
- while (totalSent < totalDataLength) {
- int64_t bytesRemaining = totalDataLength - totalSent;
- sendReportSize = MIN(sendReportSize, bytesRemaining);
- totalSent += sendReportSize;
- [self invokeOnCallbackQueueUnlessStopped:^{
- block(sendReportSize, totalSent, totalDataLength);
- }];
- }
- }
- - (void)simulateDataCallbacksForTestBlockWithBodyData:(nullable NSData *)bodyData
- response:(NSURLResponse *)response
- responseData:(NSData *)suppliedData
- error:(NSError *)suppliedError {
- __block NSData *responseData = suppliedData;
- __block NSError *responseError = suppliedError;
- // This method does the test simulation of callbacks once the upload
- // and download data are known.
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- // Get copies of ivars we'll access in async invocations. This simulation assumes
- // they won't change during fetcher execution.
- NSURL *destinationFileURL = _destinationFileURL;
- GTMSessionFetcherWillRedirectBlock willRedirectBlock = _willRedirectBlock;
- GTMSessionFetcherDidReceiveResponseBlock didReceiveResponseBlock = _didReceiveResponseBlock;
- GTMSessionFetcherSendProgressBlock sendProgressBlock = _sendProgressBlock;
- GTMSessionFetcherDownloadProgressBlock downloadProgressBlock = _downloadProgressBlock;
- GTMSessionFetcherAccumulateDataBlock accumulateDataBlock = _accumulateDataBlock;
- GTMSessionFetcherReceivedProgressBlock receivedProgressBlock = _receivedProgressBlock;
- GTMSessionFetcherWillCacheURLResponseBlock willCacheURLResponseBlock =
- _willCacheURLResponseBlock;
- GTMSessionFetcherChallengeBlock challengeBlock = _challengeBlock;
- // Simulate receipt of redirection.
- if (willRedirectBlock) {
- __auto_type block = ^{
- willRedirectBlock((NSHTTPURLResponse *)response, self->_request,
- ^(NSURLRequest *redirectRequest){
- // For simulation, we'll assume
- // the app will just continue.
- });
- };
- [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES block:block];
- }
- // If the fetcher has a challenge block, simulate a challenge.
- //
- // It might be nice to eventually let the user determine which testBlock
- // fetches get challenged rather than always executing the supplied
- // challenge block.
- if (challengeBlock) {
- __auto_type block = ^{
- NSURL *requestURL = self->_request.URL;
- NSString *host = requestURL.host;
- NSURLProtectionSpace *pspace =
- [[NSURLProtectionSpace alloc] initWithHost:host
- port:requestURL.port.integerValue
- protocol:requestURL.scheme
- realm:nil
- authenticationMethod:NSURLAuthenticationMethodHTTPBasic];
- id<NSURLAuthenticationChallengeSender> unusedSender =
- (id<NSURLAuthenticationChallengeSender>)[NSNull null];
- NSURLAuthenticationChallenge *challenge =
- [[NSURLAuthenticationChallenge alloc] initWithProtectionSpace:pspace
- proposedCredential:nil
- previousFailureCount:0
- failureResponse:nil
- error:nil
- sender:unusedSender];
- challengeBlock(self, challenge,
- ^(NSURLSessionAuthChallengeDisposition disposition,
- NSURLCredential *_Nullable credential){
- // We could change the
- // responseData and responseError
- // based on the disposition,
- // but it's easier for apps to
- // just supply the expected data
- // and error
- // directly to the test block. So
- // this simulation ignores the
- // disposition.
- });
- };
- [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES block:block];
- }
- // Simulate receipt of an initial response.
- if (response && didReceiveResponseBlock) {
- __auto_type block = ^{
- didReceiveResponseBlock(response, ^(NSURLSessionResponseDisposition desiredDisposition){
- // For simulation, we'll assume
- // the disposition is to continue.
- });
- };
- [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES block:block];
- }
- // Simulate reporting send progress.
- if (sendProgressBlock) {
- __auto_type block =
- ^(int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) {
- // This is invoked on the callback queue unless
- // stopped.
- sendProgressBlock(bytesSent, totalBytesSent, totalBytesExpectedToSend);
- };
- [self simulateByteTransferReportWithDataLength:(int64_t)bodyData.length block:block];
- }
- if (destinationFileURL) {
- // Simulate download to file progress.
- if (downloadProgressBlock) {
- __auto_type block = ^(int64_t bytesDownloaded, int64_t totalBytesDownloaded,
- int64_t totalBytesExpectedToDownload) {
- // This is invoked on the callback queue unless
- // stopped.
- downloadProgressBlock(bytesDownloaded, totalBytesDownloaded,
- totalBytesExpectedToDownload);
- };
- [self simulateByteTransferReportWithDataLength:(int64_t)responseData.length block:block];
- }
- NSError *writeError;
- [responseData writeToURL:destinationFileURL options:NSDataWritingAtomic error:&writeError];
- if (writeError) {
- // Tell the test code that writing failed.
- responseError = writeError;
- }
- } else {
- // Simulate download to NSData progress.
- if ((accumulateDataBlock || receivedProgressBlock) && responseData) {
- __auto_type block = ^(NSData *data, int64_t bytesReceived, int64_t totalBytesReceived,
- int64_t totalBytesExpectedToReceive) {
- // This is invoked on the callback queue unless stopped.
- if (accumulateDataBlock) {
- accumulateDataBlock(data);
- }
- if (receivedProgressBlock) {
- receivedProgressBlock(bytesReceived, totalBytesReceived);
- }
- };
- [self simulateByteTransferWithData:responseData block:block];
- }
- if (!accumulateDataBlock) {
- _downloadedData = [responseData mutableCopy];
- }
- if (willCacheURLResponseBlock) {
- // Simulate letting the client inspect and alter the cached response.
- NSData *cachedData = responseData ?: [[NSData alloc] init]; // Always have non-nil data.
- NSCachedURLResponse *cachedResponse =
- [[NSCachedURLResponse alloc] initWithResponse:response data:cachedData];
- __auto_type block = ^{
- willCacheURLResponseBlock(cachedResponse, ^(NSCachedURLResponse *responseToCache){
- // The app may provide an
- // alternative response, or
- // nil to defeat caching.
- });
- };
- [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES block:block];
- }
- }
- _response = response;
- } // @synchronized(self)
- NSOperationQueue *queue = self.sessionDelegateQueue;
- [queue addOperationWithBlock:^{
- // Rather than invoke failToBeginFetchWithError: we want to simulate completion of
- // a connection that started and ended, so we'll call down to finishWithError:
- NSInteger status = responseError ? responseError.code : 200;
- if (status >= 200 && status <= 399) {
- [self finishWithError:nil shouldRetry:NO];
- } else {
- [self shouldRetryNowForStatus:status
- error:responseError
- forceAssumeRetry:NO
- response:^(BOOL shouldRetry) {
- [self finishWithError:responseError shouldRetry:shouldRetry];
- }];
- }
- }];
- }
- - (void)simulateByteTransferWithData:(NSData *)responseData
- block:(GTMSessionFetcherSimulateByteTransferBlock)transferBlock {
- // This utility method simulates transfering data to the client. It divides the data into at most
- // "chunkCount" chunks and then passes each chunk along with a progress update to transferBlock.
- // This function can be used with accumulateDataBlock or receivedProgressBlock.
- NSUInteger chunkCount = MAX(self.testBlockAccumulateDataChunkCount, (NSUInteger)1);
- NSUInteger totalDataLength = responseData.length;
- NSUInteger sendDataSize = totalDataLength / chunkCount + 1;
- NSUInteger totalSent = 0;
- while (totalSent < totalDataLength) {
- NSUInteger bytesRemaining = totalDataLength - totalSent;
- sendDataSize = MIN(sendDataSize, bytesRemaining);
- NSData *chunkData = [responseData subdataWithRange:NSMakeRange(totalSent, sendDataSize)];
- totalSent += sendDataSize;
- [self invokeOnCallbackQueueUnlessStopped:^{
- transferBlock(chunkData, (int64_t)sendDataSize, (int64_t)totalSent, (int64_t)totalDataLength);
- }];
- }
- }
- #endif // !GTM_DISABLE_FETCHER_TEST_BLOCK
- - (void)setSessionTask:(NSURLSessionTask *)sessionTask {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (_sessionTask != sessionTask) {
- _sessionTask = sessionTask;
- if (_sessionTask) {
- // Request could be nil on restoring this fetcher from a background session.
- if (!_request) {
- _request = [_sessionTask.originalRequest mutableCopy];
- }
- }
- }
- } // @synchronized(self)
- }
- - (nullable NSURLSessionTask *)sessionTask {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _sessionTask;
- } // @synchronized(self)
- }
- + (NSUserDefaults *)fetcherUserDefaults {
- static NSUserDefaults *gFetcherUserDefaults = nil;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- Class fetcherUserDefaultsClass = NSClassFromString(@"GTMSessionFetcherUserDefaultsFactory");
- if (fetcherUserDefaultsClass) {
- gFetcherUserDefaults = [fetcherUserDefaultsClass fetcherUserDefaults];
- } else {
- gFetcherUserDefaults = [NSUserDefaults standardUserDefaults];
- }
- });
- return gFetcherUserDefaults;
- }
- - (void)addPersistedBackgroundSessionToDefaults {
- NSString *sessionIdentifier = self.sessionIdentifier;
- if (!sessionIdentifier) {
- return;
- }
- NSArray *oldBackgroundSessions = [[self class] activePersistedBackgroundSessions];
- if ([oldBackgroundSessions containsObject:_sessionIdentifier]) {
- return;
- }
- NSMutableArray *newBackgroundSessions = [NSMutableArray arrayWithArray:oldBackgroundSessions];
- [newBackgroundSessions addObject:sessionIdentifier];
- GTM_LOG_BACKGROUND_SESSION(@"Add to background sessions: %@", newBackgroundSessions);
- NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults];
- [userDefaults setObject:newBackgroundSessions forKey:kGTMSessionFetcherPersistedDestinationKey];
- [userDefaults synchronize];
- }
- - (void)removePersistedBackgroundSessionFromDefaults {
- NSString *sessionIdentifier = self.sessionIdentifier;
- if (!sessionIdentifier) return;
- NSArray *oldBackgroundSessions = [[self class] activePersistedBackgroundSessions];
- if (!oldBackgroundSessions) {
- return;
- }
- NSMutableArray *newBackgroundSessions = [NSMutableArray arrayWithArray:oldBackgroundSessions];
- NSUInteger sessionIndex = [newBackgroundSessions indexOfObject:sessionIdentifier];
- if (sessionIndex == NSNotFound) {
- return;
- }
- [newBackgroundSessions removeObjectAtIndex:sessionIndex];
- GTM_LOG_BACKGROUND_SESSION(@"Remove from background sessions: %@", newBackgroundSessions);
- NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults];
- if (newBackgroundSessions.count == 0) {
- [userDefaults removeObjectForKey:kGTMSessionFetcherPersistedDestinationKey];
- } else {
- [userDefaults setObject:newBackgroundSessions forKey:kGTMSessionFetcherPersistedDestinationKey];
- }
- [userDefaults synchronize];
- }
- + (nullable NSArray *)activePersistedBackgroundSessions {
- NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults];
- NSArray *oldBackgroundSessions =
- [userDefaults arrayForKey:kGTMSessionFetcherPersistedDestinationKey];
- if (oldBackgroundSessions.count == 0) {
- return nil;
- }
- NSMutableArray *activeBackgroundSessions = nil;
- NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap];
- for (NSString *sessionIdentifier in oldBackgroundSessions) {
- GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier];
- if (fetcher) {
- if (!activeBackgroundSessions) {
- activeBackgroundSessions = [[NSMutableArray alloc] init];
- }
- [activeBackgroundSessions addObject:sessionIdentifier];
- }
- }
- return activeBackgroundSessions;
- }
- + (NSArray *)fetchersForBackgroundSessions {
- NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults];
- NSArray *backgroundSessions =
- [userDefaults arrayForKey:kGTMSessionFetcherPersistedDestinationKey];
- NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap];
- NSMutableArray *fetchers = [NSMutableArray array];
- for (NSString *sessionIdentifier in backgroundSessions) {
- GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier];
- if (!fetcher) {
- fetcher = [self fetcherWithSessionIdentifier:sessionIdentifier];
- GTMSESSION_ASSERT_DEBUG(fetcher != nil, @"Unexpected invalid session identifier: %@",
- sessionIdentifier);
- if (!fetcher.clientWillReconnectBackgroundSession) {
- [fetcher beginFetchWithCompletionHandler:nil];
- }
- }
- GTM_LOG_BACKGROUND_SESSION(@"%@ restoring session %@ by creating fetcher %@ %p", [self class],
- sessionIdentifier, fetcher, fetcher);
- if (fetcher != nil) {
- [fetchers addObject:fetcher];
- }
- }
- return fetchers;
- }
- #if TARGET_OS_IPHONE && !TARGET_OS_WATCH
- + (void)application:(UIApplication *)application
- handleEventsForBackgroundURLSession:(NSString *)identifier
- completionHandler:(GTMSessionFetcherSystemCompletionHandler)completionHandler
- GTM_SWIFT_DISABLE_ASYNC {
- GTMSessionFetcher *fetcher = [self fetcherWithSessionIdentifier:identifier];
- if (fetcher != nil) {
- fetcher.systemCompletionHandler = completionHandler;
- } else {
- GTM_LOG_BACKGROUND_SESSION(@"%@ did not create background session identifier: %@", [self class],
- identifier);
- }
- }
- #endif
- - (nullable NSString *)sessionIdentifier {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _sessionIdentifier;
- } // @synchronized(self)
- }
- - (void)setSessionIdentifier:(NSString *)sessionIdentifier {
- GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier");
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- GTMSESSION_ASSERT_DEBUG(!_session, @"Unable to set session identifier after session created");
- _sessionIdentifier = [sessionIdentifier copy];
- _usingBackgroundSession = YES;
- _canShareSession = NO;
- [self restoreDefaultStateForSessionIdentifierMetadata];
- } // @synchronized(self)
- }
- - (void)setSessionIdentifierInternal:(nullable NSString *)sessionIdentifier {
- // This internal method only does a synchronized set of the session identifier.
- // It does not have side effects on the background session, shared session, or
- // session identifier metadata.
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _sessionIdentifier = [sessionIdentifier copy];
- } // @synchronized(self)
- }
- - (nullable NSDictionary *)sessionUserInfo {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (_sessionUserInfo == nil) {
- // We'll return the metadata dictionary with internal keys removed. This avoids the user
- // re-using the userInfo dictionary later and accidentally including the internal keys.
- NSMutableDictionary *metadata = [[self sessionIdentifierMetadataUnsynchronized] mutableCopy];
- NSSet *keysToRemove = [metadata keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) {
- return [key hasPrefix:@"_"];
- }];
- [metadata removeObjectsForKeys:[keysToRemove allObjects]];
- if (metadata.count > 0) {
- _sessionUserInfo = metadata;
- }
- }
- return _sessionUserInfo;
- } // @synchronized(self)
- }
- - (void)setSessionUserInfo:(nullable NSDictionary *)dictionary {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- GTMSESSION_ASSERT_DEBUG(_sessionIdentifier == nil, @"Too late to assign userInfo");
- _sessionUserInfo = dictionary;
- } // @synchronized(self)
- }
- - (nullable NSDictionary *)sessionIdentifierDefaultMetadata {
- GTMSessionCheckSynchronized(self);
- NSMutableDictionary *defaultUserInfo = [[NSMutableDictionary alloc] init];
- if (_destinationFileURL) {
- defaultUserInfo[kGTMSessionIdentifierDestinationFileURLMetadataKey] =
- [_destinationFileURL absoluteString];
- }
- if (_bodyFileURL) {
- defaultUserInfo[kGTMSessionIdentifierBodyFileURLMetadataKey] = [_bodyFileURL absoluteString];
- }
- if (_clientWillReconnectBackgroundSession) {
- defaultUserInfo[kGTMSessionIdentifierClientReconnectMetadataKey] = @"YES";
- }
- return (defaultUserInfo.count > 0) ? defaultUserInfo : nil;
- }
- - (void)restoreDefaultStateForSessionIdentifierMetadata {
- GTMSessionCheckSynchronized(self);
- NSDictionary *metadata = [self sessionIdentifierMetadataUnsynchronized];
- NSString *destinationFileURLString = metadata[kGTMSessionIdentifierDestinationFileURLMetadataKey];
- if (destinationFileURLString) {
- _destinationFileURL = [NSURL URLWithString:destinationFileURLString];
- GTM_LOG_BACKGROUND_SESSION(@"Restoring destination file URL: %@", _destinationFileURL);
- }
- NSString *bodyFileURLString = metadata[kGTMSessionIdentifierBodyFileURLMetadataKey];
- if (bodyFileURLString) {
- _bodyFileURL = [NSURL URLWithString:bodyFileURLString];
- GTM_LOG_BACKGROUND_SESSION(@"Restoring body file URL: %@", _bodyFileURL);
- }
- NSString *clientReconnectString = metadata[kGTMSessionIdentifierClientReconnectMetadataKey];
- if (clientReconnectString) {
- _clientWillReconnectBackgroundSession = [clientReconnectString boolValue];
- GTM_LOG_BACKGROUND_SESSION(@"Restoring clientWillReconnectBackgroundSession: %@",
- (_clientWillReconnectBackgroundSession ? @"YES" : @"NO"));
- }
- }
- - (nullable NSDictionary *)sessionIdentifierMetadata {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return [self sessionIdentifierMetadataUnsynchronized];
- }
- }
- - (nullable NSDictionary *)sessionIdentifierMetadataUnsynchronized {
- GTMSessionCheckSynchronized(self);
- // Session Identifier format: "com.google.<ClassName>_<UUID>_<Metadata in JSON format>
- if (!_sessionIdentifier) {
- return nil;
- }
- NSScanner *metadataScanner = [NSScanner scannerWithString:_sessionIdentifier];
- [metadataScanner setCharactersToBeSkipped:nil];
- NSString *metadataString;
- NSString *uuid;
- if ([metadataScanner scanUpToString:@"_" intoString:NULL] &&
- [metadataScanner scanString:@"_" intoString:NULL] &&
- [metadataScanner scanUpToString:@"_" intoString:&uuid] &&
- [metadataScanner scanString:@"_" intoString:NULL] &&
- [metadataScanner scanUpToString:@"\n" intoString:&metadataString]) {
- _sessionIdentifierUUID = uuid;
- NSData *metadataData = [metadataString dataUsingEncoding:NSUTF8StringEncoding];
- NSError *error;
- NSDictionary *metadataDict = [NSJSONSerialization JSONObjectWithData:metadataData
- options:0
- error:&error];
- GTM_LOG_BACKGROUND_SESSION(@"User Info from session identifier: %@ %@", metadataDict,
- error ? error : @"");
- return metadataDict;
- }
- return nil;
- }
- - (NSString *)createSessionIdentifierWithMetadata:(nullable NSDictionary *)metadataToInclude {
- NSString *result;
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- // Session Identifier format: "com.google.<ClassName>_<UUID>_<Metadata in JSON format>
- GTMSESSION_ASSERT_DEBUG(!_sessionIdentifier, @"Session identifier already created");
- _sessionIdentifierUUID = [[NSUUID UUID] UUIDString];
- _sessionIdentifier =
- [NSString stringWithFormat:@"%@_%@", kGTMSessionIdentifierPrefix, _sessionIdentifierUUID];
- // Start with user-supplied keys so they cannot accidentally override the fetcher's keys.
- NSMutableDictionary *metadataDict =
- [NSMutableDictionary dictionaryWithDictionary:(NSDictionary *_Nonnull)_sessionUserInfo];
- if (metadataToInclude) {
- [metadataDict addEntriesFromDictionary:(NSDictionary *)metadataToInclude];
- }
- NSDictionary *defaultMetadataDict = [self sessionIdentifierDefaultMetadata];
- if (defaultMetadataDict) {
- [metadataDict addEntriesFromDictionary:defaultMetadataDict];
- }
- if (metadataDict.count > 0) {
- NSData *metadataData = [NSJSONSerialization dataWithJSONObject:metadataDict
- options:0
- error:NULL];
- GTMSESSION_ASSERT_DEBUG(metadataData != nil,
- @"Session identifier user info failed to convert to JSON");
- if (metadataData.length > 0) {
- NSString *metadataString = [[NSString alloc] initWithData:metadataData
- encoding:NSUTF8StringEncoding];
- _sessionIdentifier = [_sessionIdentifier stringByAppendingFormat:@"_%@", metadataString];
- }
- }
- _didCreateSessionIdentifier = YES;
- result = _sessionIdentifier;
- } // @synchronized(self)
- return result;
- }
- - (void)failToBeginFetchWithError:(NSError *)error {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _hasStoppedFetching = YES;
- }
- if (error == nil) {
- error = [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
- code:GTMSessionFetcherErrorDownloadFailed
- userInfo:nil];
- }
- [self invokeFetchCallbacksOnCallbackQueueWithData:nil
- error:error
- mayDecorate:YES
- shouldReleaseCallbacks:YES];
- [_service fetcherDidStop:self];
- self.authorizer = nil;
- }
- + (GTMSessionCookieStorage *)staticCookieStorage {
- static GTMSessionCookieStorage *gCookieStorage = nil;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- gCookieStorage = [[GTMSessionCookieStorage alloc] init];
- });
- return gCookieStorage;
- }
- #if GTM_BACKGROUND_TASK_FETCHING
- - (void)endBackgroundTask {
- // Whenever the connection stops or background execution expires,
- // we need to tell UIApplication we're done.
- UIBackgroundTaskIdentifier bgTaskID;
- @synchronized(self) {
- bgTaskID = self.backgroundTaskIdentifier;
- if (bgTaskID != UIBackgroundTaskInvalid) {
- self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
- }
- }
- if (bgTaskID != UIBackgroundTaskInvalid) {
- id<GTMUIApplicationProtocol> app = [[self class] fetcherUIApplication];
- [app endBackgroundTask:bgTaskID];
- }
- }
- #endif // GTM_BACKGROUND_TASK_FETCHING
- - (void)authorizeRequest {
- GTMSessionCheckNotSynchronized(self);
- id authorizer = self.authorizer;
- // Prefer the block-based implementation. This *is* a change in behavior, but if authorizers
- // previously provided this method they would presumably assume they can be used for the same
- // requests as before.
- if ([authorizer respondsToSelector:@selector(authorizeRequest:completionHandler:)]) {
- // It's unknown how long an authorizer maintains ownership of the provided block, so
- // avoid potential retain cycles on self and the authorizer.
- __weak __typeof__(self) weakSelf = self;
- NSMutableURLRequest *mutableRequest = [self.request mutableCopy];
- [authorizer authorizeRequest:mutableRequest
- completionHandler:^(NSError *_Nullable error) {
- [weakSelf authorizer:nil request:mutableRequest finishedWithError:error];
- }];
- } else if ([authorizer respondsToSelector:@selector(authorizeRequest:
- delegate:didFinishSelector:)]) {
- SEL callbackSel = @selector(authorizer:request:finishedWithError:);
- NSMutableURLRequest *mutableRequest = [self.request mutableCopy];
- [authorizer authorizeRequest:mutableRequest delegate:self didFinishSelector:callbackSel];
- } else {
- GTMSESSION_ASSERT_DEBUG(authorizer == nil, @"invalid authorizer for fetch");
- // No authorizing possible, and authorizing happens only after any delay;
- // just begin fetching
- [self beginFetchMayDelay:NO mayAuthorize:NO mayDecorate:YES];
- }
- }
- // The authorizer parameter is unused, and the block-based callback will never pass
- // non-nil; the field is only for the deprecated selector-based implementation for
- // legacy reasons.
- - (void)authorizer:(nullable id __unused)auth
- request:(nullable NSMutableURLRequest *)authorizedRequest
- finishedWithError:(nullable NSError *)error {
- GTMSessionCheckNotSynchronized(self);
- if (error != nil) {
- // We can't fetch without authorization
- [self failToBeginFetchWithError:(NSError *_Nonnull)error];
- } else {
- @synchronized(self) {
- _request = authorizedRequest;
- }
- [self beginFetchMayDelay:NO mayAuthorize:NO mayDecorate:YES];
- }
- }
- - (void)applyDecoratorsAtRequestWillStart:(NSArray<id<GTMFetcherDecoratorProtocol>> *)decorators
- startingAtIndex:(NSUInteger)index {
- GTMSessionCheckNotSynchronized(self);
- if (index >= decorators.count) {
- GTMSESSION_LOG_DEBUG_VERBOSE(
- @"GTMSessionFetcher decorate requestWillStart %zu decorators complete", decorators.count);
- [self beginFetchMayDelay:NO mayAuthorize:NO mayDecorate:NO];
- return;
- }
- __weak __typeof__(self) weakSelf = self;
- id<GTMFetcherDecoratorProtocol> decorator = decorators[index];
- GTMSESSION_LOG_DEBUG_VERBOSE(
- @"GTMSessionFetcher decorate requestWillStart %zu decorators, index %zu, "
- @"retry count %zu, decorator %@",
- decorators.count, index, self.retryCount, decorator);
- [decorator fetcherWillStart:self
- completionHandler:^(NSURLRequest *_Nullable newRequest, NSError *_Nullable error) {
- GTMSESSION_LOG_DEBUG_VERBOSE(
- @"GTMSessionFetcher decorator requestWillStart index %zu "
- @"complete, newRequest %@, error %@",
- index, newRequest, error);
- __strong __typeof__(self) strongSelf = weakSelf;
- if (!strongSelf) {
- GTMSESSION_LOG_DEBUG(@"GTMSessionFetcher destroyed before requestWillStart "
- @"decorators completed, ignoring.");
- return;
- }
- if (error) {
- [self failToBeginFetchWithError:(NSError *_Nonnull)error];
- return;
- }
- if (newRequest) {
- // Copying `NSURLRequest` should be cheap, but in case profiling shows this
- // operation is prohibitively expensive, this API might need to be changed to allow
- // clients to manipulate `self.request` directly.
- [strongSelf updateMutableRequest:[newRequest mutableCopy]];
- }
- [strongSelf applyDecoratorsAtRequestWillStart:decorators startingAtIndex:index + 1];
- }];
- }
- - (void)applyDecoratorsAtRequestDidFinish:(NSArray<id<GTMFetcherDecoratorProtocol>> *)decorators
- withData:(nullable NSData *)data
- error:(nullable NSError *)error
- startingAtIndex:(NSUInteger)index
- shouldReleaseCallbacks:(BOOL)shouldReleaseCallbacks {
- GTMSessionCheckNotSynchronized(self);
- if (index >= decorators.count) {
- GTMSESSION_LOG_DEBUG_VERBOSE(
- @"GTMSessionFetcher decorate requestDidFinish %zu decorators complete", decorators.count);
- [self invokeFetchCallbacksOnCallbackQueueWithData:data
- error:error
- mayDecorate:NO
- shouldReleaseCallbacks:shouldReleaseCallbacks];
- return;
- }
- __weak __typeof__(self) weakSelf = self;
- id<GTMFetcherDecoratorProtocol> decorator = decorators[index];
- GTMSESSION_LOG_DEBUG_VERBOSE(
- @"GTMSessionFetcher decorate requestDidFinish %zu decorators, index %zu, "
- @"retry count %zu, decorator %@",
- decorators.count, index, self.retryCount, decorator);
- [decorator fetcherDidFinish:self
- withData:data
- error:error
- completionHandler:^{
- GTMSESSION_LOG_DEBUG_VERBOSE(
- @"GTMSessionFetcher decorator requestDidFinish index %zu complete", index);
- __strong __typeof__(self) strongSelf = weakSelf;
- if (!strongSelf) {
- GTMSESSION_LOG_DEBUG(@"GTMSessionFetcher destroyed before requestDidFinish "
- @"decorators completed, ignoring.");
- return;
- }
- [strongSelf applyDecoratorsAtRequestDidFinish:decorators
- withData:data
- error:error
- startingAtIndex:index + 1
- shouldReleaseCallbacks:shouldReleaseCallbacks];
- }];
- }
- - (BOOL)canFetchWithBackgroundSession {
- // Subclasses may override.
- return YES;
- }
- // Returns YES if the fetcher has been started and has not yet stopped.
- //
- // Fetching includes waiting for authorization or for retry, waiting to be allowed by the
- // service object to start the request, and actually fetching the request.
- - (BOOL)isFetching {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return [self isFetchingUnsynchronized];
- }
- }
- - (BOOL)isFetchingUnsynchronized {
- GTMSessionCheckSynchronized(self);
- BOOL hasBegun = (_initialBeginFetchDate != nil);
- return hasBegun && !_hasStoppedFetching;
- }
- - (nullable NSURLResponse *)response {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- NSURLResponse *response = [self responseUnsynchronized];
- return response;
- } // @synchronized(self)
- }
- - (nullable NSURLResponse *)responseUnsynchronized {
- GTMSessionCheckSynchronized(self);
- NSURLResponse *response = _sessionTask.response;
- if (!response) response = _response;
- return response;
- }
- - (NSInteger)statusCode {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- NSInteger statusCode = [self statusCodeUnsynchronized];
- return statusCode;
- } // @synchronized(self)
- }
- - (NSInteger)statusCodeUnsynchronized {
- GTMSessionCheckSynchronized(self);
- NSURLResponse *response = [self responseUnsynchronized];
- NSInteger statusCode;
- if ([response respondsToSelector:@selector(statusCode)]) {
- statusCode = [(NSHTTPURLResponse *)response statusCode];
- } else {
- // Default to zero, in hopes of hinting "Unknown" (we can't be
- // sure that things are OK enough to use 200).
- statusCode = 0;
- }
- return statusCode;
- }
- - (nullable NSDictionary *)responseHeaders {
- GTMSessionCheckNotSynchronized(self);
- NSURLResponse *response = self.response;
- if ([response respondsToSelector:@selector(allHeaderFields)]) {
- NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
- return headers;
- }
- return nil;
- }
- - (nullable NSDictionary *)responseHeadersUnsynchronized {
- GTMSessionCheckSynchronized(self);
- NSURLResponse *response = [self responseUnsynchronized];
- if ([response respondsToSelector:@selector(allHeaderFields)]) {
- NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
- return headers;
- }
- return nil;
- }
- - (void)releaseCallbacks {
- // The clang included with Xcode 13.3 betas added a -Wunused-but-set-variable warning,
- // which doesn't (yet) skip variables annotated with objc_precie_lifetime. Since that
- // warning is not available in all Xcodes, turn off the -Wunused warning group entirely.
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wunused"
- // Avoid releasing blocks in the sync section since objects dealloc'd by
- // the blocks being released may call back into the fetcher or fetcher
- // service.
- dispatch_queue_t NS_VALID_UNTIL_END_OF_SCOPE holdCallbackQueue;
- GTMSessionFetcherCompletionHandler NS_VALID_UNTIL_END_OF_SCOPE holdCompletionHandler;
- #pragma clang diagnostic pop
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- holdCallbackQueue = _callbackQueue;
- holdCompletionHandler = _completionHandler;
- _callbackQueue = nil;
- _completionHandler = nil; // Setter overridden in upload. Setter assumed to be used externally.
- }
- // Set local callback pointers to nil here rather than let them release at the end of the scope
- // to make any problems due to the blocks being released be a bit more obvious in a stack trace.
- holdCallbackQueue = nil;
- holdCompletionHandler = nil;
- self.configurationBlock = nil;
- self.didReceiveResponseBlock = nil;
- self.challengeBlock = nil;
- self.willRedirectBlock = nil;
- self.sendProgressBlock = nil;
- self.receivedProgressBlock = nil;
- self.downloadProgressBlock = nil;
- self.accumulateDataBlock = nil;
- self.willCacheURLResponseBlock = nil;
- self.retryBlock = nil;
- self.testBlock = nil;
- self.resumeDataBlock = nil;
- if (@available(iOS 10.0, *)) {
- self.metricsCollectionBlock = nil;
- }
- }
- - (void)forgetSessionIdentifierForFetcher {
- GTMSessionCheckSynchronized(self);
- [self forgetSessionIdentifierForFetcherWithoutSyncCheck];
- }
- - (void)forgetSessionIdentifierForFetcherWithoutSyncCheck {
- // This should be called inside a @synchronized block (except during dealloc.)
- if (_sessionIdentifier) {
- NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIdentifierToFetcherMap];
- [sessionIdentifierToFetcherMap removeObjectForKey:_sessionIdentifier];
- _sessionIdentifier = nil;
- _didCreateSessionIdentifier = NO;
- }
- }
- // External stop method
- - (void)stopFetching {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- // Prevent enqueued callbacks from executing. The completion handler will still execute if
- // the property `stopFetchingTriggersCompletionHandler` is `YES`.
- _userStoppedFetching = YES;
- } // @synchronized(self)
- [self stopFetchReleasingCallbacks:!self.stopFetchingTriggersCompletionHandler];
- }
- // Cancel the fetch of the URL that's currently in progress.
- //
- // If shouldReleaseCallbacks is NO then the fetch will be retried so the callbacks need
- // still be retained or `stopFetching` was called and `stopFetchingTriggersCompletionHandler` is
- // `YES`.
- - (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks {
- [self removePersistedBackgroundSessionFromDefaults];
- GTMSessionFetcherService *service;
- NSMutableURLRequest *request;
- // If the task or the retry timer is all that's retaining the fetcher,
- // we want to be sure this instance survives stopping at least long enough for
- // the stack to unwind.
- __autoreleasing GTMSessionFetcher *holdSelf = self;
- BOOL hasCanceledTask = NO;
- [holdSelf destroyRetryTimer];
- BOOL sendStopNotification = YES;
- BOOL callbacksPending = NO;
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _hasStoppedFetching = YES;
- service = _service;
- request = _request;
- if (_sessionTask) {
- // In case cancelling the task or session calls this recursively, we want
- // to ensure that we'll only release the task and delegate once,
- // so first set _sessionTask to nil
- //
- // This may be called in a callback from the task, so use autorelease to avoid
- // releasing the task in its own callback.
- __autoreleasing NSURLSessionTask *oldTask = _sessionTask;
- if (!_isUsingTestBlock) {
- _response = _sessionTask.response;
- }
- _sessionTask = nil;
- if ([oldTask state] != NSURLSessionTaskStateCompleted) {
- // For download tasks, when the fetch is stopped, we may provide resume data that can
- // be used to create a new session.
- BOOL mayResume = (_resumeDataBlock &&
- [oldTask respondsToSelector:@selector(cancelByProducingResumeData:)]);
- if (!mayResume) {
- [oldTask cancel];
- // A side effect of stopping the task is that URLSession:task:didCompleteWithError:
- // will be invoked asynchronously on the delegate queue.
- } else {
- void (^resumeBlock)(NSData *) = _resumeDataBlock;
- _resumeDataBlock = nil;
- // Save callbackQueue since releaseCallbacks clears it.
- dispatch_queue_t callbackQueue = _callbackQueue;
- dispatch_group_enter(_callbackGroup);
- [(NSURLSessionDownloadTask *)oldTask cancelByProducingResumeData:^(NSData *resumeData) {
- [self invokeOnCallbackQueue:callbackQueue
- afterUserStopped:YES
- block:^{
- resumeBlock(resumeData);
- dispatch_group_leave(self->_callbackGroup);
- }];
- }];
- }
- hasCanceledTask = YES;
- }
- }
- // If the task was canceled, wait until the URLSession:task:didCompleteWithError: to call
- // finishTasksAndInvalidate, since calling it immediately tends to crash, see radar 18471901.
- if (_session) {
- BOOL shouldInvalidate = _shouldInvalidateSession;
- #if TARGET_OS_IPHONE
- // Don't invalidate if we've got a systemCompletionHandler, since
- // URLSessionDidFinishEventsForBackgroundURLSession: won't be called if invalidated.
- shouldInvalidate = shouldInvalidate && !self.systemCompletionHandler;
- #endif
- if (shouldInvalidate) {
- __autoreleasing NSURLSession *oldSession = _session;
- _session = nil;
- if (!hasCanceledTask) {
- [oldSession finishTasksAndInvalidate];
- } else {
- sendStopNotification = NO;
- _sessionNeedingInvalidation = oldSession;
- }
- }
- }
- callbacksPending = _stopFetchingTriggersCompletionHandler && _userStoppedFetching;
- } // @synchronized(self)
- // If the NSURLSession needs to be invalidated, but needs to wait until the delegate method
- // URLSession:task:didCompleteWithError: is called, delay sending the fetch stopped notification
- // until then; otherwise send it now.
- if (sendStopNotification) {
- [self sendStopNotificationIfNeeded];
- }
- [_authorizer stopAuthorizationForRequest:request];
- if (shouldReleaseCallbacks) {
- [self releaseCallbacks];
- self.authorizer = nil;
- }
- [service fetcherDidStop:self callbacksPending:callbacksPending];
- #if GTM_BACKGROUND_TASK_FETCHING
- [self endBackgroundTask];
- #endif
- }
- - (void)setStopNotificationNeeded:(BOOL)flag {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _isStopNotificationNeeded = flag;
- } // @synchronized(self)
- }
- - (void)sendStopNotificationIfNeeded {
- BOOL sendNow = NO;
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (_isStopNotificationNeeded) {
- _isStopNotificationNeeded = NO;
- sendNow = YES;
- }
- } // @synchronized(self)
- if (sendNow) {
- [self postNotificationOnMainThreadWithName:kGTMSessionFetcherStoppedNotification
- userInfo:nil
- requireAsync:NO];
- }
- }
- - (void)retryFetch {
- [self stopFetchReleasingCallbacks:NO];
- // A retry will need a configuration with a fresh session identifier.
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (_sessionIdentifier && _didCreateSessionIdentifier) {
- [self forgetSessionIdentifierForFetcher];
- _configuration = nil;
- }
- if (_canShareSession) {
- // Force a grab of the current session from the fetcher service in case
- // the service's old one has become invalid.
- _session = nil;
- }
- } // @synchronized(self)
- [self beginFetchForRetry];
- }
- - (BOOL)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds {
- // Uncovered in upload fetcher testing, because the chunk fetcher is being waited on, and gets
- // released by the upload code. The uploader just holds onto it with an ivar, and that gets
- // nilled in the chunk fetcher callback.
- // Used once in while loop just to avoid unused variable compiler warning.
- __autoreleasing GTMSessionFetcher *holdSelf = self;
- NSDate *giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds];
- BOOL shouldSpinRunLoop =
- ([NSThread isMainThread] &&
- (!self.callbackQueue || self.callbackQueue == dispatch_get_main_queue()));
- BOOL expired = NO;
- // Loop until the callbacks have been called and released, and until
- // the connection is no longer pending, until there are no callback dispatches
- // in flight, or until the timeout has expired.
- int64_t delta = (int64_t)(100 * NSEC_PER_MSEC); // 100 ms
- while (1) {
- BOOL isTaskInProgress =
- (holdSelf->_sessionTask && [_sessionTask state] != NSURLSessionTaskStateCompleted);
- BOOL needsToCallCompletion = (_completionHandler != nil);
- BOOL isCallbackInProgress =
- (_callbackGroup &&
- dispatch_group_wait(_callbackGroup, dispatch_time(DISPATCH_TIME_NOW, delta)));
- if (!isTaskInProgress && !needsToCallCompletion && !isCallbackInProgress) break;
- expired = ([giveUpDate timeIntervalSinceNow] < 0);
- if (expired) {
- GTMSESSION_LOG_DEBUG(@"GTMSessionFetcher waitForCompletionWithTimeout:%0.1f expired -- "
- @"%@%@%@",
- timeoutInSeconds, isTaskInProgress ? @"taskInProgress " : @"",
- needsToCallCompletion ? @"needsToCallCompletion " : @"",
- isCallbackInProgress ? @"isCallbackInProgress" : @"");
- break;
- }
- // Run the current run loop 1/1000 of a second to give the networking
- // code a chance to work
- const NSTimeInterval kSpinInterval = 0.001;
- if (shouldSpinRunLoop) {
- NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:kSpinInterval];
- [[NSRunLoop currentRunLoop] runUntilDate:stopDate];
- } else {
- [NSThread sleepForTimeInterval:kSpinInterval];
- }
- }
- return !expired;
- }
- + (void)setGlobalTestBlock:(nullable GTMSessionFetcherTestBlock)block {
- #if GTM_DISABLE_FETCHER_TEST_BLOCK
- GTMSESSION_ASSERT_DEBUG(block == nil, @"test blocks disabled");
- #endif
- gGlobalTestBlock = [block copy];
- }
- #if GTM_BACKGROUND_TASK_FETCHING
- static _Nullable id<GTMUIApplicationProtocol> gSubstituteUIApp;
- + (void)setSubstituteUIApplication:(nullable id<GTMUIApplicationProtocol>)app {
- gSubstituteUIApp = app;
- }
- + (nullable id<GTMUIApplicationProtocol>)substituteUIApplication {
- return gSubstituteUIApp;
- }
- + (nullable id<GTMUIApplicationProtocol>)fetcherUIApplication {
- id<GTMUIApplicationProtocol> app = gSubstituteUIApp;
- if (app) return app;
- // iOS App extensions should not call [UIApplication sharedApplication], even
- // if UIApplication responds to it.
- static Class applicationClass = nil;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- BOOL isAppExtension = [[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"];
- if (!isAppExtension) {
- Class cls = NSClassFromString(@"UIApplication");
- if (cls && [cls respondsToSelector:NSSelectorFromString(@"sharedApplication")]) {
- applicationClass = cls;
- }
- }
- });
- if (applicationClass) {
- app = (id<GTMUIApplicationProtocol>)[applicationClass sharedApplication];
- }
- return app;
- }
- #endif // GTM_BACKGROUND_TASK_FETCHING
- #pragma mark NSURLSession Delegate Methods
- // NSURLSession documentation indicates that redirectRequest can be passed to the handler
- // but empirically redirectRequest lacks the HTTP body, so passing it will break POSTs.
- // Instead, we construct a new request, a copy of the original, with overrides from the
- // redirect.
- - (void)URLSession:(NSURLSession *)session
- task:(NSURLSessionTask *)task
- willPerformHTTPRedirection:(NSHTTPURLResponse *)redirectResponse
- newRequest:(NSURLRequest *)redirectRequest
- completionHandler:(void (^)(NSURLRequest *_Nullable))handler {
- [self setSessionTask:task];
- GTMSESSION_LOG_DEBUG_VERBOSE(
- @"%@ %p URLSession:%@ task:%@ willPerformHTTPRedirection:%@ newRequest:%@", [self class],
- self, session, task, redirectResponse, redirectRequest);
- if ([self userStoppedFetching]) {
- handler(nil);
- return;
- }
- if (redirectRequest && redirectResponse) {
- // Copy the original request, including the body.
- NSURLRequest *originalRequest = self.request;
- NSMutableURLRequest *newRequest = [originalRequest mutableCopy];
- // The new requests's URL overrides the original's URL.
- [newRequest setURL:[GTMSessionFetcher redirectURLWithOriginalRequestURL:originalRequest.URL
- redirectRequestURL:redirectRequest.URL]];
- // Any headers in the redirect override headers in the original.
- NSDictionary *redirectHeaders = redirectRequest.allHTTPHeaderFields;
- for (NSString *key in redirectHeaders) {
- NSString *value = [redirectHeaders objectForKey:key];
- [newRequest setValue:value forHTTPHeaderField:key];
- }
- redirectRequest = newRequest;
- // Log the response we just received
- [self setResponse:redirectResponse];
- [self logNowWithError:nil];
- GTMSessionFetcherWillRedirectBlock willRedirectBlock = self.willRedirectBlock;
- if (willRedirectBlock) {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- __auto_type block = ^{
- willRedirectBlock(redirectResponse, redirectRequest, ^(NSURLRequest *clientRequest) {
- // Update the request for future logging.
- [self updateMutableRequest:[clientRequest mutableCopy]];
- handler(clientRequest);
- });
- };
- [self invokeOnCallbackQueueAfterUserStopped:YES block:block];
- } // @synchronized(self)
- return;
- }
- // Continues here if the client did not provide a redirect block.
- // Update the request for future logging.
- [self updateMutableRequest:[redirectRequest mutableCopy]];
- }
- handler(redirectRequest);
- }
- - (void)URLSession:(NSURLSession *)session
- dataTask:(NSURLSessionDataTask *)dataTask
- didReceiveResponse:(NSURLResponse *)response
- completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))handler {
- [self setSessionTask:dataTask];
- GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ dataTask:%@ didReceiveResponse:%@",
- [self class], self, session, dataTask, response);
- __auto_type accumulateAndFinish = ^(NSURLSessionResponseDisposition dispositionValue) {
- // This method is called when the server has determined that it
- // has enough information to create the NSURLResponse
- // it can be called multiple times, for example in the case of a
- // redirect, so each time we reset the data.
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- BOOL hadPreviousData = self->_downloadedLength > 0;
- [self->_downloadedData setLength:0];
- self->_downloadedLength = 0;
- if (hadPreviousData && (dispositionValue != NSURLSessionResponseCancel)) {
- // Tell the accumulate block to discard prior data.
- GTMSessionFetcherAccumulateDataBlock accumulateBlock = self->_accumulateDataBlock;
- if (accumulateBlock) {
- [self invokeOnCallbackQueueUnlessStopped:^{
- accumulateBlock(nil);
- }];
- }
- }
- } // @synchronized(self)
- handler(dispositionValue);
- };
- GTMSessionFetcherDidReceiveResponseBlock receivedResponseBlock;
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- receivedResponseBlock = _didReceiveResponseBlock;
- if (receivedResponseBlock) {
- // We will ultimately need to call back to NSURLSession's handler with the disposition value
- // for this delegate method even if the user has stopped the fetcher.
- __auto_type block = ^{
- receivedResponseBlock(response, ^(NSURLSessionResponseDisposition desiredDisposition) {
- accumulateAndFinish(desiredDisposition);
- });
- };
- [self invokeOnCallbackQueueAfterUserStopped:YES block:block];
- }
- } // @synchronized(self)
- if (receivedResponseBlock == nil) {
- accumulateAndFinish(NSURLSessionResponseAllow);
- }
- }
- - (void)URLSession:(NSURLSession *)session
- dataTask:(NSURLSessionDataTask *)dataTask
- didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask {
- GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ dataTask:%@ didBecomeDownloadTask:%@",
- [self class], self, session, dataTask, downloadTask);
- [self setSessionTask:downloadTask];
- }
- - (void)URLSession:(NSURLSession *)session
- task:(NSURLSessionTask *)task
- didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
- completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
- NSURLCredential *_Nullable credential))handler {
- [self setSessionTask:task];
- GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ task:%@ didReceiveChallenge:%@", [self class],
- self, session, task, challenge);
- GTMSessionFetcherChallengeBlock challengeBlock = self.challengeBlock;
- if (challengeBlock) {
- // The fetcher user has provided custom challenge handling.
- //
- // We will ultimately need to call back to NSURLSession's handler with the disposition value
- // for this delegate method even if the user has stopped the fetcher.
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- [self invokeOnCallbackQueueAfterUserStopped:YES
- block:^{
- challengeBlock(self, challenge, handler);
- }];
- }
- } else {
- // No challenge block was provided by the client.
- [self respondToChallenge:challenge completionHandler:handler];
- }
- }
- - (void)respondToChallenge:(NSURLAuthenticationChallenge *)challenge
- completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
- NSURLCredential *_Nullable credential))handler {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- NSInteger previousFailureCount = [challenge previousFailureCount];
- if (previousFailureCount <= 2) {
- NSURLProtectionSpace *protectionSpace = [challenge protectionSpace];
- NSString *authenticationMethod = [protectionSpace authenticationMethod];
- if ([authenticationMethod isEqual:NSURLAuthenticationMethodServerTrust]) {
- // SSL.
- //
- // Background sessions seem to require an explicit check of the server trust object
- // rather than default handling.
- SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
- if (serverTrust == NULL) {
- // No server trust information is available.
- handler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
- } else {
- // Server trust information is available.
- __auto_type callback = ^(SecTrustRef trustRef, BOOL allow) {
- if (allow) {
- NSURLCredential *trustCredential = [NSURLCredential credentialForTrust:trustRef];
- handler(NSURLSessionAuthChallengeUseCredential, trustCredential);
- } else {
- GTMSESSION_LOG_DEBUG(@"Cancelling authentication challenge for %@",
- self->_request.URL);
- handler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
- }
- };
- if (_allowInvalidServerCertificates) {
- callback(serverTrust, YES);
- } else {
- [[self class] evaluateServerTrust:serverTrust
- forRequest:_request
- completionHandler:callback];
- }
- }
- return;
- }
- NSURLCredential *credential = _credential;
- if ([[challenge protectionSpace] isProxy] && _proxyCredential != nil) {
- credential = _proxyCredential;
- }
- if (credential) {
- handler(NSURLSessionAuthChallengeUseCredential, credential);
- } else {
- // The credential is still nil; tell the OS to use the default handling. This is needed
- // for things that can come out of the keychain (proxies, client certificates, etc.).
- //
- // Note: Looking up a credential with NSURLCredentialStorage's
- // defaultCredentialForProtectionSpace: is *not* the same invoking the handler with
- // NSURLSessionAuthChallengePerformDefaultHandling. In the case of
- // NSURLAuthenticationMethodClientCertificate, you can get nil back from
- // NSURLCredentialStorage, while using this code path instead works.
- handler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
- }
- } else {
- // We've failed auth 3 times. The completion handler will be called with code
- // NSURLErrorCancelled.
- handler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
- }
- } // @synchronized(self)
- }
- // Return redirect URL based on the original request URL and redirect request URL.
- //
- // Method disallows any scheme changes between the original request URL and redirect request URL
- // aside from "http" to "https". If a change in scheme is detected the redirect URL inherits the
- // scheme from the original request URL.
- + (nullable NSURL *)redirectURLWithOriginalRequestURL:(nullable NSURL *)originalRequestURL
- redirectRequestURL:(nullable NSURL *)redirectRequestURL {
- // In the case of an NSURLSession redirect, neither URL should ever be nil; as a sanity check
- // if either is nil return the other URL.
- if (!redirectRequestURL) return originalRequestURL;
- if (!originalRequestURL) return redirectRequestURL;
- NSString *originalScheme = originalRequestURL.scheme;
- NSString *redirectScheme = redirectRequestURL.scheme;
- BOOL insecureToSecureRedirect =
- (originalScheme != nil && [originalScheme caseInsensitiveCompare:@"http"] == NSOrderedSame &&
- redirectScheme != nil && [redirectScheme caseInsensitiveCompare:@"https"] == NSOrderedSame);
- // This can't really be nil for the inputs, but to keep the analyzer happy
- // for the -caseInsensitiveCompare: call below, give it a value if it were.
- if (!originalScheme) originalScheme = @"https";
- // Check for changes to the scheme and disallow any changes except for http to https.
- if (!insecureToSecureRedirect &&
- (redirectScheme.length != originalScheme.length ||
- [redirectScheme caseInsensitiveCompare:originalScheme] != NSOrderedSame)) {
- NSURLComponents *components =
- [NSURLComponents componentsWithURL:(NSURL *_Nonnull)redirectRequestURL
- resolvingAgainstBaseURL:NO];
- components.scheme = originalScheme;
- return components.URL;
- }
- return redirectRequestURL;
- }
- // Validate the certificate chain.
- //
- // This may become a public method if it appears to be useful to users.
- + (void)evaluateServerTrust:(SecTrustRef)serverTrust
- forRequest:(NSURLRequest *)request
- completionHandler:(void (^)(SecTrustRef trustRef, BOOL allow))handler {
- // Retain the trust object to avoid a SecTrustEvaluate() crash on iOS 7.
- CFRetain(serverTrust);
- // Evaluate the certificate chain.
- //
- // The delegate queue may be the main thread. Trust evaluation could cause some
- // blocking network activity, so we must evaluate async, as documented at
- // https://developer.apple.com/library/ios/technotes/tn2232/
- //
- // We must also avoid multiple uses of the trust object, per docs:
- // "It is not safe to call this function concurrently with any other function that uses
- // the same trust management object, or to re-enter this function for the same trust
- // management object."
- //
- // SecTrustEvaluateAsync both does sync execution of Evaluate and calls back on the
- // queue passed to it, according to at sources in
- // http://www.opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-55050.9/lib/SecTrust.cpp
- // It would require a global serial queue to ensure the evaluate happens only on a
- // single thread at a time, so we'll stick with using SecTrustEvaluate on a background
- // thread.
- dispatch_queue_t evaluateBackgroundQueue =
- dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- dispatch_async(evaluateBackgroundQueue, ^{
- // It looks like the implementation of SecTrustEvaluate() on Mac grabs a global lock,
- // so it may be redundant for us to also lock, but it's easy to synchronize here
- // anyway.
- BOOL shouldAllow;
- #if GTM_SDK_REQUIRES_SECTRUSTEVALUATEWITHERROR
- CFErrorRef errorRef = NULL;
- @synchronized([GTMSessionFetcher class]) {
- GTMSessionMonitorSynchronized([GTMSessionFetcher class]);
- // SecTrustEvaluateWithError handles both the "proceed" and "unspecified" cases,
- // so it is not necessary to check the trust result the evaluation returns true.
- shouldAllow = SecTrustEvaluateWithError(serverTrust, &errorRef);
- }
- if (errorRef) {
- GTMSESSION_LOG_DEBUG(@"Error %d evaluating trust for %@", (int)CFErrorGetCode(errorRef),
- request);
- CFRelease(errorRef);
- }
- #else
- SecTrustResultType trustEval = kSecTrustResultInvalid;
- OSStatus trustError;
- @synchronized([GTMSessionFetcher class]) {
- GTMSessionMonitorSynchronized([GTMSessionFetcher class]);
- trustError = SecTrustEvaluate(serverTrust, &trustEval);
- }
- if (trustError != errSecSuccess) {
- GTMSESSION_LOG_DEBUG(@"Error %d evaluating trust for %@",
- (int)trustError, request);
- shouldAllow = NO;
- } else {
- // Having a trust level "unspecified" by the user is the usual result, described at
- // https://developer.apple.com/library/mac/qa/qa1360
- if (trustEval == kSecTrustResultUnspecified
- || trustEval == kSecTrustResultProceed) {
- shouldAllow = YES;
- } else {
- shouldAllow = NO;
- GTMSESSION_LOG_DEBUG(@"Challenge SecTrustResultType %u for %@, properties: %@",
- trustEval, request.URL.host,
- CFBridgingRelease(SecTrustCopyProperties(serverTrust)));
- }
- }
- #endif // GTM_SDK_REQUIRES_SECTRUSTEVALUATEWITHERROR
- handler(serverTrust, shouldAllow);
- CFRelease(serverTrust);
- });
- }
- - (void)invokeOnCallbackQueueUnlessStopped:(void (^)(void))block {
- [self invokeOnCallbackQueueAfterUserStopped:NO block:block];
- }
- - (void)invokeOnCallbackQueueAfterUserStopped:(BOOL)afterStopped block:(void (^)(void))block {
- GTMSessionCheckSynchronized(self);
- [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:afterStopped block:block];
- }
- - (void)invokeOnCallbackUnsynchronizedQueueAfterUserStopped:(BOOL)afterStopped
- block:(void (^)(void))block {
- // testBlock simulation code may not be synchronizing when this is invoked.
- [self invokeOnCallbackQueue:_callbackQueue afterUserStopped:afterStopped block:block];
- }
- - (void)invokeOnCallbackQueue:(dispatch_queue_t)callbackQueue
- afterUserStopped:(BOOL)afterStopped
- block:(void (^)(void))block {
- if (callbackQueue) {
- dispatch_group_async(_callbackGroup, callbackQueue, ^{
- if (!afterStopped && !self->_stopFetchingTriggersCompletionHandler) {
- NSDate *serviceStoppedAllDate = [self->_service stoppedAllFetchersDate];
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- // Avoid a race between stopFetching and the callback.
- if (self->_userStoppedFetching) {
- return;
- }
- // Also avoid calling back if the service has stopped all fetchers
- // since this one was created. The fetcher may have stopped before
- // stopAllFetchers was invoked, so _userStoppedFetching wasn't set,
- // but the app still won't expect the callback to fire after
- // the service's stopAllFetchers was invoked.
- if (serviceStoppedAllDate &&
- [self->_initialBeginFetchDate compare:serviceStoppedAllDate] != NSOrderedDescending) {
- // stopAllFetchers was called after this fetcher began.
- return;
- }
- } // @synchronized(self)
- }
- block();
- });
- }
- }
- - (void)invokeFetchCallbacksOnCallbackQueueWithData:(nullable NSData *)data
- error:(nullable NSError *)error
- mayDecorate:(BOOL)mayDecorate
- shouldReleaseCallbacks:(BOOL)shouldReleaseCallbacks {
- if (mayDecorate && [_service respondsToSelector:@selector(decorators)]) {
- NSArray<id<GTMFetcherDecoratorProtocol>> *decorators = _service.decorators;
- if (decorators.count) {
- [self applyDecoratorsAtRequestDidFinish:decorators
- withData:data
- error:error
- startingAtIndex:0
- shouldReleaseCallbacks:shouldReleaseCallbacks];
- return;
- }
- }
- GTMSESSION_LOG_DEBUG_VERBOSE(
- @"GTMSessionFetcher invoking fetch callbacks, data length %lu, error %@",
- (unsigned long)data.length, error);
- // Callbacks will be released in the method stopFetchReleasingCallbacks:
- GTMSessionFetcherCompletionHandler handler;
- dispatch_queue_t callbackQueue;
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- // Capture the completion handler and callback queue, and call them only
- // after releasing any callbacks to ensure the order of release vs
- // callback is deterministic given potential QoS differences between
- // callback queue and whatever queue this method eventually executes on.
- handler = _completionHandler;
- callbackQueue = _callbackQueue;
- }
- if (shouldReleaseCallbacks) {
- [self releaseCallbacks];
- }
- if (handler) {
- [self invokeOnCallbackQueue:callbackQueue
- afterUserStopped:NO
- block:^{
- handler(data, error);
- // Post a notification, primarily to allow code to collect responses for
- // testing.
- //
- // The observing code is not likely on the fetcher's callback
- // queue, so this posts explicitly to the main queue.
- NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
- if (data) {
- userInfo[kGTMSessionFetcherCompletionDataKey] = data;
- }
- if (error) {
- userInfo[kGTMSessionFetcherCompletionErrorKey] = error;
- }
- [self postNotificationOnMainThreadWithName:
- kGTMSessionFetcherCompletionInvokedNotification
- userInfo:userInfo
- requireAsync:NO];
- }];
- }
- }
- - (void)postNotificationOnMainThreadWithName:(NSString *)noteName
- userInfo:(nullable NSDictionary *)userInfo
- requireAsync:(BOOL)requireAsync {
- dispatch_block_t postBlock = ^{
- [[NSNotificationCenter defaultCenter] postNotificationName:noteName
- object:self
- userInfo:userInfo];
- };
- if ([NSThread isMainThread] && !requireAsync) {
- // Post synchronously for compatibility with older code using the fetcher.
- // Avoid calling out to other code from inside a sync block to avoid risk
- // of a deadlock or of recursive sync.
- GTMSessionCheckNotSynchronized(self);
- postBlock();
- } else {
- dispatch_async(dispatch_get_main_queue(), postBlock);
- }
- }
- - (void)URLSession:(NSURLSession *)session
- task:(NSURLSessionTask *)uploadTask
- needNewBodyStream:(void (^)(NSInputStream *_Nullable bodyStream))completionHandler {
- [self setSessionTask:uploadTask];
- GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ task:%@ needNewBodyStream:", [self class],
- self, session, uploadTask);
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- GTMSessionFetcherBodyStreamProvider provider = _bodyStreamProvider;
- #if !STRIP_GTM_FETCH_LOGGING
- if ([self respondsToSelector:@selector(loggedStreamProviderForStreamProvider:)]) {
- provider = [self performSelector:@selector(loggedStreamProviderForStreamProvider:)
- withObject:provider];
- }
- #endif
- if (provider) {
- [self invokeOnCallbackQueueUnlessStopped:^{
- provider(completionHandler);
- }];
- } else {
- GTMSESSION_ASSERT_DEBUG(NO, @"NSURLSession expects a stream provider");
- completionHandler(nil);
- }
- } // @synchronized(self)
- }
- - (void)URLSession:(NSURLSession *)session
- task:(NSURLSessionTask *)task
- didSendBodyData:(int64_t)bytesSent
- totalBytesSent:(int64_t)totalBytesSent
- totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
- [self setSessionTask:task];
- GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ task:%@ didSendBodyData:%lld"
- @" totalBytesSent:%lld totalBytesExpectedToSend:%lld",
- [self class], self, session, task, bytesSent, totalBytesSent,
- totalBytesExpectedToSend);
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (!_sendProgressBlock) {
- return;
- }
- // We won't hold on to send progress block; it's ok to not send it if the upload finishes.
- [self invokeOnCallbackQueueUnlessStopped:^{
- GTMSessionFetcherSendProgressBlock progressBlock;
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- progressBlock = self->_sendProgressBlock;
- }
- if (progressBlock) {
- progressBlock(bytesSent, totalBytesSent, totalBytesExpectedToSend);
- }
- }];
- } // @synchronized(self)
- }
- - (void)URLSession:(NSURLSession *)session
- dataTask:(NSURLSessionDataTask *)dataTask
- didReceiveData:(NSData *)data {
- [self setSessionTask:dataTask];
- NSUInteger bufferLength = data.length;
- GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ dataTask:%@ didReceiveData:%p (%llu bytes)",
- [self class], self, session, dataTask, data,
- (unsigned long long)bufferLength);
- if (bufferLength == 0) {
- // Observed on completing an out-of-process upload.
- return;
- }
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- GTMSessionFetcherAccumulateDataBlock accumulateBlock = _accumulateDataBlock;
- if (accumulateBlock) {
- // Let the client accumulate the data.
- _downloadedLength += bufferLength;
- [self invokeOnCallbackQueueUnlessStopped:^{
- accumulateBlock(data);
- }];
- } else if (!_userStoppedFetching) {
- // Append to the mutable data buffer unless the fetch has been cancelled.
- // Resumed upload tasks may not yet have a data buffer.
- if (_downloadedData == nil) {
- // Using NSClassFromString for iOS 6 compatibility.
- GTMSESSION_ASSERT_DEBUG(
- ![dataTask isKindOfClass:NSClassFromString(@"NSURLSessionDownloadTask")],
- @"Resumed download tasks should not receive data bytes");
- _downloadedData = [[NSMutableData alloc] init];
- }
- [_downloadedData appendData:data];
- _downloadedLength = (int64_t)_downloadedData.length;
- // We won't hold on to receivedProgressBlock here; it's ok to not send
- // it if the transfer finishes.
- if (_receivedProgressBlock) {
- [self invokeOnCallbackQueueUnlessStopped:^{
- GTMSessionFetcherReceivedProgressBlock progressBlock;
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- progressBlock = self->_receivedProgressBlock;
- }
- if (progressBlock) {
- progressBlock((int64_t)bufferLength, self->_downloadedLength);
- }
- }];
- }
- }
- } // @synchronized(self)
- }
- - (void)URLSession:(NSURLSession *)session
- dataTask:(NSURLSessionDataTask *)dataTask
- willCacheResponse:(NSCachedURLResponse *)proposedResponse
- completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
- GTM_SWIFT_DISABLE_ASYNC {
- GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ dataTask:%@ willCacheResponse:%@ %@",
- [self class], self, session, dataTask, proposedResponse,
- proposedResponse.response);
- GTMSessionFetcherWillCacheURLResponseBlock callback;
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- callback = _willCacheURLResponseBlock;
- if (callback) {
- [self invokeOnCallbackQueueAfterUserStopped:YES
- block:^{
- callback(proposedResponse, completionHandler);
- }];
- }
- } // @synchronized(self)
- if (!callback) {
- completionHandler(proposedResponse);
- }
- }
- - (void)URLSession:(NSURLSession *)session
- downloadTask:(NSURLSessionDownloadTask *)downloadTask
- didWriteData:(int64_t)bytesWritten
- totalBytesWritten:(int64_t)totalBytesWritten
- totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
- GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ downloadTask:%@ didWriteData:%lld"
- @" bytesWritten:%lld totalBytesExpectedToWrite:%lld",
- [self class], self, session, downloadTask, bytesWritten,
- totalBytesWritten, totalBytesExpectedToWrite);
- [self setSessionTask:downloadTask];
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if ((totalBytesExpectedToWrite != NSURLSessionTransferSizeUnknown) &&
- (totalBytesExpectedToWrite < totalBytesWritten)) {
- // Have observed cases were bytesWritten == totalBytesExpectedToWrite,
- // but totalBytesWritten > totalBytesExpectedToWrite, so setting to unkown in these cases.
- totalBytesExpectedToWrite = NSURLSessionTransferSizeUnknown;
- }
- GTMSessionFetcherDownloadProgressBlock progressBlock;
- progressBlock = self->_downloadProgressBlock;
- if (progressBlock) {
- [self invokeOnCallbackQueueUnlessStopped:^{
- progressBlock(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
- }];
- }
- } // @synchronized(self)
- }
- - (void)URLSession:(NSURLSession *)session
- downloadTask:(NSURLSessionDownloadTask *)downloadTask
- didResumeAtOffset:(int64_t)fileOffset
- expectedTotalBytes:(int64_t)expectedTotalBytes {
- GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ downloadTask:%@ didResumeAtOffset:%lld"
- @" expectedTotalBytes:%lld",
- [self class], self, session, downloadTask, fileOffset,
- expectedTotalBytes);
- [self setSessionTask:downloadTask];
- }
- - (void)URLSession:(NSURLSession *)session
- downloadTask:(NSURLSessionDownloadTask *)downloadTask
- didFinishDownloadingToURL:(NSURL *)downloadLocationURL {
- // Download may have relaunched app, so update _sessionTask.
- [self setSessionTask:downloadTask];
- GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ downloadTask:%@ didFinishDownloadingToURL:%@",
- [self class], self, session, downloadTask, downloadLocationURL);
- NSNumber *fileSizeNum;
- [downloadLocationURL getResourceValue:&fileSizeNum forKey:NSURLFileSizeKey error:NULL];
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- NSURL *destinationURL = _destinationFileURL;
- _downloadedLength = fileSizeNum.longLongValue;
- // Overwrite any previous file at the destination URL.
- NSFileManager *fileMgr = [NSFileManager defaultManager];
- NSError *removeError;
- if (![fileMgr removeItemAtURL:destinationURL error:&removeError] &&
- removeError.code != NSFileNoSuchFileError) {
- GTMSESSION_LOG_DEBUG(@"Could not remove previous file at %@ due to %@",
- downloadLocationURL.path, removeError);
- }
- NSInteger statusCode = [self statusCodeUnsynchronized];
- if (statusCode < 200 || statusCode > 399) {
- // In OS X 10.11, the response body is written to a file even on a server
- // status error. For convenience of the fetcher client, we'll skip saving the
- // downloaded body to the destination URL so that clients do not need to know
- // to delete the file following fetch errors.
- GTMSESSION_LOG_DEBUG(@"Abandoning download due to status %ld, file %@", (long)statusCode,
- downloadLocationURL.path);
- // On error code, add the contents of the temporary file to _downloadTaskErrorData
- // This way fetcher clients have access to error details possibly passed by the server.
- if (_downloadedLength > 0 && _downloadedLength <= kMaximumDownloadErrorDataLength) {
- _downloadTaskErrorData = [NSData dataWithContentsOfURL:downloadLocationURL];
- } else if (_downloadedLength > kMaximumDownloadErrorDataLength) {
- GTMSESSION_LOG_DEBUG(@"Download error data for file %@ not passed to userInfo due to size "
- @"%lld",
- downloadLocationURL.path, _downloadedLength);
- }
- } else {
- NSError *moveError;
- NSURL *destinationFolderURL = [destinationURL URLByDeletingLastPathComponent];
- BOOL didMoveDownload = NO;
- if ([fileMgr createDirectoryAtURL:destinationFolderURL
- withIntermediateDirectories:YES
- attributes:nil
- error:&moveError]) {
- didMoveDownload = [fileMgr moveItemAtURL:downloadLocationURL
- toURL:destinationURL
- error:&moveError];
- }
- if (!didMoveDownload) {
- _downloadFinishedError = moveError;
- }
- GTM_LOG_BACKGROUND_SESSION(@"%@ %p Moved download from \"%@\" to \"%@\" %@", [self class],
- self, downloadLocationURL.path, destinationURL.path,
- error ? error : @"");
- }
- } // @synchronized(self)
- }
- /* Sent as the last message related to a specific task. Error may be
- * nil, which implies that no error occurred and this task is complete.
- */
- - (void)URLSession:(NSURLSession *)session
- task:(NSURLSessionTask *)task
- didCompleteWithError:(NSError *)error {
- [self setSessionTask:task];
- GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ task:%@ didCompleteWithError:%@", [self class],
- self, session, task, error);
- NSInteger status = self.statusCode;
- BOOL forceAssumeRetry = NO;
- BOOL succeeded = NO;
- BOOL userStoppedTriggerCompletion = NO;
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- #if !GTM_DISABLE_FETCHER_TEST_BLOCK
- // The task is never resumed when a testBlock is used. When the session is destroyed,
- // we should ignore the callback, since the testBlock support code itself invokes
- // shouldRetryNowForStatus: and finishWithError:shouldRetry:
- if (_isUsingTestBlock) return;
- #endif
- userStoppedTriggerCompletion = _userStoppedFetching && _stopFetchingTriggersCompletionHandler;
- if (error == nil) {
- error = _downloadFinishedError;
- }
- succeeded = (error == nil && status >= 0 && status < 300);
- if (succeeded && !userStoppedTriggerCompletion) {
- // Succeeded.
- _bodyLength = task.countOfBytesSent;
- }
- } // @synchronized(self)
- if (userStoppedTriggerCompletion) {
- NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
- [userInfo setObject:@"Operation cancelled" forKey:NSLocalizedDescriptionKey];
- if (error) {
- [userInfo setObject:error forKey:NSUnderlyingErrorKey];
- }
- NSError *cancelError = [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
- code:GTMSessionFetcherErrorUserCancelled
- userInfo:userInfo];
- [self finishWithError:cancelError shouldRetry:NO];
- return;
- }
- if (succeeded) {
- [self finishWithError:nil shouldRetry:NO];
- return;
- }
- // For background redirects, no delegate method is called, so we cannot restore a stripped
- // Authorization header, so if a 403 ("Forbidden") was generated due to a missing OAuth 2 header,
- // set the current request's URL to the redirected URL, so we in effect restore the Authorization
- // header.
- if ((status == 403) && self.usingBackgroundSession) {
- NSURL *redirectURL = self.response.URL;
- NSURLRequest *request = self.request;
- if (![request.URL isEqual:redirectURL]) {
- NSString *authorizationHeader = [request.allHTTPHeaderFields objectForKey:@"Authorization"];
- if (authorizationHeader != nil) {
- NSMutableURLRequest *mutableRequest = [request mutableCopy];
- mutableRequest.URL = redirectURL;
- [self updateMutableRequest:mutableRequest];
- // Avoid assuming the session is still valid.
- self.session = nil;
- forceAssumeRetry = YES;
- }
- }
- }
- // If invalidating the session was deferred in stopFetchReleasingCallbacks: then do it now.
- NSURLSession *oldSession = self.sessionNeedingInvalidation;
- if (oldSession) {
- [self setSessionNeedingInvalidation:NULL];
- [oldSession finishTasksAndInvalidate];
- }
- // Failed.
- [self shouldRetryNowForStatus:status
- error:error
- forceAssumeRetry:forceAssumeRetry
- response:^(BOOL shouldRetry) {
- [self finishWithError:error shouldRetry:shouldRetry];
- }];
- }
- - (void)URLSession:(NSURLSession *)session
- task:(NSURLSessionTask *)task
- didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics
- API_AVAILABLE(ios(10.0), macosx(10.12), tvos(10.0), watchos(6.0)) {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- GTMSessionFetcherMetricsCollectionBlock metricsCollectionBlock = _metricsCollectionBlock;
- if (metricsCollectionBlock) {
- [self invokeOnCallbackQueueUnlessStopped:^{
- metricsCollectionBlock(metrics);
- }];
- }
- }
- }
- #if TARGET_OS_IPHONE
- - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
- GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSessionDidFinishEventsForBackgroundURLSession:%@",
- [self class], self, session);
- [self removePersistedBackgroundSessionFromDefaults];
- GTMSessionFetcherSystemCompletionHandler handler;
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- handler = self.systemCompletionHandler;
- self.systemCompletionHandler = nil;
- } // @synchronized(self)
- if (handler) {
- GTM_LOG_BACKGROUND_SESSION(@"%@ %p Calling system completionHandler", [self class], self);
- handler();
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- NSURLSession *oldSession = _session;
- _session = nil;
- if (_shouldInvalidateSession) {
- [oldSession finishTasksAndInvalidate];
- }
- } // @synchronized(self)
- }
- }
- #endif
- - (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error {
- // This may happen repeatedly for retries. On authentication callbacks, the retry
- // may begin before the prior session sends the didBecomeInvalid delegate message.
- GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ didBecomeInvalidWithError:%@", [self class],
- self, session, error);
- if (session == (NSURLSession *)self.session) {
- GTMSESSION_LOG_DEBUG_VERBOSE(@" Unexpected retained invalid session: %@", session);
- self.session = nil;
- }
- }
- - (void)finishWithError:(nullable NSError *)error shouldRetry:(BOOL)shouldRetry {
- [self removePersistedBackgroundSessionFromDefaults];
- BOOL shouldStopFetching = YES;
- NSData *downloadedData = nil;
- #if !STRIP_GTM_FETCH_LOGGING
- BOOL shouldDeferLogging = NO;
- #endif
- BOOL shouldBeginRetryTimer = NO;
- NSInteger status = [self statusCode];
- NSURL *destinationURL = self.destinationFileURL;
- BOOL fetchSucceeded = (error == nil && status >= 0 && status < 300);
- #if !STRIP_GTM_FETCH_LOGGING
- if (!fetchSucceeded) {
- if (!shouldDeferLogging && !self.hasLoggedError) {
- [self logNowWithError:error];
- self.hasLoggedError = YES;
- }
- }
- #endif // !STRIP_GTM_FETCH_LOGGING
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- #if !STRIP_GTM_FETCH_LOGGING
- shouldDeferLogging = _deferResponseBodyLogging;
- #endif
- if (fetchSucceeded) {
- // Success
- if ((_downloadedData.length > 0) && (destinationURL != nil)) {
- // Overwrite any previous file at the destination URL.
- NSFileManager *fileMgr = [NSFileManager defaultManager];
- [fileMgr removeItemAtURL:destinationURL error:NULL];
- NSURL *destinationFolderURL = [destinationURL URLByDeletingLastPathComponent];
- BOOL didMoveDownload = NO;
- if ([fileMgr createDirectoryAtURL:destinationFolderURL
- withIntermediateDirectories:YES
- attributes:nil
- error:&error]) {
- didMoveDownload = [_downloadedData writeToURL:destinationURL
- options:NSDataWritingAtomic
- error:&error];
- }
- if (didMoveDownload) {
- _downloadedData = nil;
- } else {
- _downloadFinishedError = error;
- }
- }
- downloadedData = _downloadedData;
- } else {
- // Unsuccessful with error or status over 300. Retry or notify the delegate of failure
- if (shouldRetry) {
- // Retrying.
- shouldBeginRetryTimer = YES;
- shouldStopFetching = NO;
- } else {
- if (error == nil) {
- // Create an error.
- NSDictionary *userInfo = GTMErrorUserInfoForData(
- _downloadedData.length > 0 ? _downloadedData : _downloadTaskErrorData,
- [self responseHeadersUnsynchronized]);
- error = [NSError errorWithDomain:kGTMSessionFetcherStatusDomain
- code:status
- userInfo:userInfo];
- } else {
- // If the error had resume data, and the client supplied a resume block, pass the
- // data to the client.
- void (^resumeBlock)(NSData *) = _resumeDataBlock;
- _resumeDataBlock = nil;
- if (resumeBlock) {
- NSData *resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData];
- if (resumeData) {
- [self invokeOnCallbackQueueAfterUserStopped:YES
- block:^{
- resumeBlock(resumeData);
- }];
- }
- }
- }
- if (_downloadedData.length > 0) {
- downloadedData = _downloadedData;
- }
- // If the error occurred after retries, report the number and duration of the
- // retries. This provides a clue to a developer looking at the error description
- // that the fetcher did retry before failing with this error.
- if (_retryCount > 0) {
- NSMutableDictionary *userInfoWithRetries =
- [NSMutableDictionary dictionaryWithDictionary:(NSDictionary *)error.userInfo];
- NSTimeInterval timeSinceInitialRequest = -[_initialRequestDate timeIntervalSinceNow];
- [userInfoWithRetries setObject:@(timeSinceInitialRequest)
- forKey:kGTMSessionFetcherElapsedIntervalWithRetriesKey];
- [userInfoWithRetries setObject:@(_retryCount)
- forKey:kGTMSessionFetcherNumberOfRetriesDoneKey];
- error = [NSError errorWithDomain:(NSString *)error.domain
- code:error.code
- userInfo:userInfoWithRetries];
- }
- }
- }
- } // @synchronized(self)
- if (shouldBeginRetryTimer) {
- [self beginRetryTimer];
- }
- // We want to send the stop notification before calling the delegate's
- // callback selector, since the callback selector may release all of
- // the fetcher properties that the client is using to track the fetches.
- //
- // We'll also stop now so that, to any observers watching the notifications,
- // it doesn't look like our wait for a retry (which may be long,
- // 30 seconds or more) is part of the network activity.
- [self sendStopNotificationIfNeeded];
- if (shouldStopFetching) {
- // The upload subclass doesn't want to release callbacks until upload chunks have completed.
- BOOL shouldRelease = [self shouldReleaseCallbacksUponCompletion];
- [self invokeFetchCallbacksOnCallbackQueueWithData:downloadedData
- error:error
- mayDecorate:YES
- shouldReleaseCallbacks:shouldRelease];
- [self stopFetchReleasingCallbacks:NO];
- }
- #if !STRIP_GTM_FETCH_LOGGING
- // _hasLoggedError is only set by this method
- if (!shouldDeferLogging && !_hasLoggedError) {
- [self logNowWithError:error];
- }
- #endif
- }
- - (BOOL)shouldReleaseCallbacksUponCompletion {
- // A subclass can override this to keep callbacks around after the
- // connection has finished successfully
- return YES;
- }
- - (void)logNowWithError:(nullable NSError *)error {
- GTMSessionCheckNotSynchronized(self);
- // If the logging category is available, then log the current request,
- // response, data, and error
- if ([self respondsToSelector:@selector(logFetchWithError:)]) {
- [self performSelector:@selector(logFetchWithError:) withObject:error];
- }
- }
- #pragma mark Retries
- - (BOOL)isRetryError:(NSError *)error {
- struct RetryRecord {
- __unsafe_unretained NSString *const domain;
- NSInteger code;
- };
- struct RetryRecord retries[] = {
- {kGTMSessionFetcherStatusDomain, 408}, // request timeout
- {kGTMSessionFetcherStatusDomain, 502}, // failure gatewaying to another server
- {kGTMSessionFetcherStatusDomain, 503}, // service unavailable
- {kGTMSessionFetcherStatusDomain, 504}, // request timeout
- {NSURLErrorDomain, NSURLErrorTimedOut},
- {NSURLErrorDomain, NSURLErrorNetworkConnectionLost},
- {nil, 0}};
- // NSError's isEqual always returns false for equal but distinct instances
- // of NSError, so we have to compare the domain and code values explicitly
- NSString *domain = error.domain;
- NSInteger code = error.code;
- for (int idx = 0; retries[idx].domain != nil; idx++) {
- if (code == retries[idx].code && [domain isEqual:retries[idx].domain]) {
- return YES;
- }
- }
- return NO;
- }
- // shouldRetryNowForStatus:error: responds with YES if the user has enabled retries
- // and the status or error is one that is suitable for retrying. "Suitable"
- // means either the isRetryError:'s list contains the status or error, or the
- // user's retry block is present and returns YES when called, or the
- // authorizer may be able to fix.
- - (void)shouldRetryNowForStatus:(NSInteger)status
- error:(NSError *)error
- forceAssumeRetry:(BOOL)forceAssumeRetry
- response:(GTMSessionFetcherRetryResponse)response {
- // Determine if a refreshed authorizer may avoid an authorization error
- BOOL willRetry = NO;
- // We assume _authorizer is immutable after beginFetch, and _hasAttemptedAuthRefresh is modified
- // only in this method, and this method is invoked on the serial delegate queue.
- //
- // We want to avoid calling the authorizer from inside a sync block.
- BOOL isFirstAuthError = (_authorizer != nil && !_hasAttemptedAuthRefresh &&
- status == GTMSessionFetcherStatusUnauthorized); // 401
- BOOL hasPrimed = NO;
- if (isFirstAuthError) {
- if ([_authorizer respondsToSelector:@selector(primeForRefresh)]) {
- hasPrimed = [_authorizer primeForRefresh];
- }
- }
- BOOL shouldRetryForAuthRefresh = NO;
- if (hasPrimed) {
- shouldRetryForAuthRefresh = YES;
- _hasAttemptedAuthRefresh = YES;
- [self updateRequestValue:nil forHTTPHeaderField:@"Authorization"];
- }
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- BOOL shouldDoRetry = [self isRetryEnabledUnsynchronized];
- if (shouldDoRetry && ![self hasRetryAfterInterval]) {
- // Determine if we're doing exponential backoff retries
- shouldDoRetry = [self nextRetryIntervalUnsynchronized] < _maxRetryInterval;
- if (shouldDoRetry) {
- // If an explicit max retry interval was set, we expect repeated backoffs to take
- // up to roughly twice that for repeated fast failures. If the initial attempt is
- // already more than 3 times the max retry interval, then failures have taken a long time
- // (such as from network timeouts) so don't retry again to avoid the app becoming
- // unexpectedly unresponsive.
- if (_maxRetryInterval > 0) {
- NSTimeInterval maxAllowedIntervalBeforeRetry = _maxRetryInterval * 3;
- NSTimeInterval timeSinceInitialRequest = -[_initialRequestDate timeIntervalSinceNow];
- if (timeSinceInitialRequest > maxAllowedIntervalBeforeRetry) {
- shouldDoRetry = NO;
- }
- }
- }
- }
- BOOL canRetry = shouldRetryForAuthRefresh || forceAssumeRetry || shouldDoRetry;
- if (canRetry) {
- NSDictionary *userInfo =
- GTMErrorUserInfoForData(_downloadedData, [self responseHeadersUnsynchronized]);
- NSError *statusError = [NSError errorWithDomain:kGTMSessionFetcherStatusDomain
- code:status
- userInfo:userInfo];
- if (error == nil) {
- error = statusError;
- }
- willRetry = shouldRetryForAuthRefresh || forceAssumeRetry || [self isRetryError:error] ||
- ((error != statusError) && [self isRetryError:statusError]);
- // If the user has installed a retry callback, consult that.
- GTMSessionFetcherRetryBlock retryBlock = _retryBlock;
- if (retryBlock) {
- [self invokeOnCallbackQueueUnlessStopped:^{
- retryBlock(willRetry, error, response);
- }];
- return;
- }
- }
- } // @synchronized(self)
- response(willRetry);
- }
- - (BOOL)hasRetryAfterInterval {
- GTMSessionCheckSynchronized(self);
- NSDictionary *responseHeaders = [self responseHeadersUnsynchronized];
- NSString *retryAfterValue = [responseHeaders valueForKey:@"Retry-After"];
- return (retryAfterValue != nil);
- }
- - (NSTimeInterval)retryAfterInterval {
- GTMSessionCheckSynchronized(self);
- NSDictionary *responseHeaders = [self responseHeadersUnsynchronized];
- NSString *retryAfterValue = [responseHeaders valueForKey:@"Retry-After"];
- if (retryAfterValue == nil) {
- return 0;
- }
- // Retry-After formatted as HTTP-date | delta-seconds
- // Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
- NSDateFormatter *rfc1123DateFormatter = [[NSDateFormatter alloc] init];
- rfc1123DateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
- rfc1123DateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
- rfc1123DateFormatter.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss z";
- NSDate *retryAfterDate = [rfc1123DateFormatter dateFromString:retryAfterValue];
- NSTimeInterval retryAfterInterval =
- (retryAfterDate != nil) ? retryAfterDate.timeIntervalSinceNow : retryAfterValue.intValue;
- retryAfterInterval = MAX(0, retryAfterInterval);
- return retryAfterInterval;
- }
- - (void)beginRetryTimer {
- if (![NSThread isMainThread]) {
- // Defer creating and starting the timer until we're on the main thread to ensure it has
- // a run loop.
- dispatch_group_async(_callbackGroup, dispatch_get_main_queue(), ^{
- [self beginRetryTimer];
- });
- return;
- }
- [self destroyRetryTimer];
- #if GTM_BACKGROUND_TASK_FETCHING
- // Don't keep a background task active while awaiting retry, which can lead to the
- // app exceeding the allotted time for keeping the background task open, causing the
- // system to terminate the app. When the retry starts, a new background task will
- // be created.
- [self endBackgroundTask];
- #endif // GTM_BACKGROUND_TASK_FETCHING
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- NSTimeInterval nextInterval = [self nextRetryIntervalUnsynchronized];
- NSTimeInterval maxInterval = _maxRetryInterval;
- NSTimeInterval newInterval = MIN(nextInterval, (maxInterval > 0 ? maxInterval : DBL_MAX));
- NSTimeInterval newIntervalTolerance = (newInterval / 10) > 1.0 ?: 1.0;
- _lastRetryInterval = newInterval;
- _retryTimer = [NSTimer timerWithTimeInterval:newInterval
- target:self
- selector:@selector(retryTimerFired:)
- userInfo:nil
- repeats:NO];
- _retryTimer.tolerance = newIntervalTolerance;
- [[NSRunLoop mainRunLoop] addTimer:_retryTimer forMode:NSDefaultRunLoopMode];
- } // @synchronized(self)
- [self postNotificationOnMainThreadWithName:kGTMSessionFetcherRetryDelayStartedNotification
- userInfo:nil
- requireAsync:NO];
- }
- - (void)retryTimerFired:(NSTimer *)timer {
- [self destroyRetryTimer];
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _retryCount++;
- } // @synchronized(self)
- NSOperationQueue *queue = self.sessionDelegateQueue;
- [queue addOperationWithBlock:^{
- [self retryFetch];
- }];
- }
- - (void)destroyRetryTimer {
- BOOL shouldNotify = NO;
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (_retryTimer) {
- [_retryTimer invalidate];
- _retryTimer = nil;
- shouldNotify = YES;
- }
- }
- if (shouldNotify) {
- [self postNotificationOnMainThreadWithName:kGTMSessionFetcherRetryDelayStoppedNotification
- userInfo:nil
- requireAsync:NO];
- }
- }
- - (NSUInteger)retryCount {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _retryCount;
- } // @synchronized(self)
- }
- - (NSTimeInterval)nextRetryInterval {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- NSTimeInterval interval = [self nextRetryIntervalUnsynchronized];
- return interval;
- } // @synchronized(self)
- }
- - (NSTimeInterval)nextRetryIntervalUnsynchronized {
- GTMSessionCheckSynchronized(self);
- NSInteger statusCode = [self statusCodeUnsynchronized];
- if ((statusCode == 503) && [self hasRetryAfterInterval]) {
- NSTimeInterval secs = [self retryAfterInterval];
- return secs;
- }
- // The next wait interval is the factor (2.0) times the last interval,
- // but never less than the minimum interval.
- NSTimeInterval secs = _lastRetryInterval * _retryFactor;
- if (_maxRetryInterval > 0) {
- secs = MIN(secs, _maxRetryInterval);
- }
- secs = MAX(secs, _minRetryInterval);
- return secs;
- }
- - (NSTimer *)retryTimer {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _retryTimer;
- } // @synchronized(self)
- }
- - (BOOL)isRetryEnabled {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _isRetryEnabled;
- } // @synchronized(self)
- }
- - (BOOL)isRetryEnabledUnsynchronized {
- GTMSessionCheckSynchronized(self);
- return _isRetryEnabled;
- }
- - (void)setRetryEnabled:(BOOL)flag {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (flag && !_isRetryEnabled) {
- // We defer initializing these until the user calls setRetryEnabled
- // to avoid using the random number generator if it's not needed.
- // However, this means min and max intervals for this fetcher are reset
- // as a side effect of calling setRetryEnabled.
- //
- // Make an initial retry interval random between 1.0 and 2.0 seconds
- _minRetryInterval = InitialMinRetryInterval();
- _maxRetryInterval = kUnsetMaxRetryInterval;
- _retryFactor = 2.0;
- _lastRetryInterval = 0.0;
- }
- _isRetryEnabled = flag;
- } // @synchronized(self)
- };
- - (NSTimeInterval)maxRetryInterval {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _maxRetryInterval;
- } // @synchronized(self)
- }
- - (void)setMaxRetryInterval:(NSTimeInterval)secs {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (secs > 0) {
- _maxRetryInterval = secs;
- } else {
- _maxRetryInterval = kUnsetMaxRetryInterval;
- }
- } // @synchronized(self)
- }
- - (double)minRetryInterval {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _minRetryInterval;
- } // @synchronized(self)
- }
- - (void)setMinRetryInterval:(NSTimeInterval)secs {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (secs > 0) {
- _minRetryInterval = secs;
- } else {
- // Set min interval to a random value between 1.0 and 2.0 seconds
- // so that if multiple clients start retrying at the same time, they'll
- // repeat at different times and avoid overloading the server
- _minRetryInterval = InitialMinRetryInterval();
- }
- } // @synchronized(self)
- }
- #pragma mark iOS System Completion Handlers
- #if TARGET_OS_IPHONE
- static NSMutableDictionary *gSystemCompletionHandlers = nil;
- - (nullable GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler {
- return [[self class] systemCompletionHandlerForSessionIdentifier:_sessionIdentifier];
- }
- - (void)setSystemCompletionHandler:
- (nullable GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler {
- [[self class] setSystemCompletionHandler:systemCompletionHandler
- forSessionIdentifier:_sessionIdentifier];
- }
- + (void)setSystemCompletionHandler:
- (nullable GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler
- forSessionIdentifier:(NSString *)sessionIdentifier {
- if (!sessionIdentifier) {
- NSLog(@"%s with nil identifier", __PRETTY_FUNCTION__);
- return;
- }
- @synchronized([GTMSessionFetcher class]) {
- if (gSystemCompletionHandlers == nil && systemCompletionHandler != nil) {
- gSystemCompletionHandlers = [[NSMutableDictionary alloc] init];
- }
- // Use setValue: to remove the object if completionHandler is nil.
- [gSystemCompletionHandlers setValue:systemCompletionHandler forKey:sessionIdentifier];
- }
- }
- + (nullable GTMSessionFetcherSystemCompletionHandler)systemCompletionHandlerForSessionIdentifier:
- (NSString *)sessionIdentifier {
- if (!sessionIdentifier) {
- return nil;
- }
- @synchronized([GTMSessionFetcher class]) {
- return [gSystemCompletionHandlers objectForKey:sessionIdentifier];
- }
- }
- #endif // TARGET_OS_IPHONE
- #pragma mark Getters and Setters
- // clang-format off
- // Don't re-format the @synthesize blocks:
- @synthesize downloadResumeData = _downloadResumeData,
- configuration = _configuration,
- configurationBlock = _configurationBlock,
- sessionTask = _sessionTask,
- wasCreatedFromBackgroundSession = _wasCreatedFromBackgroundSession,
- clientWillReconnectBackgroundSession = _clientWillReconnectBackgroundSession,
- sessionUserInfo = _sessionUserInfo,
- taskDescription = _taskDescription,
- taskPriority = _taskPriority,
- usingBackgroundSession = _usingBackgroundSession,
- canShareSession = _canShareSession,
- completionHandler = _completionHandler,
- credential = _credential,
- proxyCredential = _proxyCredential,
- bodyData = _bodyData,
- bodyLength = _bodyLength,
- service = _service,
- serviceHost = _serviceHost,
- accumulateDataBlock = _accumulateDataBlock,
- receivedProgressBlock = _receivedProgressBlock,
- downloadProgressBlock = _downloadProgressBlock,
- resumeDataBlock = _resumeDataBlock,
- didReceiveResponseBlock = _didReceiveResponseBlock,
- challengeBlock = _challengeBlock,
- willRedirectBlock = _willRedirectBlock,
- sendProgressBlock = _sendProgressBlock,
- willCacheURLResponseBlock = _willCacheURLResponseBlock,
- retryBlock = _retryBlock,
- metricsCollectionBlock = _metricsCollectionBlock,
- retryFactor = _retryFactor,
- allowedInsecureSchemes = _allowedInsecureSchemes,
- allowLocalhostRequest = _allowLocalhostRequest,
- allowInvalidServerCertificates = _allowInvalidServerCertificates,
- cookieStorage = _cookieStorage,
- callbackQueue = _callbackQueue,
- initialBeginFetchDate = _initialBeginFetchDate,
- testBlock = _testBlock,
- testBlockAccumulateDataChunkCount = _testBlockAccumulateDataChunkCount,
- comment = _comment,
- log = _log,
- stopFetchingTriggersCompletionHandler = _stopFetchingTriggersCompletionHandler;
- #if !STRIP_GTM_FETCH_LOGGING
- @synthesize redirectedFromURL = _redirectedFromURL,
- logRequestBody = _logRequestBody,
- logResponseBody = _logResponseBody,
- hasLoggedError = _hasLoggedError;
- #endif
- #if GTM_BACKGROUND_TASK_FETCHING
- @synthesize backgroundTaskIdentifier = _backgroundTaskIdentifier,
- skipBackgroundTask = _skipBackgroundTask;
- #endif
- // clang-format on
- - (nullable NSURLRequest *)request {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return [_request copy];
- } // @synchronized(self)
- }
- - (void)setRequest:(nullable NSURLRequest *)request {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (![self isFetchingUnsynchronized]) {
- _request = [request mutableCopy];
- } else {
- GTMSESSION_ASSERT_DEBUG(0, @"request may not be set after beginFetch has been invoked");
- }
- } // @synchronized(self)
- }
- - (nullable NSMutableURLRequest *)mutableRequestForTesting {
- // Allow tests only to modify the request, useful during retries.
- return _request;
- }
- // Internal method for updating the request property such as on redirects.
- - (void)updateMutableRequest:(nullable NSMutableURLRequest *)request {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _request = request;
- } // @synchronized(self)
- }
- // Set a header field value on the request. Header field value changes will not
- // affect a fetch after the fetch has begun.
- - (void)setRequestValue:(nullable NSString *)value forHTTPHeaderField:(NSString *)field {
- if (![self isFetching]) {
- [self updateRequestValue:value forHTTPHeaderField:field];
- } else {
- GTMSESSION_ASSERT_DEBUG(0, @"request may not be set after beginFetch has been invoked");
- }
- }
- // Internal method for updating request headers.
- - (void)updateRequestValue:(nullable NSString *)value forHTTPHeaderField:(NSString *)field {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- [_request setValue:value forHTTPHeaderField:field];
- } // @synchronized(self)
- }
- - (void)setResponse:(nullable NSURLResponse *)response {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _response = response;
- } // @synchronized(self)
- }
- - (int64_t)bodyLength {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (_bodyLength == NSURLSessionTransferSizeUnknown) {
- if (_bodyData) {
- _bodyLength = (int64_t)_bodyData.length;
- } else if (_bodyFileURL) {
- NSNumber *fileSizeNum = nil;
- NSError *fileSizeError = nil;
- if ([_bodyFileURL getResourceValue:&fileSizeNum
- forKey:NSURLFileSizeKey
- error:&fileSizeError]) {
- _bodyLength = [fileSizeNum longLongValue];
- }
- }
- }
- return _bodyLength;
- } // @synchronized(self)
- }
- - (BOOL)useUploadTask {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _useUploadTask;
- } // @synchronized(self)
- }
- - (void)setUseUploadTask:(BOOL)flag {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (flag != _useUploadTask) {
- GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized],
- @"useUploadTask should not change after beginFetch has been invoked");
- _useUploadTask = flag;
- }
- } // @synchronized(self)
- }
- - (nullable NSURL *)bodyFileURL {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _bodyFileURL;
- } // @synchronized(self)
- }
- - (void)setBodyFileURL:(nullable NSURL *)fileURL {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- // The comparison here is a trivial optimization and forgiveness for any client that
- // repeatedly sets the property, so it just uses pointer comparison rather than isEqual:.
- if (fileURL != _bodyFileURL) {
- GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized],
- @"fileURL should not change after beginFetch has been invoked");
- _bodyFileURL = fileURL;
- }
- } // @synchronized(self)
- }
- - (nullable GTMSessionFetcherBodyStreamProvider)bodyStreamProvider {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _bodyStreamProvider;
- } // @synchronized(self)
- }
- - (void)setBodyStreamProvider:(nullable GTMSessionFetcherBodyStreamProvider)block {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized],
- @"stream provider should not change after beginFetch has been invoked");
- _bodyStreamProvider = [block copy];
- } // @synchronized(self)
- }
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated"
- - (nullable id<GTMFetcherAuthorizationProtocol>)authorizer {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _authorizer;
- } // @synchronized(self)
- }
- - (void)setAuthorizer:(nullable id<GTMFetcherAuthorizationProtocol>)authorizer {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (authorizer != _authorizer) {
- if ([self isFetchingUnsynchronized]) {
- GTMSESSION_ASSERT_DEBUG(0,
- @"authorizer should not change after beginFetch has been invoked");
- } else {
- _authorizer = authorizer;
- }
- }
- } // @synchronized(self)
- }
- #pragma clang diagnostic pop
- - (nullable NSData *)downloadedData {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _downloadedData;
- } // @synchronized(self)
- }
- - (void)setDownloadedData:(nullable NSData *)data {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _downloadedData = [data mutableCopy];
- } // @synchronized(self)
- }
- - (int64_t)downloadedLength {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _downloadedLength;
- } // @synchronized(self)
- }
- - (void)setDownloadedLength:(int64_t)length {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _downloadedLength = length;
- } // @synchronized(self)
- }
- - (nonnull dispatch_queue_t)callbackQueue {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _callbackQueue;
- } // @synchronized(self)
- }
- - (void)setCallbackQueue:(nullable dispatch_queue_t)queue {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _callbackQueue = queue ?: dispatch_get_main_queue();
- } // @synchronized(self)
- }
- - (nullable NSURLSession *)session {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _session;
- } // @synchronized(self)
- }
- - (NSInteger)servicePriority {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _servicePriority;
- } // @synchronized(self)
- }
- - (void)setServicePriority:(NSInteger)value {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (value != _servicePriority) {
- GTMSESSION_ASSERT_DEBUG(
- ![self isFetchingUnsynchronized],
- @"servicePriority should not change after beginFetch has been invoked");
- _servicePriority = value;
- }
- } // @synchronized(self)
- }
- - (void)setSession:(nullable NSURLSession *)session {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _session = session;
- } // @synchronized(self)
- }
- - (BOOL)canShareSession {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _canShareSession;
- } // @synchronized(self)
- }
- - (void)setCanShareSession:(BOOL)flag {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _canShareSession = flag;
- } // @synchronized(self)
- }
- - (BOOL)useBackgroundSession {
- // This reflects if the user requested a background session, not necessarily
- // if one was created. That is tracked with _usingBackgroundSession.
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _userRequestedBackgroundSession;
- } // @synchronized(self)
- }
- - (void)setUseBackgroundSession:(BOOL)flag {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (flag != _userRequestedBackgroundSession) {
- GTMSESSION_ASSERT_DEBUG(
- ![self isFetchingUnsynchronized],
- @"useBackgroundSession should not change after beginFetch has been invoked");
- _userRequestedBackgroundSession = flag;
- }
- } // @synchronized(self)
- }
- - (BOOL)isUsingBackgroundSession {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _usingBackgroundSession;
- } // @synchronized(self)
- }
- - (void)setUsingBackgroundSession:(BOOL)flag {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _usingBackgroundSession = flag;
- } // @synchronized(self)
- }
- - (BOOL)stopFetchingTriggersCompletionHandler {
- return _stopFetchingTriggersCompletionHandler;
- }
- - (void)setStopFetchingTriggersCompletionHandler:(BOOL)flag {
- if (_initialBeginFetchDate == nil) {
- _stopFetchingTriggersCompletionHandler = flag;
- } else {
- GTMSESSION_ASSERT_DEBUG(
- 0, @"stopFetchingTriggersCompletionHandler should not change after fetcher starts");
- }
- }
- - (nullable NSURLSession *)sessionNeedingInvalidation {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _sessionNeedingInvalidation;
- } // @synchronized(self)
- }
- - (void)setSessionNeedingInvalidation:(nullable NSURLSession *)session {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _sessionNeedingInvalidation = session;
- } // @synchronized(self)
- }
- - (nonnull NSOperationQueue *)sessionDelegateQueue {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _delegateQueue;
- } // @synchronized(self)
- }
- - (void)setSessionDelegateQueue:(nullable NSOperationQueue *)queue {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (queue != _delegateQueue) {
- if ([self isFetchingUnsynchronized]) {
- GTMSESSION_ASSERT_DEBUG(0, @"sessionDelegateQueue should not change after fetch begins");
- } else {
- _delegateQueue = queue ?: [NSOperationQueue mainQueue];
- }
- }
- } // @synchronized(self)
- }
- - (BOOL)userStoppedFetching {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _userStoppedFetching;
- } // @synchronized(self)
- }
- - (nullable id)userData {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _userData;
- } // @synchronized(self)
- }
- - (void)setUserData:(nullable id)theObj {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _userData = theObj;
- } // @synchronized(self)
- }
- - (nullable NSURL *)destinationFileURL {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _destinationFileURL;
- } // @synchronized(self)
- }
- - (void)setDestinationFileURL:(nullable NSURL *)destinationFileURL {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (((_destinationFileURL == nil) && (destinationFileURL == nil)) ||
- [_destinationFileURL isEqual:destinationFileURL]) {
- return;
- }
- if (_sessionIdentifier) {
- // This is something we don't expect to happen in production.
- // However if it ever happen, leave a system log.
- NSLog(@"%@: Destination File URL changed from (%@) to (%@) after session identifier has "
- @"been created.",
- [self class], _destinationFileURL, destinationFileURL);
- #if DEBUG
- // On both the simulator and devices, the path can change to the download file, but the name
- // shouldn't change. Technically, this isn't supported in the fetcher, but the change of
- // URL is expected to happen only across development runs through Xcode.
- __unused NSString *oldFilename = [_destinationFileURL lastPathComponent];
- __unused NSString *newFilename = [destinationFileURL lastPathComponent];
- GTMSESSION_ASSERT_DEBUG(
- [oldFilename isEqualToString:newFilename],
- @"Destination File URL cannot be changed after session identifier has been created");
- #endif
- }
- _destinationFileURL = destinationFileURL;
- } // @synchronized(self)
- }
- - (void)setProperties:(nullable NSDictionary *)dict {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _properties = [dict mutableCopy];
- } // @synchronized(self)
- }
- - (nullable NSDictionary *)properties {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _properties;
- } // @synchronized(self)
- }
- - (void)setProperty:(nullable id)obj forKey:(NSString *)key {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (_properties == nil && obj != nil) {
- _properties = [[NSMutableDictionary alloc] init];
- }
- [_properties setValue:obj forKey:key];
- } // @synchronized(self)
- }
- - (nullable id)propertyForKey:(NSString *)key {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return [_properties objectForKey:key];
- } // @synchronized(self)
- }
- - (void)addPropertiesFromDictionary:(NSDictionary *)dict {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (_properties == nil && dict != nil) {
- [self setProperties:[dict mutableCopy]];
- } else {
- [_properties addEntriesFromDictionary:dict];
- }
- } // @synchronized(self)
- }
- - (void)setCommentWithFormat:(id)format, ... {
- #if !STRIP_GTM_FETCH_LOGGING
- NSString *result = format;
- if (format) {
- va_list argList;
- va_start(argList, format);
- result = [[NSString alloc] initWithFormat:format arguments:argList];
- va_end(argList);
- }
- [self setComment:result];
- #endif
- }
- #if !STRIP_GTM_FETCH_LOGGING
- - (NSData *)loggedStreamData {
- return _loggedStreamData;
- }
- - (void)appendLoggedStreamData:dataToAdd {
- if (!_loggedStreamData) {
- _loggedStreamData = [NSMutableData data];
- }
- [_loggedStreamData appendData:dataToAdd];
- }
- - (void)clearLoggedStreamData {
- _loggedStreamData = nil;
- }
- - (void)setDeferResponseBodyLogging:(BOOL)deferResponseBodyLogging {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (deferResponseBodyLogging != _deferResponseBodyLogging) {
- _deferResponseBodyLogging = deferResponseBodyLogging;
- if (!deferResponseBodyLogging && !self.hasLoggedError) {
- [_delegateQueue addOperationWithBlock:^{
- [self logNowWithError:nil];
- }];
- }
- }
- } // @synchronized(self)
- }
- - (BOOL)deferResponseBodyLogging {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _deferResponseBodyLogging;
- } // @synchronized(self)
- }
- #else
- + (void)setLoggingEnabled:(BOOL)flag {
- }
- + (BOOL)isLoggingEnabled {
- return NO;
- }
- #endif // STRIP_GTM_FETCH_LOGGING
- @end
- @implementation GTMSessionCookieStorage {
- NSMutableArray *_cookies;
- NSHTTPCookieAcceptPolicy _policy;
- }
- - (id)init {
- self = [super init];
- if (self != nil) {
- _cookies = [[NSMutableArray alloc] init];
- }
- return self;
- }
- - (nullable NSArray *)cookies {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return [_cookies copy];
- } // @synchronized(self)
- }
- - (void)setCookie:(NSHTTPCookie *)cookie {
- if (!cookie) return;
- if (_policy == NSHTTPCookieAcceptPolicyNever) return;
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- [self internalSetCookie:cookie];
- } // @synchronized(self)
- }
- // Note: this should only be called from inside a @synchronized(self) block.
- - (void)internalSetCookie:(NSHTTPCookie *)newCookie {
- GTMSessionCheckSynchronized(self);
- if (_policy == NSHTTPCookieAcceptPolicyNever) return;
- BOOL isValidCookie =
- (newCookie.name.length > 0 && newCookie.domain.length > 0 && newCookie.path.length > 0);
- GTMSESSION_ASSERT_DEBUG(isValidCookie, @"invalid cookie: %@", newCookie);
- if (isValidCookie) {
- // Remove the cookie if it's currently in the array.
- NSHTTPCookie *oldCookie = [self cookieMatchingCookie:newCookie];
- if (oldCookie) {
- [_cookies removeObjectIdenticalTo:oldCookie];
- }
- if (![[self class] hasCookieExpired:newCookie]) {
- [_cookies addObject:newCookie];
- }
- }
- }
- // Add all cookies in the new cookie array to the storage,
- // replacing stored cookies as appropriate.
- //
- // Side effect: removes expired cookies from the storage array.
- - (void)setCookies:(nullable NSArray *)newCookies {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- [self removeExpiredCookies];
- for (NSHTTPCookie *newCookie in newCookies) {
- [self internalSetCookie:newCookie];
- }
- } // @synchronized(self)
- }
- - (void)setCookies:(NSArray *)cookies
- forURL:(nullable NSURL *)URL
- mainDocumentURL:(nullable NSURL *)mainDocumentURL {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (_policy == NSHTTPCookieAcceptPolicyNever) {
- return;
- }
- if (_policy == NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain) {
- NSString *mainHost = mainDocumentURL.host;
- NSString *associatedHost = URL.host;
- if (!mainHost || ![associatedHost hasSuffix:mainHost]) {
- return;
- }
- }
- } // @synchronized(self)
- [self setCookies:cookies];
- }
- - (void)deleteCookie:(NSHTTPCookie *)cookie {
- if (!cookie) return;
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- NSHTTPCookie *foundCookie = [self cookieMatchingCookie:cookie];
- if (foundCookie) {
- [_cookies removeObjectIdenticalTo:foundCookie];
- }
- } // @synchronized(self)
- }
- // Retrieve all cookies appropriate for the given URL, considering
- // domain, path, cookie name, expiration, security setting.
- // Side effect: removed expired cookies from the storage array.
- - (nullable NSArray *)cookiesForURL:(NSURL *)theURL {
- NSMutableArray *foundCookies = nil;
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- [self removeExpiredCookies];
- // We'll prepend "." to the desired domain, since we want the
- // actual domain "nytimes.com" to still match the cookie domain
- // ".nytimes.com" when we check it below with hasSuffix.
- NSString *host = theURL.host.lowercaseString;
- NSString *path = theURL.path;
- NSString *scheme = [theURL scheme];
- NSString *requestingDomain = nil;
- BOOL isLocalhostRetrieval = NO;
- if (IsLocalhost(host)) {
- isLocalhostRetrieval = YES;
- } else {
- if (host.length > 0) {
- requestingDomain = [@"." stringByAppendingString:host];
- }
- }
- for (NSHTTPCookie *storedCookie in _cookies) {
- NSString *cookieDomain = storedCookie.domain.lowercaseString;
- NSString *cookiePath = storedCookie.path;
- BOOL cookieIsSecure = [storedCookie isSecure];
- BOOL isDomainOK;
- if (isLocalhostRetrieval) {
- // Prior to 10.5.6, the domain stored into NSHTTPCookies for localhost
- // is "localhost.local"
- isDomainOK = (IsLocalhost(cookieDomain) || [cookieDomain isEqual:@"localhost.local"]);
- } else {
- // Ensure we're matching exact domain names. We prepended a dot to the
- // requesting domain, so we can also prepend one here if needed before
- // checking if the request contains the cookie domain.
- if (![cookieDomain hasPrefix:@"."]) {
- cookieDomain = [@"." stringByAppendingString:cookieDomain];
- }
- isDomainOK = [requestingDomain hasSuffix:cookieDomain];
- }
- BOOL isPathOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath];
- BOOL isSecureOK =
- (!cookieIsSecure || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame);
- if (isDomainOK && isPathOK && isSecureOK) {
- if (foundCookies == nil) {
- foundCookies = [NSMutableArray array];
- }
- [foundCookies addObject:storedCookie];
- }
- }
- } // @synchronized(self)
- return foundCookies;
- }
- // Override methods from the NSHTTPCookieStorage (NSURLSessionTaskAdditions) category.
- - (void)storeCookies:(NSArray *)cookies forTask:(NSURLSessionTask *)task {
- NSURLRequest *currentRequest = task.currentRequest;
- [self setCookies:cookies forURL:currentRequest.URL mainDocumentURL:nil];
- }
- - (void)getCookiesForTask:(NSURLSessionTask *)task
- completionHandler:(void (^)(NSArray<NSHTTPCookie *> *))completionHandler {
- if (completionHandler) {
- NSURLRequest *currentRequest = task.currentRequest;
- NSURL *currentRequestURL = currentRequest.URL;
- NSArray *cookies = [self cookiesForURL:currentRequestURL];
- completionHandler(cookies);
- }
- }
- // Return a cookie from the array with the same name, domain, and path as the
- // given cookie, or else return nil if none found.
- //
- // Both the cookie being tested and all cookies in the storage array should
- // be valid (non-nil name, domains, paths).
- //
- // Note: this should only be called from inside a @synchronized(self) block
- - (nullable NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie {
- GTMSessionCheckSynchronized(self);
- NSString *name = cookie.name;
- NSString *domain = cookie.domain;
- NSString *path = cookie.path;
- GTMSESSION_ASSERT_DEBUG(name && domain && path,
- @"Invalid stored cookie (name:%@ domain:%@ path:%@)", name, domain, path);
- for (NSHTTPCookie *storedCookie in _cookies) {
- if ([storedCookie.name isEqual:name] && [storedCookie.domain isEqual:domain] &&
- [storedCookie.path isEqual:path]) {
- return storedCookie;
- }
- }
- return nil;
- }
- // Internal routine to remove any expired cookies from the array, excluding
- // cookies with nil expirations.
- //
- // Note: this should only be called from inside a @synchronized(self) block
- - (void)removeExpiredCookies {
- GTMSessionCheckSynchronized(self);
- // Count backwards since we're deleting items from the array
- for (NSInteger idx = (NSInteger)_cookies.count - 1; idx >= 0; idx--) {
- NSHTTPCookie *storedCookie = [_cookies objectAtIndex:(NSUInteger)idx];
- if ([[self class] hasCookieExpired:storedCookie]) {
- [_cookies removeObjectAtIndex:(NSUInteger)idx];
- }
- }
- }
- + (BOOL)hasCookieExpired:(NSHTTPCookie *)cookie {
- NSDate *expiresDate = [cookie expiresDate];
- if (expiresDate == nil) {
- // Cookies seem to have a Expires property even when the expiresDate method returns nil.
- id expiresVal = [[cookie properties] objectForKey:NSHTTPCookieExpires];
- if ([expiresVal isKindOfClass:[NSDate class]]) {
- expiresDate = expiresVal;
- }
- }
- BOOL hasExpired = (expiresDate != nil && [expiresDate timeIntervalSinceNow] < 0);
- return hasExpired;
- }
- - (void)removeAllCookies {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- [_cookies removeAllObjects];
- } // @synchronized(self)
- }
- - (NSHTTPCookieAcceptPolicy)cookieAcceptPolicy {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _policy;
- } // @synchronized(self)
- }
- - (void)setCookieAcceptPolicy:(NSHTTPCookieAcceptPolicy)cookieAcceptPolicy {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _policy = cookieAcceptPolicy;
- } // @synchronized(self)
- }
- @end
- void GTMSessionFetcherAssertValidSelector(id _Nullable obj, SEL _Nullable sel, ...) {
- // Verify that the object's selector is implemented with the proper
- // number and type of arguments
- #if DEBUG
- va_list argList;
- va_start(argList, sel);
- if (obj && sel) {
- // Check that the selector is implemented
- if (![obj respondsToSelector:sel]) {
- NSLog(@"\"%@\" selector \"%@\" is unimplemented or misnamed",
- NSStringFromClass([(id)obj class]), NSStringFromSelector((SEL)sel));
- NSCAssert(0, @"callback selector unimplemented or misnamed");
- } else {
- const char *expectedArgType;
- unsigned int argCount = 2; // skip self and _cmd
- NSMethodSignature *sig = [obj methodSignatureForSelector:sel];
- // Check that each expected argument is present and of the correct type
- while ((expectedArgType = va_arg(argList, const char *)) != 0) {
- if ([sig numberOfArguments] > argCount) {
- const char *foundArgType = [sig getArgumentTypeAtIndex:argCount];
- if (0 != strncmp(foundArgType, expectedArgType, strlen(expectedArgType))) {
- NSLog(@"\"%@\" selector \"%@\" argument %d should be type %s",
- NSStringFromClass([(id)obj class]), NSStringFromSelector((SEL)sel),
- (argCount - 2), expectedArgType);
- NSCAssert(0, @"callback selector argument type mistake");
- }
- }
- argCount++;
- }
- // Check that the proper number of arguments are present in the selector
- if (argCount != [sig numberOfArguments]) {
- NSLog(@"\"%@\" selector \"%@\" should have %d arguments",
- NSStringFromClass([(id)obj class]), NSStringFromSelector((SEL)sel), (argCount - 2));
- NSCAssert(0, @"callback selector arguments incorrect");
- }
- }
- }
- va_end(argList);
- #endif
- }
- NSString *GTMFetcherCleanedUserAgentString(NSString *str) {
- // Reference http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html
- // and http://www-archive.mozilla.org/build/user-agent-strings.html
- if (str == nil) return @"";
- NSMutableString *result = [NSMutableString stringWithString:str];
- // Replace spaces and commas with underscores
- [result replaceOccurrencesOfString:@" "
- withString:@"_"
- options:0
- range:NSMakeRange(0, result.length)];
- [result replaceOccurrencesOfString:@","
- withString:@"_"
- options:0
- range:NSMakeRange(0, result.length)];
- // Delete http token separators and remaining whitespace
- static NSCharacterSet *charsToDelete = nil;
- if (charsToDelete == nil) {
- // Make a set of unwanted characters
- NSString *const kSeparators = @"()<>@;:\\\"/[]?={}";
- NSMutableCharacterSet *mutableChars =
- [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy];
- [mutableChars addCharactersInString:kSeparators];
- charsToDelete = [mutableChars copy]; // hang on to an immutable copy
- }
- while (1) {
- NSRange separatorRange = [result rangeOfCharacterFromSet:charsToDelete];
- if (separatorRange.location == NSNotFound) break;
- [result deleteCharactersInRange:separatorRange];
- };
- return result;
- }
- NSString *GTMFetcherSystemVersionString(void) {
- static NSString *sSavedSystemString;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- #if TARGET_OS_WATCH
- // watchOS - WKInterfaceDevice
- WKInterfaceDevice *currentDevice = [WKInterfaceDevice currentDevice];
- NSString *rawModel = [currentDevice model];
- NSString *model = GTMFetcherCleanedUserAgentString(rawModel);
- NSString *systemVersion = [currentDevice systemVersion];
- #if TARGET_OS_SIMULATOR
- NSString *hardwareModel = @"sim";
- #else
- NSString *hardwareModel;
- struct utsname unameRecord;
- if (uname(&unameRecord) == 0) {
- NSString *machineName = @(unameRecord.machine);
- hardwareModel = GTMFetcherCleanedUserAgentString(machineName);
- }
- if (hardwareModel.length == 0) {
- hardwareModel = @"unk";
- }
- #endif
- sSavedSystemString =
- [[NSString alloc] initWithFormat:@"%@/%@ hw/%@", model, systemVersion, hardwareModel];
- // Example: Apple_Watch/3.0 hw/Watch1_2
- #elif TARGET_OS_TV || TARGET_OS_IOS
- // iOS and tvOS have UIDevice, use that.
- UIDevice *currentDevice = [UIDevice currentDevice];
- NSString *rawModel = [currentDevice model];
- NSString *model = GTMFetcherCleanedUserAgentString(rawModel);
- NSString *systemVersion = [currentDevice systemVersion];
- #if TARGET_OS_SIMULATOR
- NSString *hardwareModel = @"sim";
- #else
- NSString *hardwareModel;
- struct utsname unameRecord;
- if (uname(&unameRecord) == 0) {
- NSString *machineName = @(unameRecord.machine);
- hardwareModel = GTMFetcherCleanedUserAgentString(machineName);
- }
- if (hardwareModel.length == 0) {
- hardwareModel = @"unk";
- }
- #endif
- sSavedSystemString = [[NSString alloc] initWithFormat:@"%@/%@ hw/%@",
- model, systemVersion, hardwareModel];
- // Example: iPod_Touch/2.2 hw/iPod1_1
- // Example: Apple_TV/9.2 hw/AppleTV5,3
- #elif TARGET_OS_OSX
- // Mac build
- NSProcessInfo *procInfo = [NSProcessInfo processInfo];
- NSString *versString;
- NSOperatingSystemVersion version = procInfo.operatingSystemVersion;
- versString = [NSString stringWithFormat:@"%ld.%ld.%ld", (long)version.majorVersion,
- (long)version.minorVersion, (long)version.patchVersion];
- sSavedSystemString = [[NSString alloc] initWithFormat:@"MacOSX/%@", versString];
- #elif defined(_SYS_UTSNAME_H)
- // Foundation-only build
- struct utsname unameRecord;
- uname(&unameRecord);
- sSavedSystemString = [NSString stringWithFormat:@"%s/%s",
- unameRecord.sysname, unameRecord.release]; // "Darwin/8.11.1"
- #else
- #error No branch taken for a default user agent
- #endif
- });
- return sSavedSystemString;
- }
- NSString *GTMFetcherStandardUserAgentString(NSBundle *_Nullable bundle) {
- NSString *result = [NSString stringWithFormat:@"%@ %@", GTMFetcherApplicationIdentifier(bundle),
- GTMFetcherSystemVersionString()];
- return result;
- }
- NSString *GTMFetcherApplicationIdentifier(NSBundle *_Nullable bundle) {
- @synchronized([GTMSessionFetcher class]) {
- static NSMutableDictionary *sAppIDMap = nil;
- // If there's a bundle ID, use that; otherwise, use the process name
- if (bundle == nil) {
- bundle = [NSBundle mainBundle];
- }
- NSString *bundleID = [bundle bundleIdentifier];
- if (bundleID == nil) {
- bundleID = @"";
- }
- NSString *identifier = [sAppIDMap objectForKey:bundleID];
- if (identifier) return identifier;
- // Apps may add a string to the info.plist to uniquely identify different builds.
- identifier = [bundle objectForInfoDictionaryKey:@"GTMUserAgentID"];
- if (identifier.length == 0) {
- if (bundleID.length > 0) {
- identifier = bundleID;
- } else {
- // Fall back on the procname, prefixed by "proc" to flag that it's
- // autogenerated and perhaps unreliable
- NSString *procName = [[NSProcessInfo processInfo] processName];
- identifier = [NSString stringWithFormat:@"proc_%@", procName];
- }
- }
- // Clean up whitespace and special characters
- identifier = GTMFetcherCleanedUserAgentString(identifier);
- // If there's a version number, append that
- NSString *version = [bundle objectForInfoDictionaryKey:@"GTMUserAgentVersion"];
- if (version.length == 0) {
- version = [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
- if (version.length == 0) {
- version = [bundle objectForInfoDictionaryKey:@"CFBundleVersion"];
- }
- }
- // Clean up whitespace and special characters
- version = GTMFetcherCleanedUserAgentString(version);
- // Glue the two together (cleanup done above or else cleanup would strip the
- // slash)
- if (version.length > 0) {
- identifier = [identifier stringByAppendingFormat:@"/%@", version];
- }
- if (sAppIDMap == nil) {
- sAppIDMap = [[NSMutableDictionary alloc] init];
- }
- [sAppIDMap setObject:identifier forKey:bundleID];
- return identifier;
- }
- }
- #if DEBUG && (!defined(NS_BLOCK_ASSERTIONS) || GTMSESSION_ASSERT_AS_LOG)
- @implementation GTMSessionSyncMonitorInternal {
- NSValue *_objectKey; // The synchronize target object.
- const char *_functionName; // The function containing the monitored sync block.
- }
- - (instancetype)initWithSynchronizationObject:(id)object
- allowRecursive:(BOOL)allowRecursive
- functionName:(const char *)functionName {
- self = [super init];
- if (self) {
- Class threadKey = [GTMSessionSyncMonitorInternal class];
- _objectKey = [NSValue valueWithNonretainedObject:object];
- _functionName = functionName;
- NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary;
- NSMutableDictionary *counters = threadDict[threadKey];
- if (counters == nil) {
- counters = [NSMutableDictionary dictionary];
- threadDict[(id)threadKey] = counters;
- }
- NSCountedSet *functionNamesCounter = counters[_objectKey];
- NSUInteger numberOfSyncingFunctions = functionNamesCounter.count;
- if (!allowRecursive) {
- BOOL isTopLevelSyncScope = (numberOfSyncingFunctions == 0);
- NSArray *stack = [NSThread callStackSymbols];
- GTMSESSION_ASSERT_DEBUG(isTopLevelSyncScope,
- @"*** Recursive sync on %@ at %s; previous sync at %@\n%@",
- [object class], functionName, functionNamesCounter.allObjects,
- [stack subarrayWithRange:NSMakeRange(1, stack.count - 1)]);
- }
- if (!functionNamesCounter) {
- functionNamesCounter = [NSCountedSet set];
- counters[_objectKey] = functionNamesCounter;
- }
- [functionNamesCounter addObject:(id _Nonnull) @(functionName)];
- }
- return self;
- }
- - (void)dealloc {
- Class threadKey = [GTMSessionSyncMonitorInternal class];
- NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary;
- NSMutableDictionary *counters = threadDict[threadKey];
- NSCountedSet *functionNamesCounter = counters[_objectKey];
- NSString *functionNameStr = @(_functionName);
- NSUInteger numberOfSyncsByThisFunction = [functionNamesCounter countForObject:functionNameStr];
- NSArray *stack = [NSThread callStackSymbols];
- GTMSESSION_ASSERT_DEBUG(numberOfSyncsByThisFunction > 0, @"Sync not found on %@ at %s\n%@",
- [_objectKey.nonretainedObjectValue class], _functionName,
- [stack subarrayWithRange:NSMakeRange(1, stack.count - 1)]);
- [functionNamesCounter removeObject:functionNameStr];
- if (functionNamesCounter.count == 0) {
- [counters removeObjectForKey:_objectKey];
- }
- }
- + (nullable NSArray *)functionsHoldingSynchronizationOnObject:(id)object {
- Class threadKey = [GTMSessionSyncMonitorInternal class];
- NSValue *localObjectKey = [NSValue valueWithNonretainedObject:object];
- NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary;
- NSMutableDictionary *counters = threadDict[threadKey];
- NSCountedSet *functionNamesCounter = counters[localObjectKey];
- return functionNamesCounter.count > 0 ? functionNamesCounter.allObjects : nil;
- }
- @end
- #endif // DEBUG && (!defined(NS_BLOCK_ASSERTIONS) || GTMSESSION_ASSERT_AS_LOG)
- NS_ASSUME_NONNULL_END
|