Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -29,10 +29,7 @@ namespace PlaywrightNative.Tests
{
/// <summary>
/// Official <c>library/browsercontext-proxy.spec.ts</c> parity.
/// Skipped (Node-only channel type validation):
/// <c>should throw for bad server value</c> (<c>proxy.server: expected
/// string, got number</c>; C# <see cref="Proxy.Server"/> is already a
/// string). Do not edit leftover <c>ContextProxyTests</c>,
/// Do not edit leftover <c>ContextProxyTests</c>,
/// <c>ContextProxyAuthTests</c>, or <c>LoopbackHttpProxy</c>.
/// </summary>
[TestFixture]
Expand Down Expand Up @@ -167,6 +164,24 @@ public async Task ShouldWorkWhenPassingTheProxyOnlyOnTheContextLevel()
}
}

[PlaywrightTest("browsercontext-proxy.spec.ts", "should ignore legacy 'per-context' launch proxy")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
public Task ShouldIgnoreLegacyPerContextLaunchProxy()
=> ShouldIgnoreLegacyLaunchProxyAsync("per-context");

[PlaywrightTest("browsercontext-proxy.spec.ts", "should ignore legacy 'http://per-context' launch proxy")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
public Task ShouldIgnoreLegacyHttpPerContextLaunchProxy()
=> ShouldIgnoreLegacyLaunchProxyAsync("http://per-context");

[PlaywrightTest("browsercontext-proxy.spec.ts", "should throw for bad server value")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
public void ShouldThrowForBadServerValue()
=> Assert.Ignore("Proxy.Server is a string in C#, so a numeric server value cannot be passed.");

[PlaywrightTest("browsercontext-proxy.spec.ts", "should use proxy")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
Expand Down Expand Up @@ -768,6 +783,34 @@ private async Task ProxyLocalNetworkAsync(string target, bool additionalBypass)
private string[] NonFaviconUrls()
=> _proxy.RequestUrls.Where(url => url.IndexOf("favicon", StringComparison.Ordinal) < 0).ToArray();

private async Task ShouldIgnoreLegacyLaunchProxyAsync(string launchProxy)
{
EnsureServer();
_proxy.ForwardTo(ServerPort, allowConnectRequests: true);
IBrowser browser = await BrowserLauncher.LaunchAsync(proxy: new Proxy { Server = launchProxy }).ConfigureAwait(false);
try
{
IBrowserContext context = await browser.NewContextAsync(new() { Proxy = new Proxy { Server = _proxy.Host } }).ConfigureAwait(false);
IPage page = await context.NewPageAsync().ConfigureAwait(false);
await page.GoToAsync("http://non-existent.com/target.html").ConfigureAwait(false);
Assert.That(await page.TitleAsync().ConfigureAwait(false), Is.EqualTo("Served by the proxy"));
IAPIResponse response = await context.APIRequest.GetAsync("http://non-existent.com/target.html").ConfigureAwait(false);
Assert.That(await response.TextAsync().ConfigureAwait(false), Does.Contain("Served by the proxy"));
Assert.That(_proxy.ConnectHosts, Does.Contain("non-existent.com:80"));

IBrowserContext directContext = await browser.NewContextAsync().ConfigureAwait(false);
IPage directPage = await directContext.NewPageAsync().ConfigureAwait(false);
await directPage.GoToAsync(Prefix + "/target.html").ConfigureAwait(false);
Assert.That(await directPage.TitleAsync().ConfigureAwait(false), Is.EqualTo("Served by the proxy"));
IAPIResponse directResponse = await directContext.APIRequest.GetAsync(Prefix + "/target.html").ConfigureAwait(false);
Assert.That(directResponse.Ok, Is.True);
}
finally
{
await DisposeQuietlyAsync(browser).ConfigureAwait(false);
}
}

private static async Task<Exception> CatchGotoAsync(IPage page, string url)
{
try
Expand Down
35 changes: 35 additions & 0 deletions src/PlaywrightNative.Tests/LibraryClientCertificatesParityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,41 @@ public async Task BrowserShouldPassWithMatchingCertificatesAndWhenAHttpProxyIsUs
}
}

[PlaywrightTest("client-certificates.spec.ts", "should respect launch proxy and proxy bypass")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
public async Task BrowserShouldRespectLaunchProxyAndProxyBypass()
{
await using OfficialClientCertificateServer server = await StartCcServerAsync()
.ConfigureAwait(false);
Uri serverUri = new Uri(server.Url);
await using OfficialTestProxy proxyServer = new OfficialTestProxy();
proxyServer.ForwardTo(serverUri.Port, allowConnectRequests: true);
string proxy = "localhost:" + proxyServer.Port.ToString(CultureInfo.InvariantCulture);
string serverHost = ProxiedConnectHost + ":" + serverUri.Port.ToString(CultureInfo.InvariantCulture);
ClientCertificate[] clientCertificates = new[] { Trusted(OriginOf(server.Url)) };
await using IBrowser browser = await BrowserLauncher.LaunchAsync(proxy: new Proxy { Server = proxy }).ConfigureAwait(false);
{
IPage page = await browser.NewPageAsync(new() { IgnoreHTTPSErrors = true, ClientCertificates = clientCertificates })
.ConfigureAwait(false);
await page.GoToAsync(server.Url).ConfigureAwait(false);
await Assertions.Expect(page.GetByTestId("message")).ToHaveTextAsync(TrustedMessage).ConfigureAwait(false);
// Headed Chromium also sends its own background requests through the launch proxy.
Assert.That(proxyServer.ConnectHosts, Does.Contain(serverHost));
await page.CloseAsync().ConfigureAwait(false);
}

proxyServer.ConnectHosts = Array.Empty<string>();
{
IPage page = await browser.NewPageAsync(new() { IgnoreHTTPSErrors = true, ClientCertificates = clientCertificates, Proxy = new Proxy { Server = proxy, Bypass = serverUri.Host } })
.ConfigureAwait(false);
await page.GoToAsync(server.Url).ConfigureAwait(false);
await Assertions.Expect(page.GetByTestId("message")).ToHaveTextAsync(TrustedMessage).ConfigureAwait(false);
Assert.That(proxyServer.ConnectHosts, Does.Not.Contain(serverHost));
await page.CloseAsync().ConfigureAwait(false);
}
}

[PlaywrightTest("client-certificates.spec.ts", "should pass with matching certificates and when a socks proxy is used")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
Expand Down
50 changes: 50 additions & 0 deletions src/PlaywrightNative.Tests/LibraryFetchProxyParityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,56 @@ public async Task ShouldSendCorrectAlpnProtocolToHttpsProxy()
}
}

[PlaywrightTest("fetch-proxy.spec.ts", "should apply proxy.bypass to redirect targets")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
public async Task ShouldApplyProxyBypassToRedirectTargets()
{
EnsureServer();
string crossProcessHost = new Uri(CrossProcessPrefix).Authority;
if (crossProcessHost == new Uri(Prefix).Authority)
{
Assert.Ignore("Needs two different host names for the same server");
}

_proxy.ForwardTo(ServerPort, allowConnectRequests: true);
Server.SetRedirect("/redirect-to-cross-process", CrossProcessPrefix + "/simple.json");
Server.SetRedirect("/redirect-to-same-origin", Prefix + "/simple.json");
IBrowserContext context = await _browser.NewContextAsync(new()
{
Proxy = new Proxy
{
Server = "localhost:" + _proxy.Port.ToString(CultureInfo.InvariantCulture),
Bypass = new Uri(Prefix).Host,
}
}).ConfigureAwait(false);
try
{
{
// Bypassed first hop redirects to a host that must go through the proxy.
IAPIResponse response = await context.APIRequest.GetAsync(Prefix + "/redirect-to-cross-process").ConfigureAwait(false);
Assert.That(response.Url, Is.EqualTo(CrossProcessPrefix + "/simple.json"));
JsonElement? json = await response.JsonAsync().ConfigureAwait(false);
Assert.That(json.Value.GetProperty("foo").GetString(), Is.EqualTo("bar"));
Assert.That(_proxy.ConnectHosts, Is.EqualTo(new[] { crossProcessHost }));
_proxy.ConnectHosts = Array.Empty<string>();
}

{
// Proxied first hop redirects to a bypassed host.
IAPIResponse response = await context.APIRequest.GetAsync(CrossProcessPrefix + "/redirect-to-same-origin").ConfigureAwait(false);
Assert.That(response.Url, Is.EqualTo(Prefix + "/simple.json"));
JsonElement? json = await response.JsonAsync().ConfigureAwait(false);
Assert.That(json.Value.GetProperty("foo").GetString(), Is.EqualTo("bar"));
Assert.That(_proxy.ConnectHosts, Is.EqualTo(new[] { crossProcessHost }));
}
}
finally
{
await context.CloseAsync().ConfigureAwait(false);
}
}

private static void EnsureServer()
{
if (Server == null)
Expand Down
11 changes: 7 additions & 4 deletions src/PlaywrightNative.Tests/LibraryProxyParityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ namespace PlaywrightNative.Tests
{
/// <summary>
/// Official <c>library/proxy.spec.ts</c> parity. Launch-level
/// <c>browserType.launch({ proxy })</c>. Skipped (Node-only type
/// validation): <c>should throw for bad server value</c>
/// (<c>proxy.server: expected string, got number</c>; C#
/// <see cref="Proxy.Server"/> is already a string). Official
/// <c>browserType.launch({ proxy })</c>. Official
/// <c>it.fixme</c> on <c>should use proxy with emulated user agent</c>.
/// Do not edit leftover <c>ContextProxyTests</c>,
/// <c>ContextProxyAuthTests</c>, or <c>LoopbackHttpProxy</c>.
Expand Down Expand Up @@ -130,6 +127,12 @@ public async Task TearDownAsync()
TestServerSetup.HttpsServer?.Reset();
}

[PlaywrightTest("proxy.spec.ts", "should throw for bad server value")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
public void ShouldThrowForBadServerValue()
=> Assert.Ignore("Proxy.Server is a string in C#, so a numeric server value cannot be passed.");

[PlaywrightTest("proxy.spec.ts", "should use proxy @smoke")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
Expand Down
6 changes: 3 additions & 3 deletions src/PlaywrightNative/BrowserTypeLaunchOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ public class BrowserTypeLaunchOptions
public BrowserChannel Channel { get; set; }

/// <summary>
/// Network proxy used by the browser process. Chromium also needs a launch-level
/// proxy (even a dummy <c>per-context</c> server) when contexts override
/// <see cref="IBrowser.NewContextAsync(BrowserContextOptions)"/> with their own proxy.
/// Network proxy used by the browser process. Contexts can override it with their
/// own proxy in <see cref="IBrowser.NewContextAsync(BrowserContextOptions)"/>. The
/// legacy <c>per-context</c> placeholder server is ignored.
/// </summary>
public Proxy Proxy { get; set; }

Expand Down
10 changes: 10 additions & 0 deletions src/PlaywrightNative/Helpers/ProxySettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ internal static class ProxySettings
internal static bool HasCredentials(Proxy proxy)
=> proxy != null && !string.IsNullOrEmpty(proxy.Username);

/// <summary>
/// Official <c>browserType._validateLaunchOptions</c>: drops the legacy
/// <c>per-context</c> placeholder launch proxy. It is no longer needed
/// for contexts to set their own proxy.
/// </summary>
/// <param name="proxy">Launch proxy, or <see langword="null"/>.</param>
/// <returns>The launch proxy, or <see langword="null"/> for the placeholder.</returns>
internal static Proxy NormalizeLaunchProxy(Proxy proxy)
=> proxy?.Server is "per-context" or "http://per-context" ? null : proxy;

/// <summary>
/// Builds a proxy server URL. Adds an <c>http://</c> scheme when missing.
/// When <paramref name="includeCredentials"/> is <see langword="true"/> and
Expand Down
Loading
Loading