| /* |
| Copyright 2016 The Kubernetes Authors. |
| |
| 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. |
| */ |
| |
| package mux |
| |
| import ( |
| "fmt" |
| "net/http" |
| "runtime/debug" |
| "sort" |
| "strings" |
| "sync" |
| "sync/atomic" |
| |
| "github.com/golang/glog" |
| |
| utilruntime "k8s.io/apimachinery/pkg/util/runtime" |
| "k8s.io/apimachinery/pkg/util/sets" |
| ) |
| |
| // PathRecorderMux wraps a mux object and records the registered exposedPaths. |
| type PathRecorderMux struct { |
| // name is used for logging so you can trace requests through |
| name string |
| |
| lock sync.Mutex |
| notFoundHandler http.Handler |
| pathToHandler map[string]http.Handler |
| prefixToHandler map[string]http.Handler |
| |
| // mux stores a pathHandler and is used to handle the actual serving. |
| // Turns out, we want to accept trailing slashes, BUT we don't care about handling |
| // everything under them. This does exactly matches only unless its explicitly requested to |
| // do something different |
| mux atomic.Value |
| |
| // exposedPaths is the list of paths that should be shown at / |
| exposedPaths []string |
| |
| // pathStacks holds the stacks of all registered paths. This allows us to show a more helpful message |
| // before the "http: multiple registrations for %s" panic. |
| pathStacks map[string]string |
| } |
| |
| // pathHandler is an http.Handler that will satify requests first by exact match, then by prefix, |
| // then by notFoundHandler |
| type pathHandler struct { |
| // muxName is used for logging so you can trace requests through |
| muxName string |
| |
| // pathToHandler is a map of exactly matching request to its handler |
| pathToHandler map[string]http.Handler |
| |
| // this has to be sorted by most slashes then by length |
| prefixHandlers []prefixHandler |
| |
| // notFoundHandler is the handler to use for satisfying requests with no other match |
| notFoundHandler http.Handler |
| } |
| |
| // prefixHandler holds the prefix it should match and the handler to use |
| type prefixHandler struct { |
| // prefix is the prefix to test for a request match |
| prefix string |
| // handler is used to satisfy matching requests |
| handler http.Handler |
| } |
| |
| // NewPathRecorderMux creates a new PathRecorderMux |
| func NewPathRecorderMux(name string) *PathRecorderMux { |
| ret := &PathRecorderMux{ |
| name: name, |
| pathToHandler: map[string]http.Handler{}, |
| prefixToHandler: map[string]http.Handler{}, |
| mux: atomic.Value{}, |
| exposedPaths: []string{}, |
| pathStacks: map[string]string{}, |
| } |
| |
| ret.mux.Store(&pathHandler{notFoundHandler: http.NotFoundHandler()}) |
| return ret |
| } |
| |
| // ListedPaths returns the registered handler exposedPaths. |
| func (m *PathRecorderMux) ListedPaths() []string { |
| handledPaths := append([]string{}, m.exposedPaths...) |
| sort.Strings(handledPaths) |
| |
| return handledPaths |
| } |
| |
| func (m *PathRecorderMux) trackCallers(path string) { |
| if existingStack, ok := m.pathStacks[path]; ok { |
| utilruntime.HandleError(fmt.Errorf("registered %q from %v", path, existingStack)) |
| } |
| m.pathStacks[path] = string(debug.Stack()) |
| } |
| |
| // refreshMuxLocked creates a new mux and must be called while locked. Otherwise the view of handlers may |
| // not be consistent |
| func (m *PathRecorderMux) refreshMuxLocked() { |
| newMux := &pathHandler{ |
| muxName: m.name, |
| pathToHandler: map[string]http.Handler{}, |
| prefixHandlers: []prefixHandler{}, |
| notFoundHandler: http.NotFoundHandler(), |
| } |
| if m.notFoundHandler != nil { |
| newMux.notFoundHandler = m.notFoundHandler |
| } |
| for path, handler := range m.pathToHandler { |
| newMux.pathToHandler[path] = handler |
| } |
| |
| keys := sets.StringKeySet(m.prefixToHandler).List() |
| sort.Sort(sort.Reverse(byPrefixPriority(keys))) |
| for _, prefix := range keys { |
| newMux.prefixHandlers = append(newMux.prefixHandlers, prefixHandler{ |
| prefix: prefix, |
| handler: m.prefixToHandler[prefix], |
| }) |
| } |
| |
| m.mux.Store(newMux) |
| } |
| |
| // NotFoundHandler sets the handler to use if there's no match for a give path |
| func (m *PathRecorderMux) NotFoundHandler(notFoundHandler http.Handler) { |
| m.lock.Lock() |
| defer m.lock.Unlock() |
| |
| m.notFoundHandler = notFoundHandler |
| |
| m.refreshMuxLocked() |
| } |
| |
| // Unregister removes a path from the mux. |
| func (m *PathRecorderMux) Unregister(path string) { |
| m.lock.Lock() |
| defer m.lock.Unlock() |
| |
| delete(m.pathToHandler, path) |
| delete(m.prefixToHandler, path) |
| delete(m.pathStacks, path) |
| for i := range m.exposedPaths { |
| if m.exposedPaths[i] == path { |
| m.exposedPaths = append(m.exposedPaths[:i], m.exposedPaths[i+1:]...) |
| break |
| } |
| } |
| |
| m.refreshMuxLocked() |
| } |
| |
| // Handle registers the handler for the given pattern. |
| // If a handler already exists for pattern, Handle panics. |
| func (m *PathRecorderMux) Handle(path string, handler http.Handler) { |
| m.lock.Lock() |
| defer m.lock.Unlock() |
| m.trackCallers(path) |
| |
| m.exposedPaths = append(m.exposedPaths, path) |
| m.pathToHandler[path] = handler |
| m.refreshMuxLocked() |
| } |
| |
| // HandleFunc registers the handler function for the given pattern. |
| // If a handler already exists for pattern, Handle panics. |
| func (m *PathRecorderMux) HandleFunc(path string, handler func(http.ResponseWriter, *http.Request)) { |
| m.Handle(path, http.HandlerFunc(handler)) |
| } |
| |
| // UnlistedHandle registers the handler for the given pattern, but doesn't list it. |
| // If a handler already exists for pattern, Handle panics. |
| func (m *PathRecorderMux) UnlistedHandle(path string, handler http.Handler) { |
| m.lock.Lock() |
| defer m.lock.Unlock() |
| m.trackCallers(path) |
| |
| m.pathToHandler[path] = handler |
| m.refreshMuxLocked() |
| } |
| |
| // UnlistedHandleFunc registers the handler function for the given pattern, but doesn't list it. |
| // If a handler already exists for pattern, Handle panics. |
| func (m *PathRecorderMux) UnlistedHandleFunc(path string, handler func(http.ResponseWriter, *http.Request)) { |
| m.UnlistedHandle(path, http.HandlerFunc(handler)) |
| } |
| |
| // HandlePrefix is like Handle, but matches for anything under the path. Like a standard golang trailing slash. |
| func (m *PathRecorderMux) HandlePrefix(path string, handler http.Handler) { |
| if !strings.HasSuffix(path, "/") { |
| panic(fmt.Sprintf("%q must end in a trailing slash", path)) |
| } |
| |
| m.lock.Lock() |
| defer m.lock.Unlock() |
| m.trackCallers(path) |
| |
| m.exposedPaths = append(m.exposedPaths, path) |
| m.prefixToHandler[path] = handler |
| m.refreshMuxLocked() |
| } |
| |
| // UnlistedHandlePrefix is like UnlistedHandle, but matches for anything under the path. Like a standard golang trailing slash. |
| func (m *PathRecorderMux) UnlistedHandlePrefix(path string, handler http.Handler) { |
| if !strings.HasSuffix(path, "/") { |
| panic(fmt.Sprintf("%q must end in a trailing slash", path)) |
| } |
| |
| m.lock.Lock() |
| defer m.lock.Unlock() |
| m.trackCallers(path) |
| |
| m.prefixToHandler[path] = handler |
| m.refreshMuxLocked() |
| } |
| |
| // ServeHTTP makes it an http.Handler |
| func (m *PathRecorderMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| m.mux.Load().(*pathHandler).ServeHTTP(w, r) |
| } |
| |
| // ServeHTTP makes it an http.Handler |
| func (h *pathHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| if exactHandler, ok := h.pathToHandler[r.URL.Path]; ok { |
| glog.V(5).Infof("%v: %q satisfied by exact match", h.muxName, r.URL.Path) |
| exactHandler.ServeHTTP(w, r) |
| return |
| } |
| |
| for _, prefixHandler := range h.prefixHandlers { |
| if strings.HasPrefix(r.URL.Path, prefixHandler.prefix) { |
| glog.V(5).Infof("%v: %q satisfied by prefix %v", h.muxName, r.URL.Path, prefixHandler.prefix) |
| prefixHandler.handler.ServeHTTP(w, r) |
| return |
| } |
| } |
| |
| glog.V(5).Infof("%v: %q satisfied by NotFoundHandler", h.muxName, r.URL.Path) |
| h.notFoundHandler.ServeHTTP(w, r) |
| } |
| |
| // byPrefixPriority sorts url prefixes by the order in which they should be tested by the mux |
| // this has to be sorted by most slashes then by length so that we can iterate straight |
| // through to match the "best" one first. |
| type byPrefixPriority []string |
| |
| func (s byPrefixPriority) Len() int { return len(s) } |
| func (s byPrefixPriority) Swap(i, j int) { s[i], s[j] = s[j], s[i] } |
| func (s byPrefixPriority) Less(i, j int) bool { |
| lhsNumParts := strings.Count(s[i], "/") |
| rhsNumParts := strings.Count(s[j], "/") |
| if lhsNumParts != rhsNumParts { |
| return lhsNumParts < rhsNumParts |
| } |
| |
| lhsLen := len(s[i]) |
| rhsLen := len(s[j]) |
| if lhsLen != rhsLen { |
| return lhsLen < rhsLen |
| } |
| |
| return strings.Compare(s[i], s[j]) < 0 |
| } |