diff --git a/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm b/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm index 819e14a39fd71b..a0c4b69be2eac0 100644 --- a/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm +++ b/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm @@ -15,6 +15,8 @@ #import #import +#import + static NSString *const kDebuggerMsgDisable = @"{ \"id\":1,\"method\":\"Debugger.disable\" }"; static NSString *getServerHost(NSURL *bundleURL) @@ -40,16 +42,65 @@ return [NSString stringWithFormat:@"%@:%@", host, port]; } +static NSString *getSHA256(NSString *string) +{ + const char *str = string.UTF8String; + unsigned char result[CC_SHA256_DIGEST_LENGTH]; + CC_SHA256(str, (CC_LONG)strlen(str), result); + + return [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + result[0], + result[1], + result[2], + result[3], + result[4], + result[5], + result[6], + result[7], + result[8], + result[9], + result[10], + result[11], + result[12], + result[13], + result[14], + result[15], + result[16], + result[17], + result[18], + result[19]]; +} + +// Returns an opaque ID which is stable for the current combination of device and app, stable across installs, +// and unique across devices. +static NSString *getInspectorDeviceId() +{ + // A bundle ID uniquely identifies a single app throughout the system. [Source: Apple docs] + NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier]; + + // An alphanumeric string that uniquely identifies a device to the app's vendor. [Source: Apple docs] + NSString *identifierForVendor = [[UIDevice currentDevice] identifierForVendor].UUIDString; + + NSString *rawDeviceId = [NSString stringWithFormat:@"apple-%@-%@", identifierForVendor, bundleId]; + + return getSHA256(rawDeviceId); +} + static NSURL *getInspectorDeviceUrl(NSURL *bundleURL) { NSString *escapedDeviceName = [[[UIDevice currentDevice] name] stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]; NSString *escapedAppName = [[[NSBundle mainBundle] bundleIdentifier] stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]; - return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/inspector/device?name=%@&app=%@", + + NSString *escapedInspectorDeviceId = [getInspectorDeviceId() + stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]; + + return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/inspector/device?name=%@&app=%@&device=%@", getServerHost(bundleURL), escapedDeviceName, - escapedAppName]]; + escapedAppName, + escapedInspectorDeviceId]]; } @implementation RCTInspectorDevServerHelper @@ -70,8 +121,13 @@ + (void)openDebugger:(NSURL *)bundleURL withErrorMessage:(NSString *)errorMessag NSString *appId = [[[NSBundle mainBundle] bundleIdentifier] stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]; - NSURL *url = [NSURL - URLWithString:[NSString stringWithFormat:@"http://%@/open-debugger?appId=%@", getServerHost(bundleURL), appId]]; + NSString *escapedInspectorDeviceId = [getInspectorDeviceId() + stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]; + + NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/open-debugger?appId=%@&device=%@", + getServerHost(bundleURL), + appId, + escapedInspectorDeviceId]]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; [request setHTTPMethod:@"POST"]; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java index d5c427fc67a4b4..4cef3eed690491 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java @@ -9,6 +9,7 @@ import android.net.Uri; import android.os.AsyncTask; +import android.provider.Settings; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.facebook.common.logging.FLog; @@ -30,6 +31,9 @@ import com.facebook.react.util.RNLog; import java.io.File; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -244,13 +248,73 @@ public String getWebsocketProxyURL() { mPackagerConnectionSettings.getDebugServerHost()); } + private static String getSHA256(String string) { + MessageDigest digest = null; + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError("Could not get standard SHA-256 algorithm", e); + } + digest.reset(); + byte[] result; + try { + result = digest.digest(string.getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new AssertionError("This environment doesn't support UTF-8 encoding", e); + } + return String.format( + "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + result[0], + result[1], + result[2], + result[3], + result[4], + result[5], + result[6], + result[7], + result[8], + result[9], + result[10], + result[11], + result[12], + result[13], + result[14], + result[15], + result[16], + result[17], + result[18], + result[19]); + } + + // Returns an opaque ID which is stable for the current combination of device and app, stable + // across installs, and unique across devices. + private String getInspectorDeviceId() { + // Every Android app has a unique application ID that looks like a Java or Kotlin package name, + // such as com.example.myapp. This ID uniquely identifies your app on the device and in the + // Google Play Store. + // [Source: Android docs] + String packageName = mPackageName; + + // A 64-bit number expressed as a hexadecimal string, which is either: + // * unique to each combination of app-signing key, user, and device (API level >= 26), or + // * randomly generated when the user first sets up the device and should remain constant for + // the lifetime of the user's device (API level < 26). + // [Source: Android docs] + String androidId = Settings.Secure.ANDROID_ID; + + String rawDeviceId = String.format(Locale.US, "android-%s-%s", packageName, androidId); + + return getSHA256(rawDeviceId); + } + private String getInspectorDeviceUrl() { return String.format( Locale.US, - "http://%s/inspector/device?name=%s&app=%s", + "http://%s/inspector/device?name=%s&app=%s&device=%s", mPackagerConnectionSettings.getInspectorServerHost(), - AndroidInfoHelpers.getFriendlyDeviceName(), - mPackageName); + Uri.encode(AndroidInfoHelpers.getFriendlyDeviceName()), + Uri.encode(mPackageName), + Uri.encode(getInspectorDeviceId())); } public void downloadBundleFromURL( @@ -425,9 +489,10 @@ public void openDebugger(final ReactContext context, final String errorMessage) String requestUrl = String.format( Locale.US, - "http://%s/open-debugger?appId=%s", + "http://%s/open-debugger?appId=%s&device=%s", mPackagerConnectionSettings.getInspectorServerHost(), - Uri.encode(mPackageName)); + Uri.encode(mPackageName), + Uri.encode(getInspectorDeviceId())); Request request = new Request.Builder().url(requestUrl).method("POST", RequestBody.create(null, "")).build();