Skip to content

Instantly share code, notes, and snippets.

@viktor-shcherb
Created August 30, 2026 13:22
Show Gist options
  • Select an option

  • Save viktor-shcherb/bd681189f04a84d079a110e2b44765b2 to your computer and use it in GitHub Desktop.

Select an option

Save viktor-shcherb/bd681189f04a84d079a110e2b44765b2 to your computer and use it in GitHub Desktop.
Lightpanda/Chromium authenticated-proxy CDP compatibility matrix (Jobseek recon 2026-08-30)
module example.com/lightpanda-proxy-auth-recon
go 1.24.0
require (
github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327
github.com/chromedp/chromedp v0.14.2
)
require (
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
golang.org/x/sys v0.34.0 // indirect
)
github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 h1:UQ4AU+BGti3Sy/aLU8KVseYKNALcX9UXY6DfpwQ6J8E=
github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZSzM=
github.com/chromedp/chromedp v0.14.2/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs=
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
package main
import (
"context"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"strings"
"sync"
"time"
"github.com/chromedp/cdproto/cdp"
"github.com/chromedp/cdproto/fetch"
"github.com/chromedp/cdproto/target"
"github.com/chromedp/chromedp"
)
const username = "crawler-user"
const password = "crawler-pass"
type output struct {
Backend string `json:"backend"`
PageText string `json:"page_text,omitempty"`
AuthRequiredEvents int `json:"auth_required_events"`
RequestPausedEvents int `json:"request_paused_events"`
ProxyRequests int `json:"proxy_requests"`
AuthenticatedRequest bool `json:"authenticated_request"`
Errors []string `json:"errors"`
}
type state struct {
mu sync.Mutex
authRequiredEvents int
requestPausedEvents int
proxyRequests int
authenticatedRequests int
errors []string
}
func main() {
backend := flag.String("backend", "unknown", "name printed in the result")
cdpURL := flag.String("cdp", "", "complete CDP websocket URL")
proxyListen := flag.String("proxy-listen", "0.0.0.0:18088", "proxy fixture listen address")
originListen := flag.String("origin-listen", "0.0.0.0:18089", "origin fixture listen address")
proxyServer := flag.String("proxy-server", "http://127.0.0.1:18088", "proxy URL visible to the browser")
pageURL := flag.String("page", "http://127.0.0.1:18089/", "origin URL visible to the browser")
contextProxy := flag.Bool("context-proxy", true, "create a browser context configured with proxy-server")
flag.Parse()
if *cdpURL == "" {
log.Fatal("-cdp is required")
}
st := &state{}
origin := originServer(*originListen)
defer func() { _ = origin.Shutdown(context.Background()) }()
proxy := proxyServerFixture(*proxyListen, st)
defer func() { _ = proxy.Shutdown(context.Background()) }()
rootCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
allocatorCtx, allocatorCancel := chromedp.NewRemoteAllocator(rootCtx, *cdpURL, chromedp.NoModifyURL)
defer allocatorCancel()
var tabCtx context.Context
var tabCancel context.CancelFunc
if *contextProxy {
tabCtx, tabCancel = chromedp.NewContext(
allocatorCtx,
chromedp.WithNewBrowserContext(func(params *target.CreateBrowserContextParams) *target.CreateBrowserContextParams {
return params.WithProxyServer(*proxyServer).WithProxyBypassList("<-loopback>")
}),
)
} else {
tabCtx, tabCancel = chromedp.NewContext(allocatorCtx)
}
defer tabCancel()
chromedp.ListenTarget(tabCtx, func(event any) {
switch e := event.(type) {
case *fetch.EventRequestPaused:
st.mu.Lock()
st.requestPausedEvents++
st.mu.Unlock()
go runFetchCommand(tabCtx, st, fetch.ContinueRequest(e.RequestID))
case *fetch.EventAuthRequired:
st.mu.Lock()
st.authRequiredEvents++
st.mu.Unlock()
go runFetchCommand(tabCtx, st, fetch.ContinueWithAuth(e.RequestID, &fetch.AuthChallengeResponse{
Response: fetch.AuthChallengeResponseResponseProvideCredentials,
Username: username,
Password: password,
}))
}
})
out := output{Backend: *backend, Errors: []string{}}
err := chromedp.Run(tabCtx,
fetch.Enable().WithHandleAuthRequests(true),
chromedp.Navigate(*pageURL),
chromedp.Text("body", &out.PageText, chromedp.ByQuery),
)
if err != nil {
out.Errors = append(out.Errors, err.Error())
}
st.mu.Lock()
out.AuthRequiredEvents = st.authRequiredEvents
out.RequestPausedEvents = st.requestPausedEvents
out.ProxyRequests = st.proxyRequests
out.AuthenticatedRequest = st.authenticatedRequests > 0
out.Errors = append(out.Errors, st.errors...)
st.mu.Unlock()
encoded, err := json.MarshalIndent(out, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(encoded))
}
type fetchCommand interface {
Do(context.Context) error
}
func runFetchCommand(ctx context.Context, st *state, command fetchCommand) {
chromedpContext := chromedp.FromContext(ctx)
if chromedpContext == nil || chromedpContext.Target == nil {
recordError(st, fmt.Errorf("target executor unavailable"))
return
}
executorCtx := cdp.WithExecutor(ctx, chromedpContext.Target)
if err := command.Do(executorCtx); err != nil {
recordError(st, err)
}
}
func recordError(st *state, err error) {
st.mu.Lock()
st.errors = append(st.errors, err.Error())
st.mu.Unlock()
}
func originServer(address string) *http.Server {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
body := "proxy-auth-ok"
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Length", fmt.Sprint(len(body)))
_, _ = w.Write([]byte(body))
})
server := &http.Server{Addr: address, Handler: mux, ReadHeaderTimeout: 2 * time.Second}
go serve(server)
return server
}
func proxyServerFixture(address string, st *state) *http.Server {
server := &http.Server{
Addr: address,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
st.mu.Lock()
st.proxyRequests++
st.mu.Unlock()
gotUser, gotPassword, ok := proxyBasicAuth(r)
if !ok || gotUser != username || gotPassword != password {
w.Header().Set("Proxy-Authenticate", `Basic realm="crawler-proxy"`)
w.WriteHeader(http.StatusProxyAuthRequired)
return
}
st.mu.Lock()
st.authenticatedRequests++
st.mu.Unlock()
request, err := http.NewRequestWithContext(r.Context(), r.Method, r.URL.String(), r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
request.Header = r.Header.Clone()
request.Header.Del("Proxy-Authorization")
response, err := http.DefaultTransport.RoundTrip(request)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
defer response.Body.Close()
for key, values := range response.Header {
for _, value := range values {
w.Header().Add(key, value)
}
}
w.WriteHeader(response.StatusCode)
_, _ = io.Copy(w, response.Body)
}),
ReadHeaderTimeout: 2 * time.Second,
}
go serve(server)
return server
}
func proxyBasicAuth(r *http.Request) (string, string, bool) {
value := r.Header.Get("Proxy-Authorization")
prefix := "Basic "
if !strings.HasPrefix(value, prefix) {
return "", "", false
}
raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(strings.TrimPrefix(value, prefix)))
if err != nil {
return "", "", false
}
user, pass, ok := strings.Cut(string(raw), ":")
return user, pass, ok
}
func serve(server *http.Server) {
if err := server.ListenAndServe(); err != nil && !strings.Contains(err.Error(), "Server closed") {
log.Fatal(err)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment