blocky/service/http.go

124 lines
3.0 KiB
Go

package service
import (
"errors"
"net/http"
"strings"
"github.com/0xERR0R/blocky/util"
"github.com/go-chi/chi/v5"
)
const (
HTTPProtocol = "http"
HTTPSProtocol = "https"
)
// HTTPService is a Service using a HTTP router.
type HTTPService interface {
Service
Merger
// Router returns the service's router.
Router() chi.Router
}
// HTTPInfo can be embedded in structs to help implement HTTPService.
type HTTPInfo struct {
Info
mux *chi.Mux
}
func NewHTTPInfo(name string, endpoints []Endpoint) HTTPInfo {
return HTTPInfo{
Info: NewInfo(name, endpoints),
mux: chi.NewMux(),
}
}
func (i *HTTPInfo) Router() chi.Router { return i.mux }
var _ HTTPService = (*SimpleHTTP)(nil)
// SimpleHTTP implements HTTPService usinig the default HTTP merger.
type SimpleHTTP struct{ HTTPInfo }
func NewSimpleHTTP(name string, endpoints []Endpoint) SimpleHTTP {
return SimpleHTTP{HTTPInfo: NewHTTPInfo(name, endpoints)}
}
func (s *SimpleHTTP) Merge(other Service) (Merger, error) {
return MergeHTTP(s, other)
}
// MergeHTTP merges two compatible HTTPServices.
//
// The second parameter is of type `Service` to make it easy to call
// from a `Merger.Merge` implementation.
func MergeHTTP(a HTTPService, b Service) (Merger, error) {
return newHTTPMerger(a).Merge(b)
}
var _ HTTPService = (*httpMerger)(nil)
// httpMerger can merge HTTPServices by combining their routes.
type httpMerger struct {
inner []HTTPService
router chi.Router
endpoints endpointSet
}
func newHTTPMerger(first HTTPService) *httpMerger {
return &httpMerger{
inner: []HTTPService{first},
router: first.Router(),
}
}
func (m *httpMerger) String() string { return svcString(m) }
func (m *httpMerger) ServiceName() string {
names := util.ForEach(m.inner, func(svc HTTPService) string {
return svc.ServiceName()
})
return strings.Join(names, " & ")
}
func (m *httpMerger) ExposeOn() []Endpoint { return m.endpoints.ToSlice() }
func (m *httpMerger) Router() chi.Router { return m.router }
func (m *httpMerger) Merge(other Service) (Merger, error) {
httpSvc, ok := other.(HTTPService)
if !ok {
return nil, errors.New("not an HTTPService")
}
type middleware = func(http.Handler) http.Handler
// Can't do `.Mount("/", ...)` otherwise we can only merge at most once since / will already be defined
_ = chi.Walk(httpSvc.Router(), func(method, route string, handler http.Handler, middlewares ...middleware) error {
m.router.With(middlewares...).Method(method, route, handler)
// Expose /example/ as /example too
// Workaround for chi.Walk missing the second form https://github.com/go-chi/chi/issues/830
// This means we expose the route without the slash even if it wasn't oringinally registered as such!
// The main point of this is for DoH (/dns-query).
if strings.HasSuffix(route, "/") {
route := strings.TrimSuffix(route, "/")
m.router.With(middlewares...).Method(method, route, handler)
}
return nil
})
m.inner = append(m.inner, httpSvc)
// Don't expose any service more than it expects
m.endpoints.IntersectSlice(other.ExposeOn())
return m, nil
}