Merge pull request #814 from influxdata/feature/tr-host-under-path

Enable Chronograf to be hosted under any arbitrary path
pull/10616/head
Chris Goller 2017-01-27 18:39:00 -06:00 committed by GitHub
commit 745ad17da7
8 changed files with 308 additions and 7 deletions

View File

@ -2,13 +2,14 @@
### Upcoming Bug Fixes
1. [#788](https://github.com/influxdata/chronograf/pull/788): Fix missing fields in data explorer when using non-default retention policy
1. [#774](https://github.com/influxdata/chronograf/issues/774): Fix gaps in layouts for hosts
2. [#774](https://github.com/influxdata/chronograf/issues/774): Fix gaps in layouts for hosts
### Upcoming Features
1. [#779](https://github.com/influxdata/chronograf/issues/779): Add layout for telegraf's diskio system plugin
2. [#810](https://github.com/influxdata/chronograf/issues/810): Add layout for telegraf's net system plugin
3. [#811](https://github.com/influxdata/chronograf/issues/811): Add layout for telegraf's procstat plugin
3. [#737](https://github.com/influxdata/chronograf/issues/737): Add GUI for OpsGenie kapacitor alert service
4. [#737](https://github.com/influxdata/chronograf/issues/737): Add GUI for OpsGenie kapacitor alert service
5. [#814](https://github.com/influxdata/chronograf/issues/814): Allows Chronograf to be mounted under any arbitrary URL path using the `--basepath` flag.
### Upcoming UI Improvements

View File

@ -37,10 +37,14 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
Develop: opts.Develop,
Logger: opts.Logger,
})
// Prefix any URLs found in the React assets with any configured basepath
prefixedAssets := NewDefaultURLPrefixer(basepath, assets, opts.Logger)
// The react application handles all the routing if the server does not
// know about the route. This means that we never have unknown
// routes on the server.
router.NotFound = assets
router.NotFound = prefixedAssets
/* Documentation */
router.GET("/swagger.json", Spec())

View File

@ -19,7 +19,10 @@ import (
"github.com/tylerb/graceful"
)
var startTime time.Time
var (
startTime time.Time
basepath string
)
func init() {
startTime = time.Now().UTC()
@ -47,6 +50,7 @@ type Server struct {
ReportingDisabled bool `short:"r" long:"reporting-disabled" description:"Disable reporting of usage stats (os,arch,version,cluster_id,uptime) once every 24hr" env:"REPORTING_DISABLED"`
LogLevel string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"fatal" choice:"panic" default:"info" description:"Set the logging level" env:"LOG_LEVEL"`
ShowVersion bool `short:"v" long:"version" description:"Show Chronograf version info"`
Basepath string `long:"basepath" description:"A URL path prefix under which all chronograf routes will be mounted"`
BuildInfo BuildInfo
Listener net.Listener
handler http.Handler
@ -66,6 +70,7 @@ func (s *Server) useAuth() bool {
func (s *Server) Serve() error {
logger := clog.New(clog.ParseLevel(s.LogLevel))
service := openService(s.BoltPath, s.CannedPath, logger, s.useAuth())
basepath = s.Basepath
s.handler = NewMux(MuxOpts{
Develop: s.Develop,
TokenSecret: s.TokenSecret,

166
server/url_prefixer.go Normal file
View File

@ -0,0 +1,166 @@
package server
import (
"bufio"
"bytes"
"io"
"net/http"
"github.com/influxdata/chronograf"
)
// 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 // the prefix to be appended after any detected Attrs
Next http.Handler // the http.Handler which will generate the content to be modified by this handler
Attrs [][]byte // a list of attrs that should have their URLs prefixed. For example `src="` or `href="` would be valid
Logger chronograf.Logger // The logger where prefixing errors will be dispatched to
}
type wrapResponseWriter struct {
http.ResponseWriter
Substitute *io.PipeWriter
headerWritten bool
dupHeader http.Header
}
func (wrw wrapResponseWriter) Write(p []byte) (int, error) {
return wrw.Substitute.Write(p)
}
func (wrw wrapResponseWriter) WriteHeader(code int) {
if !wrw.headerWritten {
wrw.ResponseWriter.Header().Set("Content-Type", wrw.Header().Get("Content-Type"))
wrw.headerWritten = true
}
wrw.ResponseWriter.WriteHeader(code)
}
// Header() copies the Header map from the underlying ResponseWriter to prevent
// modifications to it by callers
func (wrw wrapResponseWriter) Header() http.Header {
wrw.dupHeader = http.Header{}
origHeader := wrw.ResponseWriter.Header()
for k, v := range origHeader {
wrw.dupHeader[k] = v
}
return wrw.dupHeader
}
const CHUNK_SIZE int = 512
// ServeHTTP implements an http.Handler that prefixes relative URLs from the
// Next handler with the configured prefix. It does this by examining the
// stream through the ResponseWriter, and appending the Prefix after any of the
// Attrs detected in the stream.
func (up *URLPrefixer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
// chunked transfer because we're modifying the response on the fly, so we
// won't know the final content-length
rw.Header().Set("Connection", "Keep-Alive")
rw.Header().Set("Transfer-Encoding", "chunked")
writtenCount := 0 // number of bytes written to rw
// extract the flusher for flushing chunks
flusher, ok := rw.(http.Flusher)
if !ok {
up.Logger.
WithField("component", "prefixer").
Fatal("Expected http.ResponseWriter to be an http.Flusher, but wasn't")
}
nextRead, nextWrite := io.Pipe()
go func() {
defer nextWrite.Close()
up.Next.ServeHTTP(wrapResponseWriter{ResponseWriter: rw, Substitute: nextWrite}, r)
}()
// setup a buffer which is the max length of our target attrs
b := make([]byte, up.maxlen(up.Attrs...))
io.ReadFull(nextRead, b) // prime the buffer with the start of the input
buf := bytes.NewBuffer(b)
// Read next handler's response byte by byte
src := bufio.NewScanner(nextRead)
src.Split(bufio.ScanBytes)
for {
window := buf.Bytes()
// advance a byte if window is not a src attr
if matchlen, match := up.match(window, up.Attrs...); matchlen == 0 {
if src.Scan() {
// shift the next byte into buf
rw.Write(buf.Next(1))
writtenCount++
buf.Write(src.Bytes())
if writtenCount >= CHUNK_SIZE {
flusher.Flush()
writtenCount = 0
}
} else {
if err := src.Err(); err != nil {
up.Logger.
WithField("component", "prefixer").
Error("Error encountered while scanning: err:", err)
}
rw.Write(window)
flusher.Flush()
break
}
continue
} else {
buf.Next(matchlen) // advance to the relative URL
for i := 0; i < matchlen; i++ {
src.Scan()
buf.Write(src.Bytes())
}
rw.Write(match) // add the src attr to the output
io.WriteString(rw, up.Prefix) // write the prefix
}
}
}
// match compares the subject against a list of targets. If there is a match
// between any of them a non-zero value is returned. The returned value is the
// length of the match. It is assumed that subject's length > length of all
// targets. The matching []byte is also returned as the second return parameter
func (up *URLPrefixer) match(subject []byte, targets ...[]byte) (int, []byte) {
for _, target := range targets {
if bytes.Equal(subject[:len(target)], target) {
return len(target), target
}
}
return 0, []byte{}
}
// maxlen returns the length of the largest []byte provided to it as an argument
func (up *URLPrefixer) maxlen(targets ...[]byte) int {
max := 0
for _, tgt := range targets {
if tlen := len(tgt); tlen > max {
max = tlen
}
}
return max
}
// NewDefaultURLPrefixer returns a URLPrefixer that will prefix any src and
// href attributes found in HTML as well as any url() directives found in CSS
// with the provided prefix. Additionally, it will prefix any `data-basepath`
// attributes as well for informing front end logic about any prefixes. `next`
// is the next http.Handler that will have its output prefixed
func NewDefaultURLPrefixer(prefix string, next http.Handler, lg chronograf.Logger) *URLPrefixer {
return &URLPrefixer{
Prefix: prefix,
Next: next,
Logger: lg,
Attrs: [][]byte{
[]byte(`src="`),
[]byte(`href="`),
[]byte(`url(`),
[]byte(`data-basepath="`), // for forwarding basepath to frontend
},
}
}

108
server/url_prefixer_test.go Normal file
View File

@ -0,0 +1,108 @@
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
attrs [][]byte
}{
{
`One script tag`,
`<script type="text/javascript" src="/loljavascript.min.js">`,
`<script type="text/javascript" src="/arbitraryprefix/loljavascript.min.js">`,
false,
[][]byte{
[]byte(`src="`),
},
},
{
`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,
[][]byte{
[]byte(`src="`),
},
},
{
`Link href`,
`<link rel="shortcut icon" href="/favicon.ico">`,
`<link rel="shortcut icon" href="/arbitraryprefix/favicon.ico">`,
false,
[][]byte{
[]byte(`src="`),
[]byte(`href="`),
},
},
{
`Trailing HTML`,
`<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
<title>Chronograf</title>
<link rel="shortcut icon" href="/favicon.ico"><link href="/chronograf.css" rel="stylesheet"></head>
<body>
<div id='react-root'></div>
<script type="text/javascript" src="/manifest.7489452b099f9581ca1b.dev.js"></script><script type="text/javascript" src="/vendor.568c0101d870a13ecff9.dev.js"></script><script type="text/javascript" src="/app.13d0ce0b33609be3802b.dev.js"></script></body>
</html>`,
`<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
<title>Chronograf</title>
<link rel="shortcut icon" href="/arbitraryprefix/favicon.ico"><link href="/arbitraryprefix/chronograf.css" rel="stylesheet"></head>
<body>
<div id='react-root'></div>
<script type="text/javascript" src="/arbitraryprefix/manifest.7489452b099f9581ca1b.dev.js"></script><script type="text/javascript" src="/arbitraryprefix/vendor.568c0101d870a13ecff9.dev.js"></script><script type="text/javascript" src="/arbitraryprefix/app.13d0ce0b33609be3802b.dev.js"></script></body>
</html>`,
false,
[][]byte{
[]byte(`src="`),
[]byte(`href="`),
},
},
}
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, Attrs: test.attrs}
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:", fmt.Sprintf("%+q", expected), "\n\tGot: ", fmt.Sprintf("%+q", string(actual)))
}
}
}

View File

@ -1,7 +1,8 @@
import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import {Router, Route, browserHistory, Redirect} from 'react-router';
import {Router, Route, Redirect} from 'react-router';
import {createHistory, useBasename} from 'history';
import App from 'src/App';
import AlertsApp from 'src/alerts';
@ -28,6 +29,19 @@ const timeRange = Object.assign(defaultTimeRange, parsedTimeRange);
const store = configureStore({timeRange});
const rootNode = document.getElementById('react-root');
let browserHistory;
const basepath = rootNode.dataset.basepath;
window.basepath = basepath;
if (basepath) {
browserHistory = useBasename(createHistory)({
basename: basepath, // this is written in when available by the URL prefixer middleware
});
} else {
browserHistory = useBasename(createHistory)({
basename: "",
});
}
const Root = React.createClass({
getInitialState() {
return {

View File

@ -5,6 +5,6 @@
<title>Chronograf</title>
</head>
<body>
<div id='react-root'></div>
<div id='react-root' data-basepath=""></div>
</body>
</html
</html>

View File

@ -7,6 +7,9 @@ export default function AJAX({
params = {},
headers = {},
}) {
if (window.basepath) {
url = `${window.basepath}${url}`;
}
return axios({
url,
method,