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;
-}