Skip to content

Commit

Permalink
Add hcl scheme validation for 'inline' configurations #59
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcel Ludwig committed Nov 24, 2020
1 parent 0b4dd6c commit 191c3d9
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 160 deletions.
9 changes: 6 additions & 3 deletions config/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ func (b Backend) Schema(inline bool) *hcl.BodySchema {
}

type Inline struct {
Origin string `hcl:"origin,optional"`
Hostname string `hcl:"hostname,optional"`
Path string `hcl:"path,optional"`
Origin string `hcl:"origin,optional"`
Hostname string `hcl:"hostname,optional"`
Path string `hcl:"path,optional"`
RequestHeaders map[string]string `hcl:"request_headers,optional"`
ResponseHeaders map[string]string `hcl:"response_headers,optional"`
}

schema, _ = gohcl.ImpliedBodySchema(&Inline{})
return schema
}
Expand Down
19 changes: 0 additions & 19 deletions config/definitions.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,7 @@
package config

import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
)

type Definitions struct {
Backend []*Backend `hcl:"backend,block"`
BasicAuth []*BasicAuth `hcl:"basic_auth,block"`
JWT []*JWT `hcl:"jwt,block"`
}

func (d Definitions) Schema(inline bool) *hcl.BodySchema {
schema, _ := gohcl.ImpliedBodySchema(d)
if !inline {
return schema
}
// backend, remove label for inline usage
for i, block := range schema.Blocks {
if block.Type == "backend" && len(block.LabelNames) > 0 {
schema.Blocks[i].LabelNames = nil
}
}
return schema
}
15 changes: 13 additions & 2 deletions config/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,21 @@ func (e Endpoint) Schema(inline bool) *hcl.BodySchema {
}

type Inline struct {
Path string `hcl:"path,optional"`
Backend *Backend `hcl:"backend,block"`
Path string `hcl:"path,optional"`
}

schema, _ := gohcl.ImpliedBodySchema(&Inline{})
for i, block := range schema.Blocks {
// inline backend block MAY have no label
if block.Type == "backend" && len(block.LabelNames) > 0 {
schema.Blocks[i].LabelNames = nil
}
}

// The endpoint contains a backend reference, backend block is not allowed.
if e.Backend != "" {
schema.Blocks = nil
}

