mirror of
https://github.com/openziti/ziti.git
synced 2026-10-13 15:51:21 +00:00
Merge pull request openziti/fabric#236 from openziti/xweb.extend.validation
adds xweb share config validation points, time.ParseDuration
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user