Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handleUniversalLink always returns false when app launch after installation #6978

Closed
svedm opened this issue Nov 17, 2020 · 23 comments
Closed

Comments

@svedm
Copy link

svedm commented Nov 17, 2020

Step 1: Describe your environment

  • Xcode version: 12.1
  • Firebase SDK version: 7.0-spm-beta
  • Installation method: Swift Package Manager
  • Firebase Component: DynamicLinks

Step 2: Describe the problem

handleUniversalLink always returns false when app launch after installation

Steps to reproduce:

  1. Create dynamic link in firebase console. Link should open appstore if application not installed
  2. Remove application from device
  3. Open dynamic link in browser, check Save my place in the app. A link will be copied to continue to this page.
  4. Install app from Xcode in debug mode
  5. func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) called with url from firebase

Expected behavior:

  • DynamicLinks.dynamicLinks().handleUniversalLink(url) returns true

Real behavior:

  • DynamicLinks.dynamicLinks().handleUniversalLink(url) always returns false

When Im opening installed app from this link all works as expected.

Diagnostic output:

---- Firebase Dynamic Links diagnostic output start ----
Firebase Dynamic Links framework version 7.0.0
System information: OS iOS, OS version 13.4, model iPhone
Current date 2020-11-17 12:06:01 +0000
Device locale ru-CA (raw ru_CA), timezone America/Edmonton
	Specified custom URL scheme is my.bundle.id and Info.plist contains such scheme in CFBundleURLTypes key.
	AppID Prefix: Z8M*****M8, Team ID:  Z8M*****M8, AppId Prefix equal to Team ID: YES
performDiagnostic completed successfully! No errors found.
---- Firebase Dynamic Links diagnostic output end ----
@google-oss-bot
Copy link

I found a few problems with this issue:

  • I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.
  • This issue does not seem to follow the issue template. Make sure you provide all the required information.

@christibbs
Copy link
Contributor

Internal bug number 174061690.

@sandeep576
Copy link

same here, always getting false DynamicLinks.dynamicLinks().handleUniversalLink(url)

@nuborian
Copy link

Anything new in this? :(

@FitzAfful
Copy link

Any update with this?

@LeonXiaoZ
Copy link

LeonXiaoZ commented Apr 7, 2021

For the open after first install case, we should call 'DynamicLinks.dynamicLinks().dynamicLink(fromCustomSchemeURL: url)', right?
https://firebase.google.com/docs/dynamic-links/ios/receive
@available(iOS 9.0, *)
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool {
return application(app, open: url,
sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String,
annotation: "")
}

func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
if let dynamicLink = DynamicLinks.dynamicLinks().dynamicLink(fromCustomSchemeURL: url) {
// Handle the deep link. For example, show the deep-linked content or
// apply a promotional offer to the user's account.
// ...
return true
}
return false
}

@brunsman
Copy link

brunsman commented Nov 8, 2021

I am not sure this is the correct place to put it, but I found the following piece of code in the FDLUtilities.m in the iOS FirebaseDynamicLinks library (version 8.9.0 as we speak):

`BOOL FIRDLIsURLForAllowedCustomDomain(NSURL *_Nullable URL) {
BOOL customDomainMatchFound = false;
for (NSURL *allowedCustomDomain in FIRDLCustomDomains) {
// At least one custom domain host name should match at a minimum.
if ([allowedCustomDomain.host isEqualToString:URL.host]) {
NSString *urlStr = URL.absoluteString;
NSString *domainURIPrefixStr = allowedCustomDomain.absoluteString;

  // Next, do a string compare to check if entire domainURIPrefix matches as well.
  if (([urlStr rangeOfString:domainURIPrefixStr
                     options:NSCaseInsensitiveSearch | NSAnchoredSearch]
           .location) == 0) {
    NSString *urlWithoutDomainURIPrefix = [urlStr substringFromIndex:domainURIPrefixStr.length];

    // For a valid custom domain DL Suffix:
    // 1. At least one path exists OR
    // 2. Should have a link query param with an http/https link
    BOOL matchesRegularExpression =
        ([urlWithoutDomainURIPrefix
             rangeOfString:@"((\\/[A-Za-z0-9]+)|((\\?|\\/\\?)link=https?.*))"
                   options:NSRegularExpressionSearch]
             .location != NSNotFound);

    if (matchesRegularExpression) {
      customDomainMatchFound = true;
      break;
    }
  }
}

}
return customDomainMatchFound;
}`

This piece of code is called when using the DynamicLinks.dynamicLinks().handleUniversalLink(url).

