Add URL Prefixer
In order to support hosting chronograf under an arbitrary path[1], we need to be able to rewrite all the URLs that are served in HTML and CSS. Take, for example, the scenario where Chronograf is to be hosted under `/chronograf` using Caddy and this example Caddyfile: ``` localhost:2020 gzip proxy /chronograf localhost:8888 { without /chronograf } ``` Chronograf will not load properly when visiting `http://localhost:2020/chronograf` because the requests for CSS, and fonts will go to `http://localhost:2020/app-somegianthash.js` when they should go to `http://localhost:2020/chronograf/app-somegianthash.js`. This is the essence of issue #721. To solve this, we add a URLPrefixer http.Handler, that acts as a middleware. It inserts itself between any upstream handlers, and the handler that was passed to it as its `Next` parameter and searches for `src="` attributes. Upon discovering one of these attributes, it writes the detected attribute and then the configured prefix. It then continues writing the stream to the upstream http.ResponseWriter until encountering another attribute until EOF.pull/814/head
parent
6f7dcc7eb0
commit
33256914b3
|
@ -53,6 +53,10 @@ func Assets(opts AssetsOpts) http.Handler {
|
|||
WithField("url", r.URL).
|
||||
Info("Serving assets")
|
||||
}
|
||||
assets.Handler().ServeHTTP(w, r)
|
||||
up := URLPrefixer{
|
||||
Prefix: "/chronograf",
|
||||
Next: assets.Handler(),
|
||||
}
|
||||
up.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// URLPrefixer is a wrapper for an http.Handler that will prefix all occurrences of a relative URL with the configured Prefix
|
||||
type URLPrefixer struct {
|
||||
Prefix string
|
||||
Next http.Handler
|
||||
}
|
||||
|
||||
type wrapResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
Substitute *io.PipeWriter
|
||||
}
|
||||
|
||||
func (wrw wrapResponseWriter) Write(p []byte) (int, error) {
|
||||
outCount, err := wrw.Substitute.Write(p)
|
||||
if err != nil || outCount == len(p) {
|
||||
wrw.Substitute.Close()
|
||||
}
|
||||
return outCount, err
|
||||
}
|
||||
|
||||
// ServeHTTP implements an http.Handler that prefixes relative URLs from the Next handler with the configured prefix
|
||||
func (up *URLPrefixer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
nextRead, nextWrite := io.Pipe()
|
||||
go up.Next.ServeHTTP(wrapResponseWriter{rw, nextWrite}, r)
|
||||
|
||||
srctag := []byte(`src="`)
|
||||
|
||||
// Locate src tags, flushing everything that isn't to rw
|
||||
b := make([]byte, len(srctag))
|
||||
io.ReadFull(nextRead, b) // prime the buffer with the start of the input
|
||||
|
||||
buf := bytes.NewBuffer(b)
|
||||
src := bufio.NewScanner(nextRead)
|
||||
src.Split(bufio.ScanBytes)
|
||||
for {
|
||||
window := buf.Bytes()
|
||||
// advance a byte if window is not a src attr
|
||||
if !bytes.Equal(window, srctag) {
|
||||
if src.Scan() {
|
||||
rw.Write(buf.Next(1))
|
||||
buf.Write(src.Bytes())
|
||||
} else {
|
||||
rw.Write(window)
|
||||
break
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
buf.Next(len(srctag)) // advance to the relative URL
|
||||
for i := 0; i < len(srctag); i++ {
|
||||
src.Scan()
|
||||
buf.Write(src.Bytes())
|
||||
}
|
||||
rw.Write(srctag) // add the src attr to the output
|
||||
io.WriteString(rw, up.Prefix) // write the prefix
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package server_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/chronograf/server"
|
||||
)
|
||||
|
||||
var prefixerTests = []struct {
|
||||
name string
|
||||
subject string
|
||||
expected string
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
`One script tag`,
|
||||
`<script type="text/javascript" src="/loljavascript.min.js">`,
|
||||
`<script type="text/javascript" src="/arbitraryprefix/loljavascript.min.js">`,
|
||||
false,
|
||||
},
|
||||
{
|
||||
`Two script tags`,
|
||||
`<script type="text/javascript" src="/loljavascript.min.js"><script type="text/javascript" src="/anotherscript.min.js">`,
|
||||
`<script type="text/javascript" src="/arbitraryprefix/loljavascript.min.js"><script type="text/javascript" src="/arbitraryprefix/anotherscript.min.js">`,
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
func Test_Server_Prefixer_RewritesURLs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range prefixerTests {
|
||||
subject := test.subject
|
||||
expected := test.expected
|
||||
|
||||
backend := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, subject)
|
||||
})
|
||||
|
||||
pfx := &server.URLPrefixer{Prefix: "/arbitraryprefix", Next: backend}
|
||||
|
||||
ts := httptest.NewServer(pfx)
|
||||
defer ts.Close()
|
||||
|
||||
res, err := http.Get(ts.URL)
|
||||
if err != nil {
|
||||
t.Error("Unexpected error fetching from prefixer: err:", err)
|
||||
}
|
||||
|
||||
actual, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Error("Unable to read prefixed body: err:", err)
|
||||
}
|
||||
|
||||
if string(actual) != expected+"\n" {
|
||||
t.Error(test.name, ":\n Unsuccessful prefixing.\n\tWant:", expected, "\n\tGot:", string(actual))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue