package relay import ( "github.com/go-chi/chi/v5" conf "github.com/muety/wakapi/config" "net/http" "net/http/httputil" "net/url" "regexp" ) const targetUrlHeader = "X-Target-URL" const pathMatcherPattern = `^/api/(heartbeat|heartbeats|summary|users|v1/users|compat/wakatime)` type RelayHandler struct { config *conf.Config } func NewRelayHandler() *RelayHandler { return &RelayHandler{ config: conf.Get(), } } type filteringMiddleware struct { handler http.Handler pathMatcher *regexp.Regexp } func newFilteringMiddleware() func(http.Handler) http.Handler { return func(h http.Handler) http.Handler { return &filteringMiddleware{ handler: h, pathMatcher: regexp.MustCompile(pathMatcherPattern), } } } func (m *filteringMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { targetUrl, err := url.Parse(r.Header.Get(targetUrlHeader)) if err != nil || !m.pathMatcher.MatchString(targetUrl.Path) { w.WriteHeader(http.StatusForbidden) w.Write([]byte{}) return } m.handler.ServeHTTP(w, r) } func (h *RelayHandler) RegisterRoutes(router chi.Router) { if !h.config.Security.EnableProxy { return } r := chi.NewRouter() r.Use(newFilteringMiddleware()) r.HandleFunc("/", h.Any) router.Mount("/relay", r) } func (h *RelayHandler) Any(w http.ResponseWriter, r *http.Request) { targetUrl, err := url.Parse(r.Header.Get(targetUrlHeader)) if err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte{}) return } targetUrl.RawQuery = r.URL.RawQuery p := httputil.ReverseProxy{ Director: func(r *http.Request) { r.URL = targetUrl r.Host = targetUrl.Host }, } p.ServeHTTP(w, r) } // @Summary Proxy an GET API request to another Wakapi instance // @ID relay-get // @Tags relay // @Param X-Target-URL header string true "Original URL to perform the request to" // @Failure 403 {string} string "Returned if request path is not whitelisted" // @Failure 502 {string} string "Returned if upstream host is down" // @Router /relay [get] func (h *RelayHandler) alias1() {} // @Summary Proxy an POST API request to another Wakapi instance // @ID relay-post // @Tags relay // @Param X-Target-URL header string true "Original URL to perform the request to" // @Failure 403 {string} string "Returned if request path is not whitelisted" // @Failure 502 {string} string "Returned if upstream host is down" // @Router /relay [post] func (h *RelayHandler) alias2() {} // @Summary Proxy an PUT API request to another Wakapi instance // @ID relay-put // @Tags relay // @Param X-Target-URL header string true "Original URL to perform the request to" // @Failure 403 {string} string "Returned if request path is not whitelisted" // @Failure 502 {string} string "Returned if upstream host is down" // @Router /relay [put] func (h *RelayHandler) alias3() {} // @Summary Proxy an PATCH API request to another Wakapi instance // @ID relay-patch // @Tags relay // @Param X-Target-URL header string true "Original URL to perform the request to" // @Failure 403 {string} string "Returned if request path is not whitelisted" // @Failure 502 {string} string "Returned if upstream host is down" // @Router /relay [patch] func (h *RelayHandler) alias4() {} // @Summary Proxy an DELETE API request to another Wakapi instance // @ID relay-delete // @Tags relay // @Param X-Target-URL header string true "Original URL to perform the request to" // @Failure 403 {string} string "Returned if request path is not whitelisted" // @Failure 502 {string} string "Returned if upstream host is down" // @Router /relay [delete] func (h *RelayHandler) alias5() {}