Merge pull request openziti/fabric#236 from openziti/xweb.extend.validation

adds xweb share config validation points, time.ParseDuration
This commit is contained in:
Andrew
2021-05-06 15:34:48 -04:00
committed by GitHub
8 changed files with 439 additions and 318 deletions
+71
View File
@@ -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
}
+63
View File
@@ -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 // <interface>:<port>
Address string //<ip/host>:<port>
}
// 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
}
+47 -275
View File
@@ -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 // <interface>:<port>
Address string //<ip/host>:<port>
}
// 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")
}
}
+9 -9
View File
@@ -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
}
}
+8
View File
@@ -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
}
+49 -33
View File
@@ -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 {
+6 -1
View File
@@ -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)
+186
View File
@@ -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
}