clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

feat/atlas-next-horizon
atlas / redirects.go
redirects.go 4.2K
1package main23import (4 "net/http"5 "net/url"6 "strings"7)89// The webview cannot follow a redirect, so the host follows it.10//11// A Wails page is served over a custom URL scheme, and a scheme task has no12// way to express a redirect: WebKit hands the 302 to the page as if it were a13// document, and the reader is looking at the two words Go writes in a redirect14// body. Chromium's WebView2 and WebKitGTK answer the same way, for the same15// reason -- it is the transport, not the platform.16//17// The application is right to redirect. A world lives at exactly one URL and /18// is a doorway, not a second name for it (docs/app.md §2.2), and the headless19// host serves those redirects to a client that follows them like any other.20// So the doorway is walked through here, in the host, where the limitation is21// (decision 17). The handler is not told; `atlas serve` and the desktop shell22// mount the same bytes, and app_test.go keeps judging the redirect that is23// really sent.2425// redirectHops is how many doorways a request may walk through before the host26// decides the application is going in circles. Two is the most this27// application can need -- /open to /v/… , or / to / to /v/… on an empty28// library -- and the rest is headroom for a route that has not been written.29const redirectHops = 53031// followRedirects resolves the application's own redirects before the webview32// sees them. Only GET is resolved, because only a navigation can be redirected33// here, and only a Location that names a path on this application: anything34// else is somebody else's document and is passed to the page untouched.35func followRedirects(next http.Handler) http.Handler {36 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {37 if r.Method != http.MethodGet {38 next.ServeHTTP(w, r)39 return40 }41 request := r42 for hop := 0; ; hop++ {43 caught := &redirectCatcher{real: w}44 next.ServeHTTP(caught, request)45 if caught.location == "" {46 return47 }48 if hop == redirectHops {49 http.Error(w, "the application redirected in a circle",50 http.StatusLoopDetected)51 return52 }53 target, err := url.ParseRequestURI(caught.location)54 if err != nil {55 http.Error(w, "the application redirected somewhere unreadable",56 http.StatusInternalServerError)57 return58 }59 request = request.Clone(request.Context())60 request.URL.Path, request.URL.RawQuery = target.Path, target.RawQuery61 request.RequestURI = caught.location62 }63 })64}6566// redirectCatcher is a ResponseWriter that swallows a redirect and lets67// everything else through as it happens.68//69// Everything else means everything: the header map is its own until the status70// is known, and from the moment a non-redirect status is written the bytes go71// straight to the real writer. That is what keeps the two streaming answers --72// the events stream and the import's rows -- streaming rather than buffered73// (docs/app.md §5, §2.4).74type redirectCatcher struct {75 real http.ResponseWriter76 header http.Header77 location string78 passing bool79}8081func (c *redirectCatcher) Header() http.Header {82 if c.header == nil {83 c.header = http.Header{}84 }85 return c.header86}8788func (c *redirectCatcher) WriteHeader(code int) {89 if c.passing || c.location != "" {90 return91 }92 if code >= 300 && code < 400 {93 // A Location naming a path is this application's own doorway. An94 // absolute URL is not -- the application never writes one, and if95 // something ever does, it belongs to the page rather than to the host.96 if to := c.Header().Get("Location"); strings.HasPrefix(to, "/") {97 c.location = to98 return99 }100 }101 c.passing = true102 for name, values := range c.Header() {103 c.real.Header()[name] = values104 }105 c.real.WriteHeader(code)106}107108func (c *redirectCatcher) Write(body []byte) (int, error) {109 if !c.passing && c.location == "" {110 c.WriteHeader(http.StatusOK)111 }112 if c.location != "" {113 // "Found." and a link, which is the redirect's body and is nobody's to114 // read. Reported as written so the handler is not told it failed.115 return len(body), nil116 }117 return c.real.Write(body)118}119120// Unwrap is how http.ResponseController finds the flusher underneath, which is121// what the handler asks for on every row of an import and every event of the122// stream.123func (c *redirectCatcher) Unwrap() http.ResponseWriter { return c.real }