diff --git a/.gitignore b/.gitignore index 93b17498..6868e5c8 100644 --- a/.gitignore +++ b/.gitignore @@ -34,12 +34,9 @@ src/packages *.ide /.vs/slnx.sqlite /src/Coypu.Tests/Coypu.Tests.nuget.props -/src/Coypu.Tests /src/Coypu.NUnit/Coypu.NUnit.nuget.props -/src/Coypu.NUnit /src/Coypu.Drivers.Tests/Coypu.Drivers.Tests.nuget.targets /src/Coypu.Drivers.Tests/Coypu.Drivers.Tests.nuget.props -/src/Coypu.Drivers.Tests /src/Coypu.AcceptanceTests/Coypu.AcceptanceTests.nuget.targets /src/Coypu.AcceptanceTests/Coypu.AcceptanceTests.nuget.props /src/Coypu.AcceptanceTests/project.lock.json diff --git a/src/Coypu.Tests/Coypu.Tests.csproj b/src/Coypu.Tests/Coypu.Tests.csproj index fd0dfead..2f842d63 100644 --- a/src/Coypu.Tests/Coypu.Tests.csproj +++ b/src/Coypu.Tests/Coypu.Tests.csproj @@ -8,6 +8,8 @@ + + @@ -15,4 +17,9 @@ + + + PreserveNewest + + diff --git a/src/Coypu.Tests/ProxyTests.cs b/src/Coypu.Tests/ProxyTests.cs new file mode 100644 index 00000000..c33d18d2 --- /dev/null +++ b/src/Coypu.Tests/ProxyTests.cs @@ -0,0 +1,91 @@ +using System; +using System.Linq; +using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; +using Coypu.Drivers; +using Coypu.Drivers.Playwright; +using Coypu.Drivers.Selenium; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using WebDriverManager.DriverConfigs.Impl; + +namespace Coypu.Tests; + +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProxyTests +{ + private static WebApplication _app; + private static string _proxyServer; + + [OneTimeSetUp] + public static void SetUp() + { + var builder = WebApplication.CreateBuilder(); + builder.Configuration.AddJsonFile("proxysettings.json"); + + builder.Services.AddReverseProxy() + .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); + + _app = builder.Build(); + + _app.UseRouting(); + _app.MapReverseProxy(); + + _app.MapGet("/acceptance-test", context => + { + const string html = "

This page has been proxied successfully.

"; + + context.Response.ContentType = MediaTypeNames.Text.Html; + context.Response.ContentLength = Encoding.UTF8.GetByteCount(html); + return context.Response.WriteAsync(html); + } + ); + + _app.RunAsync(); + + _proxyServer = _app.Services.GetRequiredService() + .Features.Get() + .Addresses + .First(); + } + + [OneTimeTearDown] + public static async Task TearDown() + { + await _app.DisposeAsync(); + } + + [TestCase(typeof(PlaywrightDriver))] + [TestCase(typeof(SeleniumWebDriver))] + public void Driver_Uses_Proxy(Type driverType) + { + new WebDriverManager.DriverManager().SetUpDriver(new ChromeConfig()); + var sessionConfiguration = new SessionConfiguration + { + AcceptInsecureCertificates = true, + Proxy = new DriverProxy + { + Server = _proxyServer, + }, + Browser = Browser.Chrome, + Driver = driverType, + }; + + using var browser = new BrowserSession(sessionConfiguration); + + // Proxy turns this example.com into localhost, which has an endpoint configured for this URL in the setup + browser.Visit("http://www.example.com/acceptance-test"); + + // So we then assert we can find the title in the HTML we've served + var title = browser.FindId("title"); + + Assert.That(title.Exists(), Is.True); + Assert.That(title.Text, Is.EqualTo("This page has been proxied successfully.")); + } +} \ No newline at end of file diff --git a/src/Coypu.Tests/proxysettings.json b/src/Coypu.Tests/proxysettings.json new file mode 100644 index 00000000..1285b441 --- /dev/null +++ b/src/Coypu.Tests/proxysettings.json @@ -0,0 +1,41 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information" + } + }, + "AllowedHosts": "*", + "ReverseProxy": { + "Routes": { + "route1" : { + "ClusterId": "cluster1", + "Match": { + "Path": "{**catch-all}", + "Hosts": ["www.example.com"] + } + }, + "route2" : { + "ClusterId": "cluster2", + "Match": { + "Path": "{**catch-all}" + } + } + }, + "Clusters": { + "cluster1": { + "Destinations": { + "destination1": { + "Address": "http://localhost:5000/" + } + } + }, + "cluster2": { + "Destinations": { + "destination1": { + "Address": "https://*/" + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Coypu/DriverProxy.cs b/src/Coypu/DriverProxy.cs new file mode 100644 index 00000000..32ff4554 --- /dev/null +++ b/src/Coypu/DriverProxy.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; + +namespace Coypu +{ + /// + /// Proxy information for a Driver + /// + public class DriverProxy + { + /// + /// The Username for the proxy + /// + public string Username { get; set; } + + /// + /// The Password for the proxy + /// + public string Password { get; set; } + + /// + /// The Server of the proxy + /// + public string Server { get; set; } + + /// + /// Use proxy for SSL + /// + public bool Ssl { get; set; } = true; + + /// + /// Use type of proxy + /// + public DriverProxyType Type { get; set; } = DriverProxyType.Http; + + /// + /// Domains to bypass + /// + public IEnumerable BypassAddresses { get; set; } + } +} \ No newline at end of file diff --git a/src/Coypu/DriverProxyType.cs b/src/Coypu/DriverProxyType.cs new file mode 100644 index 00000000..f6fafd35 --- /dev/null +++ b/src/Coypu/DriverProxyType.cs @@ -0,0 +1,8 @@ +namespace Coypu +{ + public enum DriverProxyType + { + Socks, + Http + } +} \ No newline at end of file diff --git a/src/Coypu/Drivers/Playwright/PlaywrightDriver.cs b/src/Coypu/Drivers/Playwright/PlaywrightDriver.cs index f61f1c9a..3de0cf02 100644 --- a/src/Coypu/Drivers/Playwright/PlaywrightDriver.cs +++ b/src/Coypu/Drivers/Playwright/PlaywrightDriver.cs @@ -5,7 +5,6 @@ using System.Text.RegularExpressions; using Cookie = System.Net.Cookie; using Microsoft.Playwright; -using System.Collections.Immutable; #pragma warning disable 1591 @@ -31,27 +30,49 @@ public PlaywrightDriver(SessionConfiguration sessionConfiguration) _playwrightBrowser = browserType.LaunchAsync( new BrowserTypeLaunchOptions { - Headless = _headless, - Channel = PlaywrightBrowserChannel(_browser), + Proxy = MapProxy(sessionConfiguration.Proxy), + Headless = _headless, + Channel = PlaywrightBrowserChannel(_browser), } ).Sync(); NewContext(sessionConfiguration); } + private Proxy MapProxy(DriverProxy proxy) + { + if (proxy is null) + { + return null; + } + + return new Proxy + { + Username = proxy.Username, + Password = proxy.Password, + Server = proxy.Server, + Bypass = proxy.BypassAddresses == null ? null : string.Join(',', proxy.BypassAddresses) + }; + } + private void NewContext(SessionConfiguration sessionConfiguration) { - var options = new BrowserNewPageOptions(); + var options = new BrowserNewPageOptions + { + IgnoreHTTPSErrors = sessionConfiguration.AcceptInsecureCertificates + }; + if (!string.IsNullOrEmpty(sessionConfiguration.AppHost) && !string.IsNullOrEmpty(sessionConfiguration.UserInfo)) { - if (!string.IsNullOrEmpty(sessionConfiguration.UserInfo)) { - var credentials = sessionConfiguration.UserInfo.Split(':'); - options.HttpCredentials = new HttpCredentials + if (!string.IsNullOrEmpty(sessionConfiguration.UserInfo)) { - Username = credentials[0], - Password = credentials[1], - Origin = new FullyQualifiedUrlBuilder().GetFullyQualifiedUrl("", sessionConfiguration).TrimEnd('/') - }; - } + var credentials = sessionConfiguration.UserInfo.Split(':'); + options.HttpCredentials = new HttpCredentials + { + Username = credentials[0], + Password = credentials[1], + Origin = new FullyQualifiedUrlBuilder().GetFullyQualifiedUrl("", sessionConfiguration).TrimEnd('/') + }; + } } var page = _playwrightBrowser.NewPageAsync(options).Sync(); @@ -93,12 +114,12 @@ private IBrowserType PlaywrightBrowserType(Browser browser, IPlaywright playwrig public Uri Location(Scope scope) { - return new Uri(PlaywrightPage(scope).Url); + return new Uri(PlaywrightPage(scope).Url); } public string Title(Scope scope) { - return PlaywrightPage(scope).TitleAsync().Sync(); + return PlaywrightPage(scope).TitleAsync().Sync(); } public Coypu.Cookies Cookies { get; set; } @@ -106,36 +127,37 @@ public string Title(Scope scope) public Element Window => new PlaywrightWindow(_context.Pages.First()); - public IEnumerable FindFrames(string locator, - Scope scope, - Options options) - { - IPage page = Page(scope); - return new FrameFinder(page).FindFrame( - locator, - page.QuerySelectorAllAsync("iframe,frame").Sync(), - options - ); - } + public IEnumerable FindFrames(string locator, + Scope scope, + Options options) + { + IPage page = Page(scope); + return new FrameFinder(page).FindFrame( + locator, + page.QuerySelectorAllAsync("iframe,frame").Sync(), + options + ); + } - private static IPage Page(Scope scope) - { - var nativeScope = scope.Now().Native; - var page = nativeScope as IPage ?? - ((IElementHandle)nativeScope).OwnerFrameAsync().Sync().Page; - return page; - } + private static IPage Page(Scope scope) + { + var nativeScope = scope.Now().Native; + var page = nativeScope as IPage ?? + ((IElementHandle)nativeScope).OwnerFrameAsync().Sync().Page; + return page; + } - public IEnumerable FindAllCss(string cssSelector, - Scope scope, - Options options, - Regex textPattern = null) + public IEnumerable FindAllCss(string cssSelector, + Scope scope, + Options options, + Regex textPattern = null) { - try { - var results = Element(scope).QuerySelectorAllAsync($"css={cssSelector}").Sync() - .Where(ValidateTextPattern(options, textPattern)) - .Where(e => IsDisplayed(e, options)) - .Select(BuildElement); + try + { + var results = Element(scope).QuerySelectorAllAsync($"css={cssSelector}").Sync() + .Where(ValidateTextPattern(options, textPattern)) + .Where(e => IsDisplayed(e, options)) + .Select(BuildElement); return results; } catch (AggregateException e) @@ -146,18 +168,18 @@ public IEnumerable FindAllCss(string cssSelector, private Func ValidateTextPattern(Options options, Regex textPattern) { - if (options == null) throw new ArgumentNullException(nameof(options)); - Func textMatches = e => - { - if (textPattern == null) return true; + if (options == null) throw new ArgumentNullException(nameof(options)); + Func textMatches = e => + { + if (textPattern == null) return true; - var text = e.InnerTextAsync().Sync(); - return text != null && textPattern.IsMatch(text.Trim()); - }; + var text = e.InnerTextAsync().Sync(); + return text != null && textPattern.IsMatch(text.Trim()); + }; - if (textPattern != null && options.ConsiderInvisibleElements) - throw new NotSupportedException("Cannot inspect the text of invisible elements."); - return textMatches; + if (textPattern != null && options.ConsiderInvisibleElements) + throw new NotSupportedException("Cannot inspect the text of invisible elements."); + return textMatches; } private bool IsDisplayed(IElementHandle e, @@ -170,10 +192,11 @@ public IEnumerable FindAllXPath(string xpath, Scope scope, Options options) { - try { - return Element(scope).QuerySelectorAllAsync($"xpath={xpath}").Sync() - .Where(e => IsDisplayed(e, options)) - .Select(BuildElement); + try + { + return Element(scope).QuerySelectorAllAsync($"xpath={xpath}").Sync() + .Where(e => IsDisplayed(e, options)) + .Select(BuildElement); } catch (AggregateException e) { @@ -185,8 +208,8 @@ private Element BuildElement(IElementHandle element) { var tagName = element.EvaluateAsync("e => e.tagName").Sync()?.GetString(); - Element coypuElement = new[] {"iframe", "frame"}.Contains(tagName.ToLower()) - ? (Element) new PlaywrightFrame(element) : new PlaywrightElement(element); + Element coypuElement = new[] { "iframe", "frame" }.Contains(tagName.ToLower()) + ? (Element)new PlaywrightFrame(element) : new PlaywrightElement(element); return coypuElement; } @@ -222,7 +245,7 @@ public void Visit(string url, IResponse response = PlaywrightPage(scope).GotoAsync(url).Sync(); if (response != null && response.Status != 200) { - throw new Exception("Failed to load page"); + throw new Exception("Failed to load page"); } } @@ -262,13 +285,14 @@ public void MaximiseWindow(Scope scope) public void Refresh(Scope scope) { - ((IPage )scope.Now().Native).ReloadAsync().Sync(); + ((IPage)scope.Now().Native).ReloadAsync().Sync(); } public void ResizeTo(Size size, Scope scope) { - if (_playwrightBrowser.BrowserType == _playwright.Chromium && !_headless) { + if (_playwrightBrowser.BrowserType == _playwright.Chromium && !_headless) + { size = new Size(size.Width - 2, size.Height - 80); } PlaywrightPage(scope).SetViewportSizeAsync(size.Width, size.Height).Sync(); @@ -302,19 +326,20 @@ public IEnumerable FindWindows(string titleOrName, Scope scope, Options options) { - try - { - return _context.Pages - .Select(p => new PlaywrightWindow(p)) - .Where(window => { - return - options.TextPrecisionExact && ( - window.Title == titleOrName - ) || - !options.TextPrecisionExact && ( - window.Title.Contains(titleOrName) - ); - }); + try + { + return _context.Pages + .Select(p => new PlaywrightWindow(p)) + .Where(window => + { + return + options.TextPrecisionExact && ( + window.Title == titleOrName + ) || + !options.TextPrecisionExact && ( + window.Title.Contains(titleOrName) + ); + }); } catch (PlaywrightException ex) { @@ -400,26 +425,28 @@ private static object[] ConvertScriptArgs(object[] args) private IElementHandle PlaywrightElement(Element element) { - return (IElementHandle) element.Native; + return (IElementHandle)element.Native; } private IPage PlaywrightPage(Scope scope) { - return (IPage) scope.Now().Native; + return (IPage)scope.Now().Native; } private IElementHandle Element(Scope scope) { var scopeElement = scope.Now(); var frame = scopeElement.Native as IFrame; - if (frame != null) { + if (frame != null) + { return frame.QuerySelectorAsync("html").Sync(); } var page = scopeElement.Native as IPage; - if (page != null) { + if (page != null) + { return page.QuerySelectorAsync("html").Sync(); } - return (IElementHandle) scopeElement.Native; + return (IElementHandle)scopeElement.Native; } public void Dispose() diff --git a/src/Coypu/Drivers/Selenium/DriverFactory.cs b/src/Coypu/Drivers/Selenium/DriverFactory.cs index afe019bb..92b858e0 100644 --- a/src/Coypu/Drivers/Selenium/DriverFactory.cs +++ b/src/Coypu/Drivers/Selenium/DriverFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using OpenQA.Selenium; using OpenQA.Selenium.Chrome; using OpenQA.Selenium.Edge; @@ -14,40 +15,128 @@ internal class DriverFactory public IWebDriver NewWebDriver(SessionConfiguration sessionConfiguration) { var browser = sessionConfiguration.Browser; - var firefoxOptions = new FirefoxOptions(); - var chromeOptions = new ChromeOptions(); - EdgeOptions edgeOptions = new EdgeOptions(); - if (sessionConfiguration.Headless) { - firefoxOptions.AddArgument("--headless"); - chromeOptions.AddArgument("--headless=new"); - edgeOptions.AddArgument("headless"); - edgeOptions.AddArgument("disable-gpu"); - if (sessionConfiguration.Browser == Browser.Safari) { - throw new NotSupportedException("Safari does not support headless mode"); - } - if (browser == Browser.Safari) { - throw new NotSupportedException("Opera does not support headless mode"); - } - if (browser == Browser.InternetExplorer) { - throw new NotSupportedException("Internet Explorer does not support headless mode"); - } - } - if (browser == Browser.Firefox) return new FirefoxDriver(firefoxOptions); - if (browser == Browser.Chrome) return new ChromeDriver(chromeOptions); - if (browser == Browser.Edge) return new EdgeDriver(edgeOptions); - if (browser == Browser.Opera) return new OperaDriver(); - if (browser == Browser.Safari) return new SafariDriver(); + + var firefoxOptions = new FirefoxOptions + { + AcceptInsecureCertificates = sessionConfiguration.AcceptInsecureCertificates, + Proxy = MapProxy(sessionConfiguration.Proxy) + }; + + var chromeOptions = new ChromeOptions + { + AcceptInsecureCertificates = sessionConfiguration.AcceptInsecureCertificates, + Proxy = MapProxy(sessionConfiguration.Proxy) + }; + + var edgeOptions = new EdgeOptions + { + AcceptInsecureCertificates = sessionConfiguration.AcceptInsecureCertificates, + Proxy = MapProxy(sessionConfiguration.Proxy) + }; + + if (sessionConfiguration.Headless) + { + firefoxOptions.AddArgument("--headless"); + chromeOptions.AddArgument("--headless=new"); + edgeOptions.AddArgument("headless"); + edgeOptions.AddArgument("disable-gpu"); + + if (browser == Browser.Safari) + { + throw new NotSupportedException("Safari does not support headless mode"); + } + + if (browser == Browser.Opera) + { + throw new NotSupportedException("Opera does not support headless mode"); + } + + if (browser == Browser.InternetExplorer) + { + throw new NotSupportedException("Internet Explorer does not support headless mode"); + } + } + + if (browser == Browser.Firefox) + { + return new FirefoxDriver(firefoxOptions); + } + + if (browser == Browser.Chrome) + { + return new ChromeDriver(chromeOptions); + } + + if (browser == Browser.Edge) + { + return new EdgeDriver(edgeOptions); + } + + if (browser == Browser.Opera) + { + return new OperaDriver(new OperaOptions + { + AcceptInsecureCertificates = sessionConfiguration.AcceptInsecureCertificates, + Proxy = MapProxy(sessionConfiguration.Proxy) + }); + } + + if (browser == Browser.Safari) + { + return new SafariDriver(new SafariOptions + { + AcceptInsecureCertificates = sessionConfiguration.AcceptInsecureCertificates, + Proxy = MapProxy(sessionConfiguration.Proxy) + }); + } + return browser == Browser.InternetExplorer - ? new InternetExplorerDriver(new InternetExplorerOptions - { - IntroduceInstabilityByIgnoringProtectedModeSettings = true, - EnableNativeEvents = true, - IgnoreZoomLevel = true - }) - : BrowserNotSupported(browser, null); + ? new InternetExplorerDriver(new InternetExplorerOptions + { + AcceptInsecureCertificates = sessionConfiguration.AcceptInsecureCertificates, + IntroduceInstabilityByIgnoringProtectedModeSettings = true, + EnableNativeEvents = true, + IgnoreZoomLevel = true, + Proxy = MapProxy(sessionConfiguration.Proxy) + }) + : BrowserNotSupported(browser, null); + } + + private Proxy MapProxy(DriverProxy driverProxy) + { + if (driverProxy is null) + { + return null; + } + + var proxy = new Proxy(); + + if (driverProxy.Type == DriverProxyType.Socks) + { + proxy.SocksProxy = driverProxy.Server; + proxy.SocksUserName = driverProxy.Username; + proxy.SocksPassword = driverProxy.Password; + } + + if (driverProxy.Type == DriverProxyType.Http) + { + proxy.HttpProxy = driverProxy.Server; + + if (driverProxy.Ssl) + { + proxy.SslProxy = driverProxy.Server; + } + } + + if (driverProxy.BypassAddresses?.Any() == true) + { + proxy.AddBypassAddresses(driverProxy.BypassAddresses); + } + + return proxy; } - private IWebDriver BrowserNotSupported(Browser browser, + private IWebDriver BrowserNotSupported(Browser browser, Exception inner) { throw new BrowserNotSupportedException(browser, GetType(), inner); diff --git a/src/Coypu/SessionConfiguration.cs b/src/Coypu/SessionConfiguration.cs index 58cd307c..d06f6927 100644 --- a/src/Coypu/SessionConfiguration.cs +++ b/src/Coypu/SessionConfiguration.cs @@ -1,7 +1,5 @@ using System; -using Coypu.Drivers.Playwright; using Coypu.Drivers.Selenium; -using OpenQA.Selenium.DevTools.V85.HeadlessExperimental; namespace Coypu { @@ -24,7 +22,7 @@ public SessionConfiguration() Port = DEFAULT_PORT; SSL = false; Browser = Drivers.Browser.Firefox; - Driver = typeof (SeleniumWebDriver); + Driver = typeof(SeleniumWebDriver); Headless = true; } @@ -53,7 +51,7 @@ public SessionConfiguration() /// public string AppHost { - get { return appHost;} + get => appHost; set { if (Uri.IsWellFormedUriString(value, UriKind.Absolute)) @@ -80,5 +78,16 @@ public string AppHost /// Default: false /// public bool SSL { get; set; } + + /// + /// Specifies the proxy you would like to use + /// Default: null + /// + public DriverProxy Proxy { get; set; } + + /// + /// Ignore Browser related certificate errors + /// + public bool AcceptInsecureCertificates { get; set; } } }