The thing I am concerned about is this line:
BOOL matchesRegularExpression = ([urlWithoutDomainURIPrefix rangeOfString:@"((\\/[A-Za-z0-9]+)|((\\?|\\/\\?)link=https?.*))" options:NSRegularExpressionSearch] .location != NSNotFound);

This is plain wrong! We are using custom domains in our dynamic links for iOS. However, a long custom domain dynamic link will only be recognised as such when the link parameter is the FIRST paramater in the URL. When it's not, it's not recognised as a dynamic link and thus handleUniversalLink returns false.

In our app, we are also using the DynamicLinkComponents(link:domainURIPrefix:) to generate a long dynamic link. When we generate a long dynamic link using the DynamicLinkComponents, the link argument is NOT the first in the query parameters. So components from the iOS FirebaseDynamicLinks library actually don't play well together.

The fix here seems easy enough.... Update the FIRDLIsURLForAllowedCustomDomain to e.g. use URLComponents to search for the existence of the link query parameter instead of a regex...

@paulb777
Copy link
Member

paulb777 commented Nov 9, 2021

@eldhosembabu PTAL ^

@ctsrc
Copy link

ctsrc commented Mar 22, 2022

This is plain wrong! We are using custom domains in our dynamic links for iOS. However, a long custom domain dynamic link will only be recognised as such when the link parameter is the FIRST paramater in the URL. When it's not, it's not recognised as a dynamic link and thus handleUniversalLink returns false.

Just came across this too and saw the same thing. We have a dynamic link that is not being recognized as such and step debugging I too came across this regex in the Firebase pod which only matches when link query param is first argument, because the regex matches ?link=https?.* so only matches when that param is first in the list. And in our case that param appears later in the link query params.

@ctsrc
Copy link

ctsrc commented Mar 22, 2022

@paulb777 is anyone assigned to this issue? Seems like the potential fix pointed out by @brunsman should do it.

@eldhosembabu
Copy link
Contributor

Hi @ctsrc , @brunsman I've tried adding couple of test cases where the link query param is not the first argument. See here :

https://github.com/firebase/firebase-ios-sdk/pull/9511/files#diff-cff1b87a70b0568be9e6c3e6d5fd4b74c024175b50644246df7ee9ae17884e4eR1624

But in those cases, I could see the test is passing in local. Do you mind sharing some sample urls which you were having issues with so that I can test locally and check where the issue is?

@ctsrc
Copy link

ctsrc commented Mar 23, 2022

Hi @eldhosembabu

Do you mind sharing some sample urls which you were having issues with so that I can test locally and check where the issue is?

I created a minimal repro of the problem. Can put the Xcode project for the repro on GitHub as well if you like, but here is the essence:

Minimal Info.plist file for project listing the custom domain used for Firebase dynamic links in the repro:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>FirebaseDynamicLinksCustomDomains</key>
	<array>
		<string>https://repro-1648070683.ifee.fr</string>
	</array>
</dict>
</plist>

We use Firebase DynamicLinkComponents link builder, shown as a minimal example here:

func createExampleDynamicLink() -> URL? {
    guard let link = URL(string: "https://ifee.fr/musique-et-technologie/") else { return nil }
    
    let dynamicLinksDomainURIPrefix = "https://repro-1648070683.ifee.fr"
    let linkBuilder = DynamicLinkComponents(link: link, domainURIPrefix: dynamicLinksDomainURIPrefix)
    linkBuilder?.iOSParameters = DynamicLinkIOSParameters(bundleID: Bundle.main.bundleIdentifier ?? "")
    linkBuilder?.iOSParameters?.appStoreID = "1648070683"
    linkBuilder?.androidParameters = DynamicLinkAndroidParameters(packageName: "com.example.repro-1648070683")
    
    guard let longDynamicLink = linkBuilder?.url else { return nil }
    
    return longDynamicLink
}

URLs returned by link builder look like: https://repro-1648070683.ifee.fr/?ibi=fr%2Eifee%2Erepro%2D1648070683&isi=1648070683&apn=com%2Eexample%2Erepro%2D1648070683&link=https%3A%2F%2Fifee%2Efr%2Fmusique%2Det%2Dtechnologie%2F

But when we pass such a URL and a completion handler to DynamicLinks.dynamicLinks().handleUniversalLink(...), here with minimal example:

let longDynamicLink1 = createExampleDynamicLink()!
print("longDynamicLink1 is: \(longDynamicLink1)")
DynamicLinks.dynamicLinks().handleUniversalLink(longDynamicLink1) { (link, error) in
    guard error == nil else { return }
    print("The link for longDynamicLink1 is: \(String(describing: link?.url))")
}

