Skip to content

Commit

Permalink
feat(ui): flag to enable service proxy, filter proxy routes (#5800)
Browse files Browse the repository at this point in the history
  • Loading branch information
richardlt authored Apr 27, 2021
1 parent c72c813 commit 5222d9c
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 40 deletions.
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 10 additions & 4 deletions engine/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion engine/service/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 11 additions & 10 deletions engine/ui/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
4 changes: 3 additions & 1 deletion engine/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,10 @@ func (s *Service) indexHTMLReplaceVar() error {
return sdk.WrapError(err, "cannot parse base href regex")
}
indexContent := regexBaseHref.ReplaceAllString(string(read), "<base href=\""+s.Cfg.BaseURL+"\">")
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)
}

Expand Down
102 changes: 79 additions & 23 deletions engine/ui/ui_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,41 +29,97 @@ 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
r.Mux.PathPrefix("/docs").Handler(s.uiServe(http.Dir(s.DocsDir), s.DocsDir))
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

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
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 {
Expand Down
1 change: 0 additions & 1 deletion sdk/cdsclient/client_cdn.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit 5222d9c

Please sign in to comment.