12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583 |
- /* 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.h"
-
- #import <sys/utsname.h>
-
- #ifndef STRIP_GTM_FETCH_LOGGING
- #error GTMSessionFetcher headers should have defaulted this if it wasn't already defined.
- #endif
-
- GTM_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";
-
- // 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
-
- GTM_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
- #if (TARGET_OS_TV \
- || TARGET_OS_WATCH \
- || (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_11) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_11) \
- || (TARGET_OS_IPHONE && defined(__IPHONE_9_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0))
- #define GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY 1
- #endif
- #endif
-
- @interface GTMSessionFetcher ()
-
- @property(atomic, strong, readwrite, GTM_NULLABLE) NSData *downloadedData;
- @property(atomic, strong, readwrite, GTM_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:(GTM_NULLABLE NSError *)error;
- - (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream;
- - (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider:
- (GTMSessionFetcherBodyStreamProvider)streamProvider;
- @end
- #endif // !GTMSESSION_BUILD_COMBINED_SOURCES
-
- GTM_ASSUME_NONNULL_BEGIN
-
- static NSTimeInterval InitialMinRetryInterval(void) {
- return 1.0 + ((double)(arc4random_uniform(0x0FFFF)) / (double) 0x0FFFF);
- }
-
- static BOOL IsLocalhost(NSString * GTM_NULLABLE_TYPE 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 *GTM_NULLABLE_TYPE GTMErrorUserInfoForData(
- NSData *GTM_NULLABLE_TYPE data, NSDictionary *GTM_NULLABLE_TYPE 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 GTM_NULLABLE_TYPE 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 _didCreateSessionIdentifier;
- NSString *_sessionIdentifierUUID;
- BOOL _userRequestedBackgroundSession;
- BOOL _usingBackgroundSession;
- NSMutableData * GTM_NULLABLE_TYPE _downloadedData;
- NSError *_downloadFinishedError;
- NSData *_downloadResumeData; // immutable after construction
- NSData * GTM_NULLABLE_TYPE _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
-
- id<GTMFetcherAuthorizationProtocol> _authorizer; // immutable after beginFetch
-
- // The service object that created and monitors this fetcher, if any.
- id<GTMSessionFetcherServiceProtocol> _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 {
- [self fetchersForBackgroundSessions];
- }
- #endif
-
- + (instancetype)fetcherWithRequest:(GTM_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;
- }
-
- + (GTM_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:(GTM_NULLABLE NSURLRequest *)request
- configuration:(GTM_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:(GTM_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];
- }
-
- // 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];
- }
-
- - (GTMSessionFetcherCompletionHandler)completionHandlerWithTarget:(GTM_NULLABLE_TYPE id)target
- didFinishSelector:(GTM_NULLABLE_TYPE 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:(GTM_NULLABLE_TYPE id)target
- didFinishSelector:(GTM_NULLABLE_TYPE SEL)finishedSelector {
- GTMSessionCheckNotSynchronized(self);
-
- GTMSessionFetcherCompletionHandler handler = [self completionHandlerWithTarget:target
- didFinishSelector:finishedSelector];
- [self beginFetchWithCompletionHandler:handler];
- }
-
- - (void)beginFetchMayDelay:(BOOL)mayDelay
- mayAuthorize:(BOOL)mayAuthorize {
- // 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;
-
- // 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
- [self setSessionIdentifierInternal:nil];
- self.useBackgroundSession = 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 whitelist 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 = !isRecreatingSession && !self.usingBackgroundSession;
-
- if (!self.session && self.canShareSession) {
- self.session = [_service sessionForFetcherCreation];
- // If _session is nil, then the service's session creation semaphore will block
- // until this fetcher invokes fetcherDidCreateSession: below, so this *must* invoke
- // that method, even if the session fails to be created.
- }
-
- if (!self.session) {
- // 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];
-
- #if (TARGET_OS_TV \
- || TARGET_OS_WATCH \
- || (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10) \
- || (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0))
- // iOS 8/10.10 builds require the new backgroundSessionConfiguration method name.
- _configuration =
- [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionIdentifier];
- #elif (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10) \
- || (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0)
- // Do a runtime check to avoid a deprecation warning about using
- // +backgroundSessionConfiguration: on iOS 8.
- if ([NSURLSessionConfiguration respondsToSelector:@selector(backgroundSessionConfigurationWithIdentifier:)]) {
- // Running on iOS 8+/OS X 10.10+.
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wunguarded-availability"
- // Disable unguarded availability warning as we can't use the @availability macro until we require
- // all clients to build with Xcode 9 or above.
- _configuration =
- [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionIdentifier];
- #pragma clang diagnostic pop
- } else {
- // Running on iOS 7/OS X 10.9.
- _configuration =
- [NSURLSessionConfiguration backgroundSessionConfiguration:sessionIdentifier];
- }
- #else
- // Building with an SDK earlier than iOS 8/OS X 10.10.
- _configuration =
- [NSURLSessionConfiguration backgroundSessionConfiguration:sessionIdentifier];
- #endif
- self.usingBackgroundSession = YES;
- self.canShareSession = NO;
- } else {
- _configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
- }
- #if !GTM_ALLOW_INSECURE_REQUESTS
- _configuration.TLSMinimumSupportedProtocol = kTLSProtocol12;
- #endif
- } // !_configuration
- _configuration.HTTPCookieStorage = self.cookieStorage;
-
- if (_configurationBlock) {
- _configurationBlock(self, _configuration);
- }
-
- id<NSURLSessionDelegate> delegate = [_service sessionDelegate];
- if (!delegate || !self.canShareSession) {
- delegate = self;
- }
- self.session = [NSURLSession sessionWithConfiguration:_configuration
- delegate:delegate
- delegateQueue:self.sessionDelegateQueue];
- GTMSESSION_ASSERT_DEBUG(self.session, @"Couldn't create session");
-
- // Tell the service about the session created by this fetcher. This also signals the
- // service's semaphore to allow other fetchers to request this session.
- [_service fetcherDidCreateSession:self];
-
- // 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(self.session.delegate == delegate, @"Couldn't assign delegate.");
-
- if (self.session) {
- BOOL isUsingSharedDelegate = (delegate != self);
- if (!isUsingSharedDelegate) {
- _shouldInvalidateSession = YES;
- }
- }
- }
-
- 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;
- }
- }
-
- // 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 * GTM_NONNULL_TYPE)_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) {
- #if TARGET_OS_TV || TARGET_OS_WATCH
- BOOL hasTaskPriority = YES;
- #elif (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10) \
- || (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0)
- BOOL hasTaskPriority = YES;
- #else
- BOOL hasTaskPriority = [newSessionTask respondsToSelector:@selector(setPriority:)];
- #endif
- if (hasTaskPriority) {
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wunguarded-availability"
- // Disable unguarded availability warning as we can't use the @availability macro until we require
- // all clients to build with Xcode 9 or above.
- newSessionTask.priority = _taskPriority;
- #pragma clang diagnostic pop
- }
- }
-
- #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.useBackgroundSession) {
- // 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
- __block UIBackgroundTaskIdentifier bgTaskID = [app beginBackgroundTaskWithName:bgTaskName
- expirationHandler:^{
- // Background task expiration callback - this block is always invoked by
- // UIApplication on the main thread.
- if (bgTaskID != UIBackgroundTaskInvalid) {
- @synchronized(self) {
- if (bgTaskID == self.backgroundTaskIdentifier) {
- self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
- }
- }
- [app endBackgroundTask:bgTaskID];
- }
- }];
- @synchronized(self) {
- self.backgroundTaskIdentifier = bgTaskID;
- }
- }
- #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];
- }
- }
-
- NSData * GTM_NULLABLE_TYPE 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:(NSData * GTM_NULLABLE_TYPE)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;
-
- // Simulate receipt of redirection.
- if (willRedirectBlock) {
- [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
- block:^{
- willRedirectBlock((NSHTTPURLResponse *)response, self->_request,
- ^(NSURLRequest *redirectRequest) {
- // For simulation, we'll assume the app will just continue.
- });
- }];
- }
-
- // 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) {
- [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
- block:^{
- if (self->_challengeBlock) {
- 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];
- self->_challengeBlock(self, challenge, ^(NSURLSessionAuthChallengeDisposition disposition,
- NSURLCredential * GTM_NULLABLE_TYPE 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.
- });
- }
- }];
- }
-
- // Simulate receipt of an initial response.
- if (response && didReceiveResponseBlock) {
- [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
- block:^{
- didReceiveResponseBlock(response, ^(NSURLSessionResponseDisposition desiredDisposition) {
- // For simulation, we'll assume the disposition is to continue.
- });
- }];
- }
-
- // Simulate reporting send progress.
- if (sendProgressBlock) {
- [self simulateByteTransferReportWithDataLength:(int64_t)bodyData.length
- block:^(int64_t bytesSent,
- int64_t totalBytesSent,
- int64_t totalBytesExpectedToSend) {
- // This is invoked on the callback queue unless stopped.
- sendProgressBlock(bytesSent, totalBytesSent, totalBytesExpectedToSend);
- }];
- }
-
- if (destinationFileURL) {
- // Simulate download to file progress.
- if (downloadProgressBlock) {
- [self simulateByteTransferReportWithDataLength:(int64_t)responseData.length
- block:^(int64_t bytesDownloaded,
- int64_t totalBytesDownloaded,
- int64_t totalBytesExpectedToDownload) {
- // This is invoked on the callback queue unless stopped.
- downloadProgressBlock(bytesDownloaded, totalBytesDownloaded,
- totalBytesExpectedToDownload);
- }];
- }
-
- 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) {
- [self simulateByteTransferWithData:responseData
- 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);
- }
- }];
- }
-
- 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];
- [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
- block:^{
- willCacheURLResponseBlock(cachedResponse, ^(NSCachedURLResponse *responseToCache){
- // The app may provide an alternative response, or nil to defeat caching.
- });
- }];
- }
- }
- _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)
- }
-
- - (NSURLSessionTask * GTM_NULLABLE_TYPE)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];
- }
-
- + (GTM_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);
- [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 {
- 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
-
- - (NSString * GTM_NULLABLE_TYPE)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:(GTM_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)
- }
-
- - (NSDictionary * GTM_NULLABLE_TYPE)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:(NSDictionary * GTM_NULLABLE_TYPE)dictionary {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- GTMSESSION_ASSERT_DEBUG(_sessionIdentifier == nil, @"Too late to assign userInfo");
- _sessionUserInfo = dictionary;
- } // @synchronized(self)
- }
-
- - (GTM_NULLABLE NSDictionary *)sessionIdentifierDefaultMetadata {
- GTMSessionCheckSynchronized(self);
-
- NSMutableDictionary *defaultUserInfo = [[NSMutableDictionary alloc] init];
- if (_destinationFileURL) {
- defaultUserInfo[kGTMSessionIdentifierDestinationFileURLMetadataKey] =
- [_destinationFileURL absoluteString];
- }
- if (_bodyFileURL) {
- defaultUserInfo[kGTMSessionIdentifierBodyFileURLMetadataKey] = [_bodyFileURL absoluteString];
- }
- 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);
- }
- }
-
- - (NSDictionary * GTM_NULLABLE_TYPE)sessionIdentifierMetadata {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- return [self sessionIdentifierMetadataUnsynchronized];
- }
- }
-
- - (NSDictionary * GTM_NULLABLE_TYPE)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:(NSDictionary * GTM_NULLABLE_TYPE)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 * GTM_NONNULL_TYPE)_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];
- [self releaseCallbacks];
-
- [_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;
- SEL asyncAuthSel = @selector(authorizeRequest:delegate:didFinishSelector:);
- if ([authorizer respondsToSelector:asyncAuthSel]) {
- 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];
- }
- }
-
- - (void)authorizer:(id<GTMFetcherAuthorizationProtocol>)auth
- request:(NSMutableURLRequest *)authorizedRequest
- finishedWithError:(NSError *)error {
- GTMSessionCheckNotSynchronized(self);
-
- if (error != nil) {
- // We can't fetch without authorization
- [self failToBeginFetchWithError:error];
- } else {
- @synchronized(self) {
- _request = authorizedRequest;
- }
- [self beginFetchMayDelay:NO
- mayAuthorize:NO];
- }
- }
-
-
- - (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;
- }
-
- - (NSURLResponse * GTM_NULLABLE_TYPE)response {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- NSURLResponse *response = [self responseUnsynchronized];
- return response;
- } // @synchronized(self)
- }
-
- - (NSURLResponse * GTM_NULLABLE_TYPE)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;
- }
-
- - (NSDictionary * GTM_NULLABLE_TYPE)responseHeaders {
- GTMSessionCheckNotSynchronized(self);
-
- NSURLResponse *response = self.response;
- if ([response respondsToSelector:@selector(allHeaderFields)]) {
- NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
- return headers;
- }
- return nil;
- }
-
- - (NSDictionary * GTM_NULLABLE_TYPE)responseHeadersUnsynchronized {
- GTMSessionCheckSynchronized(self);
-
- NSURLResponse *response = [self responseUnsynchronized];
- if ([response respondsToSelector:@selector(allHeaderFields)]) {
- NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
- return headers;
- }
- return nil;
- }
-
- - (void)releaseCallbacks {
- // 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;
- @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;
- }
-
- - (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.
- _userStoppedFetching = YES;
- } // @synchronized(self)
- [self stopFetchReleasingCallbacks:YES];
- }
-
- // 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 to still be retained.
- - (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks {
- [self removePersistedBackgroundSessionFromDefaults];
-
- id<GTMSessionFetcherServiceProtocol> 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];
-
- @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 {
- _sessionNeedingInvalidation = oldSession;
- }
- }
- }
- } // @synchronized(self)
-
- // send the stopped notification
- [self sendStopNotificationIfNeeded];
-
- [_authorizer stopAuthorizationForRequest:request];
-
- if (shouldReleaseCallbacks) {
- [self releaseCallbacks];
-
- self.authorizer = nil;
- }
-
- [service fetcherDidStop:self];
-
- #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:(GTMSessionFetcherTestBlock GTM_NULLABLE_TYPE)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 GTM_NULLABLE_TYPE 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 * GTM_NULLABLE_TYPE))handler {
- [self setSessionTask:task];
- GTM_LOG_SESSION_DELEGATE(@"%@ %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);
- [self invokeOnCallbackQueueAfterUserStopped:YES
- block:^{
- willRedirectBlock(redirectResponse, redirectRequest, ^(NSURLRequest *clientRequest) {
-
- // Update the request for future logging.
- [self updateMutableRequest:[clientRequest mutableCopy]];
-
- handler(clientRequest);
- });
- }];
- } // @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];
- GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ dataTask:%@ didReceiveResponse:%@",
- [self class], self, session, dataTask, response);
- void (^accumulateAndFinish)(NSURLSessionResponseDisposition) =
- ^(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.
- [self invokeOnCallbackQueueAfterUserStopped:YES
- block:^{
- receivedResponseBlock(response, ^(NSURLSessionResponseDisposition desiredDisposition) {
- accumulateAndFinish(desiredDisposition);
- });
- }];
- }
- } // @synchronized(self)
-
- if (receivedResponseBlock == nil) {
- accumulateAndFinish(NSURLSessionResponseAllow);
- }
- }
-
- - (void)URLSession:(NSURLSession *)session
- dataTask:(NSURLSessionDataTask *)dataTask
- didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask {
- GTM_LOG_SESSION_DELEGATE(@"%@ %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 * GTM_NULLABLE_TYPE credential))handler {
- [self setSessionTask:task];
- GTM_LOG_SESSION_DELEGATE(@"%@ %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 * GTM_NULLABLE_TYPE 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.
- void (^callback)(SecTrustRef, BOOL) = ^(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.
- + (GTM_NULLABLE NSURL *)redirectURLWithOriginalRequestURL:(GTM_NULLABLE NSURL *)originalRequestURL
- redirectRequestURL:(GTM_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.
- SecTrustResultType trustEval = kSecTrustResultInvalid;
- BOOL shouldAllow;
- 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)));
- }
- }
- 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) {
- 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:(GTM_NULLABLE NSData *)data
- error:(GTM_NULLABLE NSError *)error {
- // Callbacks will be released in the method stopFetchReleasingCallbacks:
- GTMSessionFetcherCompletionHandler handler;
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- handler = _completionHandler;
-
- if (handler) {
- [self invokeOnCallbackQueueUnlessStopped:^{
- 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];
- }];
- }
- } // @synchronized(self)
- }
-
- - (void)postNotificationOnMainThreadWithName:(NSString *)noteName
- userInfo:(GTM_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 * GTM_NULLABLE_TYPE bodyStream))completionHandler {
- [self setSessionTask:uploadTask];
- GTM_LOG_SESSION_DELEGATE(@"%@ %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];
- GTM_LOG_SESSION_DELEGATE(@"%@ %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;
- GTM_LOG_SESSION_DELEGATE(@"%@ %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_LOG_SESSION_DELEGATE(@"%@ %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 {
- GTM_LOG_SESSION_DELEGATE(@"%@ %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;
- }
- // We won't hold on to download progress block during the enqueue;
- // it's ok to not send it if the upload finishes.
-
- [self invokeOnCallbackQueueUnlessStopped:^{
- GTMSessionFetcherDownloadProgressBlock progressBlock;
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- progressBlock = self->_downloadProgressBlock;
- }
- if (progressBlock) {
- progressBlock(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
- }
- }];
- } // @synchronized(self)
- }
-
- - (void)URLSession:(NSURLSession *)session
- downloadTask:(NSURLSessionDownloadTask *)downloadTask
- didResumeAtOffset:(int64_t)fileOffset
- expectedTotalBytes:(int64_t)expectedTotalBytes {
- GTM_LOG_SESSION_DELEGATE(@"%@ %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];
- GTM_LOG_SESSION_DELEGATE(@"%@ %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];
- GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ didCompleteWithError:%@",
- [self class], self, session, task, error);
-
- NSInteger status = self.statusCode;
- BOOL forceAssumeRetry = NO;
- BOOL succeeded = 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
-
- if (error == nil) {
- error = _downloadFinishedError;
- }
- succeeded = (error == nil && status >= 0 && status < 300);
- if (succeeded) {
- // Succeeded.
- _bodyLength = task.countOfBytesSent;
- }
- } // @synchronized(self)
-
- 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];
- }];
- }
-
- #if TARGET_OS_IPHONE
- - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
- GTM_LOG_SESSION_DELEGATE(@"%@ %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:(GTM_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.
- GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ didBecomeInvalidWithError:%@",
- [self class], self, session, error);
- if (session == (NSURLSession *)self.session) {
- GTM_LOG_SESSION_DELEGATE(@" Unexpected retained invalid session: %@", session);
- self.session = nil;
- }
- }
-
- - (void)finishWithError:(GTM_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) {
- [self invokeFetchCallbacksOnCallbackQueueWithData:downloadedData
- error:error];
- // The upload subclass doesn't want to release callbacks until upload chunks have completed.
- BOOL shouldRelease = [self shouldReleaseCallbacksUponCompletion];
- [self stopFetchReleasingCallbacks:shouldRelease];
- }
-
- #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:(GTM_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];
-
- @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;
-
- - (GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler {
- return [[self class] systemCompletionHandlerForSessionIdentifier:_sessionIdentifier];
- }
-
- - (void)setSystemCompletionHandler:(GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler {
- [[self class] setSystemCompletionHandler:systemCompletionHandler
- forSessionIdentifier:_sessionIdentifier];
- }
-
- + (void)setSystemCompletionHandler:(GTM_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];
- }
- }
-
- + (GTM_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
-
- @synthesize downloadResumeData = _downloadResumeData,
- configuration = _configuration,
- configurationBlock = _configurationBlock,
- sessionTask = _sessionTask,
- wasCreatedFromBackgroundSession = _wasCreatedFromBackgroundSession,
- 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,
- retryFactor = _retryFactor,
- allowedInsecureSchemes = _allowedInsecureSchemes,
- allowLocalhostRequest = _allowLocalhostRequest,
- allowInvalidServerCertificates = _allowInvalidServerCertificates,
- cookieStorage = _cookieStorage,
- callbackQueue = _callbackQueue,
- initialBeginFetchDate = _initialBeginFetchDate,
- testBlock = _testBlock,
- testBlockAccumulateDataChunkCount = _testBlockAccumulateDataChunkCount,
- comment = _comment,
- log = _log;
-
- #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
-
- - (GTM_NULLABLE NSURLRequest *)request {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- return [_request copy];
- } // @synchronized(self)
- }
-
- - (void)setRequest:(GTM_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)
- }
-
- - (GTM_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:(GTM_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:(GTM_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:(GTM_NULLABLE NSString *)value forHTTPHeaderField:(NSString *)field {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- [_request setValue:value forHTTPHeaderField:field];
- } // @synchronized(self)
- }
-
- - (void)setResponse:(GTM_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)
- }
-
- - (GTM_NULLABLE NSURL *)bodyFileURL {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- return _bodyFileURL;
- } // @synchronized(self)
- }
-
- - (void)setBodyFileURL:(GTM_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)
- }
-
- - (GTM_NULLABLE GTMSessionFetcherBodyStreamProvider)bodyStreamProvider {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- return _bodyStreamProvider;
- } // @synchronized(self)
- }
-
- - (void)setBodyStreamProvider:(GTM_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)
- }
-
- - (GTM_NULLABLE id<GTMFetcherAuthorizationProtocol>)authorizer {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- return _authorizer;
- } // @synchronized(self)
- }
-
- - (void)setAuthorizer:(GTM_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)
- }
-
- - (GTM_NULLABLE NSData *)downloadedData {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- return _downloadedData;
- } // @synchronized(self)
- }
-
- - (void)setDownloadedData:(GTM_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)
- }
-
- - (dispatch_queue_t GTM_NONNULL_TYPE)callbackQueue {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- return _callbackQueue;
- } // @synchronized(self)
- }
-
- - (void)setCallbackQueue:(dispatch_queue_t GTM_NULLABLE_TYPE)queue {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- _callbackQueue = queue ?: dispatch_get_main_queue();
- } // @synchronized(self)
- }
-
- - (GTM_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:(GTM_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)
- }
-
- - (GTM_NULLABLE NSURLSession *)sessionNeedingInvalidation {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- return _sessionNeedingInvalidation;
- } // @synchronized(self)
- }
-
- - (void)setSessionNeedingInvalidation:(GTM_NULLABLE NSURLSession *)session {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- _sessionNeedingInvalidation = session;
- } // @synchronized(self)
- }
-
- - (NSOperationQueue * GTM_NONNULL_TYPE)sessionDelegateQueue {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- return _delegateQueue;
- } // @synchronized(self)
- }
-
- - (void)setSessionDelegateQueue:(NSOperationQueue * GTM_NULLABLE_TYPE)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)
- }
-
- - (GTM_NULLABLE id)userData {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- return _userData;
- } // @synchronized(self)
- }
-
- - (void)setUserData:(GTM_NULLABLE id)theObj {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- _userData = theObj;
- } // @synchronized(self)
- }
-
- - (GTM_NULLABLE NSURL *)destinationFileURL {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- return _destinationFileURL;
- } // @synchronized(self)
- }
-
- - (void)setDestinationFileURL:(GTM_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.
- NSString *oldFilename = [_destinationFileURL lastPathComponent];
- NSString *newFilename = [destinationFileURL lastPathComponent];
- #pragma unused(oldFilename)
- #pragma unused(newFilename)
- 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:(GTM_NULLABLE NSDictionary *)dict {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- _properties = [dict mutableCopy];
- } // @synchronized(self)
- }
-
- - (GTM_NULLABLE NSDictionary *)properties {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- return _properties;
- } // @synchronized(self)
- }
-
- - (void)setProperty:(GTM_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)
- }
-
- - (GTM_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 GTMSessionFetcher (BackwardsCompatibilityOnly)
-
- - (void)setCookieStorageMethod:(NSInteger)method {
- // For backwards compatibility with the old fetcher, we'll support the old constants.
- //
- // Clients using the GTMSessionFetcher class should set the cookie storage explicitly
- // themselves.
- NSHTTPCookieStorage *storage = nil;
- switch(method) {
- case 0: // kGTMHTTPFetcherCookieStorageMethodStatic
- // nil storage will use [[self class] staticCookieStorage] when the fetch begins.
- break;
- case 1: // kGTMHTTPFetcherCookieStorageMethodFetchHistory
- // Do nothing; use whatever was set by the fetcher service.
- return;
- case 2: // kGTMHTTPFetcherCookieStorageMethodSystemDefault
- storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
- break;
- case 3: // kGTMHTTPFetcherCookieStorageMethodNone
- // Create temporary storage for this fetcher only.
- storage = [[GTMSessionCookieStorage alloc] init];
- break;
- default:
- GTMSESSION_ASSERT_DEBUG(0, @"Invalid cookie storage method: %d", (int)method);
- }
- self.cookieStorage = storage;
- }
-
- @end
-
- @implementation GTMSessionCookieStorage {
- NSMutableArray *_cookies;
- NSHTTPCookieAcceptPolicy _policy;
- }
-
- - (id)init {
- self = [super init];
- if (self != nil) {
- _cookies = [[NSMutableArray alloc] init];
- }
- return self;
- }
-
- - (GTM_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:(GTM_NULLABLE NSArray *)newCookies {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
-
- [self removeExpiredCookies];
-
- for (NSHTTPCookie *newCookie in newCookies) {
- [self internalSetCookie:newCookie];
- }
- } // @synchronized(self)
- }
-
- - (void)setCookies:(NSArray *)cookies forURL:(GTM_NULLABLE NSURL *)URL mainDocumentURL:(GTM_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.
- - (GTM_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 (^)(GTM_NSArrayOf(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
- - (GTM_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 GTM_NULLABLE_TYPE obj, SEL GTM_NULLABLE_TYPE 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, ^{
- // The Xcode 8 SDKs finally cleaned up this mess by providing TARGET_OS_OSX
- // and TARGET_OS_IOS, but to build with older SDKs, those don't exist and
- // instead one has to rely on TARGET_OS_MAC (which is true for iOS, watchOS,
- // and tvOS) and TARGET_OS_IPHONE (which is true for iOS, watchOS, tvOS). So
- // one has to order these carefully so you pick off the specific things
- // first.
- // If the code can ever assume Xcode 8 or higher (even when building for
- // older OSes), then
- // TARGET_OS_MAC -> TARGET_OS_OSX
- // TARGET_OS_IPHONE -> TARGET_OS_IOS
- // TARGET_IPHONE_SIMULATOR -> TARGET_OS_SIMULATOR
- #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_IPHONE
- // 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_IPHONE_SIMULATOR || 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_MAC
- // Mac build
- NSProcessInfo *procInfo = [NSProcessInfo processInfo];
- #if !defined(MAC_OS_X_VERSION_10_10)
- BOOL hasOperatingSystemVersion = NO;
- #elif MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10
- BOOL hasOperatingSystemVersion =
- [procInfo respondsToSelector:@selector(operatingSystemVersion)];
- #else
- BOOL hasOperatingSystemVersion = YES;
- #endif
- NSString *versString;
- if (hasOperatingSystemVersion) {
- #if defined(MAC_OS_X_VERSION_10_10)
- // A reference to NSOperatingSystemVersion requires the 10.10 SDK.
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wunguarded-availability"
- // Disable unguarded availability warning as we can't use the @availability macro until we require
- // all clients to build with Xcode 9 or above.
- NSOperatingSystemVersion version = procInfo.operatingSystemVersion;
- #pragma clang diagnostic pop
- versString = [NSString stringWithFormat:@"%ld.%ld.%ld",
- (long)version.majorVersion, (long)version.minorVersion,
- (long)version.patchVersion];
- #else
- #pragma unused(procInfo)
- #endif
- } else {
- // With Gestalt inexplicably deprecated in 10.8, we're reduced to reading
- // the system plist file.
- NSString *const kPath = @"/System/Library/CoreServices/SystemVersion.plist";
- NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:kPath];
- versString = [plist objectForKey:@"ProductVersion"];
- if (versString.length == 0) {
- versString = @"10.?.?";
- }
- }
-
- 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 * GTM_NULLABLE_TYPE bundle) {
- NSString *result = [NSString stringWithFormat:@"%@ %@",
- GTMFetcherApplicationIdentifier(bundle),
- GTMFetcherSystemVersionString()];
- return result;
- }
-
- NSString *GTMFetcherApplicationIdentifier(NSBundle * GTM_NULLABLE_TYPE 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:@"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];
- }
- }
-
- + (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)
- GTM_ASSUME_NONNULL_END
|