❯ go-clikit
v0.10.0 pkg.go.dev ↗

packages › local web › localweb

localweb

import "github.com/GSI-HPC/go-clikit/localweb"

Guard a local web server that acts as the user, with its listener, the Host and cross-site checks, and the headers.

since v0.8.0 imports: no package of the kit decisions 25 · 34 · 40
The place of localweb in go-clikit: the packages it imports, and the packages that import it.termtextprogressdisplaycliprogressprogresstestmcpprogresseventlogfanoutfanouttestclioutdoctorfsutilcobratreecobratreetestlocalwebsessionsselocalwebtestlaunchassetspagehtmx

A command-line program can serve a page to the user who started it, on the user’s own machine. Such a server acts as the user. localweb keeps out the three others that can reach it (decision 34):

WhoWhat keeps them out
another user of the machine, who can reach a loopback portlocalweb.Listen on loopback or a private Unix socket; localweb.PeerListener on Linux
a page of another site that makes its name resolve to 127.0.0.1localweb.Hosts: the Host header must name this server
any page in the user’s browserlocalweb.SameOrigin, by Sec-Fetch-Site and Origin

localweb.Headers sets the Content-Security-Policy and the frame, referrer and resource policies, and localweb.Serve serves with every timeout set.

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

b, err := localweb.Listen(ctx, "127.0.0.1:0", localweb.ListenOptions{})
if err != nil {
	log.Fatal(err)
}

mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
	fmt.Fprintln(w, "Hello from the user's machine.")
})
h := localweb.Headers("")(localweb.Hosts(b.Answers, localweb.HostsOptions{})(
	localweb.SameOrigin(localweb.OriginPolicy{Navigate: true})(mux)))
done := make(chan error, 1)
go func() { done <- localweb.Serve(ctx, b.Listeners, h, localweb.ServeOptions{}) }()

client := &http.Client{Transport: &http.Transport{DisableKeepAlives: true}}
for _, host := range []string{b.Host(), "rebind.example"} {
	req, err := http.NewRequest(http.MethodGet, "http://"+b.Host()+"/", nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Host = host
	res, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	body, _ := io.ReadAll(res.Body)
	_ = res.Body.Close()
	fmt.Printf("%s: %s %s", res.Status, res.Header.Get("X-Frame-Options"), body)
}

cancel()
fmt.Println(<-done)
200 OK: DENY Hello from the user's machine.
421 Misdirected Request: DENY This server answers to its loopback address only.
<nil>
ExampleListen in localweb/example_test.go. CI runs it and checks its output.

The order#

The checks are middleware of the type func(http.Handler) http.Handler, so http.ServeMux and a router such as chi both take them. Put them in this order, outermost first:

  1. Headers
  2. Hosts
  3. SameOrigin
  4. session.Manager.Middleware
  5. your handler

A check that refuses a request answers with a localweb.Refusal. A localweb.Refuser lets the program write it in its own form, and log the reason.

CAUTION

Every port of 127.0.0.1 is the same site. A SameSite cookie alone does not keep out the pages that another program serves there.

Fixed interfaces#

The refusals, the headers, the cookie and the frames of localweb and its packages (decision 34). net/http and html/template are imported in localweb and its packages alone (decision 25).