diff --git a/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Proxy.cs b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Proxy.cs new file mode 100644 index 00000000000000..0d755b5d1e830c --- /dev/null +++ b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Proxy.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class AndroidCrypto + { + internal enum AndroidProxyType + { + Direct = 0, + Http = 1, + Socks = 2, + } + + [StructLayout(LayoutKind.Sequential)] + internal struct AndroidProxyInfo + { + public int Type; // AndroidProxyType + public IntPtr Host; // NUL-terminated UTF-16; read via Marshal.PtrToStringUni; freed by FreeProxyResult + public int Port; + } + + [LibraryImport(Libraries.AndroidCryptoNative, + EntryPoint = "AndroidCryptoNative_GetProxyForUrl", + StringMarshalling = StringMarshalling.Utf8)] + internal static unsafe partial int GetProxyForUrl( + string url, + out int count, + out AndroidProxyInfo* proxies); + + [LibraryImport(Libraries.AndroidCryptoNative, + EntryPoint = "AndroidCryptoNative_FreeProxyResult")] + internal static unsafe partial void FreeProxyResult( + AndroidProxyInfo* proxies, int count); + } +} diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index f973fa8499005b..aa1fe920fccb31 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -329,11 +329,21 @@ - + + + + + + + + + diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AndroidPlatformProxy.Android.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AndroidPlatformProxy.Android.cs new file mode 100644 index 00000000000000..4727144cbcaad8 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AndroidPlatformProxy.Android.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Runtime.InteropServices; + +namespace System.Net.Http +{ + /// + /// An that defers proxy resolution to the + /// Android platform via java.net.ProxySelector. Honors Wi-Fi + /// proxy, MDM-deployed system proxy, PAC scripts, and per-network + /// or VPN proxies — the same source java.net.HttpURLConnection + /// consults. + /// + /// + /// + /// The Android proxy APIs (ProxySelector, ProxyInfo, + /// ConnectivityManager) do not surface credentials. The + /// property is required by + /// but is never populated by this class. + /// Apps that need to authenticate to a system-detected proxy should + /// set HttpClientHandler.DefaultProxyCredentials (or + /// SocketsHttpHandler.DefaultProxyCredentials) — the same + /// behavior as on macOS and Windows. + /// + /// + internal sealed class AndroidPlatformProxy : IWebProxy + { + public ICredentials? Credentials { get; set; } + + public unsafe Uri? GetProxy(Uri destination) + { + ArgumentNullException.ThrowIfNull(destination); + + string url = destination.AbsoluteUri; + + Interop.AndroidCrypto.AndroidProxyInfo* proxies = null; + int count = 0; + int rc = Interop.AndroidCrypto.GetProxyForUrl(url, out count, out proxies); + + if (rc != 0 || proxies == null) + { + return null; + } + + try + { + // ProxySelector returns entries in preference order. We take the first. + // SocketsHttpHandler does not currently have an opt-in fallback API for + // multiple proxies; if the chosen proxy is unreachable the user sees the + // connect error rather than an automatic retry. + for (int i = 0; i < count; i++) + { + if (TryCreateProxyUri(proxies[i], out Uri? proxyUri)) + { + return proxyUri; + } + } + + return null; + } + finally + { + Interop.AndroidCrypto.FreeProxyResult(proxies, count); + } + } + + internal static bool TryCreateProxyUri(Interop.AndroidCrypto.AndroidProxyInfo entry, out Uri? proxyUri) + { + proxyUri = null; + + Interop.AndroidCrypto.AndroidProxyType type = (Interop.AndroidCrypto.AndroidProxyType)entry.Type; + + if (type == Interop.AndroidCrypto.AndroidProxyType.Direct) + { + // Java's Proxy.NO_PROXY is represented as Proxy.Type.DIRECT. + // Preserve ProxySelector ordering by treating DIRECT as the + // selected result rather than skipping to a later fallback proxy. + return true; + } + + // SOCKS is a transport-level proxy protocol (RFC 1928 for SOCKS5). + // Unlike HTTP CONNECT, SOCKS tunnels arbitrary TCP at the socket + // layer. SocketsHttpHandler accepts "socks5://" via + // HttpUtilities.IsSupportedProxyScheme. Android's + // java.net.Proxy.Type.SOCKS maps to SOCKS5 on modern Android. + string? scheme = type switch + { + Interop.AndroidCrypto.AndroidProxyType.Http => "http", + Interop.AndroidCrypto.AndroidProxyType.Socks => "socks5", + _ => null, + }; + + if (scheme is null) + { + return false; + } + + // The native PAL allocates the host as NUL-terminated UTF-16 + // (Marshal.PtrToStringUni is a zero-conversion copy). + string? host = Marshal.PtrToStringUni(entry.Host); + if (string.IsNullOrEmpty(host)) + { + return false; + } + + proxyUri = new UriBuilder(scheme, host, entry.Port).Uri; + + return true; + } + + // SocketsHttpHandler's pattern is: call IsBypassed first; if it returns false, + // call GetProxy. For us, computing IsBypassed *correctly* would require calling + // GetProxy first, which would mean two JNI round-trips per request. + // Instead, we always return false (same approach as HttpWindowsProxy:349–360) so + // that SHH always calls GetProxy exactly once and discovers "no proxy" via a + // null return. + public bool IsBypassed(Uri host) => false; + } +} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SystemProxyInfo.Android.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SystemProxyInfo.Android.cs new file mode 100644 index 00000000000000..72cca1908830e6 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SystemProxyInfo.Android.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace System.Net.Http +{ + internal static partial class SystemProxyInfo + { + public static IWebProxy ConstructSystemProxy() + { + // 1. Honor HTTP_PROXY / HTTPS_PROXY / NO_PROXY env vars first. + // This matches the Unix convention and lets dev/test flows + // (e.g. Charles/Fiddler) keep working unchanged. + if (HttpEnvironmentProxy.TryCreate(out IWebProxy? envProxy)) + { + return envProxy; + } + + // 2. Defer to the Android platform proxy via java.net.ProxySelector. + // Wi-Fi proxy, MDM-deployed proxy, PAC scripts, and per-network + // or VPN-attached ProxyInfo are all surfaced through this path. + if (UseAndroidSystemProxy) + { + return new AndroidPlatformProxy(); + } + + return new HttpNoProxy(); + } + + // Feature switch: System.Net.Http.UseAndroidSystemProxy + // + // Defaults to true. Apps may opt out (set to "false" via + // ) for the following reasons: + // + // * Back-compat with apps that previously ran on SocketsHttpHandler + // (i.e. UseNativeHttpHandler=false) and got env-var-only proxy + // behavior. Suddenly honoring the system proxy could change the + // destination of their HTTP requests; this switch is the escape + // hatch. + // * Trimming: when set to false at publish time, the linker can + // trim the AndroidPlatformProxy class, the Interop P/Invoke + // declarations, and (transitively) leave the native pal_proxy + // code referenced only by other paths. + // * Testing / debugging where pure env-var behavior is required. + [FeatureSwitchDefinition("System.Net.Http.UseAndroidSystemProxy")] + private static bool UseAndroidSystemProxy { get; } = + RuntimeSettingParser.QueryRuntimeSettingSwitch( + "System.Net.Http.UseAndroidSystemProxy", + defaultValue: true); + } +} diff --git a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj index be119816ef4532..9ffb67f28d77c1 100755 --- a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj @@ -5,6 +5,9 @@ true $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-unix;$(NetCoreAppCurrent)-browser;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-maccatalyst;$(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos;$(NetCoreAppCurrent)-android + + + @@ -249,8 +252,16 @@ Link="ProductionCode\System\Net\Http\SocketsHttpHandler\SystemProxyInfo.cs" /> - + + + + + { + AppContext.SetSwitch("System.Net.Http.UseAndroidSystemProxy", bool.Parse(switchValue)); + + IWebProxy proxy = SystemProxyInfo.ConstructSystemProxy(); + + Assert.Equal(expectedTypeName, proxy.GetType().Name); + }, useAndroidSystemProxy.ToString(), expectedType.Name).DisposeAsync(); + } + + [Theory] + [InlineData(Interop.AndroidCrypto.AndroidProxyType.Direct, null, 0, true, null)] + [InlineData(Interop.AndroidCrypto.AndroidProxyType.Http, "proxy.example", 8080, true, "http://proxy.example:8080/")] + [InlineData(Interop.AndroidCrypto.AndroidProxyType.Socks, "proxy.example", 1080, true, "socks5://proxy.example:1080/")] + [InlineData((Interop.AndroidCrypto.AndroidProxyType)42, "proxy.example", 8080, false, null)] + [InlineData(Interop.AndroidCrypto.AndroidProxyType.Http, "", 8080, false, null)] + public void AndroidPlatformProxy_TryCreateProxyUri_PreservesProxySelectorEntrySemantics( + Interop.AndroidCrypto.AndroidProxyType proxyType, + string? host, + int port, + bool expectedResult, + string? expectedUri) + { + IntPtr hostPtr = host is null ? IntPtr.Zero : Marshal.StringToCoTaskMemUni(host); + try + { + bool result = AndroidPlatformProxy.TryCreateProxyUri(CreateProxyEntry(proxyType, hostPtr, port), out Uri? proxyUri); + + Assert.Equal(expectedResult, result); + Assert.Equal(expectedUri, proxyUri?.AbsoluteUri); + } + finally + { + Marshal.FreeCoTaskMem(hostPtr); + } + } + + private static Interop.AndroidCrypto.AndroidProxyInfo CreateProxyEntry( + Interop.AndroidCrypto.AndroidProxyType proxyType, + IntPtr host, + int port) + { + return new Interop.AndroidCrypto.AndroidProxyInfo + { + Type = (int)proxyType, + Host = host, + Port = port + }; + } + } +} diff --git a/src/libraries/System.Net.Http/tests/UnitTests/SystemProxyInfoTest.cs b/src/libraries/System.Net.Http/tests/UnitTests/SystemProxyInfoTest.cs index 8feca468b15421..9200b7c20b505a 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/SystemProxyInfoTest.cs +++ b/src/libraries/System.Net.Http/tests/UnitTests/SystemProxyInfoTest.cs @@ -9,7 +9,7 @@ namespace System.Net.Http.Tests { - public class SystemProxyInfoTest + public partial class SystemProxyInfoTest { // This will clean specific environmental variables // to be sure they do not interfere with the test. diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/CMakeLists.txt b/src/native/libs/System.Security.Cryptography.Native.Android/CMakeLists.txt index 9b30cdeeacd90c..d2db49ff41534d 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/CMakeLists.txt +++ b/src/native/libs/System.Security.Cryptography.Native.Android/CMakeLists.txt @@ -22,6 +22,7 @@ set(NATIVECRYPTO_SOURCES pal_memory.c pal_misc.c pal_pbkdf2.c + pal_proxy.c pal_rsa.c pal_signature.c pal_ssl.c diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.c b/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.c index 81f11ab25c2ea7..bcccc24dfa8dca 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.c +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.c @@ -489,6 +489,31 @@ jmethodID g_DotnetX509KeyManagerCtor; jclass g_PalPbkdf2; jmethodID g_PalPbkdf2Pbkdf2OneShot; +// java/net/ProxySelector +jclass g_ProxySelector; +jmethodID g_ProxySelector_getDefault; +jmethodID g_ProxySelector_select; + +// java/net/Proxy +jclass g_Proxy; +jmethodID g_ProxyType_method; +jmethodID g_Proxy_address; + +// java/net/Proxy$Type +jclass g_ProxyType; +jfieldID g_ProxyType_DIRECT; +jfieldID g_ProxyType_HTTP; +jfieldID g_ProxyType_SOCKS; + +// java/net/InetSocketAddress +jclass g_InetSocketAddress; +jmethodID g_InetSocketAddress_getHostString; +jmethodID g_InetSocketAddress_getPort; + +// java/net/URI +jclass g_URI; +jmethodID g_URI_create; + jobject ToGRef(JNIEnv *env, jobject lref) { if (lref) @@ -678,6 +703,21 @@ int GetEnumAsInt(JNIEnv *env, jobject enumObj) return value; } +uint16_t* AllocateString(JNIEnv* env, jstring source) +{ + if (source == NULL) + return NULL; + + // GetStringLength is in 16-bit Java code-units, which is exactly the format we copy + // out via GetStringRegion. The buffer holds (len + 1) uint16_t for the NUL terminator. + jsize len = (*env)->GetStringLength(env, source); + uint16_t* buffer = xmalloc(sizeof(uint16_t) * (size_t)(len + 1)); + buffer[len] = '\0'; + + (*env)->GetStringRegion(env, source, 0, len, (jchar*)buffer); + return buffer; +} + jint AndroidCryptoNative_InitLibraryOnLoad (JavaVM *vm, void *reserved) { (void)reserved; @@ -1083,5 +1123,25 @@ jint AndroidCryptoNative_InitLibraryOnLoad (JavaVM *vm, void *reserved) g_PalPbkdf2 = GetClassGRef(env, "net/dot/android/crypto/PalPbkdf2"); g_PalPbkdf2Pbkdf2OneShot = GetMethod(env, true, g_PalPbkdf2, "pbkdf2OneShot", "(Ljava/lang/String;[BLjava/nio/ByteBuffer;ILjava/nio/ByteBuffer;)I"); + g_ProxySelector = GetClassGRef(env, "java/net/ProxySelector"); + g_ProxySelector_getDefault = GetMethod(env, true, g_ProxySelector, "getDefault", "()Ljava/net/ProxySelector;"); + g_ProxySelector_select = GetMethod(env, false, g_ProxySelector, "select", "(Ljava/net/URI;)Ljava/util/List;"); + + g_Proxy = GetClassGRef(env, "java/net/Proxy"); + g_ProxyType_method = GetMethod(env, false, g_Proxy, "type", "()Ljava/net/Proxy$Type;"); + g_Proxy_address = GetMethod(env, false, g_Proxy, "address", "()Ljava/net/SocketAddress;"); + + g_ProxyType = GetClassGRef(env, "java/net/Proxy$Type"); + g_ProxyType_DIRECT = GetField(env, true, g_ProxyType, "DIRECT", "Ljava/net/Proxy$Type;"); + g_ProxyType_HTTP = GetField(env, true, g_ProxyType, "HTTP", "Ljava/net/Proxy$Type;"); + g_ProxyType_SOCKS = GetField(env, true, g_ProxyType, "SOCKS", "Ljava/net/Proxy$Type;"); + + g_InetSocketAddress = GetClassGRef(env, "java/net/InetSocketAddress"); + g_InetSocketAddress_getHostString = GetMethod(env, false, g_InetSocketAddress, "getHostString", "()Ljava/lang/String;"); + g_InetSocketAddress_getPort = GetMethod(env, false, g_InetSocketAddress, "getPort", "()I"); + + g_URI = GetClassGRef(env, "java/net/URI"); + g_URI_create = GetMethod(env, true, g_URI, "create", "(Ljava/lang/String;)Ljava/net/URI;"); + return JNI_VERSION_1_6; } diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.h b/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.h index 9285c123525a27..4aeecad400a05f 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.h +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.h @@ -503,6 +503,31 @@ extern jmethodID g_DotnetX509KeyManagerCtor; extern jclass g_PalPbkdf2; extern jmethodID g_PalPbkdf2Pbkdf2OneShot; +// java/net/ProxySelector +extern jclass g_ProxySelector; +extern jmethodID g_ProxySelector_getDefault; +extern jmethodID g_ProxySelector_select; + +// java/net/Proxy +extern jclass g_Proxy; +extern jmethodID g_ProxyType_method; // Proxy.type() -> Proxy$Type +extern jmethodID g_Proxy_address; // Proxy.address() -> SocketAddress + +// java/net/Proxy$Type +extern jclass g_ProxyType; +extern jfieldID g_ProxyType_DIRECT; +extern jfieldID g_ProxyType_HTTP; +extern jfieldID g_ProxyType_SOCKS; + +// java/net/InetSocketAddress +extern jclass g_InetSocketAddress; +extern jmethodID g_InetSocketAddress_getHostString; +extern jmethodID g_InetSocketAddress_getPort; + +// java/net/URI +extern jclass g_URI; +extern jmethodID g_URI_create; + // Compatibility macros #if !defined (__mallocfunc) #if defined (__clang__) || defined (__GNUC__) @@ -582,6 +607,16 @@ jclass GetClassGRef(JNIEnv *env, const char* name) ARGS_NON_NULL(1); // Print and clear any JNI exceptions. Returns true if there was an exception, false otherwise. bool CheckJNIExceptions(JNIEnv* env) ARGS_NON_NULL_ALL; +// Allocates a NUL-terminated UTF-16 buffer (via xmalloc) and copies the contents of the +// supplied Java String into it. Returns NULL when source is NULL. Aborts on allocation +// failure (matches xmalloc semantics). Caller frees with free(). +// +// This is the standard PAL helper for returning a String to managed code as UTF-16 — +// System.String is internally UTF-16, so Marshal.PtrToStringUni on the managed side is +// a zero-conversion memcpy. Using GetStringUTFRegion would produce *modified* UTF-8 +// (which is not standard UTF-8) and force an extra Encoding.UTF8.GetString allocation. +uint16_t* AllocateString(JNIEnv* env, jstring source) ARGS_NON_NULL(1); + // Clear any JNI exceptions without printing them. Returns true if there was an exception, false otherwise. bool TryClearJNIExceptions(JNIEnv* env) ARGS_NON_NULL_ALL; diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.c b/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.c new file mode 100644 index 00000000000000..f915b6c4e041b0 --- /dev/null +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.c @@ -0,0 +1,188 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "pal_proxy.h" + +#include + +PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, + int32_t* outCount, + AndroidProxyInfo** outProxies) +{ + abort_if_invalid_pointer_argument(outCount); + abort_if_invalid_pointer_argument(outProxies); + + *outCount = 0; + *outProxies = NULL; + + if (urlUtf8 == NULL) + return 0; // treat as "no proxy" + + JNIEnv* env = GetJNIEnv(); + int32_t ret = 0; + AndroidProxyInfo* result = NULL; + int32_t written = 0; + + // All transient outer-scope local refs go here. RELEASE_LOCALS(loc, env) at the + // cleanup label releases them exactly once on every path. + INIT_LOCALS(loc, jurl, juri, jselector, jlist, + jproxyTypeDirect, jproxyTypeHttp, jproxyTypeSocks); + + loc[jurl] = make_java_string(env, urlUtf8); // aborts on OOM + + // URI.create(url) — IllegalArgumentException for malformed input. + loc[juri] = (*env)->CallStaticObjectMethod(env, g_URI, g_URI_create, loc[jurl]); + if (TryClearJNIExceptions(env)) + goto cleanup; + + // ProxySelector.getDefault() — VM-wide singleton; may throw SecurityException. + loc[jselector] = (*env)->CallStaticObjectMethod(env, g_ProxySelector, g_ProxySelector_getDefault); + if (TryClearJNIExceptions(env)) + goto cleanup; + if (loc[jselector] == NULL) + goto cleanup; + + // ProxySelector.select(uri) + loc[jlist] = (*env)->CallObjectMethod(env, loc[jselector], g_ProxySelector_select, loc[juri]); + if (TryClearJNIExceptions(env)) + goto cleanup; + if (loc[jlist] == NULL) + goto cleanup; + + // Resolve the Proxy.Type enum constants for IsSameObject comparisons. + loc[jproxyTypeDirect] = (*env)->GetStaticObjectField(env, g_ProxyType, g_ProxyType_DIRECT); + loc[jproxyTypeHttp] = (*env)->GetStaticObjectField(env, g_ProxyType, g_ProxyType_HTTP); + loc[jproxyTypeSocks] = (*env)->GetStaticObjectField(env, g_ProxyType, g_ProxyType_SOCKS); + if (TryClearJNIExceptions(env)) + goto cleanup; + + jint n = (*env)->CallIntMethod(env, loc[jlist], g_CollectionSize); + if (TryClearJNIExceptions(env)) + goto cleanup; + if (n <= 0) + goto cleanup; + + result = (AndroidProxyInfo*)calloc((size_t)n, sizeof(AndroidProxyInfo)); + if (result == NULL) + { + ret = -1; + goto cleanup; + } + + for (jint i = 0; i < n; i++) + { + // Per-iteration locals — released at the end of each loop body. + INIT_LOCALS(iter, jproxy, jtype, jaddr, jhost); + + iter[jproxy] = (*env)->CallObjectMethod(env, loc[jlist], g_ListGet, i); + if (TryClearJNIExceptions(env) || iter[jproxy] == NULL) + { + RELEASE_LOCALS(iter, env); + continue; + } + + iter[jtype] = (*env)->CallObjectMethod(env, iter[jproxy], g_ProxyType_method); + if (TryClearJNIExceptions(env) || iter[jtype] == NULL) + { + RELEASE_LOCALS(iter, env); + continue; + } + + int32_t type; + if ((*env)->IsSameObject(env, iter[jtype], loc[jproxyTypeDirect])) + { + result[written].type = ANDROID_PROXY_TYPE_DIRECT; + result[written].host = NULL; + result[written].port = 0; + written++; + + RELEASE_LOCALS(iter, env); + continue; + } + else if ((*env)->IsSameObject(env, iter[jtype], loc[jproxyTypeHttp])) + { + type = ANDROID_PROXY_TYPE_HTTP; + } + else if ((*env)->IsSameObject(env, iter[jtype], loc[jproxyTypeSocks])) + { + // SOCKS is a transport-level proxy protocol (RFC 1928 for SOCKS5). Unlike + // HTTP CONNECT, it tunnels arbitrary TCP at the socket layer rather than + // negotiating at the HTTP layer. Android's java.net.Proxy.Type.SOCKS maps + // to SOCKS5 in modern Java; SocketsHttpHandler accepts the "socks5://" + // scheme via HttpUtilities.IsSupportedProxyScheme. + type = ANDROID_PROXY_TYPE_SOCKS; + } + else + { + // Unknown proxy type: no result entry. + RELEASE_LOCALS(iter, env); + continue; + } + + iter[jaddr] = (*env)->CallObjectMethod(env, iter[jproxy], g_Proxy_address); + if (TryClearJNIExceptions(env) + || iter[jaddr] == NULL + || !(*env)->IsInstanceOf(env, iter[jaddr], g_InetSocketAddress)) + { + RELEASE_LOCALS(iter, env); + continue; + } + + iter[jhost] = (*env)->CallObjectMethod(env, iter[jaddr], g_InetSocketAddress_getHostString); + if (TryClearJNIExceptions(env) || iter[jhost] == NULL) + { + RELEASE_LOCALS(iter, env); + continue; + } + + jint port = (*env)->CallIntMethod(env, iter[jaddr], g_InetSocketAddress_getPort); + if (TryClearJNIExceptions(env)) + { + RELEASE_LOCALS(iter, env); + continue; + } + + // Copy the host as NUL-terminated UTF-16. Marshal.PtrToStringUni on the managed + // side is a zero-conversion copy because System.String is internally UTF-16. + // AllocateString aborts on allocation failure; we never see a NULL return here. + uint16_t* host = AllocateString(env, (jstring)iter[jhost]); + + result[written].type = type; + result[written].host = host; // ownership transferred to the result; lifetime ≥ jhost + result[written].port = (int32_t)port; + written++; + + RELEASE_LOCALS(iter, env); + } + +cleanup: + RELEASE_LOCALS(loc, env); + + if (ret == 0 && written > 0) + { + *outCount = written; + *outProxies = result; + } + else if (result != NULL) + { + // Either an error path or no proxy entries survived filtering (unknown types, + // invalid addresses, etc). Free anything we may have partially populated so that + // callers can rely on outProxies == NULL whenever outCount == 0. + for (int32_t i = 0; i < written; i++) + free(result[i].host); + free(result); + } + + return ret; +} + +PALEXPORT void AndroidCryptoNative_FreeProxyResult(AndroidProxyInfo* proxies, int32_t count) +{ + if (proxies == NULL) + return; + + for (int32_t i = 0; i < count; i++) + free(proxies[i].host); + + free(proxies); +} diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.h b/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.h new file mode 100644 index 00000000000000..2e0293cce7d4ee --- /dev/null +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.h @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma once + +#include "pal_compiler.h" +#include "pal_jni.h" +#include "pal_types.h" + +typedef enum +{ + ANDROID_PROXY_TYPE_DIRECT = 0, + ANDROID_PROXY_TYPE_HTTP = 1, + ANDROID_PROXY_TYPE_SOCKS = 2, +} AndroidProxyType; + +typedef struct +{ + int32_t type; // AndroidProxyType + uint16_t* host; // NUL-terminated UTF-16, allocated via xmalloc (so .NET internals + // can read it directly with Marshal.PtrToStringUni / new string(char*)); + // freed via AndroidCryptoNative_FreeProxyResult. + int32_t port; +} AndroidProxyInfo; + +// Resolves the system proxy chain for the destination URL by querying +// java.net.ProxySelector.getDefault().select(URI.create(url)). +// +// On success returns 0. *outCount is the number of DIRECT/HTTP/SOCKS +// entries. *outProxies is an array of AndroidProxyInfo allocated via +// malloc; the caller must release it via AndroidCryptoNative_FreeProxyResult. +// DIRECT entries represent Java's Proxy.NO_PROXY / Proxy.Type.DIRECT and have +// NULL host and port 0. +// +// JNI exceptions while constructing the URI or resolving the proxy list are +// treated as "no proxy" and the function returns success with outCount == 0. +// JNI exceptions while reading an individual proxy entry are cleared and that +// entry is skipped; previously read entries may still be returned. +// +// On result-array allocation failure returns -1. Host-string allocation uses +// xmalloc via AllocateString and aborts on allocation failure. +PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, + int32_t* outCount, + AndroidProxyInfo** outProxies); + +PALEXPORT void AndroidCryptoNative_FreeProxyResult(AndroidProxyInfo* proxies, int32_t count); diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_sslstream.c b/src/native/libs/System.Security.Cryptography.Native.Android/pal_sslstream.c index c45e12b1319d6e..34737f6e2031a0 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_sslstream.c +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_sslstream.c @@ -30,8 +30,6 @@ struct ApplicationProtocolData_t int32_t length; }; -ARGS_NON_NULL(1) static uint16_t* AllocateString(JNIEnv* env, jstring source); - ARGS_NON_NULL_ALL static PAL_SSLStreamStatus DoHandshake(JNIEnv* env, SSLStream* sslStream); ARGS_NON_NULL_ALL static PAL_SSLStreamStatus DoWrap(JNIEnv* env, SSLStream* sslStream, int* handshakeStatus, int* bytesConsumed); ARGS_NON_NULL_ALL static PAL_SSLStreamStatus DoUnwrap(JNIEnv* env, SSLStream* sslStream, int* handshakeStatus); @@ -1159,19 +1157,3 @@ bool AndroidCryptoNative_SSLStreamShutdown(SSLStream* sslStream) PAL_SSLStreamStatus status = Close(env, sslStream); return status == SSLStreamStatus_Closed; } - -static uint16_t* AllocateString(JNIEnv* env, jstring source) -{ - if (source == NULL) - return NULL; - - // Length with null terminator - jsize len = (*env)->GetStringLength(env, source); - - // +1 for null terminator. - uint16_t* buffer = xmalloc(sizeof(uint16_t) * (size_t)(len + 1)); - buffer[len] = '\0'; - - (*env)->GetStringRegion(env, source, 0, len, (jchar*)buffer); - return buffer; -}