Files
termix-mobile/plugins/withAndroidLocalNetworkSsl.js
Luke Gustafson 6c40d2f0cc v1.4.0 (#31)
* fix: register physical Tab key on iPadOS/iOS hardware keyboards (#14)

The native module only registered Shift+Tab but not bare Tab, so iOS
intercepted it for UI focus navigation. Register an unmodified Tab
UIKeyCommand with wantsPriorityOverSystemBehavior and handle both
Tab and Shift+Tab in the same JS branch.

Fixes Termix-SSH/Support#557

* fix: pass jumpHosts, forceKeyboardInteractive, and overrideCredentialUsername to terminal connection (#15)

These fields were defined on SSHHost but omitted from the terminal
hostConfig passthrough. Without jumpHosts the backend cannot route
through jump servers; without forceKeyboardInteractive hosts that
require keyboard-interactive auth fail silently; without
overrideCredentialUsername shared credentials use the wrong username.

Fixes Termix-SSH/Support#422
Fixes Termix-SSH/Support#638

* fix: improve mobile terminal scroll recovery (#17)

* Add mobile remote desktop sessions (#18)

* feat: add mobile remote desktop sessions

* feat: add remote desktop input controls

* feat: polish remote desktop controls

* feat: add mobile terminal font selection (#19)

* feat: add mobile shift tab hotkey (#20)

* feat: initial UI redesign

* feat: revamp server login system

* chore: update app icon/background color

* fix: remove unused fields in the settings tab

* feat: improve the user pin system and update all settings modals to use new ui

* fix: active server not updating unless app restarts

* feat: add version in settings

* feat: improve host page UI (updated host form and added credential management)

* fix: preserve composed iOS terminal input

* ci: restore mobile checks

* feat: rewrite of connection system for all types

* fix: preserve mobile jump hosts (#30)

* fix: guard android user ca trust (#29)

* fix: capture ios hardware tab key (#27)

* fix: reset terminal input after special keys (#28)

* fix: open oidc in system browser (#26)

* fix: preserve websocket auth context (#25)

* fix: keep none-auth terminal prompts alive (#23)

* fix: allow local network SSL in Android app (#21)

* feat: allow oidc passkeys

* feat: bundeled xterm into app and added docker loading screen

* feat: add 2fa, api key, and active sessions to settings and addressed multiple bug fixes and inconsistencies with termix web

* chore: update repo images and readme

* chore: update readme to split table for screenshots

* chore: rename ios to apple in build workflow

* fix: android build failure

* feat: add snippet management and fix several bugs

* fix: web login and oidc incorrect logic

* chore: run linter

---------

Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 16:49:50 -05:00

192 lines
5.2 KiB
JavaScript

const {
withDangerousMod,
withMainApplication,
} = require("@expo/config-plugins");
const fs = require("fs");
const path = require("path");
const WEBVIEW_CLIENT_PATH = path.join(
"node_modules",
"react-native-webview",
"android",
"src",
"main",
"java",
"com",
"reactnativecommunity",
"webview",
"RNCWebViewClient.java",
);
function addOnce(contents, marker, insert, anchor) {
if (contents.includes(marker)) return contents;
if (!contents.includes(anchor)) {
throw new Error(`Unable to patch Android source: missing anchor ${anchor}`);
}
return contents.replace(anchor, `${insert}${anchor}`);
}
function patchWebViewClient(projectRoot) {
const filePath = path.join(projectRoot, WEBVIEW_CLIENT_PATH);
if (!fs.existsSync(filePath)) {
throw new Error(`react-native-webview client not found: ${filePath}`);
}
let contents = fs.readFileSync(filePath, "utf8");
contents = addOnce(
contents,
"import java.net.InetAddress;",
"import java.net.InetAddress;\nimport java.net.URI;\n",
"import java.util.concurrent.atomic.AtomicReference;\n",
);
contents = addOnce(
contents,
"isTermixLocalNetworkHost",
` private static boolean isTermixLocalNetworkHost(String url) {
try {
String host = new URI(url).getHost();
if (host == null) {
return false;
}
InetAddress address = InetAddress.getByName(host);
return address.isAnyLocalAddress()
|| address.isLoopbackAddress()
|| address.isSiteLocalAddress()
|| address.isLinkLocalAddress();
} catch (Exception ignored) {
return false;
}
}
`,
" @Override\n public void onReceivedSslError",
);
contents = addOnce(
contents,
"isTermixLocalNetworkHost(failingUrl)",
` if (isTermixLocalNetworkHost(failingUrl)) {
handler.proceed();
return;
}
`,
" // Cancel request after obtaining top-level URL.\n",
);
fs.writeFileSync(filePath, contents);
}
function patchKotlinMainApplication(contents) {
contents = addOnce(
contents,
"import com.facebook.react.modules.network.OkHttpClientProvider",
"import com.facebook.react.modules.network.OkHttpClientProvider\nimport java.net.InetAddress\nimport javax.net.ssl.HttpsURLConnection\n",
"import expo.modules.ApplicationLifecycleDispatcher\n",
);
contents = addOnce(
contents,
"isTermixLocalNetworkHost",
` private fun isTermixLocalNetworkHost(hostname: String): Boolean {
return try {
val address = InetAddress.getByName(hostname)
address.isAnyLocalAddress ||
address.isLoopbackAddress ||
address.isSiteLocalAddress ||
address.isLinkLocalAddress
} catch (_: Exception) {
false
}
}
`,
" override fun onCreate()",
);
return addOnce(
contents,
"OkHttpClientProvider.setOkHttpClientFactory",
` OkHttpClientProvider.setOkHttpClientFactory {
OkHttpClientProvider.createClientBuilder(this)
.hostnameVerifier { hostname, session ->
HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session) ||
isTermixLocalNetworkHost(hostname)
}
.build()
}
`,
" super.onCreate()\n",
);
}
function patchJavaMainApplication(contents) {
contents = addOnce(
contents,
"import com.facebook.react.modules.network.OkHttpClientProvider;",
"import com.facebook.react.modules.network.OkHttpClientProvider;\nimport java.net.InetAddress;\nimport javax.net.ssl.HttpsURLConnection;\n",
"import expo.modules.ApplicationLifecycleDispatcher;\n",
);
contents = addOnce(
contents,
"isTermixLocalNetworkHost",
` private boolean isTermixLocalNetworkHost(String hostname) {
try {
InetAddress address = InetAddress.getByName(hostname);
return address.isAnyLocalAddress()
|| address.isLoopbackAddress()
|| address.isSiteLocalAddress()
|| address.isLinkLocalAddress();
} catch (Exception ignored) {
return false;
}
}
`,
" @Override\n public void onCreate()",
);
return addOnce(
contents,
"OkHttpClientProvider.setOkHttpClientFactory",
` OkHttpClientProvider.setOkHttpClientFactory(() ->
OkHttpClientProvider.createClientBuilder(this)
.hostnameVerifier((hostname, session) ->
HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session)
|| isTermixLocalNetworkHost(hostname))
.build()
);
`,
" super.onCreate();\n",
);
}
const withAndroidLocalNetworkSsl = (config) => {
config = withMainApplication(config, (config) => {
const { modResults } = config;
if (modResults.language === "kt") {
modResults.contents = patchKotlinMainApplication(modResults.contents);
} else {
modResults.contents = patchJavaMainApplication(modResults.contents);
}
return config;
});
config = withDangerousMod(config, [
"android",
async (config) => {
patchWebViewClient(config.modRequest.projectRoot);
return config;
},
]);
return config;
};
module.exports = withAndroidLocalNetworkSsl;