Hosting the server

edit

Start the server

DevToolsServer listens on a TCP port of its own. StartAsync starts it. The clients connect to:

  • HttpEndpoint (e.g. http://127.0.0.1:9222/): the clients read the WebSocket endpoint from /json/version, as with Chromium. This is the address ConnectOverCDPAsync takes.
  • WebSocketEndpoint (e.g. ws://127.0.0.1:9222/devtools/browser/<id>): the endpoint of the browser, for the clients that take a WebSocket address (puppeteer.connect({ browserWSEndpoint }), or ConnectOverCDPAsync too).
await using var server = new DevToolsServer(); // no DocumentLoader: the pages use page.SetContentAsync() and the routes
await server.StartAsync();

using var playwright = await Microsoft.Playwright.Playwright.CreateAsync();
await using var browser = await playwright.Chromium.ConnectOverCDPAsync(server.HttpEndpoint.ToString());
var context = await browser.NewContextAsync();
var page = await context.NewPageAsync();
await page.SetContentAsync("""
    <label for=name>Name</label><input id=name>
    <button onclick="document.querySelector('output').textContent = 'Hello ' + document.querySelector('input').value">Greet</button>
    <output></output>
    """);

await page.GetByLabel("Name").FillAsync("World");
await page.GetByRole(AriaRole.Button, new() { Name = "Greet" }).ClickAsync();
await Assertions.Expect(page.Locator("output")).ToHaveTextAsync("Hello World");
var screenshot = await page.ScreenshotAsync();
DevToolsServerOptionsDefaultEffect
BindAddress, Portloopback, a free portWhere the server listens
DocumentLoadernoneThe network requests of the pages (see below)
ScriptEngineOptionsthe default limitsThe limits of the scripts
UseSystemFontsfalseThe pages can use the fonts installed on the machine
MaxConnectionsnoneThe number of clients connected at the same time

Let the pages use the network

By default, nothing is fetched: the pages only use the content they are given (SetContentAsync()) and the responses of the interception of the requests (page.RouteAsync()). Set a DocumentLoader to let them load documents and resources:

await using var server = new DevToolsServer(new DevToolsServerOptions
{
    DocumentLoader = DocumentLoader.Create(), // the pages can load any URL
});
await server.StartAsync();

using var playwright = await Microsoft.Playwright.Playwright.CreateAsync();
await using var browser = await playwright.Chromium.ConnectOverCDPAsync(server.HttpEndpoint.ToString());
var context = await browser.NewContextAsync();
var page = await context.NewPageAsync();
await page.GotoAsync("https://example.com/");
await Assertions.Expect(page.GetByRole(AriaRole.Heading, new() { Level = 1 })).ToHaveTextAsync("Example Domain");
await page.ScreenshotAsync(new() { Path = "example.png", FullPage = true });

The requests of the pages go through the loader: filter them as for any page (see Network requests). The data: and blob: URLs are not reported to the clients, and the bodies of the responses are buffered.

Host the server in ASP.NET Core

An application can give the server the WebSockets it accepts, instead of a port of its own. The clients then connect to the WebSocket address of the application:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
await using var server = new DevToolsServer(new DevToolsServerOptions { DocumentLoader = DocumentLoader.Create() });

// The clients connect to the WebSocket endpoint: playwright.Chromium.ConnectOverCDPAsync("ws://host/devtools")
app.UseWebSockets();
app.Map("/devtools", async context =>
{
    if (!context.WebSockets.IsWebSocketRequest)
    {
        context.Response.StatusCode = StatusCodes.Status400BadRequest;
        return;
    }

    using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    await server.HandleConnectionAsync(webSocket, context.RequestAborted);
});

await app.RunAsync();

Security

The scripts of the pages run in the process of the server, and a client can make the server request any URL that the DocumentLoader allows. Expose the server to trusted clients only:

  • The default BindAddress is the loopback address.
  • The path of WebSocketEndpoint is random: a client that does not know it cannot connect.
  • As Chromium, the server rejects the HTTP requests whose Host header is not an IP address or localhost, so that a web page cannot reach it with a DNS rebinding attack. An application that hosts the server checks its own requests.