diff --git a/docs/platforms/android-client.md b/docs/platforms/android-client.md index fbffa7d..4a73c28 100644 --- a/docs/platforms/android-client.md +++ b/docs/platforms/android-client.md @@ -1,6 +1,6 @@ # Android CLI 客户端 -Termux 登录不让浏览器直接访问 Termux 的 `127.0.0.1`:CLI 通过 `/system/bin/am` 启动 APK,APK 注册 `com.nitload.charac://oauth/callback` 并接收 authentik 回调,再用 `RUN_COMMAND` 调用 `charac callback` 将授权码转回 CLI 的本地监听器。 +Termux 登录不让浏览器直接访问 Termux 的 `127.0.0.1`:CLI 通过 `termux-open-url` 启动 APK,APK 注册 `com.nitload.charac://oauth/callback` 并接收 authentik 回调,再用 `RUN_COMMAND` 调用 `charac callback` 将授权码转回 CLI 的本地监听器。 推荐把 Android CLI 作为 Termux DEB 分发,而不是把 CLI 当成普通 Android APK。APK 只是可选的 One UI 启动器:它不能单独运行 CLI,必须与 Termux、Termux DEB、运行命令权限和 `allow-external-apps=true` 配置一起使用。直接安装 APK 后点击图标不能替代 Termux 环境,也不会自动安装 .NET 或 `charac`。 diff --git a/src/android/MainActivity.cs b/src/android/MainActivity.cs index 93b441e..4848bec 100644 --- a/src/android/MainActivity.cs +++ b/src/android/MainActivity.cs @@ -57,6 +57,24 @@ public sealed class MainActivity : Activity private void HandleBrokerIntent(Intent intent) { + var brokerUri = intent.Data; + if (brokerUri?.Scheme == "com.nitload.charac" && brokerUri.Host == "oauth" && + brokerUri.Path == "/start") + { + var brokerAuthorizationUrl = brokerUri.GetQueryParameter("url"); + var portText = brokerUri.GetQueryParameter("port"); + if (brokerAuthorizationUrl is null || !Uri.TryCreate(brokerAuthorizationUrl, UriKind.Absolute, out var parsedUrl) || + parsedUrl.Scheme != Uri.UriSchemeHttps || !int.TryParse(portText, out var port) || + port is <= 0 or > 65535) + { + SetStatus("OIDC 启动参数无效。请从 Termux 重新发起登录。", false); + return; + } + + StartBrowserForAuthorization(parsedUrl, port); + return; + } + if (intent.GetStringExtra(AuthUrlExtra) is { Length: > 0 } authorizationUrl) { var port = intent.GetIntExtra(CallbackPortExtra, 0); @@ -85,15 +103,11 @@ public sealed class MainActivity : Activity return; } updatedEditor.Apply(); - try - { - StartActivity(new Intent(Intent.ActionView, global::Android.Net.Uri.Parse(authorizationUrl))); - SetStatus("已打开浏览器。完成登录后将自动返回 Termux。", false); - } - catch (Exception exception) - { - SetStatus("无法打开浏览器:" + exception.Message, false); - } + if (Uri.TryCreate(authorizationUrl, UriKind.Absolute, out var parsedUrl) && + parsedUrl.Scheme == Uri.UriSchemeHttps) + StartBrowserForAuthorization(parsedUrl, port); + else + SetStatus("OIDC 授权地址无效。请从 Termux 重新发起登录。", false); return; } @@ -121,6 +135,33 @@ public sealed class MainActivity : Activity SetStatus("登录回调已交给 Termux。请返回终端等待完成。", true); } + private void StartBrowserForAuthorization(Uri authorizationUrl, int port) + { + var preferences = GetPreferences(FileCreationMode.Private); + var editor = preferences?.Edit(); + if (editor is null) + { + SetStatus("无法保存 OIDC 回调状态。请从 Termux 重新发起登录。", false); + return; + } + var updatedEditor = editor.PutInt(CallbackPortPreference, port); + if (updatedEditor is null) + { + SetStatus("无法保存 OIDC 回调状态。请从 Termux 重新发起登录。", false); + return; + } + updatedEditor.Apply(); + try + { + StartActivity(new Intent(Intent.ActionView, global::Android.Net.Uri.Parse(authorizationUrl.ToString()))); + SetStatus("已打开浏览器。完成登录后将自动返回 Termux。", false); + } + catch (Exception exception) + { + SetStatus("无法打开浏览器:" + exception.Message, false); + } + } + private bool HasTermux() { try diff --git a/src/client/OidcLogin.cs b/src/client/OidcLogin.cs index da86548..fbfaf21 100644 --- a/src/client/OidcLogin.cs +++ b/src/client/OidcLogin.cs @@ -9,6 +9,8 @@ namespace Charac.Client; internal static class OidcLogin { + private const string TermuxBin = "/data/data/com.termux/files/usr/bin"; + public static async Task LoginAsync(HttpClient client, string issuer, string clientId, string discoveryUrl, CancellationToken cancellationToken = default) @@ -106,22 +108,18 @@ internal static class OidcLogin { try { - var start = new ProcessStartInfo("/system/bin/am") { UseShellExecute = false }; - start.ArgumentList.Add("start"); - start.ArgumentList.Add("-n"); - start.ArgumentList.Add("com.nitload.charac/.MainActivity"); - start.ArgumentList.Add("--es"); - start.ArgumentList.Add("charac_auth_url"); - start.ArgumentList.Add(authorizationUri.ToString()); - start.ArgumentList.Add("--ei"); - start.ArgumentList.Add("charac_callback_port"); - start.ArgumentList.Add(port.ToString(System.Globalization.CultureInfo.InvariantCulture)); + var brokerUri = new UriBuilder("com.nitload.charac://oauth/start") + { + Query = $"url={Uri.EscapeDataString(authorizationUri.ToString())}&port={port.ToString(System.Globalization.CultureInfo.InvariantCulture)}" + }.Uri; + var start = new ProcessStartInfo(TermuxBin + "/termux-open-url") { UseShellExecute = false }; + start.ArgumentList.Add(brokerUri.ToString()); using var process = Process.Start(start); process?.WaitForExit(5000); if (process is null || process.ExitCode != 0) - throw new InvalidOperationException("Android callback broker did not start."); + throw new InvalidOperationException("Termux could not open the Android callback broker."); } - catch (Exception exception) when (exception is System.ComponentModel.Win32Exception or InvalidOperationException) + catch (Exception exception) when (exception is System.ComponentModel.Win32Exception or InvalidOperationException or System.IO.IOException) { Console.WriteLine($"Could not start the Android callback broker: {exception.Message}"); Console.WriteLine("Open the URL manually in a browser on this device.");