Skip to content

Commit 85864c6

Browse files
authored
[FDL] Refactoring and fixing url validation (#8672)
[FDL] Refactoring and fixing url validation Refactored the existing code to use Regex to validate FDL URL formats. Added checks to make sure the 'link' query param is having an http/https prefix input. == This is going to be breaking the existing SDK's unintended behavior to support links with non http/https content. The existing SDK was constructing the Dynamic link even though the backend response to resolve the link is not successful. Added support for URL(s) with Query string starting with '?' alone. Previously it was supporting '/?' formats only. This fixes 177469304: Firebase dynamic links with custom domain will only work if the custom domain has a trailing '/' #7087 Added more test cases to be in sync with FDL backend validation. Removed invalid test inputs and corrected few test cases to make the input in proper format.
1 parent b08b358 commit 85864c6

File tree

3 files changed

+166
-89
lines changed

3 files changed

+166
-89
lines changed

FirebaseDynamicLinks/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# v8.8.0
2+
- [fixed] Firebase dynamic links with custom domain will only work if the custom domain has a trailing '/'. (#7087)
3+
14
# v8.7.0
25
- [added] Refactoring and adding helper class. (#8432)
36

FirebaseDynamicLinks/Sources/Utilities/FDLUtilities.m

Lines changed: 67 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -204,64 +204,98 @@ BOOL FIRDLOSVersionSupported(NSString *_Nullable systemVersion, NSString *minSup
204204
BOOL FIRDLIsURLForAllowedCustomDomain(NSURL *_Nullable URL) {
205205
BOOL customDomainMatchFound = false;
206206
for (NSURL *allowedCustomDomain in FIRDLCustomDomains) {
207-
// All custom domain host names should match at a minimum.
207+
// At least one custom domain host name should match at a minimum.
208208
if ([allowedCustomDomain.host isEqualToString:URL.host]) {
209209
NSString *urlStr = URL.absoluteString;
210210
NSString *domainURIPrefixStr = allowedCustomDomain.absoluteString;
211211

212212
// Next, do a string compare to check if entire domainURIPrefix matches as well.
213-
if (([URL.absoluteString rangeOfString:allowedCustomDomain.absoluteString
214-
options:NSCaseInsensitiveSearch | NSAnchoredSearch]
213+
if (([urlStr rangeOfString:domainURIPrefixStr
214+
options:NSCaseInsensitiveSearch | NSAnchoredSearch]
215215
.location) == 0) {
216-
// The (short) URL needs to be longer than the domainURIPrefix, it's first character after
217-
// the domainURIPrefix needs to be '/' and should be followed by at-least one more
218-
// character.
219-
if (urlStr.length > domainURIPrefixStr.length + 1 &&
220-
([urlStr characterAtIndex:domainURIPrefixStr.length] == '/')) {
221-
// Check if there are any more '/' after the first '/'trailing the
222-
// domainURIPrefix. This does not apply to unique match links copied from the clipboard.
223-
// The clipboard links will have '?link=' after the domainURIPrefix.
224-
NSString *urlWithoutDomainURIPrefix =
225-
[urlStr substringFromIndex:domainURIPrefixStr.length + 1];
226-
if ([urlWithoutDomainURIPrefix rangeOfString:@"/"].location == NSNotFound ||
227-
[urlWithoutDomainURIPrefix rangeOfString:@"?link="].location != NSNotFound) {
228-
customDomainMatchFound = true;
229-
break;
230-
}
216+
NSString *urlWithoutDomainURIPrefix = [urlStr substringFromIndex:domainURIPrefixStr.length];
217+
218+
// For a valid custom domain DL Suffix:
219+
// 1. At least one path exists OR
220+
// 2. Should have a link query param with an http/https link
221+
BOOL matchesRegularExpression =
222+
([urlWithoutDomainURIPrefix
223+
rangeOfString:@"((\\/[A-Za-z0-9]+)|((\\?|\\/\\?)link=https?.*))"
224+
options:NSRegularExpressionSearch]
225+
.location != NSNotFound);
226+
227+
if (matchesRegularExpression) {
228+
customDomainMatchFound = true;
229+
break;
231230
}
232231
}
233232
}
234233
}
235234
return customDomainMatchFound;
236235
}
237236

237+
/* We are validating following domains in proper format.
238+
*.page.link
239+
*.app.goo.gl
240+
*.page.link/i/
241+
*.app.goo.gl/i/
242+
*/
243+
BOOL FIRDLIsAValidDLWithFDLDomain(NSURL *_Nullable URL) {
244+
BOOL matchesRegularExpression = false;
245+
NSString *urlStr = URL.absoluteString;
246+
247+
if ([URL.host containsString:@".page.link"] || [URL.host containsString:@".app.goo.gl"]) {
248+
// Matches the *.page.link and *.app.goo.gl domains.
249+
matchesRegularExpression =
250+
([urlStr rangeOfString:@"^https?://[a-zA-Z0-9]+((\\.app\\.goo\\.gl)|(\\.page\\.link))((\\/"
251+
@"?\\?link=https?.*)|(\\/[a-zA-Z0-9]+)((\\/?\\?.*=.*)?$|$))"
252+
options:NSRegularExpressionSearch]
253+
.location != NSNotFound);
254+
255+
if (!matchesRegularExpression) {
256+
// Matches the *.page.link/i/ and *.app.goo.gl/i/ domains.
257+
// Checks whether the URL is of the format :
258+
// http(s)://$DOMAIN(.page.link or .app.goo.gl)/i/$ANYTHING
259+
matchesRegularExpression =
260+
([urlStr rangeOfString:
261+
@"^https?:\\/\\/[a-zA-Z0-9]+((\\.app\\.goo\\.gl)|(\\.page\\.link))\\/i\\/.*$"
262+
options:NSRegularExpressionSearch]
263+
.location != NSNotFound);
264+
}
265+
}
266+
267+
return matchesRegularExpression;
268+
}
269+
270+
/*
271+
DL can be parsed if it :
272+
1. Has http(s)://goo.gl/app* or http(s)://page.link/app* format
273+
2. OR http(s)://$DomainPrefix.page.link or http(s)://$DomainPrefix.app.goo.gl domain with specific
274+
format
275+
3. OR the domain is a listed custom domain
276+
*/
238277
BOOL FIRDLCanParseUniversalLinkURL(NSURL *_Nullable URL) {
239278
// Handle universal links with format |https://goo.gl/app/<appcode>?<parameters>|.
240279
// Also support page.link format.
241280
BOOL isDDLWithAppcodeInPath = ([URL.host isEqual:@"goo.gl"] || [URL.host isEqual:@"page.link"]) &&
242281
[URL.path hasPrefix:@"/app"];
243-
// Handle universal links with format |https://<appcode>.app.goo.gl?<parameters>| and page.link.
244-
BOOL isDDLWithSubdomain =
245-
[URL.host hasSuffix:@".app.goo.gl"] || [URL.host hasSuffix:@".page.link"];
246-
247-
// Handle universal links for custom domains.
248-
BOOL isDDLWithCustomDomain = FIRDLIsURLForAllowedCustomDomain(URL);
249282

250-
return isDDLWithAppcodeInPath || isDDLWithSubdomain || isDDLWithCustomDomain;
283+
return isDDLWithAppcodeInPath || FIRDLIsAValidDLWithFDLDomain(URL) ||
284+
FIRDLIsURLForAllowedCustomDomain(URL);
251285
}
252286

253287
BOOL FIRDLMatchesShortLinkFormat(NSURL *URL) {
254-
// Short Durable Link URLs always have a path.
255-
BOOL hasPath = URL.path.length > 0;
256-
BOOL matchesRegularExpression =
257-
([URL.path rangeOfString:@"/[^/]+" options:NSRegularExpressionSearch].location != NSNotFound);
258-
// Must be able to parse (also checks if the URL conforms to *.app.goo.gl/* or goo.gl/app/*)
259-
BOOL canParse = FIRDLCanParseUniversalLinkURL(URL) | FIRDLIsURLForAllowedCustomDomain(URL);
260-
;
288+
// Short Durable Link URLs always have a path or it should be a custom domain.
289+
BOOL hasPathOrCustomDomain = URL.path.length > 0 || FIRDLIsURLForAllowedCustomDomain(URL);
290+
291+
// Must be able to parse (also checks if the URL conforms to *.app.goo.gl/* or goo.gl/app/* or
292+
// *.page.link or custom domain with valid suffix)
293+
BOOL canParse = FIRDLCanParseUniversalLinkURL(URL);
294+
261295
// Path cannot be prefixed with /link/dismiss
262296
BOOL isDismiss = [[URL.path lowercaseString] hasPrefix:@"/link/dismiss"];
263297

264-
return hasPath && matchesRegularExpression && !isDismiss && canParse;
298+
return hasPathOrCustomDomain && !isDismiss && canParse;
265299
}
266300

267301
NSString *FIRDLMatchTypeStringFromServerString(NSString *_Nullable serverMatchTypeString) {

FirebaseDynamicLinks/Tests/Unit/FIRDynamicLinksTest.m

Lines changed: 96 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ - (void)testUniversalLink_NoDeepLink {
491491
// https://a.firebase.com/mypath
492492
- (void)testDynamicLinkFromUniversalLinkURLWithCustomDomainLink {
493493
self.service = [[FIRDynamicLinks alloc] init];
494-
NSString *durableDeepLinkString = @"https://a.firebase.com/mypath/?link=abcd";
494+
NSString *durableDeepLinkString = @"https://a.firebase.com/mypath/?link=http://abcd";
495495
NSURL *durabledeepLinkURL = [NSURL URLWithString:durableDeepLinkString];
496496

497497
SwizzleDynamicLinkNetworkingWithMock();
@@ -501,14 +501,14 @@ - (void)testDynamicLinkFromUniversalLinkURLWithCustomDomainLink {
501501
XCTAssertNotNil(dynamicLink);
502502
NSString *deepLinkURLString = dynamicLink.url.absoluteString;
503503

504-
XCTAssertEqualObjects(@"abcd", deepLinkURLString,
504+
XCTAssertEqualObjects(@"http://abcd", deepLinkURLString,
505505
@"ddl url parameter and deep link url should be the same");
506506
UnswizzleDynamicLinkNetworking();
507507
}
508508

509509
- (void)testDynamicLinkFromUniversalLinkURLCompletionWithCustomDomainLink {
510510
self.service = [[FIRDynamicLinks alloc] init];
511-
NSString *durableDeepLinkString = @"https://a.firebase.com/mypath/?link=abcd";
511+
NSString *durableDeepLinkString = @"https://a.firebase.com/mypath/?link=http://abcd";
512512
NSURL *durabledeepLinkURL = [NSURL URLWithString:durableDeepLinkString];
513513

514514
SwizzleDynamicLinkNetworkingWithMock();
@@ -523,7 +523,7 @@ - (void)testDynamicLinkFromUniversalLinkURLCompletionWithCustomDomainLink {
523523
NSString *deepLinkURLString = dynamicLink.url.absoluteString;
524524

525525
XCTAssertEqualObjects(
526-
@"abcd", deepLinkURLString,
526+
@"http://abcd", deepLinkURLString,
527527
@"ddl url parameter and deep link url should be the same");
528528
[expectation fulfill];
529529
}];
@@ -1099,9 +1099,23 @@ - (void)testResolveLinkRespectsResponseErrorStatusCode {
10991099
[self waitForExpectationsWithTimeout:kAsyncTestTimout handler:nil];
11001100
}
11011101

1102-
- (void)testMatchesShortLinkFormat {
1103-
NSArray<NSString *> *urlStrings =
1104-
@[ @"https://test.app.goo.gl/xyz", @"https://test.app.goo.gl/xyz?link=" ];
1102+
- (void)testPassMatchesShortLinkFormatForDDLDomains {
1103+
NSArray<NSString *> *urlStrings = @[
1104+
@"https://someapp.app.goo.gl/somepath", @"https://someapp.app.goo.gl/link",
1105+
@"https://someapp.app.goo.gl/somepath?link=https://somedomain",
1106+
@"https://someapp.app.goo.gl/somepath?somekey=somevalue",
1107+
@"https://someapp.app.goo.gl/somepath/?link=https://somedomain",
1108+
@"https://someapp.app.goo.gl/somepath/?somekey=somevalue",
1109+
@"https://someapp.page.link/somepath", @"https://someapp.page.link/link",
1110+
@"https://someapp.page.link/somepath?link=https://somedomain",
1111+
@"https://someapp.page.link/somepath?somekey=somevalue",
1112+
@"https://someapp.page.link/somepath/?link=https://somedomain",
1113+
@"https://someapp.page.link/somepath/?somekey=somevalue", @"http://someapp.page.link/somepath",
1114+
@"http://someapp.page.link/link", @"http://someapp.page.link/somepath?link=https://somedomain",
1115+
@"http://someapp.page.link/somepath?somekey=somevalue",
1116+
@"http://someapp.page.link/somepath/?link=http://somedomain",
1117+
@"http://someapp.page.link/somepath/?somekey=somevalue"
1118+
];
11051119

11061120
for (NSString *urlString in urlStrings) {
11071121
NSURL *url = [NSURL URLWithString:urlString];
@@ -1112,16 +1126,36 @@ - (void)testMatchesShortLinkFormat {
11121126
}
11131127
}
11141128

1115-
// Custom domain entries in plist file:
1116-
// https://google.com
1117-
// https://google.com/one
1118-
// https://a.firebase.com/mypath
1119-
- (void)testFailMatchesShortLinkFormatForCustomDomains {
1129+
- (void)testFailMatchesShortLinkFormat {
11201130
NSArray<NSString *> *urlStrings = @[
1121-
@"https://google.com",
1122-
@"https://google.com?link=",
1123-
@"https://a.firebase.com",
1124-
@"https://a.firebase.com/mypath?link=",
1131+
@"https://someapp.app.goo.gl",
1132+
@"https://someapp.app.goo.gl/",
1133+
@"https://someapp.app.goo.gl?",
1134+
@"https://someapp.app.goo.gl/?",
1135+
@"https://someapp.app.goo.gl?somekey=somevalue",
1136+
@"https://someapp.app.goo.gl/?somekey=somevalue",
1137+
@"https://someapp.app.goo.gl/somepath/somepath2",
1138+
@"https://someapp.app.goo.gl/somepath/somepath2?somekey=somevalue",
1139+
@"https://someapp.app.goo.gl/somepath/somepath2?link=https://somedomain",
1140+
@"https://someapp.page.link",
1141+
@"https://someapp.page.link/",
1142+
@"https://someapp.page.link?",
1143+
@"https://someapp.page.link/?",
1144+
@"https://someapp.page.link?somekey=somevalue",
1145+
@"https://someapp.page.link/?somekey=somevalue",
1146+
@"https://someapp.page.link/somepath/somepath2",
1147+
@"https://someapp.page.link/somepath/somepath2?somekey=somevalue",
1148+
@"https://someapp.page.link/somepath/somepath2?link=https://somedomain",
1149+
@"https://www.google.com/maps/place/@1,1/My+Home/",
1150+
@"https://mydomain.com/t439gfde",
1151+
@"https://goo.gl/309dht4",
1152+
@"https://59eh.goo.gl/309dht4",
1153+
@"https://app.59eh.goo.gl/309dht4",
1154+
@"https://goo.gl/i/309dht4",
1155+
@"https://page.link/i/309dht4",
1156+
@"https://fjo3eh.goo.gl/i/309dht4",
1157+
@"https://app.fjo3eh.goo.gl/i/309dht4",
1158+
@"https://1234.page.link/link/dismiss"
11251159
];
11261160

11271161
for (NSString *urlString in urlStrings) {
@@ -1137,27 +1171,41 @@ - (void)testFailMatchesShortLinkFormatForCustomDomains {
11371171
// https://google.com
11381172
// https://google.com/one
11391173
// https://a.firebase.com/mypath
1140-
- (void)testPassMatchesShortLinkFormatForCustomDomains {
1174+
- (void)testFailMatchesShortLinkFormatForCustomDomains {
11411175
NSArray<NSString *> *urlStrings = @[
1142-
@"https://google.com/xyz", @"https://google.com/xyz/?link=", @"https://google.com/xyz?link=",
1143-
@"https://google.com/one/xyz", @"https://google.com/one/xyz?link=",
1144-
@"https://google.com/one?utm_campaignlink=", @"https://google.com/mylink",
1145-
@"https://google.com/one/mylink", @"https://a.firebase.com/mypath/mylink"
1176+
@"https://google.com",
1177+
@"https://a.firebase.com",
1178+
@"https://google.com/",
1179+
@"https://google.com?",
1180+
@"https://google.com/?",
1181+
@"https://google.com?utm_campgilink=someval",
1182+
@"https://google.com?somekey=someval",
11461183
];
11471184

11481185
for (NSString *urlString in urlStrings) {
11491186
NSURL *url = [NSURL URLWithString:urlString];
11501187
BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url];
11511188

1152-
XCTAssertTrue(matchesShortLinkFormat,
1153-
@"Non-DDL domain URL matched short link format with URL: %@", url);
1189+
XCTAssertFalse(matchesShortLinkFormat,
1190+
@"Non-DDL domain URL matched short link format with URL: %@", url);
11541191
}
11551192
}
11561193

1157-
- (void)testPassMatchesShortLinkFormat {
1194+
// Custom domain entries in plist file:
1195+
// https://google.com
1196+
// https://google.com/one
1197+
// https://a.firebase.com/mypath
1198+
- (void)testPassMatchesShortLinkFormatForCustomDomains {
11581199
NSArray<NSString *> *urlStrings = @[
1159-
@"https://test.app.goo.gl/xyz",
1160-
@"https://test.app.goo.gl/xyz?link=",
1200+
@"https://google.com/xyz", @"https://google.com/xyz/?link=https://somedomain",
1201+
@"https://google.com?link=https://somedomain", @"https://google.com/?link=https://somedomain",
1202+
@"https://google.com/xyz?link=https://somedomain",
1203+
@"https://google.com/xyz/?link=https://somedomain", @"https://google.com/one/xyz",
1204+
@"https://google.com/one/xyz?link=https://somedomain",
1205+
@"https://google.com/one/xyz/?link=https://somedomain",
1206+
@"https://google.com/one?utm_campaignlink=https://somedomain",
1207+
@"https://google.com/one/?utm_campaignlink=https://somedomain", @"https://google.com/mylink",
1208+
@"https://google.com/one/mylink", @"https://a.firebase.com/mypath/mylink"
11611209
];
11621210

11631211
for (NSString *urlString in urlStrings) {
@@ -1169,22 +1217,6 @@ - (void)testPassMatchesShortLinkFormat {
11691217
}
11701218
}
11711219

1172-
- (void)testFailMatchesShortLinkFormat {
1173-
NSArray<NSString *> *urlStrings = @[
1174-
@"https://test.app.goo.gl", @"https://test.app.goo.gl?link=", @"https://test.app.goo.gl/",
1175-
@"https://sample.page.link?link=https://google.com/test&ibi=com.google.sample&ius=79306483",
1176-
@"https://sample.page.link/?link=https://google.com/test&ibi=com.google.sample&ius=79306483"
1177-
];
1178-
1179-
for (NSString *urlString in urlStrings) {
1180-
NSURL *url = [NSURL URLWithString:urlString];
1181-
BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url];
1182-
1183-
XCTAssertFalse(matchesShortLinkFormat,
1184-
@"Non-DDL domain URL matched short link format with URL: %@", url);
1185-
}
1186-
}
1187-
11881220
- (void)testMatchesUnversalLinkWithShortDurableLink {
11891221
NSString *urlString = @"https://sample.page.link/79g49s";
11901222
NSURL *url = [NSURL URLWithString:urlString];
@@ -1520,14 +1552,22 @@ - (void)testValidCustomDomainNames {
15201552
NSArray<NSString *> *urlStrings = @[
15211553
@"https://google.com/mylink", // Short FDL starting with 'https://google.com'
15221554
@"https://google.com/one", // Short FDL starting with 'https://google.com'
1555+
@"https://google.com/one/", // Short FDL starting with 'https://google.com'
1556+
@"https://google.com/one?", // Short FDL starting with 'https://google.com'
15231557
@"https://google.com/one/mylink", // Short FDL starting with 'https://google.com/one'
15241558
@"https://a.firebase.com/mypath/mylink", // Short FDL starting https://a.firebase.com/mypath
1559+
@"https://google.com?link=https://somedomain", @"https://google.com/?link=https://somedomain",
1560+
@"https://google.com/somepath?link=https://somedomain",
1561+
@"https://google.com/somepath/?link=https://somedomain",
1562+
@"https://google.com/somepath/somepath2?link=https://somedomain",
1563+
@"https://google.com/somepath/somepath2/?link=https://somedomain",
1564+
@"https://google.com/somepath?utm_campgilink=someval"
15251565
];
15261566

15271567
NSArray<NSString *> *longFDLURLStrings = @[
1528-
@"https://a.firebase.com/mypath/?link=abcd&test=1", // Long FDL starting with
1529-
// https://a.firebase.com/mypath
1530-
@"https://google.com/?link=abcd", // Long FDL starting with 'https://google.com'
1568+
@"https://a.firebase.com/mypath/?link=https://abcd&test=1", // Long FDL starting with
1569+
// https://a.firebase.com/mypath
1570+
@"https://google.com/?link=http://abcd", // Long FDL starting with 'https://google.com'
15311571
];
15321572
for (NSString *urlString in urlStrings) {
15331573
NSURL *url = [NSURL URLWithString:urlString];
@@ -1550,17 +1590,17 @@ - (void)testInvalidCustomDomainNames {
15501590
// https://a.firebase.com/mypath
15511591

15521592
NSArray<NSString *> *urlStrings = @[
1553-
@"google.com", // Valid domain. No scheme.
1554-
@"https://google.com", // Valid domain. No path after domainURIPrefix.
1555-
@"https://google.com/", // Valid domain. No path after domainURIPrefix.
1556-
@"https://google.com/one/", // Valid domain. No path after domainURIPrefix.
1557-
@"https://google.com/one/two/mylink", // domainURIPrefix not exact match.
1558-
@"https://google.co.in/mylink", // No matching domainURIPrefix.
1559-
@"https://firebase.com/mypath", // No matching domainURIPrefix: Invalid (sub)domain.
1560-
@"https://b.firebase.com/mypath", // No matching domainURIPrefix: Invalid subdomain.
1561-
@"https://a.firebase.com/mypathabc", // No matching domainURIPrefix: Invalid subdomain.
1562-
@"mydomain.com", // https scheme not specified for domainURIPrefix.
1563-
@"http://mydomain", // Domain not in plist. No path after domainURIPrefix.
1593+
@"google.com", // Valid domain. No scheme.
1594+
@"https://google.com", // Valid domain. No path after domainURIPrefix.
1595+
@"https://google.com/", // Valid domain. No path after domainURIPrefix.
1596+
@"https://google.co.in/mylink", // No matching domainURIPrefix.
1597+
@"https://firebase.com/mypath", // No matching domainURIPrefix: Invalid (sub)domain.
1598+
@"https://b.firebase.com/mypath", // No matching domainURIPrefix: Invalid subdomain.
1599+
@"https://a.firebase.com/mypathabc", // No matching domainURIPrefix: Invalid subdomain.
1600+
@"mydomain.com", // https scheme not specified for domainURIPrefix.
1601+
@"http://mydomain", // Domain not in plist. No path after domainURIPrefix.
1602+
@"https://somecustom.com?", @"https://somecustom.com/?",
1603+
@"https://somecustom.com?somekey=someval"
15641604
];
15651605

15661606
for (NSString *urlString in urlStrings) {

0 commit comments

Comments
 (0)