git » gofer » commit f592381

http: Add target schemes: dir, static, redirect

author Alberto Bertogli
2020-04-25 14:23:08 UTC
committer Alberto Bertogli
2020-04-26 02:17:27 UTC
parent ca86166d37bad82e321c1420ec2bcbe955731acd

http: Add target schemes: dir, static, redirect

This patch adds to new target schemes for HTTP reverse proxying: dir,
static, and redirect.

The "dir" scheme will make gofer serve a directory directly, and
"static" will do the same but for just a file.

"redirect" will do an HTTP temporary redirection.

This can be convenient to quickly export static files, and redirect from
http to https without the need for an additional server.

proxy/http.go +80 -8
proxy/proxy_test.go +11 -0
proxy/testdata/hola +1 -0

diff --git a/proxy/http.go b/proxy/http.go
index fe338ce..b1127c9 100644
--- a/proxy/http.go
+++ b/proxy/http.go
@@ -30,7 +30,19 @@ func httpServer(conf config.HTTP) *http.Server {
 				from, to, err)
 		}
 		log.Infof("%s route %q -> %q", srv.Addr, from, toURL)
-		mux.Handle(from, makeProxy(from, *toURL))
+		switch toURL.Scheme {
+		case "http", "https":
+			mux.Handle(from, makeProxy(from, *toURL))
+		case "dir":
+			mux.Handle(from, makeDir(from, *toURL))
+		case "static":
+			mux.Handle(from, makeStatic(from, *toURL))
+		case "redirect":
+			mux.Handle(from, makeRedirect(from, *toURL))
+		default:
+			log.Fatalf("route %q -> %q: invalid destination scheme %q",
+				from, to, toURL.Scheme)
+		}
 	}
 
 	return srv
@@ -88,18 +100,13 @@ func makeProxy(from string, to url.URL) http.Handler {
 
 	// Strip the domain from `from`, if any. That is useful for the http
 	// router, but to us is irrelevant.
-	if idx := strings.Index(from, "/"); idx > 0 {
-		from = from[idx:]
-	}
+	from = stripDomain(from)
 
 	proxy.Director = func(req *http.Request) {
 		req.URL.Scheme = to.Scheme
 		req.URL.Host = to.Host
 		req.URL.RawQuery = req.URL.RawQuery
-		req.URL.Path = joinPath(to.Path, strings.TrimPrefix(req.URL.Path, from))
-		if req.URL.Path == "" || req.URL.Path[0] != '/' {
-			req.URL.Path = "/" + req.URL.Path
-		}
+		req.URL.Path = adjustPath(req.URL.Path, from, to.Path)
 
 		// If the user agent is not set, prevent a fall back to the default value.
 		if _, ok := req.Header["User-Agent"]; !ok {
@@ -124,6 +131,71 @@ func joinPath(a, b string) string {
 	return a + b
 }
 
+func stripDomain(from string) string {
+	// Strip the domain from `from`, if any. That is useful for the http
+	// router, but to us is irrelevant.
+	if idx := strings.Index(from, "/"); idx > 0 {
+		from = from[idx:]
+	}
+	return from
+}
+
+func adjustPath(req string, from string, to string) string {
+	// Strip "from" from the request path, so that if we have this config:
+	//
+	//   /a/ -> http://dst/b
+	//   www.example.com/p/ -> http://dst/q
+	//
+	// then:
+	//   /a/x  goes to  http://dst/b/x (not http://dst/b/a/x)
+	//   www.example.com/p/x  goes to  http://dst/q/x
+	//
+	// It is expected that `from` already has the domain removed using
+	// stripDomain.
+	dst := joinPath(to, strings.TrimPrefix(req, from))
+	if dst == "" || dst[0] != '/' {
+		dst = "/" + dst
+	}
+	return dst
+}
+
+func makeDir(from string, to url.URL) http.Handler {
+	from = stripDomain(from)
+
+	fs := http.FileServer(http.Dir(to.Path))
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		r.URL.Path = strings.TrimPrefix(r.URL.Path, from)
+		if r.URL.Path == "" || r.URL.Path[0] != '/' {
+			r.URL.Path = "/" + r.URL.Path
+		}
+		fs.ServeHTTP(w, r)
+	})
+}
+
+func makeStatic(from string, to url.URL) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		http.ServeFile(w, r, to.Path)
+	})
+}
+
+func makeRedirect(from string, to url.URL) http.Handler {
+	from = stripDomain(from)
+
+	dst, err := url.Parse(to.Opaque)
+	if err != nil {
+		log.Fatalf("Invalid destination %q for redirect route: %v",
+			to.Opaque, err)
+	}
+
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		target := *dst
+		target.RawQuery = r.URL.RawQuery
+		target.Path = adjustPath(r.URL.Path, from, dst.Path)
+
+		http.Redirect(w, r, target.String(), http.StatusTemporaryRedirect)
+	})
+}
+
 type loggingTransport struct{}
 
 func (t *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go
index 74035ce..95ef6ca 100644
--- a/proxy/proxy_test.go
+++ b/proxy/proxy_test.go
@@ -69,6 +69,8 @@ func TestMain(m *testing.M) {
 
 	log.Default.Level = log.Error
 
+	pwd, _ := os.Getwd()
+
 	const configTemplate = `
 [[raw]]
 addr = "$RAW_ADDR"
@@ -80,12 +82,16 @@ addr = "$HTTP_ADDR"
 [http.routes]
 "/be/" = "$BACKEND_URL"
 "localhost/xy/" = "$BACKEND_URL"
+"/static/hola" = "static:$PWD/testdata/hola"
+"/dir/" = "dir:$PWD/testdata/"
+"/redir/" = "redirect:http://$HTTP_ADDR/dir/"
 `
 	configStr := strings.NewReplacer(
 		"$RAW_ADDR", rawAddr,
 		"$HTTP_ADDR", httpAddr,
 		"$BACKEND_URL", backend.URL,
 		"$BACKEND_ADDR", backend.Listener.Addr().String(),
+		"$PWD", pwd,
 	).Replace(configTemplate)
 
 	conf, err := config.LoadString(configStr)
@@ -119,6 +125,11 @@ func TestSimple(t *testing.T) {
 	_, httpPort, _ := net.SplitHostPort(httpAddr)
 	testGet(t, "http://localhost:"+httpPort+"/be/", 200)
 	testGet(t, "http://localhost:"+httpPort+"/xy/1", 200)
+
+	// Test dir and static schemes.
+	testGet(t, "http://"+httpAddr+"/static/hola", 200)
+	testGet(t, "http://"+httpAddr+"/dir/hola", 200)
+	testGet(t, "http://"+httpAddr+"/redir/hola", 200)
 }
 
 func testGet(t *testing.T, url string, expectedStatus int) {
diff --git a/proxy/testdata/hola b/proxy/testdata/hola
new file mode 100644
index 0000000..4ab3c7e
--- /dev/null
+++ b/proxy/testdata/hola
@@ -0,0 +1 @@
+backend response