From 392726f76a4e60a3fa1963100523b543850501eb Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 11 May 2026 13:17:36 +0200 Subject: [PATCH 1/5] [Android] Bridge SocketsHttpHandler to java.net.ProxySelector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an AndroidPlatformProxy : IWebProxy that resolves the system proxy chain for SocketsHttpHandler on Android by JNI-calling java.net.ProxySelector.getDefault().select(URI.create(url)). This brings the Android system proxy (Wi-Fi proxy, MDM-deployed proxy, PAC scripts, per-network and VPN ProxyInfo) to the managed HTTP stack, matching the behavior HttpURLConnection / AndroidMessageHandler exhibit today. Closes the largest compatibility regression apps would face when flipping the default HttpClientHandler backend from AndroidMessageHandler to SocketsHttpHandler on net*-android*. Implementation lives entirely in dotnet/runtime — no dependency on dotnet/android, no [UnsafeAccessor] into Mono.Android. Native side (System.Security.Cryptography.Native.Android): * pal_proxy.{h,c}: AndroidCryptoNative_GetProxyForUrl / AndroidCryptoNative_FreeProxyResult. Pure C/JNI using the existing pal_jni.h INIT_LOCALS / RELEASE_LOCALS / ON_EXCEPTION_PRINT_AND_GOTO macros for leak-safe local-ref handling on all exception paths. * pal_jni.{h,c}: cached jclass/jmethodID/jfieldID globals for java.net.ProxySelector, Proxy, Proxy$Type, InetSocketAddress, URI, and java.util.List. * CMakeLists.txt: pal_proxy.c added to NATIVECRYPTO_SOURCES. Managed side (System.Net.Http): * AndroidPlatformProxy.Android.cs: IWebProxy implementation that P/Invokes the PAL, takes the first non-DIRECT entry, and falls back to direct on any failure. Mirrors MacProxy in shape. * SystemProxyInfo.Android.cs: env vars first (HttpEnvironmentProxy), then AndroidPlatformProxy, then HttpNoProxy. Gated by the FeatureSwitchDefinition System.Net.Http.UseAndroidSystemProxy (default true). * Interop.Proxy.cs: [LibraryImport] declarations. * csproj: new ItemGroup for TargetPlatformIdentifier=android that replaces SystemProxyInfo.Unix.cs with the Android-specific files. Credentials note: AndroidPlatformProxy.Credentials is required by the IWebProxy contract but is never populated by this class — Android's proxy APIs do not surface credentials. Users authenticate to system proxies via HttpClientHandler.DefaultProxyCredentials, the same as on macOS (MacProxy has the same limitation, tracked as #24799). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Interop.Proxy.cs | 38 ++++ .../src/System.Net.Http.csproj | 12 +- .../AndroidPlatformProxy.Android.cs | 85 ++++++++ .../SystemProxyInfo.Android.cs | 37 ++++ .../CMakeLists.txt | 1 + .../pal_jni.c | 52 +++++ .../pal_jni.h | 29 +++ .../pal_proxy.c | 195 ++++++++++++++++++ .../pal_proxy.h | 39 ++++ 9 files changed, 487 insertions(+), 1 deletion(-) create mode 100644 src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Proxy.cs create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AndroidPlatformProxy.Android.cs create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SystemProxyInfo.Android.cs create mode 100644 src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.c create mode 100644 src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.h 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..753ce7d6c1f6c1 --- /dev/null +++ b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Proxy.cs @@ -0,0 +1,38 @@ +// 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 + { + Http = 0, + Socks = 1, + } + + [StructLayout(LayoutKind.Sequential)] + internal struct AndroidProxyInfo + { + public int Type; // AndroidProxyType + public IntPtr Host; // NUL-terminated UTF-8, malloc'd in native + 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 06c5430d2db5c7..c82c1d590bde83 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..45b523c45a8ba0 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AndroidPlatformProxy.Android.cs @@ -0,0 +1,85 @@ +// 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 || count == 0 || proxies == null) + { + return null; + } + + try + { + // ProxySelector returns entries in preference order. We take the first one. + // 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++) + { + Interop.AndroidCrypto.AndroidProxyInfo entry = proxies[i]; + string scheme = entry.Type == (int)Interop.AndroidCrypto.AndroidProxyType.Socks + ? "socks5" + : "http"; + + string? host = Marshal.PtrToStringUTF8(entry.Host); + if (string.IsNullOrEmpty(host)) + { + continue; + } + + return new UriBuilder(scheme, host, entry.Port).Uri; + } + + return null; + } + finally + { + Interop.AndroidCrypto.FreeProxyResult(proxies, count); + } + } + + public bool IsBypassed(Uri host) + { + ArgumentNullException.ThrowIfNull(host); + // Computing the real answer is as expensive as GetProxy; mirror MacProxy. + Uri? proxyUri = GetProxy(host); + return proxyUri is null || Equals(proxyUri, host); + } + } +} 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..86743b46d9fcbc --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SystemProxyInfo.Android.cs @@ -0,0 +1,37 @@ +// 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(); + } + + [FeatureSwitchDefinition("System.Net.Http.UseAndroidSystemProxy")] + private static bool UseAndroidSystemProxy => + RuntimeSettingParser.QueryRuntimeSettingSwitch( + "System.Net.Http.UseAndroidSystemProxy", + defaultValue: true); + } +} 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..c9f1eaf913f779 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,35 @@ 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_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; + +// java/util/List +jclass g_List; +jmethodID g_ListSize; +jmethodID g_ListGet; + jobject ToGRef(JNIEnv *env, jobject lref) { if (lref) @@ -1083,5 +1112,28 @@ 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_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;"); + + g_List = GetClassGRef(env, "java/util/List"); + g_ListSize = GetMethod(env, false, g_List, "size", "()I"); + g_ListGet = GetMethod(env, false, g_List, "get", "(I)Ljava/lang/Object;"); + 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..7b66476c13c3cb 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,35 @@ 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_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; + +// java/util/List +extern jclass g_List; +extern jmethodID g_ListSize; +extern jmethodID g_ListGet; + // Compatibility macros #if !defined (__mallocfunc) #if defined (__clang__) || defined (__GNUC__) 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..d2a15fc5d450ed --- /dev/null +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.c @@ -0,0 +1,195 @@ +// 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 +#include + +// Copy a Java String into a NUL-terminated UTF-8 heap buffer. +// Returns NULL on allocation failure or on JNI exception / empty input. +static char* CopyJStringToUtf8(JNIEnv* env, jstring s) +{ + if (s == NULL) + return NULL; + + jsize charLen = (*env)->GetStringLength(env, s); + jsize byteLen = (*env)->GetStringUTFLength(env, s); + if (byteLen <= 0) + return NULL; + + char* buf = (char*)malloc((size_t)byteLen + 1); + if (buf == NULL) + return NULL; + + (*env)->GetStringUTFRegion(env, s, 0, charLen, buf); + if (TryClearJNIExceptions(env)) + { + free(buf); + return NULL; + } + + buf[byteLen] = '\0'; + return buf; +} + +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, + 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]); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + // ProxySelector.getDefault() — VM-wide singleton; may throw SecurityException. + loc[jselector] = (*env)->CallStaticObjectMethod(env, g_ProxySelector, g_ProxySelector_getDefault); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + if (loc[jselector] == NULL) + goto cleanup; + + // ProxySelector.select(uri) + loc[jlist] = (*env)->CallObjectMethod(env, loc[jselector], g_ProxySelector_select, loc[juri]); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + if (loc[jlist] == NULL) + goto cleanup; + + // Resolve the Proxy.Type enum constants for IsSameObject comparisons. + loc[jproxyTypeHttp] = (*env)->GetStaticObjectField(env, g_ProxyType, g_ProxyType_HTTP); + loc[jproxyTypeSocks] = (*env)->GetStaticObjectField(env, g_ProxyType, g_ProxyType_SOCKS); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + jint n = (*env)->CallIntMethod(env, loc[jlist], g_ListSize); + ON_EXCEPTION_PRINT_AND_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 (CheckJNIExceptions(env) || iter[jproxy] == NULL) + { + RELEASE_LOCALS(iter, env); + continue; + } + + iter[jtype] = (*env)->CallObjectMethod(env, iter[jproxy], g_ProxyType_method); + if (CheckJNIExceptions(env) || iter[jtype] == NULL) + { + RELEASE_LOCALS(iter, env); + continue; + } + + int32_t type; + if ((*env)->IsSameObject(env, iter[jtype], loc[jproxyTypeHttp])) + { + type = ANDROID_PROXY_TYPE_HTTP; + } + else if ((*env)->IsSameObject(env, iter[jtype], loc[jproxyTypeSocks])) + { + type = ANDROID_PROXY_TYPE_SOCKS; + } + else + { + // DIRECT or unknown — no result entry. + RELEASE_LOCALS(iter, env); + continue; + } + + iter[jaddr] = (*env)->CallObjectMethod(env, iter[jproxy], g_Proxy_address); + if (CheckJNIExceptions(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 (CheckJNIExceptions(env) || iter[jhost] == NULL) + { + RELEASE_LOCALS(iter, env); + continue; + } + + jint port = (*env)->CallIntMethod(env, iter[jaddr], g_InetSocketAddress_getPort); + if (CheckJNIExceptions(env)) + { + RELEASE_LOCALS(iter, env); + continue; + } + + char* host = CopyJStringToUtf8(env, (jstring)iter[jhost]); + if (host == NULL) + { + RELEASE_LOCALS(iter, env); + continue; + } + + result[written].type = type; + result[written].host = host; // ownership transferred to result; lifetime ≥ jhost + result[written].port = (int32_t)port; + written++; + + RELEASE_LOCALS(iter, env); + } + +cleanup: + RELEASE_LOCALS(loc, env); + + if (ret == 0) + { + *outCount = written; + *outProxies = result; + } + else if (result != NULL) + { + // Error path: free anything we may have partially populated. + 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..45eecaa855b004 --- /dev/null +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.h @@ -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. + +#pragma once + +#include "pal_compiler.h" +#include "pal_jni.h" +#include "pal_types.h" + +typedef enum +{ + ANDROID_PROXY_TYPE_HTTP = 0, + ANDROID_PROXY_TYPE_SOCKS = 1, +} AndroidProxyType; + +typedef struct +{ + int32_t type; // AndroidProxyType + char* host; // NUL-terminated UTF-8, malloc'd; 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 HTTP/SOCKS entries +// (DIRECT entries are skipped). *outProxies is an array of +// AndroidProxyInfo allocated via malloc; the caller must release it +// via AndroidCryptoNative_FreeProxyResult. +// +// Any JNI exception (malformed URI, SecurityException, ...) is treated +// as "no proxy" and the function returns success with outCount == 0. +// +// On allocation failure returns -1. +PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, + int32_t* outCount, + AndroidProxyInfo** outProxies); + +PALEXPORT void AndroidCryptoNative_FreeProxyResult(AndroidProxyInfo* proxies, int32_t count); From 6fdfd39a5b80e817ebdb4690d1e40753d81a97a2 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 11 May 2026 13:36:19 +0200 Subject: [PATCH 2/5] Address review feedback on AndroidPlatformProxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * IsBypassed: always return false (HttpWindowsProxy:349–360 pattern). The previous double-call-into-GetProxy was wasteful — SHH's contract is 'IsBypassed first; if false, call GetProxy', so returning false unconditionally results in exactly one JNI round-trip per origin. * Return hostnames as UTF-16, not modified UTF-8. Promotes the existing pal_sslstream.c::AllocateString helper to a shared pal_jni helper (pal_eckey.c and pal_x509chain.c also follow this UTF-16 pattern). Managed side reads via Marshal.PtrToStringUni which is a zero-conversion copy because System.String is internally UTF-16; modified UTF-8 from GetStringUTFRegion would have required an Encoding.UTF8.GetString allocation and was not standard UTF-8 anyway. * Document the SOCKS5 transport-level proxy choice inline. * Document why UseAndroidSystemProxy is a feature switch (back-compat for apps previously on SocketsHttpHandler, trimming, testing). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Interop.Proxy.cs | 2 +- .../AndroidPlatformProxy.Android.cs | 26 ++++++---- .../SystemProxyInfo.Android.cs | 15 ++++++ .../pal_jni.c | 15 ++++++ .../pal_jni.h | 10 ++++ .../pal_proxy.c | 49 +++++-------------- .../pal_proxy.h | 8 +-- .../pal_sslstream.c | 18 ------- 8 files changed, 75 insertions(+), 68 deletions(-) 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 index 753ce7d6c1f6c1..9793765bd969aa 100644 --- 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 @@ -18,7 +18,7 @@ internal enum AndroidProxyType internal struct AndroidProxyInfo { public int Type; // AndroidProxyType - public IntPtr Host; // NUL-terminated UTF-8, malloc'd in native + public IntPtr Host; // NUL-terminated UTF-16; read via Marshal.PtrToStringUni; freed by FreeProxyResult public int Port; } 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 index 45b523c45a8ba0..d8abb33a41f554 100644 --- 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 @@ -46,18 +46,26 @@ internal sealed class AndroidPlatformProxy : IWebProxy try { - // ProxySelector returns entries in preference order. We take the first one. + // 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++) { Interop.AndroidCrypto.AndroidProxyInfo entry = proxies[i]; + + // 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 = entry.Type == (int)Interop.AndroidCrypto.AndroidProxyType.Socks ? "socks5" : "http"; - string? host = Marshal.PtrToStringUTF8(entry.Host); + // 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)) { continue; @@ -74,12 +82,12 @@ internal sealed class AndroidPlatformProxy : IWebProxy } } - public bool IsBypassed(Uri host) - { - ArgumentNullException.ThrowIfNull(host); - // Computing the real answer is as expensive as GetProxy; mirror MacProxy. - Uri? proxyUri = GetProxy(host); - return proxyUri is null || Equals(proxyUri, host); - } + // 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 index 86743b46d9fcbc..de4e87a6c03264 100644 --- 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 @@ -28,6 +28,21 @@ public static IWebProxy ConstructSystemProxy() 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 => RuntimeSettingParser.QueryRuntimeSettingSwitch( 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 c9f1eaf913f779..0b7d2bad7dbb10 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 @@ -707,6 +707,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; 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 7b66476c13c3cb..8e58a8e529d7a2 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 @@ -611,6 +611,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 index d2a15fc5d450ed..d0bbc647dc662f 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.c +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.c @@ -4,34 +4,6 @@ #include "pal_proxy.h" #include -#include - -// Copy a Java String into a NUL-terminated UTF-8 heap buffer. -// Returns NULL on allocation failure or on JNI exception / empty input. -static char* CopyJStringToUtf8(JNIEnv* env, jstring s) -{ - if (s == NULL) - return NULL; - - jsize charLen = (*env)->GetStringLength(env, s); - jsize byteLen = (*env)->GetStringUTFLength(env, s); - if (byteLen <= 0) - return NULL; - - char* buf = (char*)malloc((size_t)byteLen + 1); - if (buf == NULL) - return NULL; - - (*env)->GetStringUTFRegion(env, s, 0, charLen, buf); - if (TryClearJNIExceptions(env)) - { - free(buf); - return NULL; - } - - buf[byteLen] = '\0'; - return buf; -} PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, int32_t* outCount, @@ -51,8 +23,8 @@ PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, 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. + // 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, jproxyTypeHttp, jproxyTypeSocks); @@ -117,6 +89,11 @@ PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, } 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 @@ -149,15 +126,13 @@ PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, continue; } - char* host = CopyJStringToUtf8(env, (jstring)iter[jhost]); - if (host == NULL) - { - 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 result; lifetime ≥ jhost + result[written].host = host; // ownership transferred to the result; lifetime ≥ jhost result[written].port = (int32_t)port; written++; 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 index 45eecaa855b004..d9bc583c75f5b3 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.h +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.h @@ -15,9 +15,11 @@ typedef enum typedef struct { - int32_t type; // AndroidProxyType - char* host; // NUL-terminated UTF-8, malloc'd; freed via AndroidCryptoNative_FreeProxyResult - int32_t port; + 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 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; -} From 9123b9d74cb8ec6304e40a1f61c7190922ca99a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Rozs=C3=ADval?= Date: Mon, 11 May 2026 14:24:07 +0200 Subject: [PATCH 3/5] Address review feedback - Remove duplicate java.util.List JNI cache (g_List/g_ListSize/g_ListGet); reuse existing g_ListClass/g_ListGet and g_CollectionSize. - pal_proxy: free result and report outProxies = NULL when no proxy entries survive filtering (DIRECT-only / unknown types), matching the managed contract that count == 0 implies a null buffer. - AndroidPlatformProxy.GetProxy: always free the native result via try/finally whenever proxies != null (defense in depth against the count == 0 case). - SystemProxyInfo.Android: cache UseAndroidSystemProxy in a static get-only property initializer so the runtime switch is read once and can participate in trimming / JIT constant propagation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SocketsHttpHandler/AndroidPlatformProxy.Android.cs | 2 +- .../Http/SocketsHttpHandler/SystemProxyInfo.Android.cs | 2 +- .../pal_jni.c | 9 --------- .../pal_jni.h | 5 ----- .../pal_proxy.c | 8 +++++--- 5 files changed, 7 insertions(+), 19 deletions(-) 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 index d8abb33a41f554..2b493cd98c8ce2 100644 --- 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 @@ -39,7 +39,7 @@ internal sealed class AndroidPlatformProxy : IWebProxy int count = 0; int rc = Interop.AndroidCrypto.GetProxyForUrl(url, out count, out proxies); - if (rc != 0 || count == 0 || proxies == null) + if (rc != 0 || proxies == null) { return null; } 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 index de4e87a6c03264..72cca1908830e6 100644 --- 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 @@ -44,7 +44,7 @@ public static IWebProxy ConstructSystemProxy() // code referenced only by other paths. // * Testing / debugging where pure env-var behavior is required. [FeatureSwitchDefinition("System.Net.Http.UseAndroidSystemProxy")] - private static bool UseAndroidSystemProxy => + private static bool UseAndroidSystemProxy { get; } = RuntimeSettingParser.QueryRuntimeSettingSwitch( "System.Net.Http.UseAndroidSystemProxy", defaultValue: true); 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 0b7d2bad7dbb10..ab12141b6eed60 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 @@ -513,11 +513,6 @@ jmethodID g_InetSocketAddress_getPort; jclass g_URI; jmethodID g_URI_create; -// java/util/List -jclass g_List; -jmethodID g_ListSize; -jmethodID g_ListGet; - jobject ToGRef(JNIEnv *env, jobject lref) { if (lref) @@ -1146,9 +1141,5 @@ jint AndroidCryptoNative_InitLibraryOnLoad (JavaVM *vm, void *reserved) g_URI = GetClassGRef(env, "java/net/URI"); g_URI_create = GetMethod(env, true, g_URI, "create", "(Ljava/lang/String;)Ljava/net/URI;"); - g_List = GetClassGRef(env, "java/util/List"); - g_ListSize = GetMethod(env, false, g_List, "size", "()I"); - g_ListGet = GetMethod(env, false, g_List, "get", "(I)Ljava/lang/Object;"); - 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 8e58a8e529d7a2..0639ce22d21407 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 @@ -527,11 +527,6 @@ extern jmethodID g_InetSocketAddress_getPort; extern jclass g_URI; extern jmethodID g_URI_create; -// java/util/List -extern jclass g_List; -extern jmethodID g_ListSize; -extern jmethodID g_ListGet; - // Compatibility macros #if !defined (__mallocfunc) #if defined (__clang__) || defined (__GNUC__) 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 index d0bbc647dc662f..6e8c03b386e847 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.c +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.c @@ -51,7 +51,7 @@ PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, loc[jproxyTypeSocks] = (*env)->GetStaticObjectField(env, g_ProxyType, g_ProxyType_SOCKS); ON_EXCEPTION_PRINT_AND_GOTO(cleanup); - jint n = (*env)->CallIntMethod(env, loc[jlist], g_ListSize); + jint n = (*env)->CallIntMethod(env, loc[jlist], g_CollectionSize); ON_EXCEPTION_PRINT_AND_GOTO(cleanup); if (n <= 0) goto cleanup; @@ -142,14 +142,16 @@ PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, cleanup: RELEASE_LOCALS(loc, env); - if (ret == 0) + if (ret == 0 && written > 0) { *outCount = written; *outProxies = result; } else if (result != NULL) { - // Error path: free anything we may have partially populated. + // Either an error path or no proxy entries survived filtering (DIRECT-only, + // unknown types, 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); From 927bfbd4e53b867d747206e56b8ff2cd0e8d1582 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 27 May 2026 12:43:34 +0200 Subject: [PATCH 4/5] Preserve Android DIRECT proxy results Return Java Proxy.Type.DIRECT entries from the Android proxy PAL and handle them explicitly in AndroidPlatformProxy so ProxySelector ordering is preserved. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Interop.Proxy.cs | 5 ++-- .../AndroidPlatformProxy.Android.cs | 23 ++++++++++++++--- .../pal_jni.c | 8 +++--- .../pal_jni.h | 1 + .../pal_proxy.c | 25 +++++++++++++------ .../pal_proxy.h | 14 ++++++----- 6 files changed, 55 insertions(+), 21 deletions(-) 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 index 9793765bd969aa..0d755b5d1e830c 100644 --- 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 @@ -10,8 +10,9 @@ internal static partial class AndroidCrypto { internal enum AndroidProxyType { - Http = 0, - Socks = 1, + Direct = 0, + Http = 1, + Socks = 2, } [StructLayout(LayoutKind.Sequential)] 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 index 2b493cd98c8ce2..dd919fddf2fce0 100644 --- 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 @@ -53,15 +53,32 @@ internal sealed class AndroidPlatformProxy : IWebProxy for (int i = 0; i < count; i++) { Interop.AndroidCrypto.AndroidProxyInfo entry = proxies[i]; + 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 null; + } // 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 = entry.Type == (int)Interop.AndroidCrypto.AndroidProxyType.Socks - ? "socks5" - : "http"; + string? scheme = type switch + { + Interop.AndroidCrypto.AndroidProxyType.Http => "http", + Interop.AndroidCrypto.AndroidProxyType.Socks => "socks5", + _ => null, + }; + + if (scheme is null) + { + continue; + } // The native PAL allocates the host as NUL-terminated UTF-16 // (Marshal.PtrToStringUni is a zero-conversion copy). 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 ab12141b6eed60..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 @@ -501,6 +501,7 @@ jmethodID g_Proxy_address; // java/net/Proxy$Type jclass g_ProxyType; +jfieldID g_ProxyType_DIRECT; jfieldID g_ProxyType_HTTP; jfieldID g_ProxyType_SOCKS; @@ -1130,9 +1131,10 @@ jint AndroidCryptoNative_InitLibraryOnLoad (JavaVM *vm, void *reserved) 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_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_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;"); 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 0639ce22d21407..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 @@ -515,6 +515,7 @@ 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; 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 index 6e8c03b386e847..abf7a86bbbf92a 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.c +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.c @@ -26,7 +26,7 @@ PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, // 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, - jproxyTypeHttp, jproxyTypeSocks); + jproxyTypeDirect, jproxyTypeHttp, jproxyTypeSocks); loc[jurl] = make_java_string(env, urlUtf8); // aborts on OOM @@ -47,8 +47,9 @@ PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, goto cleanup; // Resolve the Proxy.Type enum constants for IsSameObject comparisons. - loc[jproxyTypeHttp] = (*env)->GetStaticObjectField(env, g_ProxyType, g_ProxyType_HTTP); - loc[jproxyTypeSocks] = (*env)->GetStaticObjectField(env, g_ProxyType, g_ProxyType_SOCKS); + 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); ON_EXCEPTION_PRINT_AND_GOTO(cleanup); jint n = (*env)->CallIntMethod(env, loc[jlist], g_CollectionSize); @@ -83,7 +84,17 @@ PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, } int32_t type; - if ((*env)->IsSameObject(env, iter[jtype], loc[jproxyTypeHttp])) + 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; } @@ -98,7 +109,7 @@ PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, } else { - // DIRECT or unknown — no result entry. + // Unknown proxy type: no result entry. RELEASE_LOCALS(iter, env); continue; } @@ -149,8 +160,8 @@ PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, } else if (result != NULL) { - // Either an error path or no proxy entries survived filtering (DIRECT-only, - // unknown types, etc). Free anything we may have partially populated so that + // 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); 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 index d9bc583c75f5b3..f7c1fbc56fe422 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.h +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.h @@ -9,8 +9,9 @@ typedef enum { - ANDROID_PROXY_TYPE_HTTP = 0, - ANDROID_PROXY_TYPE_SOCKS = 1, + ANDROID_PROXY_TYPE_DIRECT = 0, + ANDROID_PROXY_TYPE_HTTP = 1, + ANDROID_PROXY_TYPE_SOCKS = 2, } AndroidProxyType; typedef struct @@ -25,10 +26,11 @@ typedef struct // 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 HTTP/SOCKS entries -// (DIRECT entries are skipped). *outProxies is an array of -// AndroidProxyInfo allocated via malloc; the caller must release it -// via AndroidCryptoNative_FreeProxyResult. +// 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. // // Any JNI exception (malformed URI, SecurityException, ...) is treated // as "no proxy" and the function returns success with outCount == 0. From 5b9611a5ac52406ca0012866420bb3f775df874d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 27 May 2026 13:18:18 +0200 Subject: [PATCH 5/5] Address Android proxy review feedback Move Android-specific proxy tests to an Android-only test file and document/directly handle Java Proxy.Type.DIRECT semantics while avoiding JNI exception logging during proxy resolution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AndroidPlatformProxy.Android.cs | 81 +++++++++++-------- .../System.Net.Http.Unit.Tests.csproj | 13 ++- .../UnitTests/SystemProxyInfoTest.Android.cs | 69 ++++++++++++++++ .../tests/UnitTests/SystemProxyInfoTest.cs | 2 +- .../pal_proxy.c | 25 +++--- .../pal_proxy.h | 9 ++- 6 files changed, 149 insertions(+), 50 deletions(-) create mode 100644 src/libraries/System.Net.Http/tests/UnitTests/SystemProxyInfoTest.Android.cs 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 index dd919fddf2fce0..4727144cbcaad8 100644 --- 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 @@ -52,43 +52,10 @@ internal sealed class AndroidPlatformProxy : IWebProxy // connect error rather than an automatic retry. for (int i = 0; i < count; i++) { - Interop.AndroidCrypto.AndroidProxyInfo entry = proxies[i]; - 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 null; - } - - // 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) - { - continue; - } - - // 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)) + if (TryCreateProxyUri(proxies[i], out Uri? proxyUri)) { - continue; + return proxyUri; } - - return new UriBuilder(scheme, host, entry.Port).Uri; } return null; @@ -99,6 +66,50 @@ internal sealed class AndroidPlatformProxy : IWebProxy } } + 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. 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 289c7e4503ac15..adf16aa2f927da 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 @@ -7,6 +7,9 @@ false + + + @@ -251,8 +254,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/pal_proxy.c b/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.c index abf7a86bbbf92a..f915b6c4e041b0 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.c +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.c @@ -32,17 +32,20 @@ PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, // URI.create(url) — IllegalArgumentException for malformed input. loc[juri] = (*env)->CallStaticObjectMethod(env, g_URI, g_URI_create, loc[jurl]); - ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + if (TryClearJNIExceptions(env)) + goto cleanup; // ProxySelector.getDefault() — VM-wide singleton; may throw SecurityException. loc[jselector] = (*env)->CallStaticObjectMethod(env, g_ProxySelector, g_ProxySelector_getDefault); - ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + 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]); - ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + if (TryClearJNIExceptions(env)) + goto cleanup; if (loc[jlist] == NULL) goto cleanup; @@ -50,10 +53,12 @@ PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, 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); - ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + if (TryClearJNIExceptions(env)) + goto cleanup; jint n = (*env)->CallIntMethod(env, loc[jlist], g_CollectionSize); - ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + if (TryClearJNIExceptions(env)) + goto cleanup; if (n <= 0) goto cleanup; @@ -70,14 +75,14 @@ PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, INIT_LOCALS(iter, jproxy, jtype, jaddr, jhost); iter[jproxy] = (*env)->CallObjectMethod(env, loc[jlist], g_ListGet, i); - if (CheckJNIExceptions(env) || iter[jproxy] == NULL) + if (TryClearJNIExceptions(env) || iter[jproxy] == NULL) { RELEASE_LOCALS(iter, env); continue; } iter[jtype] = (*env)->CallObjectMethod(env, iter[jproxy], g_ProxyType_method); - if (CheckJNIExceptions(env) || iter[jtype] == NULL) + if (TryClearJNIExceptions(env) || iter[jtype] == NULL) { RELEASE_LOCALS(iter, env); continue; @@ -115,7 +120,7 @@ PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, } iter[jaddr] = (*env)->CallObjectMethod(env, iter[jproxy], g_Proxy_address); - if (CheckJNIExceptions(env) + if (TryClearJNIExceptions(env) || iter[jaddr] == NULL || !(*env)->IsInstanceOf(env, iter[jaddr], g_InetSocketAddress)) { @@ -124,14 +129,14 @@ PALEXPORT int32_t AndroidCryptoNative_GetProxyForUrl(const char* urlUtf8, } iter[jhost] = (*env)->CallObjectMethod(env, iter[jaddr], g_InetSocketAddress_getHostString); - if (CheckJNIExceptions(env) || iter[jhost] == NULL) + if (TryClearJNIExceptions(env) || iter[jhost] == NULL) { RELEASE_LOCALS(iter, env); continue; } jint port = (*env)->CallIntMethod(env, iter[jaddr], g_InetSocketAddress_getPort); - if (CheckJNIExceptions(env)) + if (TryClearJNIExceptions(env)) { RELEASE_LOCALS(iter, env); continue; 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 index f7c1fbc56fe422..2e0293cce7d4ee 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.h +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_proxy.h @@ -32,10 +32,13 @@ typedef struct // DIRECT entries represent Java's Proxy.NO_PROXY / Proxy.Type.DIRECT and have // NULL host and port 0. // -// Any JNI exception (malformed URI, SecurityException, ...) is treated -// as "no proxy" and the function returns success with outCount == 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 allocation failure returns -1. +// 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);