browserproxy

package module
v0.0.0-...-0fab95c Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 8, 2026 License: BSD-3-Clause Imports: 18 Imported by: 0

README

go-webengine/browserproxy

go-webengine / browserproxy

CI coverage Go Reference Docs

A pure-Go, CGO_ENABLED=0 remote-browser service. It renders web pages server-side with the pure-Go go-webengine/engine and streams frames (plus a hyperlink hit-map) to a thin client, forwarding the client's clicks, scrolls and keys back as navigation. No Chromium, no cgo, no host web view.

The wire protocol is gRPC carried over grpc-transports/websocket: a single bidirectional Session stream per tab. Because that transport ships a zero-dependency syscall/js client, the same client compiles to GOOS=js/GOARCH=wasm and runs in the browser with no sidecar proxy — the full gRPC feature set (client- and bidi-streaming), which plain grpc-web cannot do.

Because rendering happens on the server:

  • any site can be shown — including pages that set X-Frame-Options: DENY or a restrictive frame-ancestors CSP, which an <iframe> embed could never load;
  • the client page can stay under COEP: require-corp (needed for SharedArrayBuffer) — a WebSocket is exempt from COEP/CORS.

It is the server half of the wasmdesk clients/browser in-desktop browser. See wasmclient/ for a worked GOOS=js/wasm client.

Run it locally

$ go run ./cmd/browserproxy -addr :8090
browserproxy: listening on :8090 (gRPC/WebSocket path /ws)

Then dial the browserproxy.v1.Browser gRPC service over the WebSocket transport at ws://localhost:8090/ws (native or GOOS=js/wasm):

opt, _ := wstransport.DialOption("ws://localhost:8090/ws", wstransport.ClientConfig{})
cc, _ := grpc.NewClient("passthrough:///browserproxy",
    grpc.WithTransportCredentials(insecure.NewCredentials()), opt)
stream, _ := browserpb.NewBrowserClient(cc).Session(ctx)

Flags:

flag default meaning
-addr :8090 listen address
-origins "" (any) comma-separated WebSocket Origin allowlist (* = any)
-max-concurrent 4 global cap on concurrent page renders
-min-nav-interval 250ms per-session minimum time between navigations
-width / -height 1024 / 768 default viewport size
-render-timeout 35s per-navigation render timeout

Protocol

One gRPC bidirectional Session stream per tab (proto/browser.proto). Client → server ClientMsg: navigate, click, scroll, key, resize, back, forward. Server → client ServerMsg: frame (png,w,h,offset_y — raw PNG bytes, no base64), state (url,title,loading,can_back, can_forward), error. Full spec: docs/protocol.md.

Security (SSRF guard)

Every fetch — the top navigation and every subresource, redirect and DNS-rebinding attempt — passes an SSRF guard before the socket connects:

  • non-http(s) schemes rejected (file:, data:, javascript:, …);
  • cloud metadata 169.254.169.254, loopback, RFC1918 private, link-local, IPv6 ULA (fc00::/7), CGNAT (100.64.0.0/10), unspecified and multicast addresses blocked at dial time (post-DNS);
  • internal namespaces (localhost, *.internal, *.local, *.lan, *.home.arpa) blocked by name.

Plus a per-session navigation rate limit, a global concurrent-render cap, and a configurable WebSocket Origin allowlist.

Testing

$ go test -short ./...     # unit tests only (no network); root package at 100%
$ go test ./...            # also runs the live example.com integration test

Three layers of end-to-end proof, all over the real gRPC-over-WebSocket wire:

  • TestIntegration_StubbedTransport — a real gRPC client drives a stub-rendered server (hermetic, no network);
  • TestWasmClientE2E — compiles wasmclient/ to js/wasm and runs it under Node against a live server, asserting a full bidirectional Session — proof a pure-Go browser client actually runs;
  • TestIntegration_ExampleCom (non--short) — the same client and transport drive a real engine-backed server against https://example.com, asserting a non-blank frame and the page title.

License

BSD-3-Clause — see LICENSE.