Then DynamicLinks.dynamicLinks().handleUniversalLink(...) does not extract the link. With step debugging we found the same reason for this as @brunsman was talking about, with Firebase using a regex that only matches when link is first query component, because it is matching ?link=https?.*.

You can see where this happen with a breakpoint at line 221 in this file from the Firebase Dynamic Links library:

// For a valid custom domain DL Suffix:
// 1. At least one path exists OR
// 2. Should have a link query param with an http/https link
BOOL matchesRegularExpression =
([urlWithoutDomainURIPrefix
rangeOfString:@"((\\/[A-Za-z0-9]+)|((\\?|\\/\\?)link=https?.*))"
options:NSRegularExpressionSearch]
.location != NSNotFound);

Whereas if we take the long dynamic link from above that it was unable to extract link from:

and we manually rearrange it so that the link param comes first:

Then DynamicLinks.dynamicLinks().handleUniversalLink(...) extracts the link as it should, as shown with the following minimal example:

let longDynamicLink2 = URL(string: "https://repro-1648070683.ifee.fr/?link=https%3A%2F%2Fifee%2Efr%2Fmusique%2Det%2Dtechnologie%2F&ibi=fr%2Eifee%2Erepro%2D1648070683&isi=1648070683&apn=com%2Eexample%2Erepro%2D1648070683")!
print("longDynamicLink2 is: \(longDynamicLink2)")
DynamicLinks.dynamicLinks().handleUniversalLink(longDynamicLink2) { (link, error) in
    guard error == nil else { return }
    print("The link for longDynamicLink2 is: \(String(describing: link?.url))")
}

The result being that DynamicLinks.dynamicLinks().handleUniversalLink(...) successfully extracts the link https://ifee.fr/musique-et-technologie/

@brunsman
Copy link

brunsman commented Mar 24, 2022

Hi @ctsrc , @brunsman I've tried adding couple of test cases where the link query param is not the first argument. See here :

https://github.com/firebase/firebase-ios-sdk/pull/9511/files#diff-cff1b87a70b0568be9e6c3e6d5fd4b74c024175b50644246df7ee9ae17884e4eR1624

But in those cases, I could see the test is passing in local. Do you mind sharing some sample urls which you were having issues with so that I can test locally and check where the issue is?

@eldhosembabu : Commenting on why your tests succeed.

@"https://google.com?some=qry&link=https://somedomain",   // Long FDL with link param as second
                                                              // argument.

The link parameter in your example is NOT urlEncoded. In our examples the link parameter IS urlencoded.

        // For a valid custom domain DL Suffix:
        // 1. At least one path exists OR
        // 2. Should have a link query param with an http/https link
        BOOL matchesRegularExpression =
            ([urlWithoutDomainURIPrefix
                 rangeOfString:@"((\\/[A-Za-z0-9]+)|((\\?|\\/\\?)link=https?.*))"
                       options:NSRegularExpressionSearch]
                 .location != NSNotFound);

take a look at the regex:
@"((\\/[A-Za-z0-9]+)|((\\?|\\/\\?)link=https?.*))"
Now take a look at the part that is being considered:
?some=qry&link=https://somedomain
the non-url encoded link actually matches the last part, thus /somedomain, as the 'one path' bit. But this is actually wrong. If we now replace the link with an URLEncoded link:

@"https://google.com?some=qry&link=https%3A%2F%2Fsomedomain",   // Long FDL with link param as second
                                                              // argument.

the test WILL fail.

Perhaps it's an idea to replace all query parameter links in those tests with urlencoded links.

@eldhosembabu
Copy link
Contributor

Thanks @brunsman and @ctsrc for explaining the issue.

The issue was not because of URL encoding, its related with the link param ordering only.
But in my test case with "?some=qry&link=https://somedomain" was passing because the regex was matching with the "/somedomain" part and not the "link" parameter as we were doing regex search.

I've modified the logic here to handle these cases : #9516

Would be great if you can clone this PR and check whether its working for you and let me know so that I can push in the changes for next release.

@ctsrc
Copy link

ctsrc commented Mar 24, 2022

@eldhosembabu I think what @brunsman meant is that when the link is URL encoded as in his case and in my case, the regex does not accidentally match the slashes in &link=https:// since we instead have &link=https%3A%2F%2F. So I think you guys are on the same page regarding that :)

@ctsrc
Copy link

ctsrc commented Mar 24, 2022

@eldhosembabu

Would be great if you can clone this PR and check whether its working for you and let me know so that I can push in the changes for next release.

Will do. Going to report back when I've done so.

@brunsman
Copy link

Thanks @brunsman and @ctsrc for explaining the issue.

The issue was not because of URL encoding, its related with the link param ordering only. But in my test case with "?some=qry&link=https://somedomain" was passing because the regex was matching with the "/somedomain" part and not the "link" parameter as we were doing regex search.

I've modified the logic here to handle these cases : #9516

Would be great if you can clone this PR and check whether its working for you and let me know so that I can push in the changes for next release.

Unfortunately have to say that the fix needs some improvements. The following link e.g. will count as valid while it is not:
@"https://google.com?some=qry&somelink=https%3A%2F%2Fsomedomain"
The 'somelink' parameter is counted as the link parameter when it should not. I am not a big fan of using regex here in general.... Suggested improvement:

BOOL FIRDLIsURLForAllowedCustomDomain(NSURL *_Nullable URL) {
    BOOL customDomainMatchFound = false;
    for (NSURL *allowedCustomDomain in FIRDLCustomDomains) {
        // At least one custom domain host name should match at a minimum.
        if ([allowedCustomDomain.host isEqualToString:URL.host]) {
            NSString *urlStr = URL.absoluteString;
            NSString *domainURIPrefixStr = allowedCustomDomain.absoluteString;
            
            // make sure the paths match since we no longer use the regex.
            // We want to avoid that a path with a longer name is considered valid when it is a completely different path
            // "https://a.firebase.com/mypath" <- FIRDLCustomDomain
            // "https://a.firebase.com/mypaths" <- URL. Will be accepted without this check
            BOOL pathPrefixMatch = allowedCustomDomain.pathComponents && URL.pathComponents && allowedCustomDomain.pathComponents.count <= URL.pathComponents.count && [allowedCustomDomain.pathComponents isEqualToArray:[URL.pathComponents subarrayWithRange:NSMakeRange(0, allowedCustomDomain.pathComponents.count)]];
            
            // Next, do a string compare to check if entire domainURIPrefix matches as well.
            // NB: left this one since it also compares e.g. the port if present
            if (([urlStr rangeOfString:domainURIPrefixStr
                               options:NSCaseInsensitiveSearch | NSAnchoredSearch]
                 .location) == 0 && pathPrefixMatch) {
                     NSString *urlWithoutDomainURIPrefix = [urlStr substringFromIndex:domainURIPrefixStr.length];
                     
                     // For a valid custom domain DL Suffix:
                     // 1. At least one path exists OR
                     // 2. Should have a link query param with an http/https link
                     NSURLComponents *components = [[NSURLComponents alloc] initWithString:urlWithoutDomainURIPrefix];
                     BOOL hasPath = components.path && components.path.length > 1;
                     BOOL hasLinkQueryParameter = false;
                     if (components.queryItems && components.queryItems.count > 0) {
                         for (NSURLQueryItem *queryItem in components.queryItems) {
                             hasLinkQueryParameter = hasLinkQueryParameter || [queryItem.name caseInsensitiveCompare:@"link"] == NSOrderedSame;
                         }
                     }
                     BOOL matchesRegularExpression = hasPath || hasLinkQueryParameter;
                     
                     if (matchesRegularExpression) {
                         customDomainMatchFound = true;
                         break;
                     }
                 }
        }
    }
    return customDomainMatchFound;
}

@eldhosembabu
Copy link
Contributor

Thanks for your time to test the change and find the test case which could fail. I've added logic to filter the same in PR : #9516.

Please take a look and let me know if you can see other cases.

@brunsman
Copy link

https://a.firebase.com/mypaths?some=qry&link=https%3A%2F%2Fsomedomain
https://a.firebase.com/mypath/?some=qry#other=b&link=https://somedomain
https://a.firebase.com/mypath/?some=qry#other=b&link=https%3A%2F%2Fsomedomain

More test cases. The top one has a different path than the allowed one (includes an s). The other links contain the link parameter in the fragment.

@eldhosembabu
Copy link
Contributor

hi @brunsman, Thanks again finding these corner cases. It seems like these bugs were from long back.

I've did a refactoring on the custom domain validation logic and have added the test cases that you have provided here : #9516.

Please let me know whether you can find any issues on this refactored code. Thanks

@eldhosembabu
Copy link
Contributor

Removed the usage of regex as there is a chance we might miss if the link param comes under other url which comes as a query param of actual url. So to make the code easy to understand and to avoid missing of those corner cases, i've refactored the code again to use native code to validate instead of regex: #9516.

@brunsman
Copy link

This looks a lot better! Great refactor.

@eldhosembabu
Copy link
Contributor

Closing this issue since the changes are merged and will be out in next release.

@firebase firebase locked and limited conversation to collaborators Apr 29, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests