Launch Android broker through Termux URL handler
Verify / verify (ubuntu-latest, linux-bionic-arm64) (push) Canceled after 0s
Verify / verify (ubuntu-latest, linux-x64) (push) Canceled after 0s
Verify / verify (windows-latest, win-x64) (push) Canceled after 0s
Verify / postgres (push) Canceled after 0s

This commit is contained in:
Nitload
2026-10-06 11:34:42 +08:00
parent 6d072b3718
commit 3f3fd613db
3 changed files with 61 additions and 22 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Android CLI 客户端 # 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`。 推荐把 Android CLI 作为 Termux DEB 分发,而不是把 CLI 当成普通 Android APK。APK 只是可选的 One UI 启动器:它不能单独运行 CLI,必须与 Termux、Termux DEB、运行命令权限和 `allow-external-apps=true` 配置一起使用。直接安装 APK 后点击图标不能替代 Termux 环境,也不会自动安装 .NET 或 `charac`。
+50 -9
View File
@@ -57,6 +57,24 @@ public sealed class MainActivity : Activity
private void HandleBrokerIntent(Intent intent) 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) if (intent.GetStringExtra(AuthUrlExtra) is { Length: > 0 } authorizationUrl)
{ {
var port = intent.GetIntExtra(CallbackPortExtra, 0); var port = intent.GetIntExtra(CallbackPortExtra, 0);
@@ -85,15 +103,11 @@ public sealed class MainActivity : Activity
return; return;
} }
updatedEditor.Apply(); updatedEditor.Apply();
try if (Uri.TryCreate(authorizationUrl, UriKind.Absolute, out var parsedUrl) &&
{ parsedUrl.Scheme == Uri.UriSchemeHttps)
StartActivity(new Intent(Intent.ActionView, global::Android.Net.Uri.Parse(authorizationUrl))); StartBrowserForAuthorization(parsedUrl, port);
SetStatus("已打开浏览器。完成登录后将自动返回 Termux。", false); else
} SetStatus("OIDC 授权地址无效。请从 Termux 重新发起登录。", false);
catch (Exception exception)
{
SetStatus("无法打开浏览器:" + exception.Message, false);
}
return; return;
} }
@@ -121,6 +135,33 @@ public sealed class MainActivity : Activity
SetStatus("登录回调已交给 Termux。请返回终端等待完成。", true); 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() private bool HasTermux()
{ {
try try
+10 -12
View File
@@ -9,6 +9,8 @@ namespace Charac.Client;
internal static class OidcLogin internal static class OidcLogin
{ {
private const string TermuxBin = "/data/data/com.termux/files/usr/bin";
public static async Task<string> LoginAsync(HttpClient client, string issuer, string clientId, public static async Task<string> LoginAsync(HttpClient client, string issuer, string clientId,
string discoveryUrl, string discoveryUrl,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
@@ -106,22 +108,18 @@ internal static class OidcLogin
{ {
try try
{ {
var start = new ProcessStartInfo("/system/bin/am") { UseShellExecute = false }; var brokerUri = new UriBuilder("com.nitload.charac://oauth/start")
start.ArgumentList.Add("start"); {
start.ArgumentList.Add("-n"); Query = $"url={Uri.EscapeDataString(authorizationUri.ToString())}&port={port.ToString(System.Globalization.CultureInfo.InvariantCulture)}"
start.ArgumentList.Add("com.nitload.charac/.MainActivity"); }.Uri;
start.ArgumentList.Add("--es"); var start = new ProcessStartInfo(TermuxBin + "/termux-open-url") { UseShellExecute = false };
start.ArgumentList.Add("charac_auth_url"); start.ArgumentList.Add(brokerUri.ToString());
start.ArgumentList.Add(authorizationUri.ToString());
start.ArgumentList.Add("--ei");
start.ArgumentList.Add("charac_callback_port");
start.ArgumentList.Add(port.ToString(System.Globalization.CultureInfo.InvariantCulture));
using var process = Process.Start(start); using var process = Process.Start(start);
process?.WaitForExit(5000); process?.WaitForExit(5000);
if (process is null || process.ExitCode != 0) 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($"Could not start the Android callback broker: {exception.Message}");
Console.WriteLine("Open the URL manually in a browser on this device."); Console.WriteLine("Open the URL manually in a browser on this device.");