Documentation

Overview

Package browserproxy renders web pages server-side with the pure-Go go-webengine/engine and streams frames (plus a hyperlink hit-map) to a client over a simple JSON WebSocket protocol, forwarding the client's input back as navigation. This file is the SSRF guard: the security boundary that keeps a proxied page from reaching the host's own network.

Index

Constants

View Source
const (

	// DefaultPath is the HTTP path the gRPC-over-WebSocket endpoint is served on.
	DefaultPath = "/ws"
)

Variables

View Source
var (
	// ErrRateLimited is returned when a session navigates faster than the
	// configured per-session minimum interval.
	ErrRateLimited = fmt.Errorf("browserproxy: navigation rate limit exceeded")
	// ErrNoHistory is returned by Back/Forward when there is nowhere to go.
	ErrNoHistory = fmt.Errorf("browserproxy: no history entry")
)

Session-level errors (in addition to guard's ErrBlocked).

View Source
var ErrBlocked = fmt.Errorf("browserproxy: request blocked by SSRF guard")

ErrBlocked is the sentinel wrapped by every guard rejection, so callers can test errors.Is(err, ErrBlocked) without matching on message text.

Functions

func CheckAddr

func CheckAddr(network, address string) error

CheckAddr is the dynamic (post-DNS) half of the guard, wired as a net.Dialer.Control so it runs after resolution and immediately before the socket connects — covering the top navigation, every subresource, every redirect target and DNS-rebinding. address is "host:port" with host already an IP literal (Control always receives a resolved address).

func CheckURL

func CheckURL(rawurl string) error

CheckURL is the static (pre-DNS) half of the SSRF guard. It rejects any URL that is not a plain http(s) request to a public host: a non-http(s) scheme (file:, data:, gopher:, ftp:, javascript:, …), a missing host, an internal-namespace host suffix, or a host given as a literal private/loopback/link-local IP. DNS names that *resolve* to a blocked address are caught later by CheckAddr at dial time (so DNS-rebinding cannot slip past this static check).

Types

type Config

type Config struct {
	// AllowedOrigins is the WebSocket Origin allowlist. An empty list allows any
	// origin (development default); a list containing "*" also allows any;
	// otherwise the browser Origin must match one entry. Non-browser clients
	// (no Origin header) are always allowed.
	AllowedOrigins []string
	// DefaultW, DefaultH are the initial viewport size for a new session.
	DefaultW, DefaultH int
	// MaxConcurrentRenders caps concurrent renders across all sessions. Zero
	// means unlimited.
	MaxConcurrentRenders int
	// MinNavInterval is the per-session minimum time between navigations.
	MinNavInterval time.Duration
	// RenderTimeout bounds a single navigation/render. Zero uses defaultRenderTimeout.
	RenderTimeout time.Duration
	// Logger, when non-nil, receives non-fatal transport diagnostics.
	Logger *log.Logger
}

Config configures a Server.

type Options

type Options struct {
	// GlobalLimiter caps the number of concurrent renders across all sessions
	// sharing it. A nil limiter means unlimited.
	GlobalLimiter chan struct{}
	// MinNavInterval is the minimum time between two successful renders in one
	// session (rate limit). Zero disables it.
	MinNavInterval time.Duration
	// MaxHistory caps each of the back/forward stacks. Zero uses defaultMaxHistory.
	MaxHistory int
}

Options configures a session's limits.

type RenderFunc

type RenderFunc func(ctx context.Context, url string, w, h int) (*image.RGBA, *engine.RenderInfo, []engine.Link, error)

RenderFunc fetches and renders url at viewport w×h, returning the full-page image, page info and hyperlink hit-map. The default is engine-backed; tests inject a fake so all session logic is exercisable without the network.

type Server

type Server struct {
	browserpb.UnimplementedBrowserServer
	// contains filtered or unexported fields
}

Server implements the browserpb.Browser gRPC service: each Session stream drives one browser tab. It is transport-agnostic — mount it with Server.HandlerListener (gRPC over WebSocket, browser-reachable) or register it on any grpc.Server via Server.Register.

func NewServer

func NewServer(cfg Config) *Server

NewServer builds a Server from cfg.

func (*Server) HandlerListener

func (srv *Server) HandlerListener(path string) (http.Handler, func())

HandlerListener returns an http.Handler that upgrades WebSocket requests on path into gRPC connections for this service, plus a shutdown func that stops the backing grpc.Server gracefully. Mounting the handler on an http.ServeMux lets the gRPC endpoint and a wasm client share one origin. The empty path defaults to DefaultPath.

func (*Server) Register

func (srv *Server) Register(gs *grpc.Server)

Register registers the Server on gs as the browserpb.Browser service. Use it when embedding the service in a grpc.Server you own (e.g. alongside other services or a custom transport); otherwise prefer Server.HandlerListener.

func (*Server) Session

func (srv *Server) Session(stream browserpb.Browser_SessionServer) error

Session implements browserpb.BrowserServer: it runs one browser tab for the lifetime of the bidirectional stream. It sends the initial (empty) chrome state, then loops receiving client input and streaming back the resulting frames and state until the client closes the stream or a send fails.

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session is one browser tab's server-side state: the current full-page image and its hit-map, the scroll offset, the viewport size, and a back/forward history. All exported methods are safe for concurrent use.

func NewSession

func NewSession(w, h int, opts Options) *Session

NewSession creates a session with a real engine-backed renderer whose HTTP client is wrapped by the SSRF-guarded dial control (so navigation and every subresource are guarded at dial time).

func (*Session) Back

func (s *Session) Back(ctx context.Context) error

Back re-loads the most recent back entry, moving the current page forward.

func (*Session) Click

func (s *Session) Click(ctx context.Context, x, y int) (bool, error)

Click resolves a content-area click (viewport pixel coords) against the hit-map at the current scroll and, if it lands inside a link, navigates to it. It reports whether a navigation happened. A miss (or a click before any page loads) is a no-op returning (false, nil).

func (*Session) Forward

func (s *Session) Forward(ctx context.Context) error

Forward re-loads the next forward entry, moving the current page back.

func (*Session) FrameSlice

func (s *Session) FrameSlice() (png []byte, w, h, offsetY int, err error)

FrameSlice returns the current viewport as a fresh w×h PNG taken at the current scroll offset, plus the slice size and offset. Before any page loads (or where the page is shorter than the viewport) the uncovered area is white, so the client canvas is always fully painted.

func (*Session) Navigate

func (s *Session) Navigate(ctx context.Context, url string) error

Navigate loads url as a new history entry: the current page (if any) is pushed onto the back stack and the forward stack is cleared.

func (*Session) Resize

func (s *Session) Resize(ctx context.Context, w, h int) error

Resize sets a new viewport size and re-renders the current page at the new width (a width change changes layout). If no page is loaded it just records the size. The scroll offset is preserved and re-clamped.

func (*Session) Scroll

func (s *Session) Scroll(dy int) int

Scroll adjusts the vertical scroll by dy pixels (positive = down), clamped to the page, without re-rendering. It reports the new offset.

func (*Session) StateMsg

func (s *Session) StateMsg() *browserpb.State

StateMsg returns the chrome model for the current page.

func (*Session) Viewport

func (s *Session) Viewport() (w, h int)

Viewport returns the current viewport (content-area) size in pixels.

Directories

Path Synopsis
cmd
browserproxy command
Command browserproxy serves the go-webengine remote-browser endpoint: it renders web pages server-side with the pure-Go engine and streams frames to a client (e.g.
Command browserproxy serves the go-webengine remote-browser endpoint: it renders web pages server-side with the pure-Go engine and streams frames to a client (e.g.
Command wasmclient is the browser-side half of the wasm end-to-end test and a worked example of a GOOS=js/wasm browserproxy client.
Command wasmclient is the browser-side half of the wasm end-to-end test and a worked example of a GOOS=js/wasm browserproxy client.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL