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;