Merge pull request #3284 from influxdata/bugfix/basepath-logout
Fix logout when using basepath & simplify basepath usagepull/10616/head
commit
7f1d870c2a
|
@ -16,6 +16,7 @@
|
||||||
1. [#3252](https://github.com/influxdata/chronograf/pull/3252): Allows users to select tickscript editor with mouse
|
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. [#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. [#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]
|
## v1.4.4.1 [2018-04-16]
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,6 @@ type MuxOpts struct {
|
||||||
Logger chronograf.Logger
|
Logger chronograf.Logger
|
||||||
Develop bool // Develop loads assets from filesystem instead of bindata
|
Develop bool // Develop loads assets from filesystem instead of bindata
|
||||||
Basepath string // URL path prefix under which all chronograf routes will be mounted
|
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
|
UseAuth bool // UseAuth turns on Github OAuth and JWT
|
||||||
Auth oauth2.Authenticator // Auth is used to authenticate and authorize
|
Auth oauth2.Authenticator // Auth is used to authenticate and authorize
|
||||||
ProviderFuncs []func(func(oauth2.Provider, oauth2.Mux))
|
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
|
// 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
|
// Compress the assets with gzip if an accepted encoding
|
||||||
compressed := gziphandler.GzipHandler(prefixedAssets)
|
compressed := gziphandler.GzipHandler(prefixedAssets)
|
||||||
|
@ -57,7 +56,7 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
||||||
var router chronograf.Router = hr
|
var router chronograf.Router = hr
|
||||||
|
|
||||||
// Set route prefix for all routes if basepath is present
|
// Set route prefix for all routes if basepath is present
|
||||||
if opts.PrefixRoutes {
|
if opts.Basepath != "" {
|
||||||
router = &MountableRouter{
|
router = &MountableRouter{
|
||||||
Prefix: opts.Basepath,
|
Prefix: opts.Basepath,
|
||||||
Delegate: hr,
|
Delegate: hr,
|
||||||
|
@ -313,11 +312,6 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
||||||
|
|
||||||
var out http.Handler
|
var out http.Handler
|
||||||
|
|
||||||
basepath := ""
|
|
||||||
if opts.PrefixRoutes {
|
|
||||||
basepath = opts.Basepath
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Authentication */
|
/* Authentication */
|
||||||
if opts.UseAuth {
|
if opts.UseAuth {
|
||||||
// Encapsulate the router with OAuth2
|
// 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")
|
allRoutes.LogoutLink = path.Join(opts.Basepath, "/oauth/logout")
|
||||||
|
|
||||||
// Create middleware that redirects to the appropriate provider logout
|
// Create middleware that redirects to the appropriate provider logout
|
||||||
router.GET(allRoutes.LogoutLink, Logout("/", basepath, allRoutes.AuthRoutes))
|
router.GET("/oauth/logout", Logout("/", opts.Basepath, allRoutes.AuthRoutes))
|
||||||
out = Logger(opts.Logger, PrefixedRedirect(opts.Basepath, auth))
|
out = Logger(opts.Logger, FlushingHandler(auth))
|
||||||
} else {
|
} else {
|
||||||
out = Logger(opts.Logger, PrefixedRedirect(opts.Basepath, router))
|
out = Logger(opts.Logger, FlushingHandler(router))
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
@ -363,13 +357,8 @@ func AuthAPI(opts MuxOpts, router chronograf.Router) (http.Handler, AuthRoutes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
rootPath := "/chronograf/v1"
|
rootPath := path.Join(opts.Basepath, "/chronograf/v1")
|
||||||
logoutPath := "/oauth/logout"
|
logoutPath := path.Join(opts.Basepath, "/oauth/logout")
|
||||||
|
|
||||||
if opts.PrefixRoutes {
|
|
||||||
rootPath = path.Join(opts.Basepath, rootPath)
|
|
||||||
logoutPath = path.Join(opts.Basepath, logoutPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenMiddleware := AuthorizedToken(opts.Auth, opts.Logger, router)
|
tokenMiddleware := AuthorizedToken(opts.Auth, opts.Logger, router)
|
||||||
// Wrap the API with token validation middleware.
|
// Wrap the API with token validation middleware.
|
||||||
|
|
|
@ -2,49 +2,32 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type interceptingResponseWriter struct {
|
type flushingResponseWriter struct {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
Flusher http.Flusher
|
|
||||||
Prefix string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *interceptingResponseWriter) WriteHeader(status int) {
|
func (f *flushingResponseWriter) WriteHeader(status int) {
|
||||||
if status >= 300 && status < 400 {
|
f.ResponseWriter.WriteHeader(status)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush is here because the underlying HTTP chunked transfer response writer
|
// Flush is here because the underlying HTTP chunked transfer response writer
|
||||||
// to implement http.Flusher. Without it data is silently buffered. This
|
// to implement http.Flusher. Without it data is silently buffered. This
|
||||||
// was discovered when proxying kapacitor chunked logs.
|
// was discovered when proxying kapacitor chunked logs.
|
||||||
func (i *interceptingResponseWriter) Flush() {
|
func (f *flushingResponseWriter) Flush() {
|
||||||
if i.Flusher != nil {
|
if flusher, ok := f.ResponseWriter.(http.Flusher); ok {
|
||||||
i.Flusher.Flush()
|
flusher.Flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrefixedRedirect alters the Location header of downstream http.Handlers
|
// FlushingHandler may not actually do anything, but it was ostensibly
|
||||||
// to include a specified prefix
|
// implemented to flush response writers that can be flushed for the
|
||||||
func PrefixedRedirect(prefix string, next http.Handler) http.Handler {
|
// purposes in the comment above.
|
||||||
|
func FlushingHandler(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
iw := &interceptingResponseWriter{
|
iw := &flushingResponseWriter{
|
||||||
ResponseWriter: w,
|
ResponseWriter: w,
|
||||||
Prefix: prefix,
|
|
||||||
}
|
|
||||||
if flusher, ok := w.(http.Flusher); ok {
|
|
||||||
iw.Flusher = flusher
|
|
||||||
}
|
}
|
||||||
next.ServeHTTP(iw, r)
|
next.ServeHTTP(iw, r)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,6 +3,7 @@ package server
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
@ -10,6 +11,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
@ -27,7 +29,6 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
basepath string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
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"`
|
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"`
|
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"`
|
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"`
|
||||||
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"`
|
|
||||||
ShowVersion bool `short:"v" long:"version" description:"Show Chronograf version info"`
|
ShowVersion bool `short:"v" long:"version" description:"Show Chronograf version info"`
|
||||||
BuildInfo chronograf.BuildInfo
|
BuildInfo chronograf.BuildInfo
|
||||||
Listener net.Listener
|
Listener net.Listener
|
||||||
|
@ -344,11 +344,13 @@ func (s *Server) Serve(ctx context.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
basepath = s.Basepath
|
if !validBasepath(s.Basepath) {
|
||||||
if basepath != "" && s.PrefixRoutes == false {
|
err := fmt.Errorf("Invalid basepath, must follow format \"/mybasepath\"")
|
||||||
logger.
|
logger.
|
||||||
WithField("component", "server").
|
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)){}
|
providerFuncs := []func(func(oauth2.Provider, oauth2.Mux)){}
|
||||||
|
@ -366,8 +368,7 @@ func (s *Server) Serve(ctx context.Context) error {
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
UseAuth: s.useAuth(),
|
UseAuth: s.useAuth(),
|
||||||
ProviderFuncs: providerFuncs,
|
ProviderFuncs: providerFuncs,
|
||||||
Basepath: basepath,
|
Basepath: s.Basepath,
|
||||||
PrefixRoutes: s.PrefixRoutes,
|
|
||||||
StatusFeedURL: s.StatusFeedURL,
|
StatusFeedURL: s.StatusFeedURL,
|
||||||
CustomLinks: s.CustomLinks,
|
CustomLinks: s.CustomLinks,
|
||||||
}, service)
|
}, 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, "") == ""
|
||||||
|
}
|
||||||
|
|
|
@ -3,10 +3,12 @@ package server
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/bouk/httprouter"
|
"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 {
|
func WithContext(ctx context.Context, r *http.Request, kv map[string]string) *http.Request {
|
||||||
params := make(httprouter.Params, 0, len(kv))
|
params := make(httprouter.Params, 0, len(kv))
|
||||||
for k, v := range 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))
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue