Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
}
}
12 changes: 11 additions & 1 deletion src/libraries/System.Net.Http/src/System.Net.Http.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -329,11 +329,21 @@
<Compile Include="System\Net\Http\SocketsHttpHandler\CurrentUserIdentityProvider.Unix.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetPlatformIdentifier)' != '' and '$(TargetPlatformIdentifier)' != 'windows' and '$(TargetPlatformIdentifier)' != 'browser' and '$(TargetPlatformIdentifier)' != 'wasi' and '$(TargetPlatformIdentifier)' != 'osx' and '$(TargetPlatformIdentifier)' != 'ios' and '$(TargetPlatformIdentifier)' != 'tvos' and '$(TargetPlatformIdentifier)' != 'maccatalyst'">
<ItemGroup Condition="'$(TargetPlatformIdentifier)' != '' and '$(TargetPlatformIdentifier)' != 'windows' and '$(TargetPlatformIdentifier)' != 'browser' and '$(TargetPlatformIdentifier)' != 'wasi' and '$(TargetPlatformIdentifier)' != 'osx' and '$(TargetPlatformIdentifier)' != 'ios' and '$(TargetPlatformIdentifier)' != 'tvos' and '$(TargetPlatformIdentifier)' != 'maccatalyst' and '$(TargetPlatformIdentifier)' != 'android'">
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpNoProxy.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\SystemProxyInfo.Unix.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'android'">
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpNoProxy.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\SystemProxyInfo.Android.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\AndroidPlatformProxy.Android.cs" />
<Compile Include="$(CommonPath)Interop\Android\Interop.Libraries.cs"
Link="Common\Interop\Android\Interop.Libraries.cs" />
<Compile Include="$(CommonPath)Interop\Android\System.Security.Cryptography.Native.Android\Interop.Proxy.cs"
Link="Common\Interop\Android\System.Security.Cryptography.Native.Android\Interop.Proxy.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'osx' or '$(TargetPlatformIdentifier)' == 'ios' or '$(TargetPlatformIdentifier)' == 'tvos' or '$(TargetPlatformIdentifier)' == 'maccatalyst'">
<Compile Include="System\Net\Http\SocketsHttpHandler\SystemProxyInfo.OSX.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\MacProxy.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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
{
/// <summary>
/// An <see cref="IWebProxy"/> that defers proxy resolution to the
/// Android platform via <c>java.net.ProxySelector</c>. Honors Wi-Fi
/// proxy, MDM-deployed system proxy, PAC scripts, and per-network
/// or VPN proxies — the same source <c>java.net.HttpURLConnection</c>
/// consults.
/// </summary>
/// <remarks>
/// <para>
/// The Android proxy APIs (<c>ProxySelector</c>, <c>ProxyInfo</c>,
/// <c>ConnectivityManager</c>) do not surface credentials. The
/// <see cref="Credentials"/> property is required by
/// <see cref="IWebProxy"/> but is never populated by this class.
/// Apps that need to authenticate to a system-detected proxy should
/// set <c>HttpClientHandler.DefaultProxyCredentials</c> (or
/// <c>SocketsHttpHandler.DefaultProxyCredentials</c>) — the same
/// behavior as on macOS and Windows.
/// </para>
/// </remarks>
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;
}
Comment thread
simonrozsival marked this conversation as resolved.

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++)
{
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))
{
continue;
}

return new UriBuilder(scheme, host, entry.Port).Uri;
}

return null;
}
finally
{
Interop.AndroidCrypto.FreeProxyResult(proxies, count);
}
}

// 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;
}
}
Original file line number Diff line number Diff line change
@@ -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();
Comment thread
simonrozsival marked this conversation as resolved.
}
Comment thread
simonrozsival marked this conversation as resolved.

// Feature switch: System.Net.Http.UseAndroidSystemProxy
//
// Defaults to true. Apps may opt out (set to "false" via
// <RuntimeHostConfigurationOption>) 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")]
Comment thread
simonrozsival marked this conversation as resolved.
private static bool UseAndroidSystemProxy { get; } =
RuntimeSettingParser.QueryRuntimeSettingSwitch(
"System.Net.Http.UseAndroidSystemProxy",
defaultValue: true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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;

Expand Down
Loading
Loading