| // 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) |
| } |
| } |