package app import ( "bytes" "compress/gzip" "fmt" "io" "net/http" "net/url" "strings" "sync" "time" "context" "github.com/NYTimes/gziphandler" "github.com/gorilla/mux" log "github.com/sirupsen/logrus" "github.com/weaveworks/scope/common/hostname" "github.com/weaveworks/scope/common/xfer" "github.com/weaveworks/scope/report" ) var ( // Version - set at buildtime. Version = "dev" // UniqueID - set at runtime. UniqueID = "0" ) // contextKey is a wrapper type for use in context.WithValue() to satisfy golint // https://github.com/golang/go/issues/17293 // https://github.com/golang/lint/pull/245 type contextKey string // RequestCtxKey is key used for request entry in context const RequestCtxKey contextKey = contextKey("request") // CtxHandlerFunc is a http.HandlerFunc, with added contexts type CtxHandlerFunc func(context.Context, http.ResponseWriter, *http.Request) func requestContextDecorator(f CtxHandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := context.WithValue(r.Context(), RequestCtxKey, r) f(ctx, w, r) } } // URLMatcher uses request.RequestURI (the raw, unparsed request) to attempt // to match pattern. It does this as go's URL.Parse method is broken, and // mistakenly unescapes the Path before parsing it. This breaks %2F (encoded // forward slashes) in the paths. func URLMatcher(pattern string) mux.MatcherFunc { return func(r *http.Request, rm *mux.RouteMatch) bool { vars, match := matchURL(r, pattern) if match { rm.Vars = vars } return match } } func matchURL(r *http.Request, pattern string) (map[string]string, bool) { matchParts := strings.Split(pattern, "/") path := strings.SplitN(r.RequestURI, "?", 2)[0] parts := strings.Split(path, "/") if len(parts) != len(matchParts) { return nil, false } vars := map[string]string{} for i, part := range parts { unescaped, err := url.QueryUnescape(part) if err != nil { return nil, false } match := matchParts[i] if strings.HasPrefix(match, "{") && strings.HasSuffix(match, "}") { vars[strings.Trim(match, "{}")] = unescaped } else if matchParts[i] != unescaped { return nil, false } } return vars, true } func gzipHandler(h http.HandlerFunc) http.Handler { return gziphandler.GzipHandler(h) } // RegisterTopologyRoutes registers the various topology routes with a http mux. func RegisterTopologyRoutes(router *mux.Router, r Reporter, capabilities map[string]bool) { get := router.Methods("GET").Subrouter() get.Handle("/api", gzipHandler(requestContextDecorator(apiHandler(r, capabilities)))) get.Handle("/api/topology", gzipHandler(requestContextDecorator(topologyRegistry.makeTopologyList(r)))) get.Handle("/api/topology/{topology}", gzipHandler(requestContextDecorator(topologyRegistry.captureRenderer(r, handleTopology)))). Name("api_topology_topology") get.Handle("/api/topology/{topology}/ws", requestContextDecorator(captureReporter(r, handleWebsocket))). // NB not gzip! Name("api_topology_topology_ws") get.MatcherFunc(URLMatcher("/api/topology/{topology}/{id}")).Handler( gzipHandler(requestContextDecorator(topologyRegistry.captureRenderer(r, handleNode)))). Name("api_topology_topology_id") get.Handle("/api/report", gzipHandler(requestContextDecorator(makeRawReportHandler(r)))) get.Handle("/api/probes", gzipHandler(requestContextDecorator(makeProbeHandler(r)))) } // RegisterReportPostHandler registers the handler for report submission func RegisterReportPostHandler(a Adder, router *mux.Router) { post := router.Methods("POST").Subrouter() post.HandleFunc("/api/report", requestContextDecorator(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { var ( buf = &bytes.Buffer{} reader = io.TeeReader(r.Body, buf) ) gzipped := strings.Contains(r.Header.Get("Content-Encoding"), "gzip") if !gzipped { reader = io.TeeReader(r.Body, gzip.NewWriter(buf)) } contentType := r.Header.Get("Content-Type") var isMsgpack bool switch { case strings.HasPrefix(contentType, "application/msgpack"): isMsgpack = true case strings.HasPrefix(contentType, "application/json"): isMsgpack = false default: respondWith(ctx, w, http.StatusBadRequest, fmt.Errorf("Unsupported Content-Type: %v", contentType)) return } rpt, err := report.MakeFromBinary(ctx, reader, gzipped, isMsgpack) if err != nil { respondWith(ctx, w, http.StatusBadRequest, err) return } // a.Add(..., buf) assumes buf is gzip'd msgpack if !isMsgpack { buf, _ = rpt.WriteBinary() } if err := a.Add(ctx, *rpt, buf.Bytes()); err != nil { log.Errorf("Error Adding report: %v", err) respondWith(ctx, w, http.StatusInternalServerError, err) return } w.WriteHeader(http.StatusOK) })) } // RegisterAdminRoutes registers routes for admin calls with a http mux. func RegisterAdminRoutes(router *mux.Router, reporter Reporter) { get := router.Methods("GET").Subrouter() get.Handle("/admin/summary", requestContextDecorator(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { summary, err := reporter.AdminSummary(ctx, time.Now()) if err != nil { respondWith(ctx, w, http.StatusBadRequest, err) } fmt.Fprintln(w, summary) })) } var newVersion = struct { sync.Mutex *xfer.NewVersionInfo }{} // NewVersion is called to expose new version information to /api func NewVersion(version, downloadURL string) { newVersion.Lock() defer newVersion.Unlock() newVersion.NewVersionInfo = &xfer.NewVersionInfo{ Version: version, DownloadURL: downloadURL, } } func apiHandler(rep Reporter, capabilities map[string]bool) CtxHandlerFunc { return func(ctx context.Context, w http.ResponseWriter, r *http.Request) { report, err := rep.Report(ctx, time.Now()) if err != nil { respondWith(ctx, w, http.StatusInternalServerError, err) return } newVersion.Lock() defer newVersion.Unlock() respondWith(ctx, w, http.StatusOK, xfer.Details{ ID: UniqueID, Version: Version, Hostname: hostname.Get(), Plugins: report.Plugins, Capabilities: capabilities, NewVersion: newVersion.NewVersionInfo, }) } }