diff --git a/controller/xweb/api.go b/controller/xweb/api.go new file mode 100644 index 000000000..d07933430 --- /dev/null +++ b/controller/xweb/api.go @@ -0,0 +1,71 @@ +/* + Copyright NetFoundry, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package xweb + +import "github.com/pkg/errors" + +// API represents some "api" or "site" by binding name. Each API configuration is used against a WebHandlerFactoryRegistry +// to locate the proper factory to generate a WebHandler. The options provided by this structure are parsed by the +// WebHandlerFactory and the behavior, valid keys, and valid values are not defined by xweb components, but by that +// WebHandlerFactory and it resulting WebHandler's. +type API struct { + binding string + options map[interface{}]interface{} +} + +// Binding returns the string that uniquely identifies bo the WebHandlerFactory and resulting WebHandler's that will be attached +// to some WebListener and its resulting Server. +func (api *API) Binding() string { + return api.binding +} + +// Options returns the options associated with thsi API binding. +func (api *API) Options() map[interface{}]interface{} { + return api.options +} + +// Parse the configuration map for an API. +func (api *API) Parse(apiConfigMap map[interface{}]interface{}) error { + if bindingInterface, ok := apiConfigMap["binding"]; ok { + if binding, ok := bindingInterface.(string); ok { + api.binding = binding + } else { + return errors.New("binding must be a string") + } + } else { + return errors.New("binding is required") + } + + if optionsInterface, ok := apiConfigMap["options"]; ok { + if optionsMap, ok := optionsInterface.(map[interface{}]interface{}); ok { + api.options = optionsMap //leave to bindings to interpret further + } else { + return errors.New("options if declared must be a map") + } + } //no else optional + + return nil +} + +// Validate this configuration object. +func (api *API) Validate() error { + if api.Binding() == "" { + return errors.New("binding must be specified") + } + + return nil +} diff --git a/controller/xweb/bindpoint.go b/controller/xweb/bindpoint.go new file mode 100644 index 000000000..52835542d --- /dev/null +++ b/controller/xweb/bindpoint.go @@ -0,0 +1,63 @@ +/* + Copyright NetFoundry, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package xweb + +import ( + "fmt" + "github.com/pkg/errors" +) + +// BindPoint represents the interface:port address of where a http.Server should listen for a WebListener and the public +// address that should be used to address it. +type BindPoint struct { + InterfaceAddress string // : + Address string //: +} + +// Parse the configuration map for a BindPoint. +func (bindPoint *BindPoint) Parse(config map[interface{}]interface{}) error { + if interfaceVal, ok := config["interface"]; ok { + if address, ok := interfaceVal.(string); ok { + bindPoint.InterfaceAddress = address + } else { + return fmt.Errorf("could not use value for address, not a string") + } + } + + if interfaceVal, ok := config["address"]; ok { + if address, ok := interfaceVal.(string); ok { + bindPoint.Address = address + } else { + return errors.New("could not use value for address, not a string") + } + } + + return nil +} + +// Validate this configuration object. +func (bindPoint *BindPoint) Validate() error { + if bindPoint.InterfaceAddress == "" { + return errors.New("value for address must be provided") + } + + if bindPoint.Address == "" { + return errors.New("value for address must be provided") + } + + return nil +} diff --git a/controller/xweb/config.go b/controller/xweb/config.go index 570e66ec9..bf1f82831 100644 --- a/controller/xweb/config.go +++ b/controller/xweb/config.go @@ -26,17 +26,21 @@ import ( // Config is the root configuration options necessary to start numerous http.Server instances via WebListener's. type Config struct { - WebListeners []*WebListener - DefaultIdentityConfig *identity.IdentityConfig - DefaultIdentity identity.Identity + SourceConfig map[interface{}]interface{} + WebListeners []*WebListener + WebSection string + + DefaultIdentityConfig *identity.IdentityConfig + DefaultIdentity identity.Identity DefaultIdentitySection string - WebSection string - enabled bool + + enabled bool } // Parse parses a configuration map, looking for sections that define an identity.IdentityConfig and an array of WebListener's. func (config *Config) Parse(configMap map[interface{}]interface{}) error { + config.SourceConfig = configMap if config.DefaultIdentitySection == "" { return errors.New("identity section not specified for configuration") @@ -103,11 +107,23 @@ func (config *Config) Validate(registry WebHandlerFactoryRegistry) error { webListener.DefaultIdentity = config.DefaultIdentity } - for i, web := range config.WebListeners { + presentApis := map[string]WebHandlerFactory{} + + for i, webListener := range config.WebListeners { //validate attributes - if err := web.Validate(registry); err != nil { + if err := webListener.Validate(registry); err != nil { return fmt.Errorf("could not validate web listener at %s[%d]: %v", config.WebSection, i, err) } + + for _, api := range webListener.APIs { + presentApis[api.Binding()] = registry.Get(api.Binding()) + } + } + + for presentApiBinding, presentApiFactory := range presentApis { + if err := presentApiFactory.Validate(config); err != nil { + return fmt.Errorf("error validating API binding %s: %v", presentApiBinding, err) + } } //enabled only after validation passes @@ -122,262 +138,6 @@ func (config *Config) Enabled() bool { return config.enabled } -// WebListener is the configuration that will eventually be used to create an xweb.Server (which in turn houses all -// of the components necessary to run multiple http.Server's). -type WebListener struct { - Name string - APIs []*API - BindPoints []*BindPoint - Options Options - - IdentityConfig *identity.IdentityConfig - Identity identity.Identity - - DefaultIdentityConfig *identity.IdentityConfig - DefaultIdentity identity.Identity -} - -// Parse parses a configuration map to set all relevant WebListener values. -func (web *WebListener) Parse(webConfigMap map[interface{}]interface{}) error { - //parse name, required, string - if nameInterface, ok := webConfigMap["name"]; ok { - if name, ok := nameInterface.(string); ok { - web.Name = name - } else { - return errors.New("name is required to be a string") - } - } else { - return errors.New("name is required") - } - - //parse apis, require 1, objet, defer - if apiInterface, ok := webConfigMap["apis"]; ok { - if apiArrayInterfaces, ok := apiInterface.([]interface{}); ok { - for i, apiInterface := range apiArrayInterfaces { - if apiMap, ok := apiInterface.(map[interface{}]interface{}); ok { - api := &API{} - if err := api.Parse(apiMap); err != nil { - return fmt.Errorf("error parsing api configuration at index [%d]: %v", i, err) - } - - web.APIs = append(web.APIs, api) - } else { - return fmt.Errorf("error parsing api configuration at index [%d]: not a map", i) - } - } - } else { - return errors.New("api section must be an array") - } - } else { - return errors.New("apis section is required") - } - - //parse listen address - if addressInterface, ok := webConfigMap["bindPoints"]; ok { - if addressesArrayInterfaces, ok := addressInterface.([]interface{}); ok { - for i, addressInterface := range addressesArrayInterfaces { - if addressMap, ok := addressInterface.(map[interface{}]interface{}); ok { - address := &BindPoint{} - if err := address.Parse(addressMap); err != nil { - return fmt.Errorf("error parsing address configuration at index [%d]: %v", i, err) - } - - web.BindPoints = append(web.BindPoints, address) - } else { - return fmt.Errorf("error parsing address configuration at index [%d]: not a map", i) - } - } - } else { - return errors.New("addresses section must be an array") - } - } else { - return errors.New("addresses section is required") - } - - //parse identity - if identityInterface, ok := webConfigMap["identity"]; ok { - if identityMap, ok := identityInterface.(map[interface{}]interface{}); ok { - if identityConfig, err := parseIdentityConfig(identityMap); err == nil { - web.IdentityConfig = identityConfig - } else { - return fmt.Errorf("error parsing identity section: %v", err) - } - - } else { - return errors.New("identity section must be a map if defined") - } - - } //no else, optional, will defer to router identity - - //parse options - web.Options = Options{} - web.Options.Default() - - if optionsInterface, ok := webConfigMap["options"]; ok { - if optionMap, ok := optionsInterface.(map[interface{}]interface{}); ok { - if err := web.Options.Parse(optionMap); err != nil { - return fmt.Errorf("error parsing options section: %v", err) - } - } //no else, options are optional - } - - return nil -} - -// Validate all WebListener values -func (web *WebListener) Validate(registry WebHandlerFactoryRegistry) error { - if web.Name == "" { - return errors.New("name must not be empty") - } - - if len(web.APIs) <= 0 { - return errors.New("no APIs specified, must specify at least one") - } - - for i, api := range web.APIs { - if err := api.Validate(); err != nil { - return fmt.Errorf("invalid API at index [%d]: %v", i, err) - } - - //check if binding is valid - if binding := registry.Get(api.Binding()); binding == nil { - return fmt.Errorf("invalid API at index [%d]: invalid binding %s", i, api.Binding()) - } - } - - if len(web.BindPoints) <= 0 { - return errors.New("no addresses specified, must specify at lest one") - } - - for i, address := range web.BindPoints { - if err := address.Validate(); err != nil { - return fmt.Errorf("invalid address at index [%d]: %v", i, err) - } - } - - //default identity config - if web.IdentityConfig == nil { - web.IdentityConfig = web.DefaultIdentityConfig - web.Identity = web.DefaultIdentity - } - - if web.Identity == nil { - if web.IdentityConfig == nil { - return errors.New("no identity specified") - } - - if id, err := identity.LoadIdentity(*web.IdentityConfig); err == nil { - web.Identity = id - } else { - return fmt.Errorf("failed to load identity: %v", err) - } - } - - if err := web.Options.TlsVersionOptions.Validate(); err != nil { - return fmt.Errorf("invalid TLS version option: %v", err) - } - - if err := web.Options.TimeoutOptions.Validate(); err != nil { - return fmt.Errorf("invalid timeout option: %v", err) - } - - return nil - -} - -// API represents some "api" or "site" by binding name. Each API configuration is used against a WebHandlerFactoryRegistry -// to locate the proper factory to generate a WebHandler. The options provided by this structure are parsed by the -// WebHandlerFactory and the behavior, valid keys, and valid values are not defined by xweb components, but by that -// WebHandlerFactory and it resulting WebHandler's. -type API struct { - binding string - options map[interface{}]interface{} -} - -// Binding returns the string that uniquely identifies bo the WebHandlerFactory and resulting WebHandler's that will be attached -// to some WebListener and its resulting Server. -func (api *API) Binding() string { - return api.binding -} - -// Options returns the options associated with thsi API binding. -func (api *API) Options() map[interface{}]interface{} { - return api.options -} - -// Parse the configuration map for an API. -func (api *API) Parse(apiConfigMap map[interface{}]interface{}) error { - if bindingInterface, ok := apiConfigMap["binding"]; ok { - if binding, ok := bindingInterface.(string); ok { - api.binding = binding - } else { - return errors.New("binding must be a string") - } - } else { - return errors.New("binding is required") - } - - if optionsInterface, ok := apiConfigMap["options"]; ok { - if optionsMap, ok := optionsInterface.(map[interface{}]interface{}); ok { - api.options = optionsMap //leave to bindings to interpret further - } else { - return errors.New("options if declared must be a map") - } - } //no else optional - - return nil -} - -// Validate this configuration object. -func (api *API) Validate() error { - if api.Binding() == "" { - return errors.New("binding must be specified") - } - - return nil -} - -// BindPoint represents the interface:port address of where a http.Server should listen for a WebListener and the public -// address that should be used to address it. -type BindPoint struct { - InterfaceAddress string // : - Address string //: -} - -// Parse the configuration map for a BindPoint. -func (bindPoint *BindPoint) Parse(config map[interface{}]interface{}) error { - if interfaceVal, ok := config["interface"]; ok { - if address, ok := interfaceVal.(string); ok { - bindPoint.InterfaceAddress = address - } else { - return fmt.Errorf("could not use value for address, not a string") - } - } - - if interfaceVal, ok := config["address"]; ok { - if address, ok := interfaceVal.(string); ok { - bindPoint.Address = address - } else { - return errors.New("could not use value for address, not a string") - } - } - - return nil -} - -// Validate this configuration object. -func (bindPoint *BindPoint) Validate() error { - if bindPoint.InterfaceAddress == "" { - return errors.New("value for address must be provided") - } - - if bindPoint.Address == "" { - return errors.New("value for address must be provided") - } - - return nil -} - // Options is the shared options for a WebListener. type Options struct { TimeoutOptions @@ -419,27 +179,39 @@ func (timeoutOptions *TimeoutOptions) Default() { // Parse parses a config map func (timeoutOptions *TimeoutOptions) Parse(config map[interface{}]interface{}) error { - if interfaceVal, ok := config["readTimeoutMs"]; ok { - if readTimeoutMs, ok := interfaceVal.(int); ok { - timeoutOptions.ReadTimeout = time.Duration(readTimeoutMs) * time.Millisecond + if interfaceVal, ok := config["readTimeout"]; ok { + if readTimeoutStr, ok := interfaceVal.(string); ok { + if readTimeout, err := time.ParseDuration(readTimeoutStr); err == nil { + timeoutOptions.ReadTimeout = readTimeout + } else { + return fmt.Errorf("could not parse readTimeout %s as a duration (e.g. 1m): %v", readTimeoutStr, err) + } } else { - return errors.New("could not use value for readTimeoutMs, not an integer") + return errors.New("could not use value for readTimeout, not a string") } } - if interfaceVal, ok := config["idleTimeoutMs"]; ok { - if idleTimeoutMs, ok := interfaceVal.(int); ok { - timeoutOptions.IdleTimeout = time.Duration(idleTimeoutMs) * time.Millisecond + if interfaceVal, ok := config["idleTimeout"]; ok { + if idleTimeoutStr, ok := interfaceVal.(string); ok { + if idleTimeout, err := time.ParseDuration(idleTimeoutStr); err == nil { + timeoutOptions.IdleTimeout = idleTimeout + } else { + return fmt.Errorf("could not parse idleTimeout %s as a duration (e.g. 1m): %v", idleTimeoutStr, err) + } } else { - return errors.New("could not use value for idleTimeoutMs, not an integer") + return errors.New("could not use value for idleTimeout, not a string") } } - if interfaceVal, ok := config["writeTimeoutMs"]; ok { - if writeTimeoutMs, ok := interfaceVal.(int); ok { - timeoutOptions.WriteTimeout = time.Duration(writeTimeoutMs) * time.Millisecond + if interfaceVal, ok := config["writeTimeout"]; ok { + if writeTimeoutStr, ok := interfaceVal.(string); ok { + if writeTimeout, err := time.ParseDuration(writeTimeoutStr); err == nil { + timeoutOptions.WriteTimeout = writeTimeout + } else { + return fmt.Errorf("could not parse writeTimeout %s as a duration (e.g. 1m): %v", writeTimeoutStr, err) + } } else { - return errors.New("could not use value for writeTimeoutMs, not an integer") + return errors.New("could not use value for writeTimeout, not a string") } } diff --git a/controller/xweb/contextutil.go b/controller/xweb/contextutil.go index 1a15c717f..2277fdf8b 100644 --- a/controller/xweb/contextutil.go +++ b/controller/xweb/contextutil.go @@ -18,9 +18,9 @@ package xweb import "context" -// WebHandlerFromContext us a utility function to retrieve a WebHandler reference, that the demux http.Handler +// WebHandlerFromRequestContext us a utility function to retrieve a WebHandler reference, that the demux http.Handler // deferred to, during downstream http.Handler processing from the http.Request context. -func WebHandlerFromContext(ctx context.Context) *WebHandler { +func WebHandlerFromRequestContext(ctx context.Context) *WebHandler { if val := ctx.Value(WebHandlerContextKey); val != nil { if handler, ok := val.(*WebHandler); ok { return handler @@ -29,13 +29,13 @@ func WebHandlerFromContext(ctx context.Context) *WebHandler { return nil } -// ListenAddressFromContext is a utility function to retrieve a BindPoint reference from the http.Request context -// that indicates what interface and port combo the incoming http.Request was handled on. -func ListenAddressFromContext(ctx context.Context) *BindPoint { - if val := ctx.Value(BindPointContextKey); val != nil { - if handler, ok := val.(*BindPoint); ok { - return handler +// WebContextFromRequestContext is a utility function to retrieve a *XWebContext reference from the http.Request +// that provides access to XWeb configuration like BindPoint, WebListener, and Config values. +func WebContextFromRequestContext(ctx context.Context) *XWebContext { + if val := ctx.Value(WebContextKey); val != nil { + if webContext, ok := val.(*XWebContext); ok { + return webContext } } return nil -} \ No newline at end of file +} diff --git a/controller/xweb/registry.go b/controller/xweb/registry.go index d229258fb..e46bbc197 100644 --- a/controller/xweb/registry.go +++ b/controller/xweb/registry.go @@ -78,6 +78,14 @@ type WebHandler interface { // instances based on need. This interface allows WebHandler logic to be reused across multiple xweb.Server's while // delegating the instance management to the factory. type WebHandlerFactory interface { + // Binding returns the string binding value used in configurations Binding() string + + // New creates a new instances of this factories API New(webListener *WebListener, options map[interface{}]interface{}) (WebHandler, error) + + // Validate is used for factory level configuration checks. It is not meant for individual instance validation. + // It is meant for validating configuration that all instances generated by a factory share (i.e. sanity across + // instances or configuration from another section). + Validate(config *Config) error } diff --git a/controller/xweb/server.go b/controller/xweb/server.go index bcd7bc8ef..5763155d0 100644 --- a/controller/xweb/server.go +++ b/controller/xweb/server.go @@ -20,41 +20,61 @@ import ( "context" "crypto/tls" "fmt" - "github.com/gorilla/handlers" "github.com/michaelquigley/pfxlog" "github.com/openziti/foundation/util/debugz" "io" "log" + "net" "net/http" ) type ContextKey string const ( - WebHandlerContextKey = ContextKey("XWebHandlerContextKey") - BindPointContextKey = ContextKey("XWebBindPointContextKey") + WebHandlerContextKey = ContextKey("XWebHandlerContextKey") + WebContextKey = ContextKey("XWebContext") ) +type XWebContext struct { + BindPoint *BindPoint + WebListener *WebListener + XWebConfig *Config +} + type namedHttpServer struct { *http.Server - ApiBindingList []string - WebListenerName string + ApiBindingList []string + BindPoint *BindPoint + WebListener *WebListener + XWebConfig *Config +} + +func (s namedHttpServer) NewBaseContext(_ net.Listener) context.Context { + xwebContext := &XWebContext{ + BindPoint: s.BindPoint, + WebListener: s.WebListener, + XWebConfig: s.XWebConfig, + } + ctx := context.Background() + ctx = context.WithValue(ctx, WebContextKey, xwebContext) + + return ctx } // Server represents all of the http.Server's and http.Handler's necessary to run a single xweb.WebListener type Server struct { - httpServers []*namedHttpServer - corsOptions []handlers.CORSOption - logWriter *io.PipeWriter - options *Options - config interface{} - Handle http.Handler - OnHandlerPanic func(writer http.ResponseWriter, request *http.Request, panicVal interface{}) + httpServers []*namedHttpServer + logWriter *io.PipeWriter + options *Options + config interface{} + Handle http.Handler + OnHandlerPanic func(writer http.ResponseWriter, request *http.Request, panicVal interface{}) + ParentWebListener *WebListener } // NewServer creates a new xweb.Server from an xweb.WebListener. All necessary http.Handler's will be created from the supplied // DemuxFactory and WebHandlerFactoryRegistry. -func NewServer(webListener *WebListener, demuxFactory DemuxFactory, handlerFactoryRegistry WebHandlerFactoryRegistry) (*Server, error) { +func NewServer(webListener *WebListener, demuxFactory DemuxFactory, handlerFactoryRegistry WebHandlerFactoryRegistry, config *Config) (*Server, error) { logWriter := pfxlog.Logger().Writer() tlsConfig := webListener.Identity.ServerTLSConfig() @@ -64,9 +84,10 @@ func NewServer(webListener *WebListener, demuxFactory DemuxFactory, handlerFacto tlsConfig.MaxVersion = uint16(webListener.Options.MaxTLSVersion) server := &Server{ - logWriter: logWriter, - config: &webListener, - httpServers: []*namedHttpServer{}, + logWriter: logWriter, + config: &webListener, + httpServers: []*namedHttpServer{}, + ParentWebListener: webListener, } var webHandlers []WebHandler @@ -93,33 +114,31 @@ func NewServer(webListener *WebListener, demuxFactory DemuxFactory, handlerFacto for _, bindPoint := range webListener.BindPoints { namedServer := &namedHttpServer{ - ApiBindingList: apiBindingList, - WebListenerName: webListener.Name, + ApiBindingList: apiBindingList, + WebListener: webListener, + BindPoint: bindPoint, + XWebConfig: config, Server: &http.Server{ Addr: bindPoint.InterfaceAddress, WriteTimeout: webListener.Options.WriteTimeout, ReadTimeout: webListener.Options.ReadTimeout, IdleTimeout: webListener.Options.WriteTimeout, - Handler: server.wrap(demuxWebHandler, bindPoint), + Handler: server.wrapPanicRecovery(demuxWebHandler), TLSConfig: tlsConfig, ErrorLog: log.New(logWriter, "", 0), }, } + + namedServer.BaseContext = namedServer.NewBaseContext + server.httpServers = append(server.httpServers, namedServer) } return server, nil } -// Wrap a http.Handler with another http.Handler that ensures the BindPoint information is -// embedded in the http.Request, provides CORS support, and panic recovery. -func (server *Server) wrap(handler http.Handler, bindPoint *BindPoint) http.Handler { - - //todo: move all cors functionality external to xweb - if server.corsOptions != nil { - corsHandler := handlers.CORS(server.corsOptions...) - handler = corsHandler(handler) - } +// wrapPanicRecovery wraps a http.Handler with another http.Handler that provides recovery. +func (server *Server) wrapPanicRecovery(handler http.Handler, ) http.Handler { wrappedHandler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { defer func() { @@ -132,10 +151,7 @@ func (server *Server) wrap(handler http.Handler, bindPoint *BindPoint) http.Hand } }() - //store the BindPoint on the request context, useful for logging and responses with server addresses - ctx := context.WithValue(request.Context(), BindPointContextKey, &bindPoint) - newRequest := request.WithContext(ctx) - handler.ServeHTTP(writer, newRequest) + handler.ServeHTTP(writer, request) }) return wrappedHandler @@ -146,7 +162,7 @@ func (server *Server) Start() error { logger := pfxlog.Logger() for _, httpServer := range server.httpServers { - logger.Infof("starting API to listen and serve tls on %s for web listener %s with APIs: %v", httpServer.Addr, httpServer.WebListenerName, httpServer.ApiBindingList) + logger.Infof("starting API to listen and serve tls on %s for web listener %s with APIs: %v", httpServer.Addr, httpServer.WebListener.Name, httpServer.ApiBindingList) err := httpServer.ListenAndServeTLS("", "") if err != http.ErrServerClosed { diff --git a/controller/xweb/web.go b/controller/xweb/web.go index d16e869e9..4b2ed3b38 100644 --- a/controller/xweb/web.go +++ b/controller/xweb/web.go @@ -39,6 +39,11 @@ to the correct xweb.WebHandler. The responsibility is handled by another configu "xweb demux handler". This handler's responsibility is to inspect incoming requests and forward them to the correct xweb.WebHandler. It is specified by an xweb.DemuxFactory and a reference implementation, xweb.PathPrefixDemuxFactory has been provided. + +Another way to say it: each Xweb defines a configuration section (default `web`) to define WebListener's and their +hosted APIs. Each WebListener maps to one http.Server per BindPoint. No two WebListeners can have colliding BindPoint's +due to port conflicts. + */ package xweb @@ -104,7 +109,7 @@ func (xwebimpl *XwebImpl) LoadConfig(cfgmap map[interface{}]interface{}) error { // Run starts the necessary xweb.Server's func (xwebimpl *XwebImpl) Run() { for _, webListener := range xwebimpl.Config.WebListeners { - server, err := NewServer(webListener, xwebimpl.DemuxFactory, xwebimpl.Registry) + server, err := NewServer(webListener, xwebimpl.DemuxFactory, xwebimpl.Registry, xwebimpl.Config) if err != nil { pfxlog.Logger().Fatalf("error starting xweb server for %s: %v", webListener.Name, err) diff --git a/controller/xweb/weblistener.go b/controller/xweb/weblistener.go new file mode 100644 index 000000000..92f406ca0 --- /dev/null +++ b/controller/xweb/weblistener.go @@ -0,0 +1,186 @@ +/* + Copyright NetFoundry, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package xweb + +import ( + "fmt" + "github.com/openziti/foundation/identity/identity" + "github.com/pkg/errors" +) + +// WebListener is the configuration that will eventually be used to create an xweb.Server (which in turn houses all +// of the components necessary to run multiple http.Server's). +type WebListener struct { + Name string + APIs []*API + BindPoints []*BindPoint + Options Options + + IdentityConfig *identity.IdentityConfig + Identity identity.Identity + + DefaultIdentityConfig *identity.IdentityConfig + DefaultIdentity identity.Identity +} + +// Parse parses a configuration map to set all relevant WebListener values. +func (web *WebListener) Parse(webConfigMap map[interface{}]interface{}) error { + //parse name, required, string + if nameInterface, ok := webConfigMap["name"]; ok { + if name, ok := nameInterface.(string); ok { + web.Name = name + } else { + return errors.New("name is required to be a string") + } + } else { + return errors.New("name is required") + } + + //parse apis, require 1, objet, defer + if apiInterface, ok := webConfigMap["apis"]; ok { + if apiArrayInterfaces, ok := apiInterface.([]interface{}); ok { + for i, apiInterface := range apiArrayInterfaces { + if apiMap, ok := apiInterface.(map[interface{}]interface{}); ok { + api := &API{} + if err := api.Parse(apiMap); err != nil { + return fmt.Errorf("error parsing api configuration at index [%d]: %v", i, err) + } + + web.APIs = append(web.APIs, api) + } else { + return fmt.Errorf("error parsing api configuration at index [%d]: not a map", i) + } + } + } else { + return errors.New("api section must be an array") + } + } else { + return errors.New("apis section is required") + } + + //parse listen address + if addressInterface, ok := webConfigMap["bindPoints"]; ok { + if addressesArrayInterfaces, ok := addressInterface.([]interface{}); ok { + for i, addressInterface := range addressesArrayInterfaces { + if addressMap, ok := addressInterface.(map[interface{}]interface{}); ok { + address := &BindPoint{} + if err := address.Parse(addressMap); err != nil { + return fmt.Errorf("error parsing address configuration at index [%d]: %v", i, err) + } + + web.BindPoints = append(web.BindPoints, address) + } else { + return fmt.Errorf("error parsing address configuration at index [%d]: not a map", i) + } + } + } else { + return errors.New("addresses section must be an array") + } + } else { + return errors.New("addresses section is required") + } + + //parse identity + if identityInterface, ok := webConfigMap["identity"]; ok { + if identityMap, ok := identityInterface.(map[interface{}]interface{}); ok { + if identityConfig, err := parseIdentityConfig(identityMap); err == nil { + web.IdentityConfig = identityConfig + } else { + return fmt.Errorf("error parsing identity section: %v", err) + } + + } else { + return errors.New("identity section must be a map if defined") + } + + } //no else, optional, will defer to router identity + + //parse options + web.Options = Options{} + web.Options.Default() + + if optionsInterface, ok := webConfigMap["options"]; ok { + if optionMap, ok := optionsInterface.(map[interface{}]interface{}); ok { + if err := web.Options.Parse(optionMap); err != nil { + return fmt.Errorf("error parsing options section: %v", err) + } + } //no else, options are optional + } + + return nil +} + +// Validate all WebListener values +func (web *WebListener) Validate(registry WebHandlerFactoryRegistry) error { + if web.Name == "" { + return errors.New("name must not be empty") + } + + if len(web.APIs) <= 0 { + return errors.New("no APIs specified, must specify at least one") + } + + for i, api := range web.APIs { + if err := api.Validate(); err != nil { + return fmt.Errorf("invalid API at index [%d]: %v", i, err) + } + + //check if binding is valid + if binding := registry.Get(api.Binding()); binding == nil { + return fmt.Errorf("invalid API at index [%d]: invalid binding %s", i, api.Binding()) + } + } + + if len(web.BindPoints) <= 0 { + return errors.New("no addresses specified, must specify at lest one") + } + + for i, address := range web.BindPoints { + if err := address.Validate(); err != nil { + return fmt.Errorf("invalid address at index [%d]: %v", i, err) + } + } + + //default identity config + if web.IdentityConfig == nil { + web.IdentityConfig = web.DefaultIdentityConfig + web.Identity = web.DefaultIdentity + } + + if web.Identity == nil { + if web.IdentityConfig == nil { + return errors.New("no identity specified") + } + + if id, err := identity.LoadIdentity(*web.IdentityConfig); err == nil { + web.Identity = id + } else { + return fmt.Errorf("failed to load identity: %v", err) + } + } + + if err := web.Options.TlsVersionOptions.Validate(); err != nil { + return fmt.Errorf("invalid TLS version option: %v", err) + } + + if err := web.Options.TimeoutOptions.Validate(); err != nil { + return fmt.Errorf("invalid timeout option: %v", err) + } + + return nil + +}