2020-01-08 19:19:18 +00:00
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
2020-05-21 18:30:19 +00:00
|
|
|
"context"
|
2020-08-19 20:19:18 +00:00
|
|
|
"fmt"
|
2020-01-08 19:19:18 +00:00
|
|
|
"net/http"
|
|
|
|
"path"
|
2021-04-08 13:57:47 +00:00
|
|
|
"regexp"
|
2020-01-08 19:19:18 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2021-09-13 19:12:35 +00:00
|
|
|
"github.com/go-chi/chi"
|
2021-03-30 18:10:02 +00:00
|
|
|
"github.com/influxdata/influxdb/v2/kit/platform"
|
|
|
|
"github.com/influxdata/influxdb/v2/kit/platform/errors"
|
2020-04-03 17:39:20 +00:00
|
|
|
"github.com/influxdata/influxdb/v2/kit/tracing"
|
2020-01-08 19:19:18 +00:00
|
|
|
ua "github.com/mileusna/useragent"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Middleware constructor.
|
|
|
|
type Middleware func(http.Handler) http.Handler
|
|
|
|
|
|
|
|
func SetCORS(next http.Handler) http.Handler {
|
|
|
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if origin := r.Header.Get("Origin"); origin != "" {
|
2020-04-07 05:27:02 +00:00
|
|
|
// Access-Control-Allow-Origin must be present in every response
|
2020-01-08 19:19:18 +00:00
|
|
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
2020-04-07 05:27:02 +00:00
|
|
|
}
|
|
|
|
if r.Method == http.MethodOptions {
|
|
|
|
// allow and stop processing in pre-flight requests
|
2020-09-24 18:49:00 +00:00
|
|
|
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, PATCH")
|
2020-04-01 21:41:07 +00:00
|
|
|
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, Authorization, User-Agent")
|
2020-04-07 05:27:02 +00:00
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
return
|
2020-01-08 19:19:18 +00:00
|
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
}
|
|
|
|
return http.HandlerFunc(fn)
|
|
|
|
}
|
|
|
|
|
|
|
|
func Metrics(name string, reqMetric *prometheus.CounterVec, durMetric *prometheus.HistogramVec) Middleware {
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
|
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
statusW := NewStatusResponseWriter(w)
|
|
|
|
|
|
|
|
defer func(start time.Time) {
|
2021-07-29 16:22:11 +00:00
|
|
|
statusCode := statusW.Code()
|
|
|
|
// only log metrics for 2XX or 5XX requests
|
|
|
|
if !reportFromCode(statusCode) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-01-08 19:19:18 +00:00
|
|
|
label := prometheus.Labels{
|
2020-08-19 20:19:18 +00:00
|
|
|
"handler": name,
|
|
|
|
"method": r.Method,
|
|
|
|
"path": normalizePath(r.URL.Path),
|
|
|
|
"status": statusW.StatusCodeClass(),
|
2021-07-29 16:22:11 +00:00
|
|
|
"response_code": fmt.Sprintf("%d", statusCode),
|
2020-08-19 20:19:18 +00:00
|
|
|
"user_agent": UserAgent(r),
|
2020-01-08 19:19:18 +00:00
|
|
|
}
|
2021-07-29 16:22:11 +00:00
|
|
|
|
2020-01-08 19:19:18 +00:00
|
|
|
durMetric.With(label).Observe(time.Since(start).Seconds())
|
|
|
|
reqMetric.With(label).Inc()
|
|
|
|
}(time.Now())
|
|
|
|
|
|
|
|
next.ServeHTTP(statusW, r)
|
|
|
|
}
|
|
|
|
return http.HandlerFunc(fn)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func SkipOptions(next http.Handler) http.Handler {
|
|
|
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
2020-02-06 14:04:29 +00:00
|
|
|
// Preflight CORS requests from the browser will send an options request,
|
|
|
|
// so we need to make sure we satisfy them
|
|
|
|
if origin := r.Header.Get("Origin"); origin == "" && r.Method == http.MethodOptions {
|
|
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
2020-01-08 19:19:18 +00:00
|
|
|
return
|
|
|
|
}
|
2020-02-06 14:04:29 +00:00
|
|
|
|
2020-01-08 19:19:18 +00:00
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
}
|
|
|
|
return http.HandlerFunc(fn)
|
|
|
|
}
|
|
|
|
|
|
|
|
func Trace(name string) Middleware {
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
|
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
span, r := tracing.ExtractFromHTTPRequest(r, name)
|
|
|
|
defer span.Finish()
|
|
|
|
|
|
|
|
span.LogKV("user_agent", UserAgent(r))
|
|
|
|
for k, v := range r.Header {
|
|
|
|
if len(v) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if k == "Authorization" || k == "User-Agent" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// If header has multiple values, only the first value will be logged on the trace.
|
|
|
|
span.LogKV(k, v[0])
|
|
|
|
}
|
|
|
|
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
}
|
|
|
|
return http.HandlerFunc(fn)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func UserAgent(r *http.Request) string {
|
|
|
|
header := r.Header.Get("User-Agent")
|
|
|
|
if header == "" {
|
|
|
|
return "unknown"
|
|
|
|
}
|
|
|
|
|
|
|
|
return ua.Parse(header).Name
|
|
|
|
}
|
|
|
|
|
2021-04-08 13:57:47 +00:00
|
|
|
// Constants used for normalizing paths
|
|
|
|
const (
|
|
|
|
fileSlug = ":file_name"
|
|
|
|
shardSlug = ":shard_id"
|
|
|
|
)
|
|
|
|
|
2020-01-08 19:19:18 +00:00
|
|
|
func normalizePath(p string) string {
|
2021-04-08 13:57:47 +00:00
|
|
|
// Normalize any paths used during backup or restore processes
|
|
|
|
p = normalizeBackupAndRestore(p)
|
|
|
|
|
|
|
|
// Go through each part of the path and normalize IDs and UI assets
|
2020-01-08 19:19:18 +00:00
|
|
|
var parts []string
|
|
|
|
for head, tail := shiftPath(p); ; head, tail = shiftPath(tail) {
|
|
|
|
piece := head
|
2021-04-08 13:57:47 +00:00
|
|
|
|
|
|
|
// Normalize any ID's in the path as the ":id" slug
|
2021-03-30 18:10:02 +00:00
|
|
|
if len(piece) == platform.IDLength {
|
|
|
|
if _, err := platform.IDFromString(head); err == nil {
|
2020-01-08 19:19:18 +00:00
|
|
|
piece = ":id"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
parts = append(parts, piece)
|
2021-04-08 13:57:47 +00:00
|
|
|
|
2020-01-08 19:19:18 +00:00
|
|
|
if tail == "/" {
|
2021-04-08 13:57:47 +00:00
|
|
|
// Normalize UI asset file names. The UI asset file is the last part of the path.
|
|
|
|
parts[len(parts)-1] = normalizeAssetFile(parts[len(parts)-1])
|
2020-01-08 19:19:18 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "/" + path.Join(parts...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func shiftPath(p string) (head, tail string) {
|
|
|
|
p = path.Clean("/" + p)
|
|
|
|
i := strings.Index(p[1:], "/") + 1
|
|
|
|
if i <= 0 {
|
|
|
|
return p[1:], "/"
|
|
|
|
}
|
|
|
|
return p[1:i], p[i:]
|
|
|
|
}
|
2020-05-21 18:30:19 +00:00
|
|
|
|
2021-04-08 13:57:47 +00:00
|
|
|
// Normalize the file name for a UI asset
|
|
|
|
// For example: 838442d56d.svg will return as :file_id.svg
|
|
|
|
// Files names that do not have one of the listed extensions will be returned unchanged
|
|
|
|
func normalizeAssetFile(f string) string {
|
|
|
|
exts := []string{
|
|
|
|
".js",
|
|
|
|
".svg",
|
|
|
|
".woff2",
|
|
|
|
".wasm",
|
|
|
|
".map",
|
|
|
|
".LICENSE",
|
2021-05-20 12:58:18 +00:00
|
|
|
".ttf",
|
|
|
|
".woff",
|
|
|
|
".eot",
|
2021-04-08 13:57:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, ext := range exts {
|
|
|
|
if strings.HasSuffix(f, ext) {
|
|
|
|
return fileSlug + ext
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
|
|
|
// Normalize paths used during the backup and restore process.
|
|
|
|
// Paths not matching any of the patterns will be returned unchanged.
|
|
|
|
func normalizeBackupAndRestore(pth string) string {
|
|
|
|
patterns := map[string]string{
|
|
|
|
`restore/shards/\d+`: path.Join("restore/shards", shardSlug),
|
|
|
|
`backup/shards/\d+`: path.Join("backup/shards", shardSlug),
|
|
|
|
}
|
|
|
|
|
|
|
|
for p, s := range patterns {
|
|
|
|
re := regexp.MustCompile(p)
|
|
|
|
if re.MatchString(pth) {
|
|
|
|
return re.ReplaceAllString(pth, s)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return pth
|
|
|
|
}
|
|
|
|
|
2020-05-21 18:30:19 +00:00
|
|
|
type OrgContext string
|
|
|
|
|
|
|
|
const CtxOrgKey OrgContext = "orgID"
|
|
|
|
|
|
|
|
// ValidResource make sure a resource exists when a sub system needs to be mounted to an api
|
2021-03-30 18:10:02 +00:00
|
|
|
func ValidResource(api *API, lookupOrgByResourceID func(context.Context, platform.ID) (platform.ID, error)) Middleware {
|
2020-05-21 18:30:19 +00:00
|
|
|
return func(next http.Handler) http.Handler {
|
|
|
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
statusW := NewStatusResponseWriter(w)
|
2021-03-30 18:10:02 +00:00
|
|
|
id, err := platform.IDFromString(chi.URLParam(r, "id"))
|
2020-05-21 18:30:19 +00:00
|
|
|
if err != nil {
|
2021-03-30 18:10:02 +00:00
|
|
|
api.Err(w, r, platform.ErrCorruptID(err))
|
2020-05-21 18:30:19 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx := r.Context()
|
|
|
|
|
|
|
|
orgID, err := lookupOrgByResourceID(ctx, *id)
|
|
|
|
if err != nil {
|
2020-06-30 20:48:42 +00:00
|
|
|
// if this function returns an error we will squash the error message and replace it with a not found error
|
2021-03-30 18:10:02 +00:00
|
|
|
api.Err(w, r, &errors.Error{
|
|
|
|
Code: errors.ENotFound,
|
2020-06-30 20:48:42 +00:00
|
|
|
Msg: "404 page not found",
|
|
|
|
})
|
2020-05-21 18:30:19 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// embed OrgID into context
|
|
|
|
next.ServeHTTP(statusW, r.WithContext(context.WithValue(ctx, CtxOrgKey, orgID)))
|
|
|
|
}
|
|
|
|
return http.HandlerFunc(fn)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// OrgIDFromContext ....
|
2021-03-30 18:10:02 +00:00
|
|
|
func OrgIDFromContext(ctx context.Context) *platform.ID {
|
2020-05-21 18:30:19 +00:00
|
|
|
v := ctx.Value(CtxOrgKey)
|
|
|
|
if v == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2021-03-30 18:10:02 +00:00
|
|
|
id := v.(platform.ID)
|
2020-05-21 18:30:19 +00:00
|
|
|
return &id
|
|
|
|
}
|
2021-07-29 16:22:11 +00:00
|
|
|
|
|
|
|
// reportFromCode is a helper function to determine if telemetry data should be
|
|
|
|
// reported for this response.
|
|
|
|
func reportFromCode(c int) bool {
|
|
|
|
return (c >= 200 && c <= 299) || (c >= 500 && c <= 599)
|
|
|
|
}
|