gitlab.com/ignitionrobotics/web/ign-go@v1.0.0-rc4/router.go (about) 1 package ign 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "github.com/codegangsta/negroni" 7 "github.com/gorilla/mux" 8 "gitlab.com/ignitionrobotics/web/ign-go/monitoring" 9 "net/http" 10 "regexp" 11 "sort" 12 "strings" 13 ) 14 15 // NewRouter just creates a new Gorilla/mux router 16 func NewRouter() *mux.Router { 17 // We need to set StrictSlash to "false" (default) to avoid getting 18 // routes redirected automatically. 19 return mux.NewRouter().StrictSlash(false) 20 } 21 22 // RouterConfigurer is used to configure a mux.Router with declared routes 23 // and middlewares. It also adds support for default global OPTIONS handler 24 // based on the route declarations. 25 type RouterConfigurer struct { 26 // Embedded type mux.Router 27 // See https://golang.org/doc/effective_go.html#embedding 28 *mux.Router 29 // An optional list of middlewares to be injected between the common 30 // middlewares and the final route handler. 31 customHandlers []negroni.Handler 32 33 corsMap map[string]int 34 // sortedREs keeps a sorted list of registered routes in corsMap. 35 // It allows us to iterate the corsMap in 'order'. 36 sortedREs []string 37 // declared Routes 38 routes *Routes 39 // private field to keep a reference to JWT middlewares 40 authOptMiddleware negroni.HandlerFunc 41 authReqMiddleware negroni.HandlerFunc 42 // monitoring provides a middleware to keep track of server request metrics. 43 monitoring monitoring.Provider 44 } 45 46 // NewRouterConfigurer creates a new RouterConfigurer, used to 47 // configure a mux.Router with routes declarations. 48 func NewRouterConfigurer(r *mux.Router, monitoring monitoring.Provider) *RouterConfigurer { 49 rc := &RouterConfigurer{ 50 Router: r, 51 corsMap: make(map[string]int, 0), 52 monitoring: monitoring, 53 } 54 return rc 55 } 56 57 // SetCustomHandlers - allows to set a list of optional middlewares 58 // that will be injected between the common middlewares and the final route handler. 59 func (rc *RouterConfigurer) SetCustomHandlers(handlers ...negroni.Handler) *RouterConfigurer { 60 rc.customHandlers = handlers 61 return rc 62 } 63 64 // SetAuthHandlers - set the JWT handlers to be used by the router for secure and unsecure 65 // routes. 66 func (rc *RouterConfigurer) SetAuthHandlers(optionalJWT, requiredJWT negroni.HandlerFunc) *RouterConfigurer { 67 rc.authOptMiddleware = optionalJWT 68 rc.authReqMiddleware = requiredJWT 69 return rc 70 } 71 72 // ConfigureRouter - given an array of Route declarations, 73 // this method confifures the router to handle them. 74 // This is the main method to invoke with a RouterConfigurer. 75 // 76 // It supports an optional pathPrefix used to differentiate API versions (eg. "/2.0/"). 77 func (rc *RouterConfigurer) ConfigureRouter(pathPrefix string, routes Routes) *RouterConfigurer { 78 // Store the route declarations in the router. 79 rc.routes = &routes 80 81 for routeIndex, route := range routes { 82 83 // Process unsecure routes 84 for _, method := range route.Methods { 85 for _, formatHandler := range method.Handlers { 86 rc.registerRouteHandler(routeIndex, method.Type, false, formatHandler) 87 rc.registerRouteInOptionsHandler(pathPrefix, routeIndex, formatHandler) 88 } 89 } 90 91 // Process secure routes 92 for _, method := range route.SecureMethods { 93 for _, formatHandler := range method.Handlers { 94 rc.registerRouteHandler(routeIndex, method.Type, true, formatHandler) 95 rc.registerRouteInOptionsHandler(pathPrefix, routeIndex, formatHandler) 96 } 97 } 98 } 99 100 // Sorting corsMap is needed to correctly resolve OPTION requests 101 // that need to match a regex. 102 rc.sortedREs = getSortedREs(rc.corsMap) 103 104 return rc 105 } 106 107 ///////////////////////////////////////////////// 108 109 // Internal method that registers the route (with its format) 110 // into the router's corsMap, for later use by the OPTIONS handler. 111 func (rc *RouterConfigurer) registerRouteInOptionsHandler(pathPrefix string, 112 routeIndex int, formatHandler FormatHandler) { 113 route := (*rc.routes)[routeIndex] 114 // Setup a helper regex for "{_text_}" URL parameters. 115 re := regexp.MustCompile("{[^}]+?}") 116 namedVarRE := regexp.MustCompile("{[^}]+:[^{]+}") 117 // Register the route in the corsMap. Used by the global OPTIONS handler 118 uriPath := route.URI + formatHandler.Extension 119 prefixedURIPath := strings.Replace(pathPrefix+uriPath, "//", "/", -1) 120 // Store route information for the global OPTIONS handler 121 newStr := strings.Replace(prefixedURIPath, ".", "\\.", -1) 122 reString := namedVarRE.ReplaceAllString(newStr, ".+") 123 reString = re.ReplaceAllString(reString, "[^/]+") 124 rc.corsMap[reString] = routeIndex 125 126 rc. 127 Methods("OPTIONS"). 128 Path(uriPath). 129 Name(route.Name + formatHandler.Extension). 130 Handler(http.HandlerFunc(rc.globalOptionsHandler)) 131 } 132 133 // Helper function that registers the given route handler AND 134 // automatically creates and registers an HTTP OPTIONS method handler on the route. 135 // 136 // formatHandler is the given most route handler. 137 func (rc *RouterConfigurer) registerRouteHandler(routeIndex int, methodType string, 138 secure bool, formatHandler FormatHandler) { 139 140 handler := formatHandler.Handler 141 142 // TODO move to top chain middlewares 143 144 // Configure auth middleware 145 var authMiddleware negroni.HandlerFunc 146 if !secure { 147 authMiddleware = rc.authOptMiddleware 148 } else { 149 authMiddleware = rc.authReqMiddleware 150 } 151 152 routeName := (*rc.routes)[routeIndex].Name 153 154 recovery := negroni.NewRecovery() 155 // PrintStack is set to false to avoid sending stacktrace to client. 156 recovery.PrintStack = false 157 158 // Configure middleware chain 159 middlewares := negroni.New() 160 // Add monitoring middleware if monitoring provider is present. 161 // It must be the first middleware in the chain 162 if rc.monitoring != nil { 163 middlewares = middlewares.With(rc.monitoring.Middleware()) 164 } 165 // Add default middlewares 166 middlewares = middlewares.With( 167 recovery, 168 negroni.HandlerFunc(requireDBMiddleware), 169 negroni.HandlerFunc(addCORSheadersMiddleware), 170 authMiddleware, 171 negroni.HandlerFunc(newGaEventTracking(routeName)), 172 ) 173 // Inject custom handlers just before the final handler 174 middlewares = middlewares.With(rc.customHandlers...) 175 middlewares.Use(negroni.Wrap(http.Handler(handler))) 176 177 // Last, wrap everything with a Logger middleware 178 handler = logger(middlewares, routeName) 179 180 uriPath := (*rc.routes)[routeIndex].URI + formatHandler.Extension 181 182 // Create the route handler. 183 rc. 184 Methods(methodType). 185 Path(uriPath). 186 Name(routeName + formatHandler.Extension). 187 Handler(handler) 188 } 189 190 // globalOptionsHandler is an OPTIONS method handler that will return 191 // documentation of a route based on its Route definition. 192 func (rc *RouterConfigurer) globalOptionsHandler(w http.ResponseWriter, r *http.Request) { 193 index := 0 194 ok := false 195 // Find the matching URL 196 for _, key := range rc.sortedREs { 197 // Make sure the regular expression matches the complete URL path 198 if regexp.MustCompile(key).FindString(r.URL.Path) == r.URL.Path { 199 ok = true 200 index = rc.corsMap[key] 201 break 202 } 203 } 204 route := (*rc.routes)[index] 205 if ok { 206 if output, e := json.Marshal(route); e != nil { 207 err := NewErrorMessageWithBase(ErrorMarshalJSON, e) 208 reportJSONError(w, r, *err) 209 } else { 210 // Find all the route supported HTTP Methods 211 allow := make([]string, 0) 212 for _, m := range route.Methods { 213 allow = append(allow, m.Type) 214 } 215 for _, m := range route.SecureMethods { 216 allow = append(allow, m.Type) 217 } 218 w.Header().Set("Allow", strings.Join(allow[:], ",")) 219 w.Header().Set("Content-Type", "application/json") 220 addCORSheaders(w) 221 fmt.Fprintln(w, string(output)) 222 } 223 return 224 } 225 226 // Return error if a URL did not match 227 err := ErrorMessage(ErrorNameNotFound) 228 reportJSONError(w, r, err) 229 } 230 231 ///////////////////////////////////////////////// 232 // sortRE is an internal []string wrapper type used to sort by 233 // the number of "[^/]+" string occurrences found in a regex (ie. count). 234 // If the same count is found then the larger string will take precedence. 235 type sortRE []string 236 237 func (s sortRE) Len() int { 238 return len(s) 239 } 240 241 func (s sortRE) Swap(i, j int) { 242 s[i], s[j] = s[j], s[i] 243 } 244 245 func (s sortRE) Less(i, j int) bool { 246 ci := strings.Count(s[i], "[^/]+") 247 cj := strings.Count(s[j], "[^/]+") 248 if ci == cj { 249 return len(s[i]) > len(s[j]) 250 } 251 return ci < cj 252 } 253 254 func getSortedREs(m map[string]int) []string { 255 var keys []string 256 for k := range m { 257 keys = append(keys, k) 258 } 259 sort.Sort(sortRE(keys)) 260 return keys 261 }