~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
129 lines · 2.5 KB Raw History
package exposurelocal

import (
	"errors"
	"net"
	"net/http"
	"net/url"
	"strings"
	"sync"
)

type Registry struct {
	mu sync.RWMutex

	routes map[string]route
}

type route struct {
	host string

	path string

	handler http.Handler
}

func NewRegistry() *Registry { return &Registry{routes: map[string]route{}} }

func (r *Registry) Register(owner, host, path string, handler http.Handler) error {
	if r == nil {
		return errors.New("exposurelocal: the handler registry is nil")
	}
	owner = strings.TrimSpace(owner)
	if owner == "" {
		return errors.New("exposurelocal: a route needs an owner")
	}
	if handler == nil {
		return errors.New("exposurelocal: a route needs a handler")
	}
	host = normalizeHost(host)
	if host == "" {
		return errors.New("exposurelocal: a route needs a host")
	}
	r.mu.Lock()
	if r.routes == nil {
		r.routes = map[string]route{}
	}
	r.routes[owner] = route{host: host, path: normalizePath(path), handler: handler}
	r.mu.Unlock()
	return nil
}

func (r *Registry) Remove(owner string) {
	if r == nil {
		return
	}
	r.mu.Lock()
	delete(r.routes, owner)
	r.mu.Unlock()
}

func (r *Registry) Lookup(rawHost, rawPath string) (http.Handler, bool) {
	if r == nil {
		return nil, false
	}
	host := normalizeHost(rawHost)
	if host == "" {
		return nil, false
	}
	target := normalizePath(rawPath)
	r.mu.RLock()
	defer r.mu.RUnlock()
	var (
		best    http.Handler
		bestLen = -1
		bestOwn string
	)
	for owner, rt := range r.routes {
		if rt.host != host || !pathMatches(rt.path, target) {
			continue
		}
		if len(rt.path) > bestLen || (len(rt.path) == bestLen && owner < bestOwn) {
			best, bestLen, bestOwn = rt.handler, len(rt.path), owner
		}
	}
	return best, best != nil
}

func (r *Registry) HandlerFor(rawHost, rawPath string) (http.Handler, bool) {
	return r.Lookup(rawHost, rawPath)
}

func (r *Registry) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	handler, ok := r.Lookup(req.Host, req.URL.Path)
	if !ok {
		http.NotFound(w, req)
		return
	}
	handler.ServeHTTP(w, req)
}

func normalizeHost(raw string) string {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return ""
	}
	if !strings.Contains(raw, "://") {
		raw = "https://" + raw
	}
	u, err := url.Parse(raw)
	if err != nil {
		return ""
	}
	host := strings.ToLower(strings.TrimSuffix(u.Hostname(), "."))
	if host == "" {
		return ""
	}
	port := u.Port()
	if port == "" || port == "80" || port == "443" {
		return host
	}
	return net.JoinHostPort(host, port)
}

func pathMatches(prefix, target string) bool {
	if prefix == "/" {
		return true
	}
	return target == prefix || strings.HasPrefix(target, prefix+"/")
}