diff --git a/app/main-axios.ts b/app/main-axios.ts index d633a55..ade06c1 100644 --- a/app/main-axios.ts +++ b/app/main-axios.ts @@ -2210,6 +2210,37 @@ function extractJwtFromSetCookie(headers: any): string | null { return null; } +function isLocalNetworkHttpsUrl(url: string): boolean { + if (!url.startsWith("https://")) return false; + + try { + const host = new URL(url).hostname.replace(/^\[|\]$/g, ""); + if (host === "localhost" || host === "::1") return true; + + const parts = host.split(".").map((part) => Number(part)); + if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part))) { + return false; + } + + const [first, second] = parts; + return ( + first === 10 || + first === 127 || + (first === 169 && second === 254) || + (first === 172 && second >= 16 && second <= 31) || + (first === 192 && second === 168) + ); + } catch { + return false; + } +} + +function isNativeNetworkFailure(error: unknown): boolean { + return ( + error instanceof Error && error.message.includes("Network request failed") + ); +} + /** * Detects whether the server URL sits behind a reverse-proxy authentication * gate (Cloudflare Access, Authelia, etc.) that intercepts requests and serves @@ -2272,11 +2303,23 @@ async function loginWithFetch( password: string, ): Promise<{ data: any; token: string | null }> { const url = `${baseUrl.replace(/\/$/, "")}/users/login`; - const fetchResponse = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ username, password }), - }); + let fetchResponse: Response; + try { + fetchResponse = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password }), + }); + } catch (error) { + if (isLocalNetworkHttpsUrl(url) && isNativeNetworkFailure(error)) { + throw new ApiError( + "The app could not trust HTTPS for this local IP. Use a DNS name with a valid certificate, a certificate for this IP, or HTTP on a trusted local/VPN network.", + 0, + "LOCAL_IP_HTTPS_CERTIFICATE_ERROR", + ); + } + throw error; + } const contentType = ( fetchResponse.headers.get("content-type") || "" diff --git a/plugins/withNetworkSecurityConfig.js b/plugins/withNetworkSecurityConfig.js index 1266957..8851ecb 100644 --- a/plugins/withNetworkSecurityConfig.js +++ b/plugins/withNetworkSecurityConfig.js @@ -5,6 +5,27 @@ const { const path = require("path"); const fs = require("fs"); +const NETWORK_SECURITY_CONFIG = ` + + + + + + + + + + localhost + 127.0.0.1 + 10.0.2.2 + + + + + + +`; + const withNetworkSecurityConfig = (config) => { config = withAndroidManifest(config, async (config) => { const mainApplication = config.modResults.manifest.application[0]; @@ -38,28 +59,7 @@ const withNetworkSecurityConfig = (config) => { "network_security_config.xml", ); - const networkSecurityConfig = ` - - - - - - - - - - localhost - 127.0.0.1 - 10.0.2.2 - - - - - - -`; - - fs.writeFileSync(networkSecurityConfigPath, networkSecurityConfig); + fs.writeFileSync(networkSecurityConfigPath, NETWORK_SECURITY_CONFIG); return config; }, @@ -69,3 +69,4 @@ const withNetworkSecurityConfig = (config) => { }; module.exports = withNetworkSecurityConfig; +module.exports.NETWORK_SECURITY_CONFIG = NETWORK_SECURITY_CONFIG;