Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): flag to enable service proxy, filter proxy routes #5800

Merged
merged 3 commits into from
Apr 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
richardlt marked this conversation as resolved.
Show resolved Hide resolved
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