return schema
}
140 changes: 23 additions & 117 deletions config/runtime/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ import (
"io/ioutil"
"net"
"net/http"
url2 "net/url"
"os"
"path"
"regexp"
"strconv"
"strings"

Expand Down Expand Up @@ -38,13 +36,6 @@ var (
errorMissingServer = fmt.Errorf("missing server definitions")
)

var (
// reValidFormat validates the format only, validating for a valid host or port is out of scope.
reValidFormat = regexp.MustCompile(`^([a-z0-9.-]+|\*)(:\*|:\d{1,5})?$`)
reCleanPattern = regexp.MustCompile(`{([^}]+)}`)
rePortCheck = regexp.MustCompile(`^(0|[1-9][0-9]{0,4})$`)
)

type backendDefinition struct {
conf *config.Backend
handler http.Handler
Expand Down Expand Up @@ -161,6 +152,10 @@ func NewServerConfiguration(conf *config.Gateway, httpConf *HTTPConfig, log *log
}
endpoints[cleanPattern] = true

if err := validateInlineScheme(conf.Context, endpoint.InlineDefinition, endpoint); err != nil {
return nil, err
}

// setACHandlerFn individual wrap for access_control configuration per endpoint
setACHandlerFn := func(protectedHandler http.Handler) {
api[endpoint] = configureProtectedHandler(accessControls, serverOptions.APIErrTpl,
Expand Down Expand Up @@ -278,27 +273,6 @@ func newBackendsFromDefinitions(conf *config.Gateway, log *logrus.Entry) (map[st
return backends, nil
}

func validateOrigin(origin string, ctxRange hcl.Range) error {
diagErr := &hcl.Diagnostic{
Subject: &ctxRange,
Summary: "invalid backend.origin value",
}
if origin == "" {
diagErr.Detail = "origin attribute is required"
return hcl.Diagnostics{diagErr}
}
url, err := url2.Parse(origin)
if err != nil {
diagErr.Detail = fmt.Sprintf("url parse error: %v", err)
return hcl.Diagnostics{diagErr}
}
if url.Scheme != "http" && url.Scheme != "https" {
diagErr.Detail = fmt.Sprintf("valid http scheme required for origin: %q", origin)
return hcl.Diagnostics{diagErr}
}
return nil
}

// hasAttribute checks for a configured string value and ignores unrelated errors.
func getAttribute(ctx *hcl.EvalContext, name string, body hcl.Body) string {
attr, _ := body.JustAttributes()
Expand All @@ -311,63 +285,6 @@ func getAttribute(ctx *hcl.EvalContext, name string, body hcl.Body) string {
return seetie.ValueToString(val)
}

// validatePortHosts ensures expected host:port formats and unique hosts per port.
// Host options:
// "*:<port>" listen for all hosts on given port
// "*:<port(configuredPort)> given port equals configured default port, listen for all hosts
// "*" equals to "*:configuredPort"
// "host:*" equals to "host:configuredPort"
// "host" listen on configured default port for given host
func validatePortHosts(conf *config.Gateway, configuredPort int) (ports, hosts, error) {
portMap := make(ports)
hostMap := make(hosts)
isHostsMandatory := len(conf.Server) > 1

for _, srv := range conf.Server {
if isHostsMandatory && len(srv.Hosts) == 0 {
return nil, nil, fmt.Errorf("hosts attribute is mandatory for multiple servers: %q", srv.Name)
}

srvPortMap := make(ports)
for _, host := range srv.Hosts {
if !reValidFormat.MatchString(host) {
return nil, nil, fmt.Errorf("host format is invalid: %q", host)
}

ho, po, err := splitWildcardHostPort(host, configuredPort)
if err != nil {
return nil, nil, err
}

if _, ok := srvPortMap[po]; !ok {
srvPortMap[po] = make(hosts)
}

srvPortMap[po][ho] = true

hostMap[fmt.Sprintf("%s:%d", ho, po)] = true
}

// srvPortMap contains all unique host port combinations for
// the current server and should not exist multiple times.
for po, ho := range srvPortMap {
if _, ok := portMap[po]; !ok {
portMap[po] = make(hosts)
}

for h := range ho {
if _, ok := portMap[po][h]; ok {
return nil, nil, fmt.Errorf("conflict: host %q already defined for port: %d", h, po)
}

portMap[po][h] = true
}
}
}

return portMap, hostMap, nil
}

func splitWildcardHostPort(host string, configuredPort int) (string, Port, error) {
if !strings.Contains(host, ":") {
return host, Port(configuredPort), nil
Expand Down Expand Up @@ -461,20 +378,6 @@ func configureAccessControls(conf *config.Gateway) (ac.Map, error) {
return accessControls, nil
}

func validateACName(accessControls ac.Map, name, acType string) (string, error) {
name = strings.TrimSpace(name)

if name == "" {
return name, fmt.Errorf("Missing a non-empty label for %q", acType)
}

if _, ok := accessControls[name]; ok {
return name, fmt.Errorf("Label %q already exists in the ACL", name)
}

return name, nil
}

func configureProtectedHandler(m ac.Map, errTpl *errors.Template, parentAC, handlerAC config.AccessControl, h http.Handler) http.Handler {
var acList ac.List
for _, acName := range parentAC.
Expand All @@ -489,22 +392,31 @@ func configureProtectedHandler(m ac.Map, errTpl *errors.Template, parentAC, hand
}

func newInlineBackend(evalCtx *hcl.EvalContext, backends map[string]backendDefinition, inlineDef hcl.Body, cors *config.CORS, log *logrus.Entry, srvOpts *server.Options) (http.Handler, *config.Backend, error) {
content, _, diags := inlineDef.PartialContent(config.Definitions{}.Schema(true))
// ignore diag errors here, would fail anyway with our retry
content, _, diags := inlineDef.PartialContent(config.Endpoint{}.Schema(true))
if diags.HasErrors() {
return nil, nil, diags
}

if content == nil || len(content.Blocks) == 0 {
// no inline conf, retry for override definitions with label
content, _, diags = inlineDef.PartialContent(config.Definitions{}.Schema(false))
if diags.HasErrors() {
return nil, nil, diags
}
return nil, nil, errorMissingBackend
}

if content == nil || len(content.Blocks) == 0 {
return nil, nil, errorMissingBackend
var inlineBlock *hcl.Block
for _, block := range content.Blocks {
if block.Type == "backend" {
inlineBlock = block
}
}
if inlineBlock == nil {
return nil, nil, errorMissingBackend
}

if err := validateInlineScheme(evalCtx, inlineBlock.Body, config.Backend{}); err != nil {
return nil, nil, err
}

beConf := &config.Backend{}
diags = gohcl.DecodeBody(content.Blocks[0].Body, evalCtx, beConf)
diags = gohcl.DecodeBody(inlineBlock.Body, evalCtx, beConf)
if diags.HasErrors() {
return nil, nil, diags
}
Expand Down Expand Up @@ -562,9 +474,3 @@ func setRoutesFromHosts(srvConf *ServerConfiguration, confPort int, hosts []stri
}
return nil
}

func isUnique(endpoints map[string]bool, pattern string) (bool, string) {
pattern = reCleanPattern.ReplaceAllString(pattern, "{}")

return !endpoints[pattern], pattern
}
130 changes: 130 additions & 0 deletions config/runtime/server_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package runtime

import (
"fmt"
"net/url"
"reflect"
"regexp"
"strings"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"

ac "github.com/avenga/couper/accesscontrol"
"github.com/avenga/couper/config"
)

var (
// reValidFormat validates the format only, validating for a valid host or port is out of scope.
reValidFormat = regexp.MustCompile(`^([a-z0-9.-]+|\*)(:\*|:\d{1,5})?$`)
reCleanPattern = regexp.MustCompile(`{([^}]+)}`)
rePortCheck = regexp.MustCompile(`^(0|[1-9][0-9]{0,4})$`)
)

// validatePortHosts ensures expected host:port formats and unique hosts per port.
// Host options:
// "*:<port>" listen for all hosts on given port
// "*:<port(configuredPort)> given port equals configured default port, listen for all hosts
// "*" equals to "*:configuredPort"
// "host:*" equals to "host:configuredPort"
// "host" listen on configured default port for given host
func validatePortHosts(conf *config.Gateway, configuredPort int) (ports, hosts, error) {
portMap := make(ports)
hostMap := make(hosts)
isHostsMandatory := len(conf.Server) > 1

for _, srv := range conf.Server {
if isHostsMandatory && len(srv.Hosts) == 0 {
return nil, nil, fmt.Errorf("hosts attribute is mandatory for multiple servers: %q", srv.Name)
}

srvPortMap := make(ports)
for _, host := range srv.Hosts {
if !reValidFormat.MatchString(host) {
return nil, nil, fmt.Errorf("host format is invalid: %q", host)
}

ho, po, err := splitWildcardHostPort(host, configuredPort)
if err != nil {
return nil, nil, err
}

if _, ok := srvPortMap[po]; !ok {
srvPortMap[po] = make(hosts)
}

srvPortMap[po][ho] = true

hostMap[fmt.Sprintf("%s:%d", ho, po)] = true
}

// srvPortMap contains all unique host port combinations for
// the current server and should not exist multiple times.
for po, ho := range srvPortMap {
if _, ok := portMap[po]; !ok {
portMap[po] = make(hosts)
}

for h := range ho {
if _, ok := portMap[po][h]; ok {
return nil, nil, fmt.Errorf("conflict: host %q already defined for port: %d", h, po)
}

portMap[po][h] = true
}
}
}

return portMap, hostMap, nil
}

func validateOrigin(origin string, ctxRange hcl.Range) error {
diagErr := &hcl.Diagnostic{
Subject: &ctxRange,
Summary: "invalid backend.origin value",
}
if origin == "" {
diagErr.Detail = "origin attribute is required"
return hcl.Diagnostics{diagErr}
}
u, err := url.Parse(origin)
if err != nil {
diagErr.Detail = fmt.Sprintf("url parse error: %v", err)
return hcl.Diagnostics{diagErr}
}
if u.Scheme != "http" && u.Scheme != "https" {
diagErr.Detail = fmt.Sprintf("valid http scheme required for origin: %q", origin)
return hcl.Diagnostics{diagErr}
}
return nil
}

func validateACName(accessControls ac.Map, name, acType string) (string, error) {
name = strings.TrimSpace(name)

if name == "" {
return name, fmt.Errorf("Missing a non-empty label for %q", acType)
}

if _, ok := accessControls[name]; ok {
return name, fmt.Errorf("Label %q already exists in the ACL", name)
}

return name, nil
}

func validateInlineScheme(ctx *hcl.EvalContext, body hcl.Body, inlineType config.Inline) error {
_, partialBody, diags := body.PartialContent(inlineType.Schema(true))
result := reflect.New(reflect.TypeOf(inlineType))
diags = append(diags, gohcl.DecodeBody(partialBody, ctx, &result)...)
if diags.HasErrors() {
return diags
}
return nil
}

func isUnique(endpoints map[string]bool, pattern string) (bool, string) {
pattern = reCleanPattern.ReplaceAllString(pattern, "{}")

return !endpoints[pattern], pattern
}
Loading

0 comments on commit 191c3d9

Please sign in to comment.