From f02289a60367c61645aa42237a652a696e25dfe3 Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Thu, 25 Feb 2021 18:08:32 -0800 Subject: [PATCH 1/2] Modify URL parsing for React Native compat. --- .../auth-exp/src/core/action_code_url.ts | 29 ++++++----- .../auth-exp/src/core/auth/emulator.ts | 50 ++++++++++++++++--- .../auth-exp/src/core/util/handler.ts | 20 +++----- .../persistence/react_native.ts | 6 ++- packages/util/src/query.ts | 23 +++++++-- 5 files changed, 91 insertions(+), 37 deletions(-) diff --git a/packages-exp/auth-exp/src/core/action_code_url.ts b/packages-exp/auth-exp/src/core/action_code_url.ts index ea353138728..a11647eac96 100644 --- a/packages-exp/auth-exp/src/core/action_code_url.ts +++ b/packages-exp/auth-exp/src/core/action_code_url.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { extractQuerystring, querystringDecode } from '@firebase/util'; import { ActionCodeOperation } from '../model/public_types'; import { AuthErrorCode } from './errors'; import { _assert } from './util/assert'; @@ -63,14 +64,18 @@ function parseMode(mode: string | null): ActionCodeOperation | null { * @param url */ function parseDeepLink(url: string): string { - const uri = new URL(url); - const link = uri.searchParams.get('link'); + const link = querystringDecode(extractQuerystring(url))['link']; + // Double link case (automatic redirect). - const doubleDeepLink = link ? new URL(link).searchParams.get('link') : null; + const doubleDeepLink = link + ? querystringDecode(extractQuerystring(link))['deep_link_id'] + : null; // iOS custom scheme links. - const iOSDeepLink = uri.searchParams.get('deep_link_id'); + const iOSDeepLink = querystringDecode(extractQuerystring(url))[ + 'deep_link_id' + ]; const iOSDoubleDeepLink = iOSDeepLink - ? new URL(iOSDeepLink).searchParams.get('link') + ? querystringDecode(extractQuerystring(iOSDeepLink))['link'] : null; return iOSDoubleDeepLink || iOSDeepLink || doubleDeepLink || link || url; } @@ -115,18 +120,18 @@ export class ActionCodeURL { * @internal */ constructor(actionLink: string) { - const uri = new URL(actionLink); - const apiKey = uri.searchParams.get(QueryField.API_KEY); - const code = uri.searchParams.get(QueryField.CODE); - const operation = parseMode(uri.searchParams.get(QueryField.MODE)); + const searchParams = querystringDecode(extractQuerystring(actionLink)); + const apiKey = searchParams[QueryField.API_KEY] ?? null; + const code = searchParams[QueryField.CODE] ?? null; + const operation = parseMode(searchParams[QueryField.MODE] ?? null); // Validate API key, code and mode. _assert(apiKey && code && operation, AuthErrorCode.ARGUMENT_ERROR); this.apiKey = apiKey; this.operation = operation; this.code = code; - this.continueUrl = uri.searchParams.get(QueryField.CONTINUE_URL); - this.languageCode = uri.searchParams.get(QueryField.LANGUAGE_CODE); - this.tenantId = uri.searchParams.get(QueryField.TENANT_ID); + this.continueUrl = searchParams[QueryField.CONTINUE_URL] ?? null; + this.languageCode = searchParams[QueryField.LANGUAGE_CODE] ?? null; + this.tenantId = searchParams[QueryField.TENANT_ID] ?? null; } /** diff --git a/packages-exp/auth-exp/src/core/auth/emulator.ts b/packages-exp/auth-exp/src/core/auth/emulator.ts index 85d24fbfd28..ff0361f131b 100644 --- a/packages-exp/auth-exp/src/core/auth/emulator.ts +++ b/packages-exp/auth-exp/src/core/auth/emulator.ts @@ -58,22 +58,60 @@ export function useAuthEmulator( AuthErrorCode.INVALID_EMULATOR_SCHEME ); - const parsedUrl = new URL(url); const disableWarnings = !!options?.disableWarnings; - // Store the normalized URL whose path is always nonempty (i.e. containing at least a single '/'). - authInternal.config.emulator = { url: parsedUrl.toString() }; + const protocol = extractProtocol(url); + const { host, port } = extractHostAndPort(url); + const portStr = port === null ? '' : `:${port}`; + + // Always replace path with "/" (even if input url had no path at all, or had a different one). + authInternal.config.emulator = { url: `${protocol}//${host}${portStr}/` }; authInternal.settings.appVerificationDisabledForTesting = true; authInternal.emulatorConfig = Object.freeze({ - host: parsedUrl.hostname, - port: parsedUrl.port ? Number(parsedUrl.port) : null, - protocol: parsedUrl.protocol.replace(':', ''), + host, + port, + protocol: protocol.replace(':', ''), options: Object.freeze({ disableWarnings }) }); emitEmulatorWarning(disableWarnings); } +function extractProtocol(url: string): string { + const protocolEnd = url.indexOf(':'); + return protocolEnd < 0 ? '' : url.substr(0, protocolEnd + 1); +} + +function extractHostAndPort( + url: string +): { host: string; port: number | null } { + const protocol = extractProtocol(url); + const authority = /(\/\/)?([^?#/]+)/.exec(url.substr(protocol.length)); // Between // and /, ? or #. + if (!authority) { + return { host: '', port: null }; + } + const hostAndPort = authority[2].split('@').pop() || ''; // Strip out "username:password@". + const bracketedIPv6 = /^(\[[^\]]+\])(:|$)/.exec(hostAndPort); + if (bracketedIPv6) { + const host = bracketedIPv6[1]; + return { host, port: parsePort(hostAndPort.substr(host.length + 1)) }; + } else { + const [host, port] = hostAndPort.split(':'); + return { host, port: parsePort(port) }; + } +} + +function parsePort(portStr: string): number | null { + if (!portStr) { + return null; + } + const port = Number(portStr); + if (isNaN(port)) { + return null; + } + return port; +} + function emitEmulatorWarning(disableBanner: boolean): void { function attachBanner(): void { const el = document.createElement('p'); diff --git a/packages-exp/auth-exp/src/core/util/handler.ts b/packages-exp/auth-exp/src/core/util/handler.ts index 63cfc3b874b..615bb4a739c 100644 --- a/packages-exp/auth-exp/src/core/util/handler.ts +++ b/packages-exp/auth-exp/src/core/util/handler.ts @@ -94,22 +94,16 @@ export function _getRedirectUrl( params.tid = auth.tenantId; } - for (const key of Object.keys(params)) { - if ((params as Record)[key] === undefined) { - delete (params as Record)[key]; - } - } - // TODO: maybe set eid as endipointId // TODO: maybe set fw as Frameworks.join(",") - const url = new URL( - `${getHandlerBase(auth)}?${querystring( - params as Record - ).slice(1)}` - ); - - return url.toString(); + const paramsDict = params as Record; + for (const key of Object.keys(paramsDict)) { + if (paramsDict[key] === undefined) { + delete paramsDict[key]; + } + } + return `${getHandlerBase(auth)}?${querystring(paramsDict).slice(1)}`; } function getHandlerBase({ config }: AuthInternal): string { diff --git a/packages-exp/auth-exp/src/platform_react_native/persistence/react_native.ts b/packages-exp/auth-exp/src/platform_react_native/persistence/react_native.ts index 58d605ec3b6..8d118ea0b96 100644 --- a/packages-exp/auth-exp/src/platform_react_native/persistence/react_native.ts +++ b/packages-exp/auth-exp/src/platform_react_native/persistence/react_native.ts @@ -74,11 +74,13 @@ export function getReactNativePersistence( } _addListener(_key: string, _listener: StorageEventListener): void { - debugFail('not implemented'); + // Listeners are not supported for React Native storage. + return; } _removeListener(_key: string, _listener: StorageEventListener): void { - debugFail('not implemented'); + // Listeners are not supported for React Native storage. + return; } }; } diff --git a/packages/util/src/query.ts b/packages/util/src/query.ts index 589eeaba185..0efe7ad094c 100644 --- a/packages/util/src/query.ts +++ b/packages/util/src/query.ts @@ -42,15 +42,30 @@ export function querystring(querystringParams: { * Decodes a querystring (e.g. ?arg=val&arg2=val2) into a params object * (e.g. {arg: 'val', arg2: 'val2'}) */ -export function querystringDecode(querystring: string): object { - const obj: { [key: string]: unknown } = {}; +export function querystringDecode(querystring: string): Record { + const obj: Record = {}; const tokens = querystring.replace(/^\?/, '').split('&'); tokens.forEach(token => { if (token) { - const key = token.split('='); - obj[key[0]] = key[1]; + const [key, value] = token.split('='); + obj[decodeURIComponent(key)] = decodeURIComponent(value); } }); return obj; } + +/** + * Extract the query string part of a URL, including the leading question mark (if present). + */ +export function extractQuerystring(url: string): string { + const queryStart = url.indexOf('?'); + if (!queryStart) { + return ''; + } + const fragmentStart = url.indexOf('#', queryStart); + return url.substring( + queryStart, + fragmentStart > 0 ? fragmentStart : undefined + ); +} From 03aaa516e9b9a0e56d272d0f5797a68b55bc35e2 Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Fri, 26 Feb 2021 12:41:00 -0800 Subject: [PATCH 2/2] Remove unused import. --- .../src/platform_react_native/persistence/react_native.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages-exp/auth-exp/src/platform_react_native/persistence/react_native.ts b/packages-exp/auth-exp/src/platform_react_native/persistence/react_native.ts index 8d118ea0b96..69be82f630b 100644 --- a/packages-exp/auth-exp/src/platform_react_native/persistence/react_native.ts +++ b/packages-exp/auth-exp/src/platform_react_native/persistence/react_native.ts @@ -24,7 +24,6 @@ import { STORAGE_AVAILABLE_KEY, StorageEventListener } from '../../core/persistence'; -import { debugFail } from '../../core/util/assert'; /** * Returns a persistence class that wraps AsyncStorage imported from