From 06cc2965077cdfbb9ff7e377f0cf811eea767349 Mon Sep 17 00:00:00 2001 From: richardlt Date: Wed, 21 Apr 2021 12:14:24 +0200 Subject: [PATCH 1/3] feat(ui): flag to enable service proxy, filter proxy routes --- docker-compose.yml | 1 + engine/ui/types.go | 21 ++++----- engine/ui/ui.go | 4 +- engine/ui/ui_router.go | 87 +++++++++++++++++++++++++++---------- sdk/cdsclient/client_cdn.go | 1 - 5 files changed, 78 insertions(+), 36 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 726f1073e9..85bf0240a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -105,6 +105,7 @@ services: elasticsearch.elasticsearch.indexMetrics=cds-index-metrics \ elasticsearch.elasticsearch.url=http://elasticsearch:9200 \ ui.url=http://${HOSTNAME}:8080 \ + ui.enableServiceProxy=true \ ui.api.http.url=http://cds-api:8081 \ ui.hooksURL=http://cds-hooks:8083 \ ui.cdnURL=http://cds-cdn:8089; diff --git a/engine/ui/types.go b/engine/ui/types.go index 9a2ca6abfd..9fa84e49e9 100644 --- a/engine/ui/types.go +++ b/engine/ui/types.go @@ -19,14 +19,15 @@ type Service struct { // Configuration is the ui configuration structure type Configuration struct { - Name string `toml:"name" comment:"Name of this CDS UI Service\n Enter a name to enable this service" json:"name"` - Staticdir string `toml:"staticdir" default:"./ui_static_files" comment:"This directory must contain the dist directory." json:"staticdir"` - BaseURL string `toml:"baseURL" commented:"true" comment:"If you expose CDS UI with https://your-domain.com/ui, enter the value '/ui/'. Optional" json:"baseURL"` - DeployURL string `toml:"deployURL" commented:"true" comment:"You can start CDS UI proxy on a sub path like https://your-domain.com/ui with value '/ui' (the value should not be given when the sub path is added by a proxy in front of CDS). Optional" json:"deployURL"` - SentryURL string `toml:"sentryURL" commented:"true" comment:"Sentry URL. Optional" json:"-"` - HTTP service.HTTPRouterConfiguration `toml:"http" comment:"######################\n CDS UI HTTP Configuration \n######################" json:"http"` - URL string `toml:"url" comment:"Public URL of this UI service." default:"http://localhost:8080" json:"url"` - API service.APIServiceConfiguration `toml:"api" comment:"######################\n CDS API Settings \n######################" json:"api"` - HooksURL string `toml:"hooksURL" comment:"Hooks µService URL" default:"http://localhost:8083" json:"hooksURL"` - CDNURL string `toml:"cdnURL" comment:"CDN µService URL" default:"http://localhost:8089" json:"cdnURL"` + Name string `toml:"name" comment:"Name of this CDS UI Service\n Enter a name to enable this service" json:"name"` + Staticdir string `toml:"staticdir" default:"./ui_static_files" comment:"This directory must contain the dist directory." json:"staticdir"` + BaseURL string `toml:"baseURL" commented:"true" comment:"If you expose CDS UI with https://your-domain.com/ui, enter the value '/ui/'. Optional" json:"baseURL"` + DeployURL string `toml:"deployURL" commented:"true" comment:"You can start CDS UI proxy on a sub path like https://your-domain.com/ui with value '/ui' (the value should not be given when the sub path is added by a proxy in front of CDS). Optional" json:"deployURL"` + SentryURL string `toml:"sentryURL" commented:"true" comment:"Sentry URL. Optional" json:"-"` + HTTP service.HTTPRouterConfiguration `toml:"http" comment:"######################\n CDS UI HTTP Configuration \n######################" json:"http"` + URL string `toml:"url" comment:"Public URL of this UI service." default:"http://localhost:8080" json:"url"` + API service.APIServiceConfiguration `toml:"api" comment:"######################\n CDS API Settings \n######################" json:"api"` + HooksURL string `toml:"hooksURL" comment:"Hooks µService URL" default:"http://localhost:8083" json:"hooksURL"` + CDNURL string `toml:"cdnURL" comment:"CDN µService URL" default:"http://localhost:8089" json:"cdnURL"` + EnableServiceProxy bool `toml:"enableServiceProxy" default:"false" commented:"true" comment:"Enable service proxy will allows CDS UI to handle request to API, Hooks and CDN services. Optional" json:"enableServiceProxy"` } diff --git a/engine/ui/ui.go b/engine/ui/ui.go index df0e97a574..5ff275ff17 100644 --- a/engine/ui/ui.go +++ b/engine/ui/ui.go @@ -240,8 +240,10 @@ func (s *Service) indexHTMLReplaceVar() error { return sdk.WrapError(err, "cannot parse base href regex") } indexContent := regexBaseHref.ReplaceAllString(string(read), "") - indexContent = strings.Replace(indexContent, "window.cds_sentry_url = '';", "window.cds_sentry_url = '"+s.Cfg.SentryURL+"';", -1) indexContent = strings.Replace(indexContent, "window.cds_version = '';", "window.cds_version='"+sdk.VERSION+"';", -1) + if s.Cfg.SentryURL != "" { + indexContent = strings.Replace(indexContent, "window.cds_sentry_url = '';", "window.cds_sentry_url = '"+s.Cfg.SentryURL+"';", -1) + } return ioutil.WriteFile(indexHTML, []byte(indexContent), 0) } diff --git a/engine/ui/ui_router.go b/engine/ui/ui_router.go index d7335c5a8d..ead8614506 100644 --- a/engine/ui/ui_router.go +++ b/engine/ui/ui_router.go @@ -29,15 +29,20 @@ func (s *Service) initRouter(ctx context.Context) { r.Handle(s.Cfg.DeployURL+"/mon/metrics", nil, r.GET(service.GetPrometheustMetricsHandler(s))) r.Handle(s.Cfg.DeployURL+"/mon/metrics/all", nil, r.GET(service.GetMetricsHandler)) - // proxypass - if s.Cfg.API.HTTP.URL != "" { - r.Mux.PathPrefix(s.Cfg.DeployURL + "/cdsapi").Handler(s.getReverseProxy(s.Cfg.DeployURL+"/cdsapi", s.Cfg.API.HTTP.URL)) - } - if s.Cfg.HooksURL != "" { - r.Mux.PathPrefix(s.Cfg.DeployURL + "/cdshooks").Handler(s.getReverseProxy(s.Cfg.DeployURL+"/cdshooks", s.Cfg.HooksURL)) - } - if s.Cfg.CDNURL != "" { - r.Mux.PathPrefix(s.Cfg.DeployURL + "/cdscdn").Handler(s.getReverseProxy(s.Cfg.DeployURL+"/cdscdn", s.Cfg.CDNURL)) + // proxypass if enabled + if s.Cfg.EnableServiceProxy { + if s.Cfg.API.HTTP.URL != "" { + apiPath := s.Cfg.DeployURL + "/cdsapi" + r.Mux.PathPrefix(apiPath).Handler(s.getReverseProxy(ctx, apiPath, s.Cfg.API.HTTP.URL)) + } + if s.Cfg.HooksURL != "" { + hooksPath := s.Cfg.DeployURL + "/cdshooks" + r.Mux.PathPrefix(hooksPath).Handler(s.getReverseProxy(ctx, hooksPath, s.Cfg.HooksURL)) + } + if s.Cfg.CDNURL != "" { + cdnPath := s.Cfg.DeployURL + "/cdscdn" + r.Mux.PathPrefix(cdnPath).Handler(s.getReverseProxy(ctx, cdnPath, s.Cfg.CDNURL)) + } } // serve static UI files @@ -45,25 +50,59 @@ func (s *Service) initRouter(ctx context.Context) { r.Mux.PathPrefix("/").Handler(s.uiServe(http.Dir(s.HTMLDir), s.HTMLDir)) } -func (s *Service) getReverseProxy(path, urlRemote string) *httputil.ReverseProxy { - origin, _ := url.Parse(urlRemote) +func (s *Service) getReverseProxy(ctx context.Context, path, urlRemote string) http.Handler { + filter := func(req *http.Request) bool { + reqPath := strings.TrimPrefix(req.URL.Path, path) + + // on api proxypass, deny request on /mon/metrics + if strings.HasSuffix(path, "api") && strings.HasPrefix(reqPath, "/mon/metrics") { + return false + } + + // on hooks proxypass, allow only request on /webhook/ path + if strings.HasSuffix(path, "hooks") && !strings.HasPrefix(reqPath, "/webhook/") { + return false + } + + // on cdn proxypass, allow only request on /item + if strings.HasSuffix(path, "cdn") && !strings.HasPrefix(reqPath, "/item") { + return false + } + return true + } + + origin, _ := url.Parse(urlRemote) director := func(req *http.Request) { reqPath := strings.TrimPrefix(req.URL.Path, path) - // on proxypass /cdshooks, allow only request on /webhook/ path - if strings.HasSuffix(path, "/cdshooks") && !strings.HasPrefix(reqPath, "/webhook/") { - // return 502 bad gateway - req = &http.Request{} // nolint - } else { - req.Header.Add("X-Forwarded-Host", req.Host) - req.Header.Add("X-Origin-Host", origin.Host) - req.URL.Scheme = origin.Scheme - req.URL.Host = origin.Host - req.URL.Path = origin.Path + reqPath - req.Host = origin.Host - } + req.Header.Add("X-Forwarded-Host", req.Host) + req.Header.Add("X-Origin-Host", origin.Host) + req.URL.Scheme = origin.Scheme + req.URL.Host = origin.Host + req.URL.Path = origin.Path + reqPath + req.Host = origin.Host + } + + return &reverseProxyWithFilter{ + ctx: ctx, + rp: &httputil.ReverseProxy{Director: director}, + filter: filter, + } +} + +type reverseProxyWithFilter struct { + ctx context.Context + rp *httputil.ReverseProxy + filter func(r *http.Request) bool +} + +func (r *reverseProxyWithFilter) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if !r.filter(req) { + log.Debug(r.ctx, "proxy deny on target route") + rw.WriteHeader(http.StatusBadGateway) + return } - return &httputil.ReverseProxy{Director: director} + r.rp.ServeHTTP(rw, req) } func (s *Service) uiServe(fs http.FileSystem, dir string) http.Handler { diff --git a/sdk/cdsclient/client_cdn.go b/sdk/cdsclient/client_cdn.go index b07e8b937b..1864ecab28 100644 --- a/sdk/cdsclient/client_cdn.go +++ b/sdk/cdsclient/client_cdn.go @@ -47,7 +47,6 @@ func (c *client) CDNItemUpload(ctx context.Context, cdnAddr string, signature st time.Sleep(1 * time.Second) continue } - //_, _, _, err = c.Request(ctx, http.MethodPost, fmt.Sprintf("%s/item/upload", cdnAddr), f, SetHeader("X-CDS-WORKER-SIGNATURE", signature)) savedError = nil break } From 1f7aaa562fabd157a5574dbb5be963e3510e3bcb Mon Sep 17 00:00:00 2001 From: richardlt Date: Mon, 26 Apr 2021 17:06:57 +0200 Subject: [PATCH 2/3] fix: code review add forwarded for header --- engine/ui/ui_router.go | 1 + 1 file changed, 1 insertion(+) diff --git a/engine/ui/ui_router.go b/engine/ui/ui_router.go index ead8614506..cc1ca3bab4 100644 --- a/engine/ui/ui_router.go +++ b/engine/ui/ui_router.go @@ -75,6 +75,7 @@ func (s *Service) getReverseProxy(ctx context.Context, path, urlRemote string) h origin, _ := url.Parse(urlRemote) director := func(req *http.Request) { reqPath := strings.TrimPrefix(req.URL.Path, path) + req.Header.Add("X-Forwarded-For", req.RemoteAddr) req.Header.Add("X-Forwarded-Host", req.Host) req.Header.Add("X-Origin-Host", origin.Host) req.URL.Scheme = origin.Scheme From 8d559b9874827d587c54729c2c08eb1d6513d309 Mon Sep 17 00:00:00 2001 From: richardlt Date: Mon, 26 Apr 2021 17:16:37 +0200 Subject: [PATCH 3/3] fix: read forwarded for info from request --- engine/api/router.go | 14 ++++++++++---- engine/service/types.go | 2 +- engine/ui/ui_router.go | 18 +++++++++++++++++- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/engine/api/router.go b/engine/api/router.go index 96edfb3d38..1d7ffb0a30 100644 --- a/engine/api/router.go +++ b/engine/api/router.go @@ -340,8 +340,11 @@ func (r *Router) handle(uri string, scope HandlerScope, handlers ...*service.Han telemetry.Path, req.URL.Path, telemetry.Method, req.Method) - // Retrieve the client ip address from the header (X-Forwarded-For by default) - clientIP := req.Header.Get(r.Config.HeaderXForwardedFor) + var clientIP string + if r.Config.HeaderXForwardedFor != "" { + // Retrieve the client ip address from the header (X-Forwarded-For by default) + clientIP = req.Header.Get(r.Config.HeaderXForwardedFor) + } if clientIP == "" { // If the header has not been found, fallback on the remote adress from the http request clientIP = req.RemoteAddr @@ -545,8 +548,11 @@ func MaintenanceAware() service.HandlerConfigParam { func (r *Router) NotFoundHandler(w http.ResponseWriter, req *http.Request) { ctx := req.Context() - // Retrieve the client ip address from the header (X-Forwarded-For by default) - clientIP := req.Header.Get(r.Config.HeaderXForwardedFor) + var clientIP string + if r.Config.HeaderXForwardedFor != "" { + // Retrieve the client ip address from the header (X-Forwarded-For by default) + clientIP = req.Header.Get(r.Config.HeaderXForwardedFor) + } if clientIP == "" { // If the header has not been found, fallback on the remote adress from the http request clientIP = req.RemoteAddr diff --git a/engine/service/types.go b/engine/service/types.go index 0f42601b33..98e45d76d9 100644 --- a/engine/service/types.go +++ b/engine/service/types.go @@ -27,7 +27,7 @@ type APIServiceConfiguration struct { type HTTPRouterConfiguration struct { Addr string `toml:"addr" default:"" commented:"true" comment:"Listen HTTP address without port, example: 127.0.0.1" json:"addr"` Port int `toml:"port" default:"8081" json:"port"` - HeaderXForwardedFor string `toml:"headerXForwardedFor" default:"X-Forwarded-For" json:"header_w_forwarded_for"` + HeaderXForwardedFor string `toml:"headerXForwardedFor" commented:"true" comment:"Forward source addr from given header, let empty to use request addr." default:"X-Forwarded-For" json:"header_w_forwarded_for"` } // HatcheryCommonConfiguration is the base configuration for all hatcheries diff --git a/engine/ui/ui_router.go b/engine/ui/ui_router.go index cc1ca3bab4..f94c3c149b 100644 --- a/engine/ui/ui_router.go +++ b/engine/ui/ui_router.go @@ -75,7 +75,23 @@ func (s *Service) getReverseProxy(ctx context.Context, path, urlRemote string) h origin, _ := url.Parse(urlRemote) director := func(req *http.Request) { reqPath := strings.TrimPrefix(req.URL.Path, path) - req.Header.Add("X-Forwarded-For", req.RemoteAddr) + + var clientIP string + if s.Cfg.HTTP.HeaderXForwardedFor != "" { + // Retrieve the client ip address from the header (X-Forwarded-For by default) + clientIP = req.Header.Get(s.Cfg.HTTP.HeaderXForwardedFor) + } + if clientIP == "" { + // If the header has not been found, fallback on the remote adress from the http request + clientIP = req.RemoteAddr + } + + headerForward := "X-Forwarded-For" + if s.Cfg.HTTP.HeaderXForwardedFor != "" { + headerForward = s.Cfg.HTTP.HeaderXForwardedFor + } + + req.Header.Add(headerForward, clientIP) req.Header.Add("X-Forwarded-Host", req.Host) req.Header.Add("X-Origin-Host", origin.Host) req.URL.Scheme = origin.Scheme