1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579 |
- /* 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);
-
- // 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
|