Merge pull request #3284 from influxdata/bugfix/basepath-logout

Fix logout when using basepath & simplify basepath usage
pull/10616/head
Jared Scheib 2018-04-23 17:41:33 -07:00 committed by GitHub
commit 7f1d870c2a
6 changed files with 82 additions and 141 deletions

View File

@ -16,6 +16,7 @@
1. [#3252](https://github.com/influxdata/chronograf/pull/3252): Allows users to select tickscript editor with mouse
1. [#3279](https://github.com/influxdata/chronograf/pull/3279): Change color when value is equal to or greater than threshold value
1. [#3281](https://github.com/influxdata/chronograf/pull/3281): Fix base path for kapacitor logs
1. [#3284](https://github.com/influxdata/chronograf/pull/3284): Fix logout when using basepath & simplify basepath usage (deprecates `PREFIX_ROUTES`)
## v1.4.4.1 [2018-04-16]

View File

@ -25,7 +25,6 @@ type MuxOpts struct {
Logger chronograf.Logger
Develop bool // Develop loads assets from filesystem instead of bindata
Basepath string // URL path prefix under which all chronograf routes will be mounted
PrefixRoutes bool // Mounts all backend routes under route specified by the Basepath
UseAuth bool // UseAuth turns on Github OAuth and JWT
Auth oauth2.Authenticator // Auth is used to authenticate and authorize
ProviderFuncs []func(func(oauth2.Provider, oauth2.Mux))
@ -44,7 +43,7 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
})
// Prefix any URLs found in the React assets with any configured basepath
prefixedAssets := NewDefaultURLPrefixer(basepath, assets, opts.Logger)
prefixedAssets := NewDefaultURLPrefixer(opts.Basepath, assets, opts.Logger)
// Compress the assets with gzip if an accepted encoding
compressed := gziphandler.GzipHandler(prefixedAssets)
@ -57,7 +56,7 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
var router chronograf.Router = hr
// Set route prefix for all routes if basepath is present
if opts.PrefixRoutes {
if opts.Basepath != "" {
router = &MountableRouter{
Prefix: opts.Basepath,
Delegate: hr,
@ -313,11 +312,6 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
var out http.Handler
basepath := ""
if opts.PrefixRoutes {
basepath = opts.Basepath
}
/* Authentication */
if opts.UseAuth {
// Encapsulate the router with OAuth2
@ -326,10 +320,10 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
allRoutes.LogoutLink = path.Join(opts.Basepath, "/oauth/logout")
// Create middleware that redirects to the appropriate provider logout
router.GET(allRoutes.LogoutLink, Logout("/", basepath, allRoutes.AuthRoutes))
out = Logger(opts.Logger, PrefixedRedirect(opts.Basepath, auth))
router.GET("/oauth/logout", Logout("/", opts.Basepath, allRoutes.AuthRoutes))
out = Logger(opts.Logger, FlushingHandler(auth))
} else {
out = Logger(opts.Logger, PrefixedRedirect(opts.Basepath, router))
out = Logger(opts.Logger, FlushingHandler(router))
}
return out
@ -363,13 +357,8 @@ func AuthAPI(opts MuxOpts, router chronograf.Router) (http.Handler, AuthRoutes)
})
}
rootPath := "/chronograf/v1"
logoutPath := "/oauth/logout"
if opts.PrefixRoutes {
rootPath = path.Join(opts.Basepath, rootPath)
logoutPath = path.Join(opts.Basepath, logoutPath)
}
rootPath := path.Join(opts.Basepath, "/chronograf/v1")
logoutPath := path.Join(opts.Basepath, "/oauth/logout")
tokenMiddleware := AuthorizedToken(opts.Auth, opts.Logger, router)
// Wrap the API with token validation middleware.

View File

@ -2,49 +2,32 @@ package server
import (
"net/http"
"net/url"
"path"
"strings"
)
type interceptingResponseWriter struct {
type flushingResponseWriter struct {
http.ResponseWriter
Flusher http.Flusher
Prefix string
}
func (i *interceptingResponseWriter) WriteHeader(status int) {
if status >= 300 && status < 400 {
location := i.ResponseWriter.Header().Get("Location")
if u, err := url.Parse(location); err == nil && !u.IsAbs() {
hasPrefix := strings.HasPrefix(u.Path, i.Prefix)
if !hasPrefix || (hasPrefix && !strings.HasPrefix(u.Path[len(i.Prefix):], i.Prefix)) {
i.ResponseWriter.Header().Set("Location", path.Join(i.Prefix, location)+"/")
}
}
}
i.ResponseWriter.WriteHeader(status)
func (f *flushingResponseWriter) WriteHeader(status int) {
f.ResponseWriter.WriteHeader(status)
}
// Flush is here because the underlying HTTP chunked transfer response writer
// to implement http.Flusher. Without it data is silently buffered. This
// was discovered when proxying kapacitor chunked logs.
func (i *interceptingResponseWriter) Flush() {
if i.Flusher != nil {
i.Flusher.Flush()
func (f *flushingResponseWriter) Flush() {
if flusher, ok := f.ResponseWriter.(http.Flusher); ok {
flusher.Flush()
}
}
// PrefixedRedirect alters the Location header of downstream http.Handlers
// to include a specified prefix
func PrefixedRedirect(prefix string, next http.Handler) http.Handler {
// FlushingHandler may not actually do anything, but it was ostensibly
// implemented to flush response writers that can be flushed for the
// purposes in the comment above.
func FlushingHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
iw := &interceptingResponseWriter{
iw := &flushingResponseWriter{
ResponseWriter: w,
Prefix: prefix,
}
if flusher, ok := w.(http.Flusher); ok {
iw.Flusher = flusher
}
next.ServeHTTP(iw, r)
})

View File

@ -1,87 +0,0 @@
package server
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
var prefixingRedirectTests = []struct {
CaseName string
RedirectTarget string
Prefix string
Expected string
}{
{
"ChronografBasepath",
"/chronograf/v1/",
"/chronograf",
"/chronograf/chronograf/v1/",
},
{
"DifferentBasepath",
"/chronograf/v1/",
"/delorean",
"/delorean/chronograf/v1/",
},
{
"TrailingSlashPrefix",
"/chronograf/v1/",
"/delorean/",
"/delorean/chronograf/v1/",
},
{
"NoPrefix",
"/chronograf/v1/",
"",
"/chronograf/v1/",
},
{
"SlashPrefix",
"/chronograf/v1/",
"/",
"/chronograf/v1/",
},
{
"AlreadyPrefixed",
"/chronograf/chronograf/v1/",
"/chronograf",
"/chronograf/chronograf/v1/",
},
}
func Test_PrefixingRedirector(t *testing.T) {
t.Parallel()
for _, p := range prefixingRedirectTests {
t.Run(p.CaseName, func(subt *testing.T) {
hf := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Location", p.RedirectTarget)
rw.WriteHeader(http.StatusTemporaryRedirect)
})
pr := PrefixedRedirect(p.Prefix, hf)
ts := httptest.NewServer(pr)
defer ts.Close()
hc := http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
mockBody := strings.NewReader("")
req, _ := http.NewRequest("GET", ts.URL, mockBody)
resp, err := hc.Do(req)
if err != nil {
subt.Fatal("Unexpected http err:", err)
}
expected := p.Expected
if loc := resp.Header.Get("Location"); loc != expected {
subt.Fatal("Unexpected redirected location. Expected:", expected, "Actual:", loc)
}
})
}
}

View File

@ -3,6 +3,7 @@ package server
import (
"context"
"crypto/tls"
"fmt"
"log"
"math/rand"
"net"
@ -10,6 +11,7 @@ import (
"net/url"
"os"
"path"
"regexp"
"runtime"
"strconv"
"time"
@ -27,7 +29,6 @@ import (
var (
startTime time.Time
basepath string
)
func init() {
@ -96,8 +97,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:"error" default:"info" description:"Set the logging level" env:"LOG_LEVEL"`
Basepath string `short:"p" long:"basepath" description:"A URL path prefix under which all chronograf routes will be mounted" env:"BASE_PATH"`
PrefixRoutes bool `long:"prefix-routes" description:"Force chronograf server to require that all requests to it are prefixed with the value set in --basepath" env:"PREFIX_ROUTES"`
Basepath string `short:"p" long:"basepath" description:"A URL path prefix under which all chronograf routes will be mounted. (Note: PREFIX_ROUTES has been deprecated. Now, if basepath is set, all routes will be prefixed with it.)" env:"BASE_PATH"`
ShowVersion bool `short:"v" long:"version" description:"Show Chronograf version info"`
BuildInfo chronograf.BuildInfo
Listener net.Listener
@ -344,11 +344,13 @@ func (s *Server) Serve(ctx context.Context) error {
return err
}
basepath = s.Basepath
if basepath != "" && s.PrefixRoutes == false {
if !validBasepath(s.Basepath) {
err := fmt.Errorf("Invalid basepath, must follow format \"/mybasepath\"")
logger.
WithField("component", "server").
Info("Note: you may want to use --prefix-routes with --basepath. Try `./chronograf --help` for more info.")
WithField("basepath", "invalid").
Error(err)
return err
}
providerFuncs := []func(func(oauth2.Provider, oauth2.Mux)){}
@ -366,8 +368,7 @@ func (s *Server) Serve(ctx context.Context) error {
Logger: logger,
UseAuth: s.useAuth(),
ProviderFuncs: providerFuncs,
Basepath: basepath,
PrefixRoutes: s.PrefixRoutes,
Basepath: s.Basepath,
StatusFeedURL: s.StatusFeedURL,
CustomLinks: s.CustomLinks,
}, service)
@ -537,3 +538,8 @@ func clientUsage(values client.Values) *client.Usage {
},
}
}
func validBasepath(basepath string) bool {
re := regexp.MustCompile(`(\/{1}\w+)+`)
return re.ReplaceAllLiteralString(basepath, "") == ""
}

View File

@ -3,10 +3,12 @@ package server
import (
"context"
"net/http"
"testing"
"github.com/bouk/httprouter"
)
// WithContext is a helper function to cut down on boilerplate in server test files
func WithContext(ctx context.Context, r *http.Request, kv map[string]string) *http.Request {
params := make(httprouter.Params, 0, len(kv))
for k, v := range kv {
@ -17,3 +19,50 @@ func WithContext(ctx context.Context, r *http.Request, kv map[string]string) *ht
}
return r.WithContext(httprouter.WithParams(ctx, params))
}
func Test_validBasepath(t *testing.T) {
type args struct {
basepath string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "Basepath can be empty",
args: args{
basepath: "",
},
want: true,
},
{
name: "Basepath is not empty and valid",
args: args{
basepath: "/russ",
},
want: true,
},
{
name: "Basepath is not empty and invalid - no slashes",
args: args{
basepath: "russ",
},
want: false,
},
{
name: "Basepath is not empty and invalid - extra slashes",
args: args{
basepath: "//russ//",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := validBasepath(tt.args.basepath); got != tt.want {
t.Errorf("validBasepath() = %v, want %v", got, tt.want)
}
})
}
}