chronograf/server/mux.go

263 lines
9.3 KiB
Go
Raw Normal View History

2016-10-25 15:20:06 +00:00
package server
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"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
"github.com/influxdata/chronograf/oauth2"
2016-10-25 15:20:06 +00:00
)
const (
// JSONType the mimetype for a json request
JSONType = "application/json"
)
// MuxOpts are the options for the router. Mostly related to auth.
2016-10-25 15:20:06 +00:00
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
UseAuth bool // UseAuth turns on Github OAuth and JWT
TokenSecret string // TokenSecret is the JWT secret
GithubClientID string // GithubClientID is the GH OAuth id
GithubClientSecret string // GithubClientSecret is the GH OAuth secret
GithubOrgs []string // GithubOrgs is the list of organizations a user may be a member of
GoogleClientID string // GoogleClientID is the Google OAuth id
GoogleClientSecret string // GoogleClientSecret is the Google OAuth secret
GoogleDomains []string // GoogleDomains is the list of domains a user may be a member of
PublicURL string // PublicURL is the public facing URL for the server
2016-10-25 15:20:06 +00:00
}
2017-02-15 20:07:33 +00:00
func (m *MuxOpts) UseGithub() bool {
return m.TokenSecret != "" && m.GithubClientID != "" && m.GithubClientSecret != ""
}
func (m *MuxOpts) UseGoogle() bool {
return m.TokenSecret != "" && m.GoogleClientID != "" && m.GoogleClientSecret != "" && m.PublicURL != ""
}
func (m *MuxOpts) Routes() AuthRoutes {
routes := AuthRoutes{}
2017-02-15 20:07:33 +00:00
if m.UseGithub() {
routes = append(routes, NewGithubRoute())
}
if m.UseGoogle() {
routes = append(routes, NewGoogleRoute())
}
return routes
}
// 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,
})
// Prefix any URLs found in the React assets with any configured basepath
prefixedAssets := NewDefaultURLPrefixer(basepath, assets, opts.Logger)
// 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.
router.NotFound = compressed
2016-10-25 15:20:06 +00:00
/* Documentation */
router.GET("/swagger.json", Spec())
router.GET("/docs", Redoc("/swagger.json"))
2016-10-25 15:20:06 +00:00
/* API */
// Sources
router.GET("/chronograf/v1/sources", service.Sources)
router.POST("/chronograf/v1/sources", service.NewSource)
2016-10-25 15:20: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
// Source Proxy
router.POST("/chronograf/v1/sources/:id/proxy", service.Proxy)
2016-10-25 15:20:06 +00:00
// Kapacitor
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
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-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)
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-25 15:20:06 +00:00
// Kapacitor Proxy
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
router.GET("/chronograf/v1/mappings", service.GetMappings)
2016-10-25 15:20:06 +00:00
// Layouts
router.GET("/chronograf/v1/layouts", service.Layouts)
router.POST("/chronograf/v1/layouts", service.NewLayout)
2016-10-25 15:20: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
router.GET("/chronograf/v1/me", service.Me)
router.POST("/chronograf/v1/users", service.NewUser)
router.GET("/chronograf/v1/users/:id", service.UserID)
router.PATCH("/chronograf/v1/users/:id", service.UpdateUser)
router.DELETE("/chronograf/v1/users/:id", service.RemoveUser)
// 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)
router.PUT("/chronograf/v1/dashboards/:id", service.UpdateDashboard)
2017-02-15 20:07:33 +00:00
authRoutes := opts.Routes()
// Root Routes returns all top-level routes in the API and
// optional authentication routes
router.GET("/chronograf/v1/", AllRoutes(authRoutes, opts.Logger))
router.GET("/chronograf/v1", AllRoutes(authRoutes, opts.Logger))
2016-10-25 15:20:06 +00:00
/* Authentication */
if opts.UseAuth {
// Create middleware to redirect to the appropriate provider logout
targetURL := "/"
router.GET("/oauth/logout", Logout(targetURL, authRoutes))
// Encapsulate the router with OAuth2
2016-10-25 15:20:06 +00:00
auth := AuthAPI(opts, router)
return Logger(opts.Logger, auth)
}
logged := Logger(opts.Logger, router)
return logged
2016-10-25 15:20:06 +00:00
}
// AuthAPI adds the OAuth routes if auth is enabled.
// TODO: this function is not great. Would be good if providers added their routes.
2016-10-25 15:20:06 +00:00
func AuthAPI(opts MuxOpts, router *httprouter.Router) http.Handler {
auth := oauth2.NewJWT(opts.TokenSecret)
2017-02-15 20:07:33 +00:00
if opts.UseGithub() {
gh := oauth2.Github{
ClientID: opts.GithubClientID,
ClientSecret: opts.GithubClientSecret,
Orgs: opts.GithubOrgs,
Logger: opts.Logger,
}
ghMux := oauth2.NewJWTMux(&gh, &auth, opts.Logger)
router.Handler("GET", "/oauth/github/login", ghMux.Login())
router.Handler("GET", "/oauth/github/logout", ghMux.Logout())
router.Handler("GET", "/oauth/github/callback", ghMux.Callback())
}
2017-02-15 20:07:33 +00:00
if opts.UseGoogle() {
redirectURL := opts.PublicURL + opts.Basepath + "/oauth/google/callback"
google := oauth2.Google{
ClientID: opts.GoogleClientID,
ClientSecret: opts.GoogleClientSecret,
Domains: opts.GoogleDomains,
RedirectURL: redirectURL,
Logger: opts.Logger,
}
goMux := oauth2.NewJWTMux(&google, &auth, opts.Logger)
router.Handler("GET", "/oauth/google/login", goMux.Login())
router.Handler("GET", "/oauth/google/logout", goMux.Logout())
router.Handler("GET", "/oauth/google/callback", goMux.Callback())
}
2016-10-25 15:20:06 +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) {
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
}
router.ServeHTTP(w, r)
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
}
}
// 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
}