mirror of
https://github.com/Termix-SSH/Mobile.git
synced 2026-07-28 05:51:43 +02:00
6c40d2f0cc
* 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>
192 lines
5.2 KiB
JavaScript
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;
|