diff --git a/cmd/zrok/http_frontend.go b/cmd/zrok/http_frontend.go index d786a604..51691fe2 100644 --- a/cmd/zrok/http_frontend.go +++ b/cmd/zrok/http_frontend.go @@ -3,7 +3,7 @@ package main import ( "fmt" "github.com/michaelquigley/cf" - "github.com/openziti-test-kitchen/zrok/endpoints/frontend" + "github.com/openziti-test-kitchen/zrok/endpoints/public_frontend" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -29,7 +29,7 @@ func newHttpFrontendCommand() *httpFrontendCommand { } func (self *httpFrontendCommand) run(_ *cobra.Command, args []string) { - cfg := frontend.DefaultConfig() + cfg := public_frontend.DefaultConfig() if len(args) == 1 { if err := cfg.Load(args[0]); err != nil { if !panicInstead { @@ -39,7 +39,7 @@ func (self *httpFrontendCommand) run(_ *cobra.Command, args []string) { } } logrus.Infof(cf.Dump(cfg, cf.DefaultOptions())) - httpListener, err := frontend.NewHTTP(cfg) + httpListener, err := public_frontend.NewHTTP(cfg) if err != nil { if !panicInstead { showError("unable to create http frontend", err) diff --git a/endpoints/private_frontend/config.go b/endpoints/private_frontend/config.go new file mode 100644 index 00000000..7b567486 --- /dev/null +++ b/endpoints/private_frontend/config.go @@ -0,0 +1,14 @@ +package private_frontend + +type Config struct { + IdentityName string + SvcName string + Address string +} + +func DefaultConfig(identityName string) *Config { + return &Config{ + IdentityName: identityName, + Address: "0.0.0.0:8080", + } +} diff --git a/endpoints/private_frontend/http.go b/endpoints/private_frontend/http.go new file mode 100644 index 00000000..9b8ec33c --- /dev/null +++ b/endpoints/private_frontend/http.go @@ -0,0 +1,199 @@ +package private_frontend + +import ( + "context" + "fmt" + "github.com/openziti-test-kitchen/zrok/endpoints" + "github.com/openziti-test-kitchen/zrok/endpoints/public_frontend/notfound_ui" + "github.com/openziti-test-kitchen/zrok/model" + "github.com/openziti-test-kitchen/zrok/util" + "github.com/openziti-test-kitchen/zrok/zrokdir" + "github.com/openziti/sdk-golang/ziti" + "github.com/openziti/sdk-golang/ziti/config" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "net" + "net/http" + "net/http/httputil" + "net/url" +) + +type httpFrontend struct { + cfg *Config + zCtx ziti.Context + svcName string + handler http.Handler +} + +func NewHTTP(cfg *Config) (*httpFrontend, error) { + zCfgPath, err := zrokdir.ZitiIdentityFile(cfg.IdentityName) + if err != nil { + return nil, errors.Wrapf(err, "error getting ziti identity '%v' from zrokdir", cfg.IdentityName) + } + zCfg, err := config.NewFromFile(zCfgPath) + if err != nil { + return nil, errors.Wrap(err, "error loading config") + } + zCfg.ConfigTypes = []string{model.ZrokProxyConfig} + zCtx := ziti.NewContextWithConfig(zCfg) + zDialCtx := zitiDialContext{ctx: zCtx, svcName: cfg.SvcName} + zTransport := http.DefaultTransport.(*http.Transport).Clone() + zTransport.DialContext = zDialCtx.Dial + + proxy, err := newServiceProxy(cfg, zCtx) + if err != nil { + return nil, err + } + proxy.Transport = zTransport + + handler := authHandler(cfg.SvcName, util.NewProxyHandler(proxy), "zrok", cfg, zCtx) + return &httpFrontend{ + cfg: cfg, + zCtx: zCtx, + handler: handler, + }, nil +} + +type zitiDialContext struct { + ctx ziti.Context + svcName string +} + +func (zdc *zitiDialContext) Dial(_ context.Context, _ string, addr string) (net.Conn, error) { + conn, err := zdc.ctx.Dial(zdc.svcName) + if err != nil { + return conn, err + } + return conn, nil +} + +func newServiceProxy(cfg *Config, ctx ziti.Context) (*httputil.ReverseProxy, error) { + proxy := serviceTargetProxy(cfg, ctx) + director := proxy.Director + proxy.Director = func(req *http.Request) { + director(req) + req.Header.Set("X-Proxy", "zrok") + } + proxy.ModifyResponse = func(resp *http.Response) error { + return nil + } + proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { + logrus.Errorf("error proxying: %v", err) + notfound_ui.WriteNotFound(w) + } + return proxy, nil +} + +func serviceTargetProxy(cfg *Config, ctx ziti.Context) *httputil.ReverseProxy { + director := func(req *http.Request) { + targetSvc := cfg.SvcName + if svc, found := endpoints.GetRefreshedService(targetSvc, ctx); found { + if cfg, found := svc.Configs[model.ZrokProxyConfig]; found { + logrus.Debugf("auth model: %v", cfg) + } else { + logrus.Warn("no config!") + } + if target, err := url.Parse(fmt.Sprintf("http://%v", targetSvc)); err == nil { + logrus.Infof("[%v] -> %v", targetSvc, req.URL) + + targetQuery := target.RawQuery + req.URL.Scheme = target.Scheme + req.URL.Host = target.Host + req.URL.Path, req.URL.RawPath = endpoints.JoinURLPath(target, req.URL) + if targetQuery == "" || req.URL.RawQuery == "" { + req.URL.RawQuery = targetQuery + req.URL.RawQuery + } else { + req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery + } + if _, ok := req.Header["User-Agent"]; !ok { + // explicitly disable User-Agent so it's not set to default value + req.Header.Set("User-Agent", "") + } + } else { + logrus.Errorf("error proxying: %v", err) + } + } + } + return &httputil.ReverseProxy{Director: director} +} + +func authHandler(svcName string, handler http.Handler, realm string, cfg *Config, ctx ziti.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if svc, found := endpoints.GetRefreshedService(svcName, ctx); found { + if cfg, found := svc.Configs[model.ZrokProxyConfig]; found { + if scheme, found := cfg["auth_scheme"]; found { + switch scheme { + case string(model.None): + logrus.Debugf("auth scheme none '%v'", svcName) + handler.ServeHTTP(w, r) + return + + case string(model.Basic): + logrus.Debugf("auth scheme basic '%v", svcName) + inUser, inPass, ok := r.BasicAuth() + if !ok { + writeUnauthorizedResponse(w, realm) + return + } + authed := false + if v, found := cfg["basic_auth"]; found { + if basicAuth, ok := v.(map[string]interface{}); ok { + if v, found := basicAuth["users"]; found { + if arr, ok := v.([]interface{}); ok { + for _, v := range arr { + if um, ok := v.(map[string]interface{}); ok { + username := "" + if v, found := um["username"]; found { + if un, ok := v.(string); ok { + username = un + } + } + password := "" + if v, found := um["password"]; found { + if pw, ok := v.(string); ok { + password = pw + } + } + if username == inUser && password == inPass { + authed = true + break + } + } + } + } + } + } + } + + if !authed { + writeUnauthorizedResponse(w, realm) + return + } + + handler.ServeHTTP(w, r) + + default: + logrus.Infof("invalid auth scheme '%v'", scheme) + writeUnauthorizedResponse(w, realm) + return + } + } else { + logrus.Warnf("%v -> no auth scheme for '%v'", r.RemoteAddr, svcName) + notfound_ui.WriteNotFound(w) + } + } else { + logrus.Warnf("%v -> no proxy config for '%v'", r.RemoteAddr, svcName) + notfound_ui.WriteNotFound(w) + } + } else { + logrus.Warnf("%v -> service '%v' not found", r.RemoteAddr, svcName) + notfound_ui.WriteNotFound(w) + } + } +} + +func writeUnauthorizedResponse(w http.ResponseWriter, realm string) { + w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`) + w.WriteHeader(401) + w.Write([]byte("No Authorization\n")) +} diff --git a/endpoints/frontend/config.go b/endpoints/public_frontend/config.go similarity index 96% rename from endpoints/frontend/config.go rename to endpoints/public_frontend/config.go index cce61855..b2953cda 100644 --- a/endpoints/frontend/config.go +++ b/endpoints/public_frontend/config.go @@ -1,4 +1,4 @@ -package frontend +package public_frontend import ( "github.com/michaelquigley/cf" diff --git a/endpoints/frontend/health_ui/embed.go b/endpoints/public_frontend/health_ui/embed.go similarity index 100% rename from endpoints/frontend/health_ui/embed.go rename to endpoints/public_frontend/health_ui/embed.go diff --git a/endpoints/frontend/health_ui/handler.go b/endpoints/public_frontend/health_ui/handler.go similarity index 100% rename from endpoints/frontend/health_ui/handler.go rename to endpoints/public_frontend/health_ui/handler.go diff --git a/endpoints/frontend/health_ui/index.html b/endpoints/public_frontend/health_ui/index.html similarity index 100% rename from endpoints/frontend/health_ui/index.html rename to endpoints/public_frontend/health_ui/index.html diff --git a/endpoints/frontend/http.go b/endpoints/public_frontend/http.go similarity index 78% rename from endpoints/frontend/http.go rename to endpoints/public_frontend/http.go index 1a921e6d..b31209cf 100644 --- a/endpoints/frontend/http.go +++ b/endpoints/public_frontend/http.go @@ -1,16 +1,16 @@ -package frontend +package public_frontend import ( "context" "fmt" - "github.com/openziti-test-kitchen/zrok/endpoints/frontend/health_ui" - "github.com/openziti-test-kitchen/zrok/endpoints/frontend/notfound_ui" + "github.com/openziti-test-kitchen/zrok/endpoints" + "github.com/openziti-test-kitchen/zrok/endpoints/public_frontend/health_ui" + "github.com/openziti-test-kitchen/zrok/endpoints/public_frontend/notfound_ui" "github.com/openziti-test-kitchen/zrok/model" "github.com/openziti-test-kitchen/zrok/util" "github.com/openziti-test-kitchen/zrok/zrokdir" "github.com/openziti/sdk-golang/ziti" "github.com/openziti/sdk-golang/ziti/config" - "github.com/openziti/sdk-golang/ziti/edge" "github.com/pkg/errors" "github.com/sirupsen/logrus" "net" @@ -20,14 +20,14 @@ import ( "strings" ) -type httpListen struct { +type httpFrontend struct { cfg *Config zCtx ziti.Context handler http.Handler metrics *metricsAgent } -func NewHTTP(cfg *Config) (*httpListen, error) { +func NewHTTP(cfg *Config) (*httpFrontend, error) { ma, err := newMetricsAgent(cfg) if err != nil { return nil, err @@ -55,7 +55,7 @@ func NewHTTP(cfg *Config) (*httpListen, error) { proxy.Transport = zTransport handler := authHandler(util.NewProxyHandler(proxy), "zrok", cfg, zCtx) - return &httpListen{ + return &httpFrontend{ cfg: cfg, zCtx: zCtx, handler: handler, @@ -63,7 +63,7 @@ func NewHTTP(cfg *Config) (*httpListen, error) { }, nil } -func (self *httpListen) Run() error { +func (self *httpFrontend) Run() error { return http.ListenAndServe(self.cfg.Address, self.handler) } @@ -95,14 +95,13 @@ func newServiceProxy(cfg *Config, ctx ziti.Context) (*httputil.ReverseProxy, err logrus.Errorf("error proxying: %v", err) notfound_ui.WriteNotFound(w) } - return proxy, nil } func hostTargetReverseProxy(cfg *Config, ctx ziti.Context) *httputil.ReverseProxy { director := func(req *http.Request) { targetSvc := resolveService(cfg.HostMatch, req.Host) - if svc, found := getRefreshedService(targetSvc, ctx); found { + if svc, found := endpoints.GetRefreshedService(targetSvc, ctx); found { if cfg, found := svc.Configs[model.ZrokProxyConfig]; found { logrus.Debugf("auth model: %v", cfg) } else { @@ -114,7 +113,7 @@ func hostTargetReverseProxy(cfg *Config, ctx ziti.Context) *httputil.ReverseProx targetQuery := target.RawQuery req.URL.Scheme = target.Scheme req.URL.Host = target.Host - req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL) + req.URL.Path, req.URL.RawPath = endpoints.JoinURLPath(target, req.URL) if targetQuery == "" || req.URL.RawQuery == "" { req.URL.RawQuery = targetQuery + req.URL.RawQuery } else { @@ -132,44 +131,11 @@ func hostTargetReverseProxy(cfg *Config, ctx ziti.Context) *httputil.ReverseProx return &httputil.ReverseProxy{Director: director} } -func joinURLPath(a, b *url.URL) (path, rawpath string) { - if a.RawPath == "" && b.RawPath == "" { - return singleJoiningSlash(a.Path, b.Path), "" - } - // Same as singleJoiningSlash, but uses EscapedPath to determine - // whether a slash should be added - apath := a.EscapedPath() - bpath := b.EscapedPath() - - aslash := strings.HasSuffix(apath, "/") - bslash := strings.HasPrefix(bpath, "/") - - switch { - case aslash && bslash: - return a.Path + b.Path[1:], apath + bpath[1:] - case !aslash && !bslash: - return a.Path + "/" + b.Path, apath + "/" + bpath - } - return a.Path + b.Path, apath + bpath -} - -func singleJoiningSlash(a, b string) string { - aslash := strings.HasSuffix(a, "/") - bslash := strings.HasPrefix(b, "/") - switch { - case aslash && bslash: - return a + b[1:] - case !aslash && !bslash: - return a + "/" + b - } - return a + b -} - func authHandler(handler http.Handler, realm string, cfg *Config, ctx ziti.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { svcName := resolveService(cfg.HostMatch, r.Host) if svcName != "" { - if svc, found := getRefreshedService(svcName, ctx); found { + if svc, found := endpoints.GetRefreshedService(svcName, ctx); found { if cfg, found := svc.Configs[model.ZrokProxyConfig]; found { if scheme, found := cfg["auth_scheme"]; found { switch scheme { @@ -262,15 +228,3 @@ func resolveService(hostMatch string, host string) string { } return "" } - -func getRefreshedService(name string, ctx ziti.Context) (*edge.Service, bool) { - svc, found := ctx.GetService(name) - if !found { - if err := ctx.RefreshServices(); err != nil { - logrus.Errorf("error refreshing services: %v", err) - return nil, false - } - return ctx.GetService(name) - } - return svc, found -} diff --git a/endpoints/frontend/metrics.go b/endpoints/public_frontend/metrics.go similarity index 99% rename from endpoints/frontend/metrics.go rename to endpoints/public_frontend/metrics.go index f8748384..166ca825 100644 --- a/endpoints/frontend/metrics.go +++ b/endpoints/public_frontend/metrics.go @@ -1,4 +1,4 @@ -package frontend +package public_frontend import ( "github.com/openziti-test-kitchen/zrok/model" diff --git a/endpoints/frontend/metricsconn.go b/endpoints/public_frontend/metricsconn.go similarity index 97% rename from endpoints/frontend/metricsconn.go rename to endpoints/public_frontend/metricsconn.go index 5664fc78..49200f41 100644 --- a/endpoints/frontend/metricsconn.go +++ b/endpoints/public_frontend/metricsconn.go @@ -1,4 +1,4 @@ -package frontend +package public_frontend import ( "net" diff --git a/endpoints/frontend/notfound_ui/embed.go b/endpoints/public_frontend/notfound_ui/embed.go similarity index 100% rename from endpoints/frontend/notfound_ui/embed.go rename to endpoints/public_frontend/notfound_ui/embed.go diff --git a/endpoints/frontend/notfound_ui/handler.go b/endpoints/public_frontend/notfound_ui/handler.go similarity index 100% rename from endpoints/frontend/notfound_ui/handler.go rename to endpoints/public_frontend/notfound_ui/handler.go diff --git a/endpoints/frontend/notfound_ui/index.html b/endpoints/public_frontend/notfound_ui/index.html similarity index 100% rename from endpoints/frontend/notfound_ui/index.html rename to endpoints/public_frontend/notfound_ui/index.html diff --git a/endpoints/util.go b/endpoints/util.go new file mode 100644 index 00000000..ede4befe --- /dev/null +++ b/endpoints/util.go @@ -0,0 +1,54 @@ +package endpoints + +import ( + "github.com/openziti/sdk-golang/ziti" + "github.com/openziti/sdk-golang/ziti/edge" + "github.com/sirupsen/logrus" + "net/url" + "strings" +) + +func GetRefreshedService(name string, ctx ziti.Context) (*edge.Service, bool) { + svc, found := ctx.GetService(name) + if !found { + if err := ctx.RefreshServices(); err != nil { + logrus.Errorf("error refreshing services: %v", err) + return nil, false + } + return ctx.GetService(name) + } + return svc, found +} + +func JoinURLPath(a, b *url.URL) (path, rawpath string) { + if a.RawPath == "" && b.RawPath == "" { + return singleJoiningSlash(a.Path, b.Path), "" + } + // Same as singleJoiningSlash, but uses EscapedPath to determine + // whether a slash should be added + apath := a.EscapedPath() + bpath := b.EscapedPath() + + aslash := strings.HasSuffix(apath, "/") + bslash := strings.HasPrefix(bpath, "/") + + switch { + case aslash && bslash: + return a.Path + b.Path[1:], apath + bpath[1:] + case !aslash && !bslash: + return a.Path + "/" + b.Path, apath + "/" + bpath + } + return a.Path + b.Path, apath + bpath +} + +func singleJoiningSlash(a, b string) string { + aslash := strings.HasSuffix(a, "/") + bslash := strings.HasPrefix(b, "/") + switch { + case aslash && bslash: + return a + b[1:] + case !aslash && !bslash: + return a + "/" + b + } + return a + b +}