22// ADJLinkResolution.m
33// Adjust
44//
5- // Created by Pedro S. on 26.04.21 .
6- // Copyright © 2021 adjust GmbH. All rights reserved.
5+ // Created by Pedro Silva (@nonelse) on 26th April 2021 .
6+ // Copyright © 2021-Present Adjust GmbH. All rights reserved.
77//
88
99#import " ADJLinkResolution.h"
10+ #import " ADJUtil.h"
1011
1112static NSUInteger kMaxRecursions = 10 ;
1213
@@ -22,19 +23,21 @@ + (nonnull ADJLinkResolutionDelegate *)sharedInstance;
2223
2324+ (nullable NSURL *)convertUrlToHttps : (nullable NSURL *)url ;
2425
26+ + (NSURLRequest *)replaceUrlWithRequest : (NSURLRequest *)request
27+ urlToReplace : (nonnull NSURL *)urlToReplace ;
28+
2529@end
2630
2731@implementation ADJLinkResolutionDelegate
2832
2933- (nonnull instancetype )init {
3034 self = [super init ];
31-
3235 return self;
3336}
3437
3538+ (nonnull ADJLinkResolutionDelegate *)sharedInstance {
3639 static ADJLinkResolutionDelegate *sharedInstance = nil ;
37- static dispatch_once_t onceToken; // onceToken = 0
40+ static dispatch_once_t onceToken;
3841 dispatch_once (&onceToken, ^{
3942 sharedInstance = [[self alloc ] init ];
4043 });
@@ -44,8 +47,7 @@ + (nonnull ADJLinkResolutionDelegate *)sharedInstance {
4447- (void )URLSession : (NSURLSession *)session task : (NSURLSessionTask *)task
4548 willPerformHTTPRedirection : (NSHTTPURLResponse *)response
4649 newRequest : (NSURLRequest *)request
47- completionHandler : (void (^)(NSURLRequest * _Nullable))completionHandler
48- {
50+ completionHandler : (void (^)(NSURLRequest * _Nullable))completionHandler {
4951 // if we're already at a terminal host (adjust.com / adj.st / go.link),
5052 // stop auto-following to preserve the terminal URL (avoid jumping to App Store links)
5153 if ([ADJLinkResolution isTerminalUrlWithHost: response.URL.host]) {
@@ -55,7 +57,7 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
5557
5658 NSURL *_Nullable convertedUrl = [ADJLinkResolutionDelegate convertUrlToHttps: request.URL];
5759
58- if (request.URL != nil && convertedUrl != nil && ! [request.URL isEqual: convertedUrl]) {
60+ if (request.URL != nil && convertedUrl != nil && ![request.URL isEqual: convertedUrl]) {
5961 completionHandler ([ADJLinkResolutionDelegate replaceUrlWithRequest: request
6062 urlToReplace: convertedUrl]);
6163 } else {
@@ -67,24 +69,18 @@ + (nullable NSURL *)convertUrlToHttps:(nullable NSURL *)url {
6769 if (url == nil ) {
6870 return nil ;
6971 }
70-
71- if (! [url.absoluteString hasPrefix: @" http:" ]) {
72+ if (![url.absoluteString hasPrefix: @" http:" ]) {
7273 return url;
7374 }
7475
7576 NSString *_Nonnull urlStringWithoutPrefix = [url.absoluteString substringFromIndex: 5 ];
76-
77- return [NSURL URLWithString:
78- [NSString stringWithFormat: @" https:%@ " , urlStringWithoutPrefix]];
77+ return [NSURL URLWithString: [NSString stringWithFormat: @" https:%@ " , urlStringWithoutPrefix]];
7978}
8079
8180+ (NSURLRequest *)replaceUrlWithRequest : (NSURLRequest *)request
82- urlToReplace : (nonnull NSURL *)urlToReplace
83- {
81+ urlToReplace : (nonnull NSURL *)urlToReplace {
8482 NSMutableURLRequest *mutableRequest = [request mutableCopy ];
85-
8683 [mutableRequest setURL: urlToReplace];
87-
8884 return [mutableRequest copy ];
8985}
9086
@@ -94,93 +90,99 @@ @implementation ADJLinkResolution
9490
9591+ (void )resolveLinkWithUrl : (nonnull NSURL *)url
9692 resolveUrlSuffixArray : (nullable NSArray <NSString *> *)resolveUrlSuffixArray
97- callback : (nonnull void (^)(NSURL *_Nullable resolvedLink))callback
98- {
93+ callback : (nonnull void (^)(NSURL *_Nullable resolvedLink))callback {
9994 if (callback == nil ) {
10095 return ;
10196 }
102-
103- if (url == nil ) {
104- callback (nil );
97+ if (url == nil || url.host == nil || url.host .length == 0 ) {
98+ [ADJUtil launchInMainThread: ^{
99+ callback (url);
100+ }];
105101 return ;
106102 }
107103
108- if (! [ADJLinkResolution urlMatchesSuffixWithHost: url.host
109- suffixArray: resolveUrlSuffixArray])
110- {
111- callback (url);
104+ // if suffix array is provided and URL doesn't match, return URL unchanged
105+ if (![ADJLinkResolution urlMatchesSuffixWithHost: url.host
106+ suffixArray: resolveUrlSuffixArray]) {
107+ [ADJUtil launchInMainThread: ^{
108+ callback (url);
109+ }];
112110 return ;
113111 }
114112
115- ADJLinkResolutionDelegate *_Nonnull linkResolutionDelegate =
116- [ADJLinkResolutionDelegate sharedInstance ];
113+ ADJLinkResolutionDelegate *_Nonnull linkResolutionDelegate = [ADJLinkResolutionDelegate sharedInstance ];
117114
118- NSURLSession *_Nonnull session =
119- [NSURLSession
120- sessionWithConfiguration: NSURLSessionConfiguration .defaultSessionConfiguration
121- delegate: linkResolutionDelegate
122- delegateQueue: nil ];
115+ // reuse shared session for better performance
116+ static NSURLSession *sharedSession = nil ;
117+ static dispatch_once_t sessionOnceToken;
118+ dispatch_once (&sessionOnceToken, ^{
119+ sharedSession =
120+ [NSURLSession sessionWithConfiguration: NSURLSessionConfiguration .defaultSessionConfiguration
121+ delegate: linkResolutionDelegate
122+ delegateQueue: nil ];
123+ });
123124
124125 NSURL *_Nullable httpsUrl = [ADJLinkResolutionDelegate convertUrlToHttps: url];
125-
126- NSURLSessionDataTask *task =
127- [session
128- dataTaskWithURL: httpsUrl
129- completionHandler:
130- ^(NSData * _Nullable data,
131- NSURLResponse * _Nullable response,
132- NSError * _Nullable error)
133- {
134- // bootstrap the recursion of resolving the link
135- [ADJLinkResolution
136- resolveLinkWithResponseUrl: response != nil ? response.URL : nil
137- previousUrl: httpsUrl
138- recursionNumber: 0
139- session: session
140- callback: callback];
141- }];
126+ NSURLSessionDataTask *task = [sharedSession dataTaskWithURL: httpsUrl
127+ completionHandler: ^(NSData * _Nullable data,
128+ NSURLResponse * _Nullable response,
129+ NSError * _Nullable error) {
130+ // bootstrap the recursion of resolving the link
131+ [ADJLinkResolution resolveLinkWithResponseUrl: response != nil ? response.URL : nil
132+ previousUrl: httpsUrl
133+ recursionNumber: 0
134+ session: sharedSession
135+ callback: callback];
136+ }];
142137 [task resume ];
143138}
144139
145140+ (void )resolveLinkWithResponseUrl : (nullable NSURL *)responseUrl
146141 previousUrl : (nullable NSURL *)previousUrl
147142 recursionNumber : (NSUInteger )recursionNumber
148143 session : (nonnull NSURLSession *)session
149- callback : (nonnull void (^)(NSURL *_Nullable resolvedLink))callback
150- {
144+ callback : (nonnull void (^)(NSURL *_Nullable resolvedLink))callback {
151145 // return (possible nil) previous url when the current one does not exist
152146 if (responseUrl == nil ) {
153- callback (previousUrl);
147+ [ADJUtil launchInMainThread: ^{
148+ callback (previousUrl);
149+ }];
154150 return ;
155151 }
156-
157- // return found url with expected host
152+ // stop recursion when URL stops changing (prevents infinite loops)
153+ if (previousUrl != nil && [responseUrl isEqual: previousUrl]) {
154+ [ADJUtil launchInMainThread: ^{
155+ callback (responseUrl);
156+ }];
157+ return ;
158+ }
159+ // return found url with expected host (Adjust terminal domains)
160+ // these are domains where we stop to avoid redirecting to App Store
158161 if ([ADJLinkResolution isTerminalUrlWithHost: responseUrl.host]) {
159- callback (responseUrl);
162+ [ADJUtil launchInMainThread: ^{
163+ callback (responseUrl);
164+ }];
160165 return ;
161166 }
162-
163- // return previous (non-nil) url when it reached the max number of recursive tries
167+ // return current url when it reached the max number of recursive tries
164168 if (recursionNumber >= kMaxRecursions ) {
165- callback (responseUrl);
169+ [ADJUtil launchInMainThread: ^{
170+ callback (responseUrl);
171+ }];
166172 return ;
167173 }
168174
169175 // when found a non expected url host, use it to recursively resolve the link
170- NSURLSessionDataTask *task =
171- [session
172- dataTaskWithURL: responseUrl
173- completionHandler:
174- ^(NSData * _Nullable data,
175- NSURLResponse * _Nullable response,
176- NSError * _Nullable error)
177- {
178- [ADJLinkResolution resolveLinkWithResponseUrl: response != nil ? response.URL : nil
179- previousUrl: responseUrl
180- recursionNumber: (recursionNumber + 1 )
181- session: session
182- callback: callback];
183- }];
176+ NSURLSessionDataTask *task = [session dataTaskWithURL: responseUrl
177+ completionHandler: ^(NSData * _Nullable data,
178+ NSURLResponse * _Nullable response,
179+ NSError * _Nullable error) {
180+ [ADJLinkResolution resolveLinkWithResponseUrl: response != nil ? response.URL : nil
181+ previousUrl: responseUrl
182+ recursionNumber: (recursionNumber + 1 )
183+ session: session
184+ callback: callback];
185+ }];
184186 [task resume ];
185187}
186188
@@ -189,16 +191,22 @@ + (BOOL)isTerminalUrlWithHost:(nullable NSString *)urlHost {
189191 return NO ;
190192 }
191193
192- NSArray <NSString *> *_Nonnull terminalUrlHostSuffixArray =
193- @[@" adjust.com" , @" adj.st" , @" go.link" , @" adjust.cn" , @" adjust.net.in" , @" adjust.world" , @" adjust.io" ];
194+ // check hardcoded Adjust terminal domains
195+ // these are domains where we stop recursion to avoid redirecting to App Store
196+ NSArray <NSString *> *_Nonnull terminalUrlHostSuffixArray = @[@" adjust.com" ,
197+ @" adj.st" ,
198+ @" go.link" ,
199+ @" adjust.cn" ,
200+ @" adjust.net.in" ,
201+ @" adjust.world" ,
202+ @" adjust.io" ];
194203
195204 return [ADJLinkResolution urlMatchesSuffixWithHost: urlHost
196205 suffixArray: terminalUrlHostSuffixArray];
197206}
198207
199208+ (BOOL )urlMatchesSuffixWithHost : (nullable NSString *)urlHost
200- suffixArray : (nullable NSArray <NSString *> *)suffixArray
201- {
209+ suffixArray : (nullable NSArray <NSString *> *)suffixArray {
202210 if (urlHost == nil ) {
203211 return NO ;
204212 }
0 commit comments