blob: b09760868c01992cc6d52315ea8066c8ebe44632 [file] [log] [blame]
// Package routing defines the HTTP routes for Traffic Ops and provides tools to
// register those routes with appropriate middleware.
package routing
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/apache/trafficcontrol/lib/go-log"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/auth"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/plugin"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/routing/middleware"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/trafficvault"
"github.com/jmoiron/sqlx"
)
// RoutePrefix is a prefix that all API routes must match.
const RoutePrefix = "^api" // TODO config?
type backendConfigSynced struct {
cfg config.BackendConfig
*sync.RWMutex
}
// backendCfg stores the current backend config supplied to traffic ops.
var backendCfg = backendConfigSynced{RWMutex: &sync.RWMutex{}}
// GetBackendConfig returns the current BackendConfig.
func GetBackendConfig() config.BackendConfig {
backendCfg.RLock()
defer backendCfg.RUnlock()
return backendCfg.cfg
}
// SetBackendConfig sets the BackendConfig to the value supplied.
func SetBackendConfig(backendConfig config.BackendConfig) {
backendCfg.Lock()
defer backendCfg.Unlock()
backendCfg.cfg = backendConfig
}
// A Route defines an association with a client request and a handler for that
// request.
type Route struct {
// Order matters! Do not reorder this! Routes() uses positional construction for readability.
Version api.Version
Method string
Path string
Handler http.HandlerFunc
RequiredPrivLevel int
RequiredPermissions []string
Authenticated bool
Middlewares []middleware.Middleware
ID int // unique ID for referencing this Route
}
func (r Route) String() string {
return fmt.Sprintf("id=%d\tmethod=%s\tversion=%d.%d\tpath=%s", r.ID, r.Method, r.Version.Major, r.Version.Minor, r.Path)
}
// SetMiddleware sets up a Route's Middlewares to include the default set of
// Middlewares if necessary.
func (r *Route) SetMiddleware(authBase middleware.AuthBase, requestTimeout time.Duration) {
if r.Middlewares == nil {
r.Middlewares = middleware.GetDefault(authBase.Secret, requestTimeout)
}
if r.Authenticated { // a privLevel of zero is an unauthenticated endpoint.
authWrapper := authBase.GetWrapper(r.RequiredPrivLevel)
r.Middlewares = append(r.Middlewares, authWrapper)
}
r.Middlewares = append(r.Middlewares, middleware.RequiredPermissionsMiddleware(r.RequiredPermissions))
}
// ServerData ...
type ServerData struct {
config.Config
DB *sqlx.DB
Profiling *bool // Yes this is a field in the config but we want to live reload this value and NOT the entire config
Plugins plugin.Plugins
TrafficVault trafficvault.TrafficVault
Mux *http.ServeMux
}
// CompiledRoute ...
type CompiledRoute struct {
Handler http.HandlerFunc
Regex *regexp.Regexp
Params []string
ID int
}
func getSortedRouteVersions(rs []Route) []api.Version {
majorsToMinors := map[uint64][]uint64{}
majors := map[uint64]struct{}{}
for _, r := range rs {
majors[r.Version.Major] = struct{}{}
if _, ok := majorsToMinors[r.Version.Major]; ok {
previouslyIncluded := false
for _, prevMinor := range majorsToMinors[r.Version.Major] {
if prevMinor == r.Version.Minor {
previouslyIncluded = true
}
}
if !previouslyIncluded {
majorsToMinors[r.Version.Major] = append(majorsToMinors[r.Version.Major], r.Version.Minor)
}
} else {
majorsToMinors[r.Version.Major] = []uint64{r.Version.Minor}
}
}
sortedMajors := []uint64{}
for major := range majors {
sortedMajors = append(sortedMajors, major)
}
sort.Slice(sortedMajors, func(i, j int) bool { return sortedMajors[i] < sortedMajors[j] })
versions := []api.Version{}
for _, major := range sortedMajors {
sort.Slice(majorsToMinors[major], func(i, j int) bool { return majorsToMinors[major][i] < majorsToMinors[major][j] })
for _, minor := range majorsToMinors[major] {
version := api.Version{Major: major, Minor: minor}
versions = append(versions, version)
}
}
return versions
}
func indexOfApiVersion(versions []api.Version, desiredVersion api.Version) int {
for i, v := range versions {
if v.Major > desiredVersion.Major {
return i
}
if v.Major == desiredVersion.Major && v.Minor >= desiredVersion.Minor {
return i
}
}
return len(versions) - 1
}
// PathHandler ...
type PathHandler struct {
Path string
Handler http.HandlerFunc
ID int
}
// CreateRouteMap returns a map of methods to a slice of paths and handlers; wrapping the handlers in the appropriate middleware. Uses Semantic Versioning: routes are added to every subsequent minor version, but not subsequent major versions. For example, a 1.2 route is added to 1.3 but not 2.1. Also truncates '2.0' to '2', creating succinct major versions.
// Returns the map of routes, and a map of API versions served.
func CreateRouteMap(rs []Route, disabledRouteIDs []int, perlHandler http.HandlerFunc, authBase middleware.AuthBase, reqTimeOutSeconds int) (map[string][]PathHandler, map[api.Version]struct{}) {
// TODO strong types for method, path
versions := getSortedRouteVersions(rs)
requestTimeout := middleware.DefaultRequestTimeout
if reqTimeOutSeconds > 0 {
requestTimeout = time.Second * time.Duration(reqTimeOutSeconds)
}
disabledRoutes := GetRouteIDMap(disabledRouteIDs)
m := map[string][]PathHandler{}
for _, r := range rs {
versionI := indexOfApiVersion(versions, r.Version)
nextMajorVer := r.Version.Major + 1
_, isDisabledRoute := disabledRoutes[r.ID]
r.SetMiddleware(authBase, requestTimeout)
for _, version := range versions[versionI:] {
if version.Major >= nextMajorVer {
break
}
vstr := strconv.FormatUint(version.Major, 10) + "." + strconv.FormatUint(version.Minor, 10)
path := RoutePrefix + "/" + vstr + "/" + r.Path
if isDisabledRoute {
m[r.Method] = append(m[r.Method], PathHandler{Path: path, Handler: middleware.WrapAccessLog(authBase.Secret, middleware.DisabledRouteHandler()), ID: r.ID})
} else {
m[r.Method] = append(m[r.Method], PathHandler{Path: path, Handler: middleware.Use(r.Handler, r.Middlewares), ID: r.ID})
}
log.Infof("adding route %v %v\n", r.Method, path)
}
}
versionSet := map[api.Version]struct{}{}
for _, version := range versions {
versionSet[version] = struct{}{}
}
return m, versionSet
}
// CompileRoutes - takes a map of methods to paths and handlers, and returns a map of methods to CompiledRoutes
func CompileRoutes(routes map[string][]PathHandler) map[string][]CompiledRoute {
compiledRoutes := map[string][]CompiledRoute{}
for method, mRoutes := range routes {
for _, pathHandler := range mRoutes {
route := pathHandler.Path
handler := pathHandler.Handler
var params []string
for open := strings.Index(route, "{"); open > 0; open = strings.Index(route, "{") {
close := strings.Index(route, "}")
if close < 0 {
panic("malformed route")
}
param := route[open+1 : close]
params = append(params, param)
route = route[:open] + `([^/]+)` + route[close+1:]
}
regex := regexp.MustCompile(route)
id := pathHandler.ID
compiledRoutes[method] = append(compiledRoutes[method], CompiledRoute{Handler: handler, Regex: regex, Params: params, ID: id})
}
}
return compiledRoutes
}
// Handler - generic handler func used by the Handlers hooking into the routes
func Handler(
routes map[string][]CompiledRoute,
versions map[api.Version]struct{},
catchall http.Handler,
db *sqlx.DB,
cfg *config.Config,
getReqID func() uint64,
plugins plugin.Plugins,
tv trafficvault.TrafficVault,
w http.ResponseWriter,
r *http.Request,
) {
reqID := getReqID()
reqIDStr := strconv.FormatUint(reqID, 10)
log.Infoln(r.Method + " " + r.URL.Path + "?" + r.URL.RawQuery + " handling (reqid " + reqIDStr + ")")
start := time.Now()
defer func() {
log.Infoln(r.Method + " " + r.URL.Path + "?" + r.URL.RawQuery + " handled (reqid " + reqIDStr + ") in " + time.Since(start).String())
}()
ctx := r.Context()
ctx = context.WithValue(ctx, api.DBContextKey, db)
ctx = context.WithValue(ctx, api.ConfigContextKey, cfg)
ctx = context.WithValue(ctx, api.ReqIDContextKey, reqID)
ctx = context.WithValue(ctx, api.TrafficVaultContextKey, tv)
// plugins have no pre-parsed path params, but add an empty map so they can use the api helper funcs that require it.
pluginCtx := context.WithValue(ctx, api.PathParamsKey, map[string]string{})
pluginReq := r.WithContext(pluginCtx)
onReqData := plugin.OnRequestData{Data: plugin.Data{RequestID: reqID, AppCfg: *cfg}, W: w, R: pluginReq}
if handled := plugins.OnRequest(onReqData); handled {
return
}
requested := r.URL.Path[1:]
mRoutes, ok := routes[r.Method]
if !ok {
catchall.ServeHTTP(w, r)
return
}
for _, compiledRoute := range mRoutes {
match := compiledRoute.Regex.FindStringSubmatch(requested)
if len(match) == 0 {
continue
}
params := map[string]string{}
for i, v := range compiledRoute.Params {
params[v] = match[i+1]
}
routeCtx := context.WithValue(ctx, api.PathParamsKey, params)
routeCtx = context.WithValue(routeCtx, middleware.RouteID, compiledRoute.ID)
r = r.WithContext(routeCtx)
compiledRoute.Handler(w, r)
return
}
if IsRequestAPIAndUnknownVersion(r, versions) {
h := middleware.WrapAccessLog(cfg.Secrets[0], middleware.NotImplementedHandler())
h.ServeHTTP(w, r)
return
}
var backendRouteHandled bool
backendConfig := GetBackendConfig()
for i, backendRoute := range backendConfig.Routes {
var params []string
routeParams := map[string]string{}
if backendRoute.Method == r.Method {
for open := strings.Index(backendRoute.Path, "{"); open > 0; open = strings.Index(backendRoute.Path, "{") {
close := strings.Index(backendRoute.Path, "}")
if close < 0 {
panic("malformed route")
}
param := backendRoute.Path[open+1 : close]
params = append(params, param)
backendRoute.Path = backendRoute.Path[:open] + `([^/]+)` + backendRoute.Path[close+1:]
}
regex := regexp.MustCompile(backendRoute.Path)
match := regex.FindStringSubmatch(r.URL.Path)
if len(match) == 0 {
continue
}
for i, v := range params {
routeParams[v] = match[i+1]
}
if backendRoute.Opts.Algorithm == "" || backendRoute.Opts.Algorithm == "roundrobin" {
index := backendRoute.Index % len(backendRoute.Hosts)
host := backendRoute.Hosts[index]
backendRoute.Index++
backendConfig.Routes[i] = backendRoute
backendRouteHandled = true
rp := httputil.NewSingleHostReverseProxy(&url.URL{
Host: host.Hostname + ":" + strconv.Itoa(host.Port),
Scheme: host.Protocol,
})
rp.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: backendRoute.Insecure},
}
rp.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
api.HandleErr(w, r, nil, http.StatusInternalServerError, nil, err)
return
}
routeCtx := context.WithValue(ctx, api.DBContextKey, db)
routeCtx = context.WithValue(routeCtx, api.PathParamsKey, routeParams)
routeCtx = context.WithValue(routeCtx, middleware.RouteID, backendRoute.ID)
r = r.WithContext(routeCtx)
userErr, sysErr, code := HandleBackendRoute(cfg, backendRoute, w, r)
if userErr != nil || sysErr != nil {
h2 := middleware.WrapAccessLog(cfg.Secrets[0], middleware.BackendErrorHandler(code, userErr, sysErr))
h2.ServeHTTP(w, r)
return
}
backendHandler := middleware.WrapAccessLog(cfg.Secrets[0], rp)
backendHandler.ServeHTTP(w, r)
return
} else {
h2 := middleware.WrapAccessLog(cfg.Secrets[0], middleware.BackendErrorHandler(http.StatusBadRequest, errors.New("only an algorithm of roundrobin is supported by the backend options currently"), nil))
h2.ServeHTTP(w, r)
return
}
}
}
if !backendRouteHandled {
catchall.ServeHTTP(w, r)
}
}
// HandleBackendRoute does all the pre processing for the backend routes.
func HandleBackendRoute(cfg *config.Config, route config.BackendRoute, w http.ResponseWriter, r *http.Request) (error, error, int) {
var userErr, sysErr error
var errCode int
var user auth.CurrentUser
var inf *api.APIInfo
user, userErr, sysErr, errCode = api.GetUserFromReq(w, r, cfg.Secrets[0])
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
if cfg.RoleBasedPermissions {
missingPerms := user.MissingPermissions(route.Permissions...)
if len(missingPerms) != 0 {
msg := strings.Join(missingPerms, ", ")
return fmt.Errorf("missing required Permissions: %s", msg), nil, http.StatusForbidden
}
}
api.AddUserToReq(r, user)
var params []string
inf, userErr, sysErr, errCode = api.NewInfo(r, params, nil)
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
defer inf.Close()
return nil, nil, http.StatusOK
}
// IsRequestAPIAndUnknownVersion returns true if the request starts with `/api` and is a version not in the list of versions.
func IsRequestAPIAndUnknownVersion(req *http.Request, versions map[api.Version]struct{}) bool {
pathParts := strings.Split(req.URL.Path, "/")
if len(pathParts) < 2 {
return false // path doesn't start with `/api`, so it's not an api request
}
if strings.ToLower(pathParts[1]) != "api" {
return false // path doesn't start with `/api`, so it's not an api request
}
if len(pathParts) < 3 {
return true // path starts with `/api` but not `/api/{version}`, so it's an api request, and an unknown/nonexistent version.
}
version, err := stringVersionToApiVersion(pathParts[2])
if err != nil {
return true // path starts with `/api`, and version isn't a number, so it's an unknown/nonexistent version
}
if _, versionExists := versions[version]; versionExists {
return false // path starts with `/api` and version exists, so it's API but a known version
}
return true // path starts with `/api`, and version is unknown
}
func stringVersionToApiVersion(version string) (api.Version, error) {
versionParts := strings.Split(version, ".")
if len(versionParts) < 2 {
return api.Version{}, errors.New("error parsing version " + version)
}
major, err := strconv.ParseUint(versionParts[0], 10, 64)
if err != nil {
return api.Version{}, errors.New("error parsing version " + version)
}
minor, err := strconv.ParseUint(versionParts[1], 10, 64)
if err != nil {
return api.Version{}, errors.New("error parsing version " + version)
}
return api.Version{Major: major, Minor: minor}, nil
}
// RegisterRoutes - parses the routes and registers the handlers with the Go Router
func RegisterRoutes(d ServerData) error {
routeSlice, catchall, err := Routes(d)
if err != nil {
return err
}
authBase := middleware.AuthBase{Secret: d.Config.Secrets[0], Override: nil} //we know d.Config.Secrets is a slice of at least one or start up would fail.
routes, versions := CreateRouteMap(routeSlice, d.DisabledRoutes, handlerToFunc(catchall), authBase, d.RequestTimeout)
compiledRoutes := CompileRoutes(routes)
getReqID := nextReqIDGetter()
d.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
Handler(compiledRoutes, versions, catchall, d.DB, &d.Config, getReqID, d.Plugins, d.TrafficVault, w, r)
})
return nil
}
// nextReqIDGetter returns a function for getting incrementing identifiers. The returned func is safe for calling with multiple goroutines. Note the returned identifiers will not be unique after the max uint64 value.
func nextReqIDGetter() func() uint64 {
id := uint64(0)
return func() uint64 {
return atomic.AddUint64(&id, 1)
}
}