dneuman64 closed pull request #2513: Add TO Go plugin system
URL: https://github.com/apache/trafficcontrol/pull/2513
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/traffic_ops/build/build_rpm.sh b/traffic_ops/build/build_rpm.sh
index 9fec83c0c..b7cf92bce 100755
--- a/traffic_ops/build/build_rpm.sh
+++ b/traffic_ops/build/build_rpm.sh
@@ -29,7 +29,6 @@ function importFunctions() {
        . "$functions_sh"
 }
 
-
 # ---------------------------------------
 function initBuildArea() {
        echo "Initializing the build area."
@@ -53,7 +52,8 @@ function initBuildArea() {
        cp -p bin/supermicro_udev_mapper.pl "$to_ort_dest"
        tar -czvf "$to_ort_dest".tgz -C "$RPMBUILD"/SOURCES $(basename 
"$to_ort_dest") || \
                 { echo "Could not create tar archive $to_ort_dest: $?"; exit 
1; }
-       
+
+       export PLUGINS=$(grep -l -P '(?<!func )AddPlugin\(' 
${TO_DIR}/traffic_ops_golang/plugin/*.go | xargs -I '{}' basename {} '.go')
        echo "The build area has been initialized."
 }
 
diff --git a/traffic_ops/build/traffic_ops.spec 
b/traffic_ops/build/traffic_ops.spec
index b47251e89..4d428ae0a 100644
--- a/traffic_ops/build/traffic_ops.spec
+++ b/traffic_ops/build/traffic_ops.spec
@@ -44,7 +44,10 @@ Requires(postun): /usr/sbin/userdel
 %define PACKAGEDIR %{prefix}
 
 %description
-Installs Traffic Ops.
+Traffic Ops is the tool for administration (configuration and monitoring) of 
all components in a Traffic Control CDN.
+
+This package provides Traffic Ops with the following plugins:
+%{getenv:PLUGINS}
 
 Built: %(date) by %{getenv: USER}
 
diff --git a/traffic_ops/traffic_ops_golang/api/api.go 
b/traffic_ops/traffic_ops_golang/api/api.go
index 6d3d5d0ea..d46df5f8c 100644
--- a/traffic_ops/traffic_ops_golang/api/api.go
+++ b/traffic_ops/traffic_ops_golang/api/api.go
@@ -37,6 +37,7 @@ import (
        "github.com/apache/trafficcontrol/lib/go-util"
        "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/tocookie"
 
        "github.com/jmoiron/sqlx"
        "github.com/lib/pq"
@@ -264,7 +265,7 @@ type APIInfo struct {
 //
 // It is encouraged to call APIInfo.Tx.Tx.Commit() manually when all queries 
are finished, to release database resources early, and also to return an error 
to the user if the commit failed.
 //
-// NewInfo guarantees the returned APIInfo.Tx is nil or valid, even if a 
returned error is not nil. Hence, it is safe to pass the Tx to HandleErr when 
this returns errors.
+// NewInfo guarantees the returned APIInfo.Tx is non-nil and APIInfo.Tx.Tx is 
nil or valid, even if a returned error is not nil. Hence, it is safe to pass 
the Tx.Tx to HandleErr when this returns errors.
 //
 // Close() must be called to free resources, and should be called in a defer 
immediately after NewInfo(), to finish the transaction.
 //
@@ -292,29 +293,29 @@ type APIInfo struct {
 func NewInfo(r *http.Request, requiredParams []string, intParamNames []string) 
(*APIInfo, error, error, int) {
        db, err := getDB(r.Context())
        if err != nil {
-               return &APIInfo{}, errors.New("getting db: " + err.Error()), 
nil, http.StatusInternalServerError
+               return &APIInfo{Tx: &sqlx.Tx{}}, errors.New("getting db: " + 
err.Error()), nil, http.StatusInternalServerError
        }
        cfg, err := getConfig(r.Context())
        if err != nil {
-               return &APIInfo{}, errors.New("getting config: " + 
err.Error()), nil, http.StatusInternalServerError
+               return &APIInfo{Tx: &sqlx.Tx{}}, errors.New("getting config: " 
+ err.Error()), nil, http.StatusInternalServerError
        }
        reqID, err := getReqID(r.Context())
        if err != nil {
-               return &APIInfo{}, errors.New("getting reqID: " + err.Error()), 
nil, http.StatusInternalServerError
+               return &APIInfo{Tx: &sqlx.Tx{}}, errors.New("getting reqID: " + 
err.Error()), nil, http.StatusInternalServerError
        }
 
        user, err := auth.GetCurrentUser(r.Context())
        if err != nil {
-               return &APIInfo{}, errors.New("getting user: " + err.Error()), 
nil, http.StatusInternalServerError
+               return &APIInfo{Tx: &sqlx.Tx{}}, errors.New("getting user: " + 
err.Error()), nil, http.StatusInternalServerError
        }
        params, intParams, userErr, sysErr, errCode := AllParams(r, 
requiredParams, intParamNames)
        if userErr != nil || sysErr != nil {
-               return &APIInfo{}, userErr, sysErr, errCode
+               return &APIInfo{Tx: &sqlx.Tx{}}, userErr, sysErr, errCode
        }
        dbCtx, _ := context.WithTimeout(r.Context(), 
time.Duration(cfg.DBQueryTimeoutSeconds)*time.Second) //only place we could 
call cancel here is in APIInfo.Close(), which already will rollback the 
transaction (which is all cancel will do.)
        tx, err := db.BeginTxx(dbCtx, nil)                                      
                           // must be last, MUST not return an error if this 
succeeds, without closing the tx
        if err != nil {
-               return &APIInfo{}, userErr, errors.New("could not begin 
transaction: " + err.Error()), http.StatusInternalServerError
+               return &APIInfo{Tx: &sqlx.Tx{}}, userErr, errors.New("could not 
begin transaction: " + err.Error()), http.StatusInternalServerError
        }
        return &APIInfo{
                Config:    cfg,
@@ -479,3 +480,57 @@ func ParseDBError(ierr error) (error, error, int) {
 
        return nil, err, http.StatusInternalServerError
 }
+
+// GetUserFromReq returns the current user, any user error, any system error, 
and an error code to be returned if either error was not nil.
+// This also uses the given ResponseWriter to refresh the cookie, if it was 
valid.
+func GetUserFromReq(w http.ResponseWriter, r *http.Request, secret string) 
(auth.CurrentUser, error, error, int) {
+       cookie, err := r.Cookie(tocookie.Name)
+       if err != nil {
+               return auth.CurrentUser{}, errors.New("Unauthorized, please log 
in."), errors.New("error getting cookie: " + err.Error()), 
http.StatusUnauthorized
+       }
+
+       if cookie == nil {
+               return auth.CurrentUser{}, errors.New("Unauthorized, please log 
in."), nil, http.StatusUnauthorized
+       }
+
+       oldCookie, err := tocookie.Parse(secret, cookie.Value)
+       if err != nil {
+               return auth.CurrentUser{}, errors.New("Unauthorized, please log 
in."), errors.New("error parsing cookie: " + err.Error()), 
http.StatusUnauthorized
+       }
+
+       username := oldCookie.AuthData
+       if username == "" {
+               return auth.CurrentUser{}, errors.New("Unauthorized, please log 
in."), nil, http.StatusUnauthorized
+       }
+       db := (*sqlx.DB)(nil)
+       val := r.Context().Value(DBContextKey)
+       if val == nil {
+               return auth.CurrentUser{}, nil, errors.New("request context db 
missing"), http.StatusInternalServerError
+       }
+       switch v := val.(type) {
+       case *sqlx.DB:
+               db = v
+       default:
+               return auth.CurrentUser{}, nil, fmt.Errorf("request context db 
unknown type %T", val), http.StatusInternalServerError
+       }
+
+       cfg, err := GetConfig(r.Context())
+       if err != nil {
+               return auth.CurrentUser{}, nil, errors.New("request context 
config missing"), http.StatusInternalServerError
+       }
+
+       user, userErr, sysErr, code := auth.GetCurrentUserFromDB(db, username, 
time.Duration(cfg.DBQueryTimeoutSeconds)*time.Second)
+       if userErr != nil || sysErr != nil {
+               return auth.CurrentUser{}, userErr, sysErr, code
+       }
+
+       newCookieVal := tocookie.Refresh(oldCookie, secret)
+       http.SetCookie(w, &http.Cookie{Name: tocookie.Name, Value: 
newCookieVal, Path: "/", HttpOnly: true})
+       return user, nil, nil, http.StatusOK
+}
+
+func AddUserToReq(r *http.Request, u auth.CurrentUser) *http.Request {
+       ctx := r.Context()
+       ctx = context.WithValue(ctx, auth.CurrentUserKey, u)
+       return r.WithContext(ctx)
+}
diff --git a/traffic_ops/traffic_ops_golang/config/config.go 
b/traffic_ops/traffic_ops_golang/config/config.go
index cef768e32..78d8be76b 100644
--- a/traffic_ops/traffic_ops_golang/config/config.go
+++ b/traffic_ops/traffic_ops_golang/config/config.go
@@ -60,29 +60,32 @@ type ConfigHypnotoad struct {
 
 // ConfigTrafficOpsGolang carries settings specific to traffic_ops_golang 
server
 type ConfigTrafficOpsGolang struct {
-       Port                     string         `json:"port"`
-       ProxyTimeout             int            `json:"proxy_timeout"`
-       ProxyKeepAlive           int            `json:"proxy_keep_alive"`
-       ProxyTLSTimeout          int            `json:"proxy_tls_timeout"`
-       ProxyReadHeaderTimeout   int            
`json:"proxy_read_header_timeout"`
-       ReadTimeout              int            `json:"read_timeout"`
-       RequestTimeout           int            `json:"request_timeout"`
-       ReadHeaderTimeout        int            `json:"read_header_timeout"`
-       WriteTimeout             int            `json:"write_timeout"`
-       IdleTimeout              int            `json:"idle_timeout"`
-       LogLocationError         string         `json:"log_location_error"`
-       LogLocationWarning       string         `json:"log_location_warning"`
-       LogLocationInfo          string         `json:"log_location_info"`
-       LogLocationDebug         string         `json:"log_location_debug"`
-       LogLocationEvent         string         `json:"log_location_event"`
-       Insecure                 bool           `json:"insecure"`
-       MaxDBConnections         int            `json:"max_db_connections"`
-       DBMaxIdleConnections     int            `json:"db_max_idle_connections"`
-       DBConnMaxLifetimeSeconds int            
`json:"db_conn_max_lifetime_seconds"`
-       BackendMaxConnections    map[string]int `json:"backend_max_connections"`
-       DBQueryTimeoutSeconds    int            
`json:"db_query_timeout_seconds"`
-       ProfilingEnabled         bool           `json:"profiling_enabled"`
-       ProfilingLocation        string         `json:"profiling_location"`
+       Port                     string                     `json:"port"`
+       ProxyTimeout             int                        
`json:"proxy_timeout"`
+       ProxyKeepAlive           int                        
`json:"proxy_keep_alive"`
+       ProxyTLSTimeout          int                        
`json:"proxy_tls_timeout"`
+       ProxyReadHeaderTimeout   int                        
`json:"proxy_read_header_timeout"`
+       ReadTimeout              int                        
`json:"read_timeout"`
+       RequestTimeout           int                        
`json:"request_timeout"`
+       ReadHeaderTimeout        int                        
`json:"read_header_timeout"`
+       WriteTimeout             int                        
`json:"write_timeout"`
+       IdleTimeout              int                        
`json:"idle_timeout"`
+       LogLocationError         string                     
`json:"log_location_error"`
+       LogLocationWarning       string                     
`json:"log_location_warning"`
+       LogLocationInfo          string                     
`json:"log_location_info"`
+       LogLocationDebug         string                     
`json:"log_location_debug"`
+       LogLocationEvent         string                     
`json:"log_location_event"`
+       Insecure                 bool                       `json:"insecure"`
+       MaxDBConnections         int                        
`json:"max_db_connections"`
+       DBMaxIdleConnections     int                        
`json:"db_max_idle_connections"`
+       DBConnMaxLifetimeSeconds int                        
`json:"db_conn_max_lifetime_seconds"`
+       BackendMaxConnections    map[string]int             
`json:"backend_max_connections"`
+       DBQueryTimeoutSeconds    int                        
`json:"db_query_timeout_seconds"`
+       Plugins                  []string                   `json:"plugins"`
+       PluginConfig             map[string]json.RawMessage 
`json:"plugin_config"`
+       PluginSharedConfig       map[string]interface{}     
`json:"plugin_shared_config"`
+       ProfilingEnabled         bool                       
`json:"profiling_enabled"`
+       ProfilingLocation        string                     
`json:"profiling_location"`
 }
 
 // ConfigDatabase reflects the structure of the database.conf file
diff --git a/traffic_ops/traffic_ops_golang/plugin/README.md 
b/traffic_ops/traffic_ops_golang/plugin/README.md
new file mode 100644
index 000000000..715c54ddf
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/plugin/README.md
@@ -0,0 +1,74 @@
+<!--
+    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.
+-->
+
+# Adding a Plugin
+
+To add a plugin, create a new `.go` file in the `traffic_ops_golang/plugin` 
directory. This file should have a unique name, to avoid conflicts. Consider 
prefixing it with your company name, website, or a UUID.
+
+The filename, sans `.go`, is the name of your plugin, and will be the key used 
for configuration in the remap file. For example, if your file is 
`f49e54fc-fd17-4e1c-92c6-67028fde8504-hello-world.go`, the name of your plugin 
is `f49e54fc-fd17-4e1c-92c6-67028fde8504-hello-world`.
+
+Plugins are registered via calls to `AddPlugin` inside an `init` function in 
the plugin's file. The `AddPlugin` function takes a priority, and a set of hook 
functions. The priority is the order in which plugins are called, starting from 
0. Note the priority of plugins included with Traffic Control use a base 
priority of 10000, unless priority order matters for them.
+
+The `Funcs` object contains functions for each hook, as well as a load 
function for loading configuration from the remap file. The current hooks are 
`load`, `startup`, and `onRequest`. If your plugin does not use a hook, it may 
be nil.
+
+* `load` is called when the application starts, is given config data, and must 
return the loaded configuration object.
+
+* `startup` is called when the application starts.
+
+* `onRequest` is called immediately when a request is received. It returns a 
boolean indicating whether to stop processing. Note this is called without 
authentication. If a plugin should be authenticated, it must do so itself. It 
is recommended to use `api.GetUserFromReq`, which will return an error if 
authentication fails.
+
+The simplest example is the `hello_world` plugin. See `plugin/hello_world.go`.
+
+```go
+import (
+       "strings"
+)
+func init() {
+       AddPlugin(10000, Funcs{onRequest: hello})
+}
+const HelloPath = "/_hello"
+func hello(d OnRequestData) IsRequestHandled {
+       if !strings.HasPrefix(d.R.URL.Path, HelloPath) {
+               return RequestUnhandled
+       }
+       d.W.Header().Set("Content-Type", "text/plain")
+       d.W.Write([]byte("Hello, World!"))
+       return RequestHandled
+}
+```
+
+The plugin is initialized via `AddPlugin`, and its `hello` function is set as 
the `startup` hook. The `hello` function has the signature of 
`plugin.StartupFunc`.
+
+# Examples
+
+Example plugins are included in the `/plugin` directory
+
+*hello_world*: Example of a simple HTTP endpoint.
+*hello_config*: Example of loading and using config file data.
+*hello_shared_config*: Example of loading and using config data which is 
shared among all plugins.
+*hello_context*: Example of passing context data between hook functions.
+*hello_startup*: Example of running a plugin function when the application 
starts.
+
+# Glossary
+
+Definitions of terms used in this document.
+
+*Plugin*: A self-contained component whose code is executed when certain 
events in the main application occur.
+*Hook*: A plugin function which is called when a certain event happens in the 
main application.
+*Plugin Data*: Application data given to a plugin, as a function parameter 
passed to a hook function, including configuration data, running state, and 
HTTP request state.
diff --git a/traffic_ops/traffic_ops_golang/plugin/hello_config.go 
b/traffic_ops/traffic_ops_golang/plugin/hello_config.go
new file mode 100644
index 000000000..04dbfe8ae
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/plugin/hello_config.go
@@ -0,0 +1,53 @@
+package plugin
+
+/*
+   Licensed 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 (
+       "encoding/json"
+
+       "github.com/apache/trafficcontrol/lib/go-log"
+)
+
+func init() {
+       AddPlugin(10000, Funcs{load: helloConfigLoad, onStartup: 
helloConfigStartup})
+}
+
+type HelloConfig struct {
+       Hello string `json:"hello"`
+}
+
+func helloConfigLoad(b json.RawMessage) interface{} {
+       cfg := HelloConfig{}
+       err := json.Unmarshal(b, &cfg)
+       if err != nil {
+               log.Debugln(`Hello! This is a config plugin! Unfortunately, 
your config JSON is not properly formatted. Config should look like: 
{"plugin_config": {"hello_config":{"hello": "anything can go here"}}}`)
+               return nil
+       }
+       log.Debugln("Hello! This is a config plugin! Successfully loaded 
config!")
+       return &cfg
+}
+
+func helloConfigStartup(d StartupData) {
+       if d.Cfg == nil {
+               log.Debugln("Hello! This is a config plugin! Unfortunately, 
your config is not set properly.")
+       }
+       cfg, ok := d.Cfg.(*HelloConfig)
+       if !ok {
+               // should never happen
+               log.Debugf("helloLoadConfig config '%v' type '%T' expected 
*HelloConfig\n", d.Cfg, d.Cfg)
+               return
+       }
+       log.Debugf("Hello! This is a config plugin! Your config is: %+v\n", cfg)
+}
diff --git a/traffic_ops/traffic_ops_golang/plugin/hello_context.go 
b/traffic_ops/traffic_ops_golang/plugin/hello_context.go
new file mode 100644
index 000000000..9e6b133e0
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/plugin/hello_context.go
@@ -0,0 +1,34 @@
+package plugin
+
+/*
+   Licensed 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 (
+       "github.com/apache/trafficcontrol/lib/go-log"
+)
+
+func init() {
+       AddPlugin(10000, Funcs{onStartup: helloCtxStart, onRequest: 
helloCtxOnReq})
+}
+
+func helloCtxStart(d StartupData) {
+       *d.Ctx = 42
+       log.Debugf("Hello! This is a context plugin! Start set context: %+v\n", 
*d.Ctx)
+}
+
+func helloCtxOnReq(d OnRequestData) IsRequestHandled {
+       ctx, ok := (*d.Ctx).(int)
+       log.Debugf("Hello! This is a context plugin! On Request got context: 
%+v %+v\n", ok, ctx)
+       return RequestUnhandled
+}
diff --git a/traffic_ops/traffic_ops_golang/plugin/hello_shared_config.go 
b/traffic_ops/traffic_ops_golang/plugin/hello_shared_config.go
new file mode 100644
index 000000000..3a16ede93
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/plugin/hello_shared_config.go
@@ -0,0 +1,33 @@
+package plugin
+
+/*
+   Licensed 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 (
+       "encoding/json"
+
+       "github.com/apache/trafficcontrol/lib/go-log"
+)
+
+func init() {
+       AddPlugin(10000, Funcs{onStartup: helloSharedConfigStartup})
+}
+
+func helloSharedConfigStartup(d StartupData) {
+       if b, err := json.Marshal(d.SharedCfg); err == nil {
+               log.Debugln("Hello! This is a shared plugin data config! Your 
shared plugin config is: " + string(b))
+       } else {
+               log.Debugf("Hello! This is a shared plugin data config! Your 
shared plugin config is: %+v\n", d.SharedCfg)
+       }
+}
diff --git a/traffic_ops/traffic_ops_golang/plugin/hello_startup.go 
b/traffic_ops/traffic_ops_golang/plugin/hello_startup.go
new file mode 100644
index 000000000..9e6b21df9
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/plugin/hello_startup.go
@@ -0,0 +1,27 @@
+package plugin
+
+/*
+   Licensed 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 (
+       "github.com/apache/trafficcontrol/lib/go-log"
+)
+
+func init() {
+       AddPlugin(10000, Funcs{onStartup: helloStartup})
+}
+
+func helloStartup(d StartupData) {
+       log.Debugln("Hello! This is a startup plugin! Config Version: " + 
d.AppCfg.Version)
+}
diff --git a/traffic_ops/traffic_ops_golang/plugin/hello_world.go 
b/traffic_ops/traffic_ops_golang/plugin/hello_world.go
new file mode 100644
index 000000000..ce36f282d
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/plugin/hello_world.go
@@ -0,0 +1,34 @@
+package plugin
+
+/*
+   Licensed 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 (
+       "strings"
+)
+
+func init() {
+       AddPlugin(10000, Funcs{onRequest: hello})
+}
+
+const HelloPath = "/_hello"
+
+func hello(d OnRequestData) IsRequestHandled {
+       if !strings.HasPrefix(d.R.URL.Path, HelloPath) {
+               return RequestUnhandled
+       }
+       d.W.Header().Set("Content-Type", "text/plain")
+       d.W.Write([]byte("Hello, World!"))
+       return RequestHandled
+}
diff --git a/traffic_ops/traffic_ops_golang/plugin/plugin.go 
b/traffic_ops/traffic_ops_golang/plugin/plugin.go
new file mode 100644
index 000000000..998303f23
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/plugin/plugin.go
@@ -0,0 +1,192 @@
+package plugin
+
+/*
+   Licensed 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 (
+       "encoding/json"
+       "fmt"
+       "net/http"
+       "os"
+       "path"
+       "runtime"
+       "sort"
+       "strings"
+       "time"
+
+       "github.com/apache/trafficcontrol/lib/go-log"
+       "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+)
+
+// List returns the list of plugins compiled into the calling executable.
+func List() []string {
+       l := []string{}
+       for _, p := range initPlugins {
+               l = append(l, p.name)
+       }
+       return l
+}
+
+func Get(appCfg config.Config) Plugins {
+       log.Infof("plugin.Get given: %+v\n", appCfg.Plugins)
+       pluginSlice := getEnabled(appCfg.Plugins)
+       pluginCfg := loadConfig(pluginSlice, appCfg.PluginConfig)
+       ctx := map[string]*interface{}{}
+       return plugins{slice: pluginSlice, cfg: pluginCfg, ctx: ctx}
+}
+
+func getEnabled(enabled []string) pluginsSlice {
+       enabledM := map[string]struct{}{}
+       for _, name := range enabled {
+               enabledM[name] = struct{}{}
+       }
+       enabledPlugins := pluginsSlice{}
+       for _, plugin := range initPlugins {
+               if _, ok := enabledM[plugin.name]; !ok {
+                       log.Infoln("getEnabled skipping: '" + plugin.name + "'")
+                       continue
+               }
+               log.Infoln("plugin enabling: '" + plugin.name + "'")
+               enabledPlugins = append(enabledPlugins, plugin)
+       }
+       sort.Sort(enabledPlugins)
+       return enabledPlugins
+}
+
+func loadConfig(ps pluginsSlice, configJSON map[string]json.RawMessage) 
map[string]interface{} {
+       pluginConfigLoaders := loadFuncs(ps)
+       cfg := make(map[string]interface{}, len(configJSON))
+       for name, b := range configJSON {
+               if loadF := pluginConfigLoaders[name]; loadF != nil {
+                       cfg[name] = loadF(b)
+               }
+       }
+       return cfg
+}
+
+func loadFuncs(ps pluginsSlice) map[string]LoadFunc {
+       lf := map[string]LoadFunc{}
+       for _, plugin := range ps {
+               if plugin.funcs.load == nil {
+                       continue
+               }
+               lf[plugin.name] = LoadFunc(plugin.funcs.load)
+       }
+       return lf
+}
+
+type Plugins interface {
+       OnStartup(d StartupData)
+       OnRequest(d OnRequestData) bool
+}
+
+func AddPlugin(priority uint64, funcs Funcs) {
+       _, filename, _, ok := runtime.Caller(1)
+       if !ok {
+               fmt.Println(time.Now().Format(time.RFC3339Nano) + " Error 
plugin.AddPlugin: runtime.Caller failed, can't get plugin names") // print, 
because this is called in init, loggers don't exist yet
+               os.Exit(1)
+       }
+
+       pluginName := strings.TrimSuffix(path.Base(filename), ".go")
+       log.Debugln("AddPlugin adding " + pluginName)
+       initPlugins = append(initPlugins, pluginObj{funcs: funcs, priority: 
priority, name: pluginName})
+}
+
+type Funcs struct {
+       load      LoadFunc
+       onStartup StartupFunc
+       onRequest OnRequestFunc
+}
+
+// Data is the common plugin data, given to most plugin hooks. This is 
designed to be embedded in the data structs for specific hooks.
+type Data struct {
+       Cfg       interface{}
+       Ctx       *interface{}
+       SharedCfg map[string]interface{}
+       RequestID uint64
+       AppCfg    config.Config
+}
+
+type StartupData struct {
+       Data
+}
+
+type OnRequestData struct {
+       Data
+       W http.ResponseWriter
+       R *http.Request
+}
+
+type IsRequestHandled bool
+
+const (
+       RequestHandled   = IsRequestHandled(true)
+       RequestUnhandled = IsRequestHandled(false)
+)
+
+type LoadFunc func(json.RawMessage) interface{}
+type StartupFunc func(d StartupData)
+type OnRequestFunc func(d OnRequestData) IsRequestHandled
+
+type pluginObj struct {
+       funcs    Funcs
+       priority uint64
+       name     string
+}
+
+type plugins struct {
+       slice pluginsSlice
+       cfg   map[string]interface{}
+       ctx   map[string]*interface{}
+}
+
+type pluginsSlice []pluginObj
+
+func (p pluginsSlice) Len() int           { return len(p) }
+func (p pluginsSlice) Less(i, j int) bool { return p[i].priority < 
p[j].priority }
+func (p pluginsSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
+
+// initPlugins is where plugins are registered via their init functions.
+var initPlugins = pluginsSlice{}
+
+func (ps plugins) OnStartup(d StartupData) {
+       for _, p := range ps.slice {
+               ictx := interface{}(nil)
+               ps.ctx[p.name] = &ictx
+               if p.funcs.onStartup == nil {
+                       continue
+               }
+               d.Ctx = ps.ctx[p.name]
+               d.Cfg = ps.cfg[p.name]
+               p.funcs.onStartup(d)
+       }
+}
+
+// OnRequest returns a boolean whether to immediately stop processing the 
request. If a plugin returns true, this is immediately returned with no further 
plugins processed.
+func (ps plugins) OnRequest(d OnRequestData) bool {
+       log.Debugln("DEBUG plugins.OnRequest calling %+v\n", len(ps.slice))
+       for _, p := range ps.slice {
+               if p.funcs.onRequest == nil {
+                       log.Debugln("plugins.OnRequest plugging " + p.name + " 
- no onRequest func")
+                       continue
+               }
+               d.Ctx = ps.ctx[p.name]
+               d.Cfg = ps.cfg[p.name]
+               log.Debugln("plugins.OnRequest plugging " + p.name)
+               if stop := p.funcs.onRequest(d); stop {
+                       return true
+               }
+       }
+       return false
+}
diff --git a/traffic_ops/traffic_ops_golang/plugin/proxy.go 
b/traffic_ops/traffic_ops_golang/plugin/proxy.go
new file mode 100644
index 000000000..57a5b4bc1
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/plugin/proxy.go
@@ -0,0 +1,103 @@
+package plugin
+
+/*
+   Licensed 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 (
+       "encoding/json"
+       "net/http"
+       "net/http/httputil"
+       "net/url"
+       "strings"
+
+       "github.com/apache/trafficcontrol/lib/go-log"
+       "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+// The proxy plugin reverse-proxies to other HTTP services, as configured.
+//
+// Configuration is in `cdn.conf` (like all plugins) and of the form 
`{"plugin_config": {"proxy":[{"path": "/my-extension-route", "uri": 
"https://example.net"}]}}`
+//
+// Users are required to be authenticated. For modifications such as removing 
authentication or amending the proxied request, forking this plugin is 
encouraged.
+
+func init() {
+       AddPlugin(10000, Funcs{load: proxyLoad, onRequest: proxyOnReq})
+}
+
+type ProxyConfig []ProxyRemap
+
+type ProxyRemap struct {
+       Path string   `json:"path"`
+       URI  *url.URL `json:"uri"`
+}
+
+func (r *ProxyRemap) UnmarshalJSON(b []byte) error {
+       type ProxyRemapJSON struct {
+               Path string `json:"path"`
+               URI  string `json:"uri"`
+       }
+       rj := ProxyRemapJSON{}
+       if err := json.Unmarshal(b, &rj); err != nil {
+               return err
+       }
+       uri, err := url.Parse(rj.URI)
+       if err != nil {
+               return err
+       }
+       r.Path = rj.Path
+       r.URI = uri
+       return nil
+}
+
+func proxyLoad(b json.RawMessage) interface{} {
+       cfg := ProxyConfig{}
+       err := json.Unmarshal(b, &cfg)
+       if err != nil {
+               log.Debugln(`plugin proxy: malformed config. Config should look 
like: {"plugin_config": {"proxy":[{"path": "/my-extension-route", "uri": 
"https://example.net"}]}}`)
+               return nil
+       }
+       log.Debugf("plugin proxy: loaded config %+v\n", cfg)
+       return &cfg
+}
+
+func proxyOnReq(d OnRequestData) IsRequestHandled {
+       if d.Cfg == nil {
+               return RequestUnhandled
+       }
+       cfg, ok := d.Cfg.(*ProxyConfig)
+       if !ok {
+               // should never happen
+               log.Errorf("plugin proxy config '%v' type '%T' expected 
*ProxyConfig\n", d.Cfg, d.Cfg)
+               return RequestUnhandled
+       }
+
+       for _, remap := range *cfg {
+               if !strings.HasPrefix(d.R.URL.Path, remap.Path) {
+                       continue
+               }
+               return proxyHandle(d.W, d.R, d, remap.URI)
+       }
+       return RequestUnhandled
+}
+
+func proxyHandle(w http.ResponseWriter, r *http.Request, d OnRequestData, 
proxyURI *url.URL) IsRequestHandled {
+       _, userErr, sysErr, errCode := api.GetUserFromReq(w, r, 
d.AppCfg.Secrets[0]) // require login
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, nil, errCode, userErr, sysErr)
+               return RequestHandled
+       }
+       rp := httputil.NewSingleHostReverseProxy(proxyURI)
+       rp.ServeHTTP(w, r)
+       return RequestHandled
+}
diff --git a/traffic_ops/traffic_ops_golang/routing.go 
b/traffic_ops/traffic_ops_golang/routing.go
index 7f4fda852..a6a75ce51 100644
--- a/traffic_ops/traffic_ops_golang/routing.go
+++ b/traffic_ops/traffic_ops_golang/routing.go
@@ -32,6 +32,7 @@ import (
        "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/config"
+       "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/plugin"
 
        "github.com/jmoiron/sqlx"
 )
@@ -74,6 +75,7 @@ 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
 }
 
 // CompiledRoute ...
@@ -170,7 +172,16 @@ func CompileRoutes(routes map[string][]PathHandler) 
map[string][]CompiledRoute {
 }
 
 // Handler - generic handler func used by the Handlers hooking into the routes
-func Handler(routes map[string][]CompiledRoute, catchall http.Handler, db 
*sqlx.DB, cfg *config.Config, getReqID func() uint64, w http.ResponseWriter, r 
*http.Request) {
+func Handler(
+       routes map[string][]CompiledRoute,
+       catchall http.Handler,
+       db *sqlx.DB,
+       cfg *config.Config,
+       getReqID func() uint64,
+       plugins plugin.Plugins,
+       w http.ResponseWriter,
+       r *http.Request,
+) {
        reqID := getReqID()
 
        reqIDStr := strconv.FormatUint(reqID, 10)
@@ -180,6 +191,20 @@ func Handler(routes map[string][]CompiledRoute, catchall 
http.Handler, db *sqlx.
                log.Infoln(r.Method + " " + r.URL.Path + " 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)
+
+       // 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 {
@@ -197,12 +222,8 @@ func Handler(routes map[string][]CompiledRoute, catchall 
http.Handler, db *sqlx.
                        params[v] = match[i+1]
                }
 
-               ctx := r.Context()
-               ctx = context.WithValue(ctx, api.PathParamsKey, params)
-               ctx = context.WithValue(ctx, api.DBContextKey, db)
-               ctx = context.WithValue(ctx, api.ConfigContextKey, cfg)
-               ctx = context.WithValue(ctx, api.ReqIDContextKey, reqID)
-               r = r.WithContext(ctx)
+               routeCtx := context.WithValue(ctx, api.PathParamsKey, params)
+               r = r.WithContext(routeCtx)
                compiledRoute.Handler(w, r)
                return
        }
@@ -221,7 +242,7 @@ func RegisterRoutes(d ServerData) error {
        compiledRoutes := CompileRoutes(routes)
        getReqID := nextReqIDGetter()
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-               Handler(compiledRoutes, catchall, d.DB, &d.Config, getReqID, w, 
r)
+               Handler(compiledRoutes, catchall, d.DB, &d.Config, getReqID, 
d.Plugins, w, r)
        })
        return nil
 }
diff --git a/traffic_ops/traffic_ops_golang/traffic_ops_golang.go 
b/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
index c042d51b4..3fad34d96 100644
--- a/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
+++ b/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
@@ -29,12 +29,14 @@ import (
        "os/signal"
        "path/filepath"
        "runtime/pprof"
+       "strings"
        "time"
 
        "github.com/apache/trafficcontrol/lib/go-log"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/about"
        "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/jmoiron/sqlx"
        _ "github.com/lib/pq"
@@ -50,6 +52,7 @@ func init() {
 
 func main() {
        showVersion := flag.Bool("version", false, "Show version and exit")
+       showPlugins := flag.Bool("plugins", false, "Show the list of plugins 
and exit")
        configFileName := flag.String("cfg", "", "The config file path")
        dbConfigFileName := flag.String("dbcfg", "", "The db config file path")
        riakConfigFileName := flag.String("riakcfg", "", "The riak config file 
path")
@@ -59,6 +62,10 @@ func main() {
                fmt.Println(about.About.RPMVersion)
                os.Exit(0)
        }
+       if *showPlugins {
+               fmt.Println(strings.Join(plugin.List(), "\n"))
+               os.Exit(0)
+       }
        if len(os.Args) < 2 {
                flag.Usage()
                os.Exit(1)
@@ -131,6 +138,8 @@ func main() {
        db.SetMaxIdleConns(cfg.DBMaxIdleConnections)
        db.SetConnMaxLifetime(time.Duration(cfg.DBConnMaxLifetimeSeconds) * 
time.Second)
 
+       // TODO combine
+       plugins := plugin.Get(cfg)
        profiling := cfg.ProfilingEnabled
 
        pprofMux := http.DefaultServeMux
@@ -146,11 +155,13 @@ func main() {
                log.Errorln(debugServer.ListenAndServe())
        }()
 
-       if err := RegisterRoutes(ServerData{DB: db, Config: cfg, Profiling: 
&profiling}); err != nil {
+       if err := RegisterRoutes(ServerData{DB: db, Config: cfg, Profiling: 
&profiling, Plugins: plugins}); err != nil {
                log.Errorf("registering routes: %v\n", err)
                return
        }
 
+       plugins.OnStartup(plugin.StartupData{Data: plugin.Data{SharedCfg: 
cfg.PluginSharedConfig, AppCfg: cfg}})
+
        log.Infof("Listening on " + cfg.Port)
 
        server := &http.Server{
diff --git a/traffic_ops/traffic_ops_golang/wrappers.go 
b/traffic_ops/traffic_ops_golang/wrappers.go
index 914f706e3..6d3b0d98d 100644
--- a/traffic_ops/traffic_ops_golang/wrappers.go
+++ b/traffic_ops/traffic_ops_golang/wrappers.go
@@ -22,10 +22,8 @@ package main
 import (
        "bytes"
        "compress/gzip"
-       "context"
        "crypto/sha512"
        "encoding/base64"
-       "encoding/json"
        "errors"
        "fmt"
        "net/http"
@@ -38,9 +36,7 @@ import (
        tc "github.com/apache/trafficcontrol/lib/go-tc"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/about"
        "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/tocookie"
-       "github.com/jmoiron/sqlx"
 )
 
 // ServerName - the server identifier
@@ -59,81 +55,16 @@ func (a AuthBase) GetWrapper(privLevelRequired int) 
Middleware {
        }
        return func(handlerFunc http.HandlerFunc) http.HandlerFunc {
                return func(w http.ResponseWriter, r *http.Request) {
-                       // TODO remove, and make username available to 
wrapLogTime
-                       iw := &Interceptor{w: w}
-                       w = iw
-                       handleErr := func(status int, err error) {
-                               errBytes, jsonErr := 
json.Marshal(tc.CreateErrorAlerts(err))
-                               if jsonErr != nil {
-                                       log.Errorf("failed to marshal error: 
%s\n", jsonErr)
-                                       
w.WriteHeader(http.StatusInternalServerError)
-                                       fmt.Fprintf(w, 
http.StatusText(http.StatusInternalServerError))
-                                       return
-                               }
-                               w.Header().Set(tc.ContentType, 
tc.ApplicationJson)
-                               w.WriteHeader(status)
-                               fmt.Fprintf(w, "%s", errBytes)
-                       }
-
-                       cookie, err := r.Cookie(tocookie.Name)
-                       if err != nil {
-                               log.Errorf("error getting cookie: %s", err)
-                               handleErr(http.StatusUnauthorized, 
errors.New("Unauthorized, please log in."))
-                               return
-                       }
-
-                       if cookie == nil {
-                               handleErr(http.StatusUnauthorized, 
errors.New("Unauthorized, please log in."))
-                               return
-                       }
-
-                       oldCookie, err := tocookie.Parse(a.secret, cookie.Value)
-                       if err != nil {
-                               log.Errorf("error parsing cookie: %s", err)
-                               handleErr(http.StatusUnauthorized, 
errors.New("Unauthorized, please log in."))
-                               return
-                       }
-
-                       username := oldCookie.AuthData
-                       if username == "" {
-                               handleErr(http.StatusUnauthorized, 
errors.New("Unauthorized, please log in."))
-                               return
-                       }
-                       var DB *sqlx.DB
-                       val := r.Context().Value(api.DBContextKey)
-                       if val != nil {
-                               switch v := val.(type) {
-                               case *sqlx.DB:
-                                       DB = v
-                               default:
-                                       
handleErr(http.StatusInternalServerError, errors.New("No DB found"))
-                               }
-                       } else {
-                               handleErr(http.StatusInternalServerError, 
errors.New("No DB found"))
-                       }
-
-                       cfg, err := api.GetConfig(r.Context())
-                       if err != nil {
-                               handleErr(http.StatusInternalServerError, 
errors.New("No config found"))
-                       }
-
-                       currentUserInfo, userErr, sysErr, code := 
auth.GetCurrentUserFromDB(DB, username, 
time.Duration(cfg.DBQueryTimeoutSeconds)*time.Second)
+                       user, userErr, sysErr, errCode := api.GetUserFromReq(w, 
r, a.secret)
                        if userErr != nil || sysErr != nil {
-                               api.HandleErr(w, r, nil, code, userErr, sysErr)
+                               api.HandleErr(w, r, nil, errCode, userErr, 
sysErr)
                                return
                        }
-                       if currentUserInfo.PrivLevel < privLevelRequired {
-                               handleErr(http.StatusForbidden, 
errors.New("Forbidden."))
-                               return
+                       if user.PrivLevel < privLevelRequired {
+                               api.HandleErr(w, r, nil, http.StatusForbidden, 
errors.New("Forbidden."), nil)
                        }
-
-                       newCookieVal := tocookie.Refresh(oldCookie, a.secret)
-                       http.SetCookie(w, &http.Cookie{Name: tocookie.Name, 
Value: newCookieVal, Path: "/", HttpOnly: true})
-
-                       ctx := r.Context()
-                       ctx = context.WithValue(ctx, auth.CurrentUserKey, 
currentUserInfo)
-
-                       handlerFunc(w, r.WithContext(ctx))
+                       r = api.AddUserToReq(r, user)
+                       handlerFunc(w, r)
                }
        }
 }


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
[email protected]


With regards,
Apache Git Services

Reply via email to