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
Tim Raymond 2017-01-25 16:30:42 -05:00
parent 6f7dcc7eb0
commit 33256914b3
3 changed files with 133 additions and 1 deletions

View File

@ -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)
})
}

65
server/url_prefixer.go Normal file
View File

@ -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
}
}
}

View File

@ -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))
}
}
}