2016-10-25 15:20:06 +00:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
2017-02-09 20:35:38 +00:00
|
|
|
"github.com/NYTimes/gziphandler"
|
2016-10-25 15:20:06 +00:00
|
|
|
"github.com/bouk/httprouter"
|
2016-11-19 17:41:06 +00:00
|
|
|
"github.com/influxdata/chronograf" // When julienschmidt/httprouter v2 w/ context is out, switch
|
2017-02-14 21:14:24 +00:00
|
|
|
"github.com/influxdata/chronograf/oauth2"
|
2016-10-25 15:20:06 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// JSONType the mimetype for a json request
|
|
|
|
JSONType = "application/json"
|
|
|
|
)
|
|
|
|
|
2016-10-28 16:27:06 +00:00
|
|
|
// MuxOpts are the options for the router. Mostly related to auth.
|
2016-10-25 15:20:06 +00:00
|
|
|
type MuxOpts struct {
|
2017-02-23 22:17:28 +00:00
|
|
|
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
|
|
|
|
UseAuth bool // UseAuth turns on Github OAuth and JWT
|
|
|
|
TokenSecret string
|
2017-02-16 17:56:59 +00:00
|
|
|
|
2017-02-23 22:17:28 +00:00
|
|
|
ProviderFuncs []func(func(oauth2.Provider, oauth2.Mux))
|
2016-10-25 15:20:06 +00:00
|
|
|
}
|
|
|
|
|
2016-10-28 16:27:06 +00:00
|
|
|
// NewMux attaches all the route handlers; handler returned servers chronograf.
|
|
|
|
func NewMux(opts MuxOpts, service Service) http.Handler {
|
2016-10-25 15:20:06 +00:00
|
|
|
router := httprouter.New()
|
|
|
|
|
|
|
|
/* React Application */
|
|
|
|
assets := Assets(AssetsOpts{
|
|
|
|
Develop: opts.Develop,
|
|
|
|
Logger: opts.Logger,
|
|
|
|
})
|
2017-01-28 00:14:21 +00:00
|
|
|
|
|
|
|
// Prefix any URLs found in the React assets with any configured basepath
|
2017-01-28 00:24:51 +00:00
|
|
|
prefixedAssets := NewDefaultURLPrefixer(basepath, assets, opts.Logger)
|
2017-01-28 00:14:21 +00:00
|
|
|
|
2017-02-10 19:48:42 +00:00
|
|
|
// Compress the assets with gzip if an accepted encoding
|
|
|
|
compressed := gziphandler.GzipHandler(prefixedAssets)
|
|
|
|
|
2016-10-25 15:20:06 +00:00
|
|
|
// 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.
|
2017-02-10 19:48:42 +00:00
|
|
|
router.NotFound = compressed
|
2016-10-25 15:20:06 +00:00
|
|
|
|
|
|
|
/* Documentation */
|
2016-10-28 16:27:06 +00:00
|
|
|
router.GET("/swagger.json", Spec())
|
|
|
|
router.GET("/docs", Redoc("/swagger.json"))
|
2016-10-25 15:20:06 +00:00
|
|
|
|
|
|
|
/* API */
|
|
|
|
// Sources
|
2016-10-28 16:27:06 +00:00
|
|
|
router.GET("/chronograf/v1/sources", service.Sources)
|
|
|
|
router.POST("/chronograf/v1/sources", service.NewSource)
|
2016-10-25 15:20:06 +00:00
|
|
|
|
2016-10-28 16:27:06 +00:00
|
|
|
router.GET("/chronograf/v1/sources/:id", service.SourcesID)
|
|
|
|
router.PATCH("/chronograf/v1/sources/:id", service.UpdateSource)
|
|
|
|
router.DELETE("/chronograf/v1/sources/:id", service.RemoveSource)
|
2016-10-25 15:20:06 +00:00
|
|
|
|
2017-02-17 20:02:02 +00:00
|
|
|
// Source Proxy to Influx
|
|
|
|
router.POST("/chronograf/v1/sources/:id/proxy", service.Influx)
|
2016-10-25 15:20:06 +00:00
|
|
|
|
2017-02-19 20:00:34 +00:00
|
|
|
// All possible permissions for users in this source
|
|
|
|
router.GET("/chronograf/v1/sources/:id/permissions", service.Permissions)
|
|
|
|
|
2017-02-18 02:47:23 +00:00
|
|
|
// Users associated with the data source
|
|
|
|
router.GET("/chronograf/v1/sources/:id/users", service.SourceUsers)
|
|
|
|
router.POST("/chronograf/v1/sources/:id/users", service.NewSourceUser)
|
|
|
|
|
|
|
|
router.GET("/chronograf/v1/sources/:id/users/:uid", service.SourceUserID)
|
|
|
|
router.DELETE("/chronograf/v1/sources/:id/users/:uid", service.RemoveSourceUser)
|
|
|
|
router.PATCH("/chronograf/v1/sources/:id/users/:uid", service.UpdateSourceUser)
|
|
|
|
|
2017-02-24 03:54:20 +00:00
|
|
|
// Roles associated with the data source
|
|
|
|
router.GET("/chronograf/v1/sources/:id/roles", service.Roles)
|
|
|
|
router.POST("/chronograf/v1/sources/:id/roles", service.NewRole)
|
|
|
|
|
|
|
|
router.GET("/chronograf/v1/sources/:id/roles/:rid", service.RoleID)
|
|
|
|
router.DELETE("/chronograf/v1/sources/:id/roles/:rid", service.RemoveRole)
|
|
|
|
router.PATCH("/chronograf/v1/sources/:id/roles/:rid", service.UpdateRole)
|
|
|
|
|
2016-10-25 15:20:06 +00:00
|
|
|
// Kapacitor
|
2016-10-28 16:27:06 +00:00
|
|
|
router.GET("/chronograf/v1/sources/:id/kapacitors", service.Kapacitors)
|
|
|
|
router.POST("/chronograf/v1/sources/:id/kapacitors", service.NewKapacitor)
|
2016-10-25 15:20:06 +00:00
|
|
|
|
2016-10-28 16:27:06 +00:00
|
|
|
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid", service.KapacitorsID)
|
|
|
|
router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid", service.UpdateKapacitor)
|
|
|
|
router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid", service.RemoveKapacitor)
|
2016-10-25 15:20:06 +00:00
|
|
|
|
2016-11-04 06:53:54 +00:00
|
|
|
// Kapacitor rules
|
|
|
|
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/rules", service.KapacitorRulesGet)
|
|
|
|
router.POST("/chronograf/v1/sources/:id/kapacitors/:kid/rules", service.KapacitorRulesPost)
|
2016-11-03 06:42:52 +00:00
|
|
|
|
2016-11-04 06:53:54 +00:00
|
|
|
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesID)
|
|
|
|
router.PUT("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesPut)
|
2017-02-10 19:48:42 +00:00
|
|
|
router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesStatus)
|
2016-11-04 06:53:54 +00:00
|
|
|
router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesDelete)
|
2016-10-31 23:11:05 +00:00
|
|
|
|
2016-10-25 15:20:06 +00:00
|
|
|
// Kapacitor Proxy
|
2016-10-28 16:27:06 +00:00
|
|
|
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", service.KapacitorProxyGet)
|
|
|
|
router.POST("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", service.KapacitorProxyPost)
|
|
|
|
router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", service.KapacitorProxyPatch)
|
|
|
|
router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", service.KapacitorProxyDelete)
|
2016-10-25 15:20:06 +00:00
|
|
|
|
|
|
|
// Mappings
|
2016-10-28 16:27:06 +00:00
|
|
|
router.GET("/chronograf/v1/mappings", service.GetMappings)
|
2016-10-25 15:20:06 +00:00
|
|
|
|
|
|
|
// Layouts
|
2016-10-28 16:27:06 +00:00
|
|
|
router.GET("/chronograf/v1/layouts", service.Layouts)
|
|
|
|
router.POST("/chronograf/v1/layouts", service.NewLayout)
|
2016-10-25 15:20:06 +00:00
|
|
|
|
2016-10-28 16:27:06 +00:00
|
|
|
router.GET("/chronograf/v1/layouts/:id", service.LayoutsID)
|
|
|
|
router.PUT("/chronograf/v1/layouts/:id", service.UpdateLayout)
|
|
|
|
router.DELETE("/chronograf/v1/layouts/:id", service.RemoveLayout)
|
2016-10-25 15:20:06 +00:00
|
|
|
|
|
|
|
// Users
|
2016-11-17 23:57:46 +00:00
|
|
|
router.GET("/chronograf/v1/me", service.Me)
|
|
|
|
|
2016-12-07 23:18:04 +00:00
|
|
|
// Dashboards
|
|
|
|
router.GET("/chronograf/v1/dashboards", service.Dashboards)
|
|
|
|
router.POST("/chronograf/v1/dashboards", service.NewDashboard)
|
|
|
|
|
|
|
|
router.GET("/chronograf/v1/dashboards/:id", service.DashboardID)
|
2017-01-27 12:59:13 +00:00
|
|
|
router.DELETE("/chronograf/v1/dashboards/:id", service.RemoveDashboard)
|
2017-02-22 15:46:16 +00:00
|
|
|
router.PUT("/chronograf/v1/dashboards/:id", service.ReplaceDashboard)
|
|
|
|
router.PATCH("/chronograf/v1/dashboards/:id", service.UpdateDashboard)
|
2016-12-07 23:18:04 +00:00
|
|
|
|
2017-02-23 22:17:28 +00:00
|
|
|
var authRoutes AuthRoutes
|
2016-12-07 23:18:04 +00:00
|
|
|
|
2017-02-23 22:17:28 +00:00
|
|
|
var out http.Handler
|
2016-10-25 15:20:06 +00:00
|
|
|
/* Authentication */
|
|
|
|
if opts.UseAuth {
|
2017-02-23 22:17:28 +00:00
|
|
|
// Encapsulate the router with OAuth2
|
|
|
|
var auth http.Handler
|
|
|
|
auth, authRoutes = AuthAPI(opts, router)
|
|
|
|
|
2017-02-15 22:28:17 +00:00
|
|
|
// Create middleware to redirect to the appropriate provider logout
|
|
|
|
targetURL := "/"
|
|
|
|
router.GET("/oauth/logout", Logout(targetURL, authRoutes))
|
|
|
|
|
2017-02-23 22:17:28 +00:00
|
|
|
out = Logger(opts.Logger, auth)
|
|
|
|
} else {
|
|
|
|
out = Logger(opts.Logger, router)
|
2016-10-25 15:20:06 +00:00
|
|
|
}
|
2017-02-09 20:35:38 +00:00
|
|
|
|
2017-02-23 22:17:28 +00:00
|
|
|
router.GET("/chronograf/v1/", AllRoutes(authRoutes, opts.Logger))
|
|
|
|
router.GET("/chronograf/v1", AllRoutes(authRoutes, opts.Logger))
|
|
|
|
|
|
|
|
return out
|
2016-10-25 15:20:06 +00:00
|
|
|
}
|
|
|
|
|
2016-10-28 16:27:06 +00:00
|
|
|
// AuthAPI adds the OAuth routes if auth is enabled.
|
2017-02-15 22:28:17 +00:00
|
|
|
// TODO: this function is not great. Would be good if providers added their routes.
|
2017-02-23 22:17:28 +00:00
|
|
|
func AuthAPI(opts MuxOpts, router *httprouter.Router) (http.Handler, AuthRoutes) {
|
2017-02-15 05:09:34 +00:00
|
|
|
auth := oauth2.NewJWT(opts.TokenSecret)
|
2017-02-23 22:17:28 +00:00
|
|
|
routes := AuthRoutes{}
|
|
|
|
for _, pf := range opts.ProviderFuncs {
|
|
|
|
pf(func(p oauth2.Provider, m oauth2.Mux) {
|
|
|
|
loginPath := fmt.Sprintf("%s/oauth/%s/login", opts.Basepath, strings.ToLower(p.Name()))
|
|
|
|
logoutPath := fmt.Sprintf("%s/oauth/%s/logout", opts.Basepath, strings.ToLower(p.Name()))
|
|
|
|
callbackPath := fmt.Sprintf("%s/oauth/%s/callback", opts.Basepath, strings.ToLower(p.Name()))
|
|
|
|
router.Handler("GET", loginPath, m.Login())
|
|
|
|
router.Handler("GET", logoutPath, m.Logout())
|
|
|
|
router.Handler("GET", callbackPath, m.Callback())
|
|
|
|
routes = append(routes, AuthRoute{
|
|
|
|
Name: p.Name(),
|
|
|
|
Label: strings.Title(p.Name()),
|
|
|
|
Login: loginPath,
|
|
|
|
Logout: logoutPath,
|
|
|
|
Callback: callbackPath,
|
|
|
|
})
|
|
|
|
})
|
2017-02-16 17:56:59 +00:00
|
|
|
}
|
|
|
|
|
2017-02-14 21:14:24 +00:00
|
|
|
tokenMiddleware := oauth2.AuthorizedToken(&auth, &oauth2.CookieExtractor{Name: "session"}, opts.Logger, router)
|
2016-10-25 15:20:06 +00:00
|
|
|
// Wrap the API with token validation middleware.
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2017-02-15 22:28:17 +00:00
|
|
|
if strings.HasPrefix(r.URL.Path, "/chronograf/v1/") || r.URL.Path == "/oauth/logout" {
|
2016-10-25 15:20:06 +00:00
|
|
|
tokenMiddleware.ServeHTTP(w, r)
|
|
|
|
return
|
|
|
|
}
|
2016-10-28 16:27:06 +00:00
|
|
|
router.ServeHTTP(w, r)
|
2017-02-23 22:17:28 +00:00
|
|
|
}), routes
|
2016-10-25 15:20:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func encodeJSON(w http.ResponseWriter, status int, v interface{}, logger chronograf.Logger) {
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
w.WriteHeader(status)
|
|
|
|
if err := json.NewEncoder(w).Encode(v); err != nil {
|
2016-11-19 17:41:06 +00:00
|
|
|
unknownErrorWithMessage(w, err, logger)
|
2016-10-25 15:20:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-28 16:27:06 +00:00
|
|
|
// Error writes an JSON message
|
2016-11-19 17:41:06 +00:00
|
|
|
func Error(w http.ResponseWriter, code int, msg string, logger chronograf.Logger) {
|
|
|
|
e := ErrorMessage{
|
2016-10-25 15:20:06 +00:00
|
|
|
Code: code,
|
|
|
|
Message: msg,
|
|
|
|
}
|
|
|
|
b, err := json.Marshal(e)
|
|
|
|
if err != nil {
|
|
|
|
code = http.StatusInternalServerError
|
|
|
|
b = []byte(`{"code": 500, "message":"server_error"}`)
|
|
|
|
}
|
2016-11-19 17:41:06 +00:00
|
|
|
|
|
|
|
logger.
|
|
|
|
WithField("component", "server").
|
|
|
|
WithField("http_status ", code).
|
|
|
|
Error("Error message ", msg)
|
2016-10-25 15:20:06 +00:00
|
|
|
w.Header().Set("Content-Type", JSONType)
|
|
|
|
w.WriteHeader(code)
|
2016-12-20 20:59:56 +00:00
|
|
|
_, _ = w.Write(b)
|
2016-10-25 15:20:06 +00:00
|
|
|
}
|
|
|
|
|
2016-11-19 17:41:06 +00:00
|
|
|
func invalidData(w http.ResponseWriter, err error, logger chronograf.Logger) {
|
|
|
|
Error(w, http.StatusUnprocessableEntity, fmt.Sprintf("%v", err), logger)
|
2016-10-25 15:20:06 +00:00
|
|
|
}
|
|
|
|
|
2016-11-19 17:41:06 +00:00
|
|
|
func invalidJSON(w http.ResponseWriter, logger chronograf.Logger) {
|
|
|
|
Error(w, http.StatusBadRequest, "Unparsable JSON", logger)
|
2016-10-25 15:20:06 +00:00
|
|
|
}
|
|
|
|
|
2016-11-19 17:41:06 +00:00
|
|
|
func unknownErrorWithMessage(w http.ResponseWriter, err error, logger chronograf.Logger) {
|
|
|
|
Error(w, http.StatusInternalServerError, fmt.Sprintf("Unknown error: %v", err), logger)
|
2016-10-25 15:20:06 +00:00
|
|
|
}
|
|
|
|
|
2016-11-19 17:41:06 +00:00
|
|
|
func notFound(w http.ResponseWriter, id int, logger chronograf.Logger) {
|
|
|
|
Error(w, http.StatusNotFound, fmt.Sprintf("ID %d not found", id), logger)
|
2016-10-25 15:20:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func paramID(key string, r *http.Request) (int, error) {
|
|
|
|
ctx := r.Context()
|
|
|
|
param := httprouter.GetParamFromContext(ctx, key)
|
|
|
|
id, err := strconv.Atoi(param)
|
|
|
|
if err != nil {
|
|
|
|
return -1, fmt.Errorf("Error converting ID %s", param)
|
|
|
|
}
|
|
|
|
return id, nil
|
|
|
|
}
|