Repository: avenga/couper
Pull request #68 (2020-11-19 17:55:52)

Change handle endpoint.path, backend.{path,origin,hostname} inline
…http context


Patch:
diff --git a/config/backend.go b/config/backend.go
index 351aeac4..ce9ec0c9 100644
--- a/config/backend.go
+++ b/config/backend.go
@@ -2,20 +2,36 @@ package config
 
 import (
 	"github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/gohcl"
 )
 
 type Backend struct {
 	ConnectTimeout   string   `hcl:"connect_timeout,optional"`
-	Hostname         string   `hcl:"hostname,optional"`
 	Name             string   `hcl:"name,label"`
 	Options          hcl.Body `hcl:",remain"`
-	Origin           string   `hcl:"origin,optional"` // mixed, not required for overrides
-	Path             string   `hcl:"path,optional"`
 	RequestBodyLimit string   `hcl:"request_body_limit,optional"`
 	TTFBTimeout      string   `hcl:"ttfb_timeout,optional"`
 	Timeout          string   `hcl:"timeout,optional"`
 }
 
+func (b Backend) Schema(inline bool) *hcl.BodySchema {
+	schema, _ := gohcl.ImpliedBodySchema(b)
+	if !inline {
+		return schema
+	}
+
+	type Inline struct {
+		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
+}
+
 // Merge overrides the left backend configuration and returns a new instance.
 func (b *Backend) Merge(other *Backend) (*Backend, []hcl.Body) {
 	if b == nil || other == nil {
@@ -26,10 +42,6 @@ func (b *Backend) Merge(other *Backend) (*Backend, []hcl.Body) {
 
 	result := *b
 
-	if other.Hostname != "" {
-		result.Hostname = other.Hostname
-	}
-
 	if other.Name != "" {
 		result.Name = other.Name
 	}
@@ -43,14 +55,6 @@ func (b *Backend) Merge(other *Backend) (*Backend, []hcl.Body) {
 		result.Options = other.Options
 	}
 
-	if other.Origin != "" {
-		result.Origin = other.Origin
-	}
-
-	if other.Path != "" {
-		result.Path = other.Path
-	}
-
 	if other.ConnectTimeout != "" {
 		result.ConnectTimeout = other.ConnectTimeout
 	}
diff --git a/config/definitions.go b/config/definitions.go
index 379d5dbc..461c02b4 100644
--- a/config/definitions.go
+++ b/config/definitions.go
@@ -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
-}
diff --git a/config/endpoint.go b/config/endpoint.go
index df74ffd1..05a038a2 100644
--- a/config/endpoint.go
+++ b/config/endpoint.go
@@ -2,6 +2,7 @@ package config
 
 import (
 	"github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/gohcl"
 )
 
 type Endpoint struct {
@@ -9,6 +10,31 @@ type Endpoint struct {
 	Backend              string   `hcl:"backend,optional"`
 	DisableAccessControl []string `hcl:"disable_access_control,optional"`
 	InlineDefinition     hcl.Body `hcl:",remain" json:"-"`
-	Path                 string   `hcl:"path,optional"`
 	Pattern              string   `hcl:"path,label"`
 }
+
+func (e Endpoint) Schema(inline bool) *hcl.BodySchema {
+	if !inline {
+		schema, _ := gohcl.ImpliedBodySchema(e)
+		return schema
+	}
+
+	type Inline struct {
+		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
+}
diff --git a/config/gateway.go b/config/gateway.go
index f05424cd..f08b7bd8 100644
--- a/config/gateway.go
+++ b/config/gateway.go
@@ -3,6 +3,7 @@ package config
 import "github.com/hashicorp/hcl/v2"
 
 type Gateway struct {
+	Bytes       []byte           `hcl:"-"`
 	Context     *hcl.EvalContext `hcl:"-"`
 	Definitions *Definitions     `hcl:"definitions,block"`
 	Server      []*Server        `hcl:"server,block"`
diff --git a/config/load.go b/config/load.go
index 604a1abf..982d0bc1 100644
--- a/config/load.go
+++ b/config/load.go
@@ -5,31 +5,38 @@ import (
 	"io/ioutil"
 	"os"
 	"path"
+	"path/filepath"
 
 	"github.com/hashicorp/hcl/v2/hclsimple"
 
 	"github.com/avenga/couper/eval"
 )
 
-func LoadFile(filename string) (*Gateway, error) {
+func LoadFile(filePath string) (*Gateway, error) {
 	wd, err := os.Getwd()
 	if err != nil {
 		return nil, err
 	}
-	src, err := ioutil.ReadFile(path.Join(wd, filename))
+	src, err := ioutil.ReadFile(path.Join(wd, filePath))
 	if err != nil {
 		return nil, fmt.Errorf("failed to load configuration: %w", err)
 	}
-	return LoadBytes(src)
+	return LoadBytes(src, filePath)
 }
 
-func LoadBytes(src []byte) (*Gateway, error) {
+func LoadBytes(src []byte, filePath string) (*Gateway, error) {
 	config := &Gateway{
+		Bytes:    src[:],
 		Context:  eval.NewENVContext(src),
 		Settings: &Settings{DefaultPort: DefaultListenPort},
 	}
+	filename := filepath.Base(filePath)
+	if filepath.Ext(filename) != ".hcl" {
+		return nil, fmt.Errorf("configuration must be a hcl file")
+	}
+
 	// filename must match .hcl ending for further []byte processing
-	if err := hclsimple.Decode("loadBytes.hcl", src, config.Context, config); err != nil {
+	if err := hclsimple.Decode(filename, src, config.Context, config); err != nil {
 		return nil, fmt.Errorf("Failed to load configuration bytes: %w", err)
 	}
 	return config, nil
diff --git a/config/runtime/server.go b/config/runtime/server.go
index e7225c86..c6f1b94e 100644
--- a/config/runtime/server.go
+++ b/config/runtime/server.go
@@ -5,9 +5,9 @@ import (
 	"io/ioutil"
 	"net"
 	"net/http"
+	"net/http/httptest"
 	"os"
 	"path"
-	"regexp"
 	"strconv"
 	"strings"
 
@@ -20,6 +20,7 @@ import (
 	"github.com/avenga/couper/config"
 	"github.com/avenga/couper/config/runtime/server"
 	"github.com/avenga/couper/errors"
+	"github.com/avenga/couper/eval"
 	"github.com/avenga/couper/handler"
 	"github.com/avenga/couper/internal/seetie"
 	"github.com/avenga/couper/utils"
@@ -37,13 +38,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
@@ -83,17 +77,23 @@ func NewServerConfiguration(conf *config.Gateway, httpConf *HTTPConfig, log *log
 		defaultPort = httpConf.ListenPort
 	}
 
+	// confCtx is created to evaluate request / response related configuration errors on start.
+	noopReq := httptest.NewRequest(http.MethodGet, "https://couper.io", nil)
+	noopResp := httptest.NewRecorder().Result()
+	noopResp.Request = noopReq
+	confCtx := eval.NewHTTPContext(conf.Context, 0, noopReq, noopReq, noopResp)
+
 	validPortMap, hostsMap, err := validatePortHosts(conf, defaultPort)
 	if err != nil {
 		return nil, err
 	}
 
-	backends, err := newBackendsFromDefinitions(conf, log)
+	backends, err := newBackendsFromDefinitions(conf, confCtx, log)
 	if err != nil {
 		return nil, err
 	}
 
-	accessControls, err := configureAccessControls(conf)
+	accessControls, err := configureAccessControls(conf, confCtx)
 	if err != nil {
 		return nil, err
 	}
@@ -160,16 +160,12 @@ func NewServerConfiguration(conf *config.Gateway, httpConf *HTTPConfig, log *log
 				}
 				endpoints[cleanPattern] = true
 
-				// setACHandlerFn individual wrap for access_control configuration per endpoint
-				setACHandlerFn := func(protectedBackend backendDefinition) {
-					protectedHandler := protectedBackend.handler
-
-					// prefer endpoint 'path' definition over 'backend.Path'
-					if endpoint.Path != "" {
-						beConf, remainCtx := protectedBackend.conf.Merge(&config.Backend{Path: endpoint.Path})
-						protectedHandler = newProxy(conf.Context, beConf, srvConf.API.CORS, remainCtx, log, serverOptions)
-					}
+				if err := validateInlineScheme(confCtx, 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,
 						config.NewAccessControl(srvConf.AccessControl, srvConf.DisableAccessControl).
 							Merge(config.NewAccessControl(srvConf.API.AccessControl, srvConf.API.DisableAccessControl)),
@@ -185,13 +181,10 @@ func NewServerConfiguration(conf *config.Gateway, httpConf *HTTPConfig, log *log
 
 					// set server context for defined backends
 					be := backends[endpoint.Backend]
-					beConf, remain := be.conf.Merge(&config.Backend{Options: endpoint.InlineDefinition})
-					refBackend := newProxy(conf.Context, be.conf, srvConf.API.CORS, remain, log, serverOptions)
+					_, remain := be.conf.Merge(&config.Backend{Options: endpoint.InlineDefinition})
+					refBackend := newProxy(confCtx, be.conf, srvConf.API.CORS, remain, log, serverOptions)
 
-					setACHandlerFn(backendDefinition{
-						conf:    beConf,
-						handler: refBackend,
-					})
+					setACHandlerFn(refBackend)
 					err = setRoutesFromHosts(serverConfiguration, defaultPort, srvConf.Hosts, pattern, api[endpoint], KindAPI)
 					if err != nil {
 						return nil, err
@@ -200,32 +193,38 @@ func NewServerConfiguration(conf *config.Gateway, httpConf *HTTPConfig, log *log
 				}
 
 				// otherwise try to parse an inline block and fallback for api reference or inline block
-				inlineBackend, inlineConf, err := newInlineBackend(conf.Context, backends, endpoint.InlineDefinition, srvConf.API.CORS, log, serverOptions)
+				inlineBackend, inlineConf, err := newInlineBackend(confCtx, backends, endpoint.InlineDefinition, srvConf.API.CORS, log, serverOptions)
 				if err == errorMissingBackend {
 					if srvConf.API.Backend != "" {
 						if _, ok := backends[srvConf.API.Backend]; !ok {
 							return nil, fmt.Errorf("backend %q is not defined", srvConf.API.Backend)
 						}
-						setACHandlerFn(backends[srvConf.API.Backend])
+						setACHandlerFn(backends[srvConf.API.Backend].handler)
 						err = setRoutesFromHosts(serverConfiguration, defaultPort, srvConf.Hosts, pattern, api[endpoint], KindAPI)
 						if err != nil {
 							return nil, err
 						}
 						continue
 					}
-					inlineBackend, inlineConf, err = newInlineBackend(conf.Context, backends, srvConf.API.InlineDefinition, srvConf.API.CORS, log, serverOptions)
+					inlineBackend, inlineConf, err = newInlineBackend(confCtx, backends, srvConf.API.InlineDefinition, srvConf.API.CORS, log, serverOptions)
 					if err != nil {
 						return nil, err
 					}
 
-					if inlineConf.Name == "" && inlineConf.Origin == "" {
+					if inlineConf.Name == "" && getAttribute(confCtx, "origin", inlineConf.Options, conf.Bytes) == "" {
 						return nil, fmt.Errorf("api inline backend requires an origin attribute: %q", pattern)
 					}
 				} else if err != nil { // TODO hcl.diagnostics error
 					return nil, fmt.Errorf("range: %s: %v", endpoint.InlineDefinition.MissingItemRange().String(), err)
 				}
 
-				setACHandlerFn(backendDefinition{conf: inlineConf, handler: inlineBackend})
+				if e := validateOrigin(
+					getAttribute(confCtx, "origin", inlineConf.Options, conf.Bytes),
+					inlineConf.Options.MissingItemRange()); e != nil {
+					return nil, e
+				}
+
+				setACHandlerFn(inlineBackend)
 				err = setRoutesFromHosts(serverConfiguration, defaultPort, srvConf.Hosts, pattern, api[endpoint], KindAPI)
 				if err != nil {
 					return nil, err
@@ -254,7 +253,7 @@ func newProxy(ctx *hcl.EvalContext, beConf *config.Backend, corsOpts *config.COR
 	return proxy
 }
 
-func newBackendsFromDefinitions(conf *config.Gateway, log *logrus.Entry) (map[string]backendDefinition, error) {
+func newBackendsFromDefinitions(conf *config.Gateway, confCtx *hcl.EvalContext, log *logrus.Entry) (map[string]backendDefinition, error) {
 	backends := make(map[string]backendDefinition)
 
 	if conf.Definitions == nil {
@@ -266,8 +265,9 @@ func newBackendsFromDefinitions(conf *config.Gateway, log *logrus.Entry) (map[st
 			return nil, fmt.Errorf("backend name must be unique: %q", beConf.Name)
 		}
 
-		if beConf.Origin == "" {
-			return nil, fmt.Errorf("backend %q: origin attribute is required", beConf.Name)
+		origin := getAttribute(confCtx, "origin", beConf.Options, conf.Bytes)
+		if e := validateOrigin(origin, beConf.Options.MissingItemRange()); e != nil {
+			return nil, e
 		}
 
 		beConf, _ = defaultBackendConf.Merge(beConf)
@@ -275,67 +275,28 @@ func newBackendsFromDefinitions(conf *config.Gateway, log *logrus.Entry) (map[st
 		srvOpts, _ := server.NewServerOptions(&config.Server{})
 		backends[beConf.Name] = backendDefinition{
 			conf:    beConf,
-			handler: newProxy(conf.Context, beConf, nil, []hcl.Body{beConf.Options}, log, srvOpts),
+			handler: newProxy(confCtx, beConf, nil, []hcl.Body{beConf.Options}, log, srvOpts),
 		}
 	}
 	return backends, nil
 }
 
-// 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)
-			}
+// hasAttribute checks for a configured string value and ignores unrelated errors.
+func getAttribute(ctx *hcl.EvalContext, name string, body hcl.Body, configBytes []byte) string {
+	attr, _ := body.JustAttributes()
 
-			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)
-				}
+	if _, ok := attr[name]; !ok {
+		return ""
+	}
 
-				portMap[po][h] = true
-			}
+	val, diags := attr[name].Expr.Value(ctx)
+	if diags.HasErrors() && attr[name].Expr.Range().CanSliceBytes(configBytes) { // fallback to origin string
+		rawString := attr[name].Expr.Range().SliceBytes(configBytes)
+		if len(rawString) > 2 { // more then quotes
+			return string(attr[name].Expr.Range().SliceBytes(configBytes)[1 : len(rawString)-1]) //unquote
 		}
 	}
-
-	return portMap, hostMap, nil
+	return seetie.ValueToString(val)
 }
 
 func splitWildcardHostPort(host string, configuredPort int) (string, Port, error) {
@@ -363,7 +324,7 @@ func splitWildcardHostPort(host string, configuredPort int) (string, Port, error
 	return ho, Port(po), nil
 }
 
-func configureAccessControls(conf *config.Gateway) (ac.Map, error) {
+func configureAccessControls(conf *config.Gateway, confCtx *hcl.EvalContext) (ac.Map, error) {
 	accessControls := make(ac.Map)
 
 	if conf.Definitions != nil {
@@ -413,7 +374,7 @@ func configureAccessControls(conf *config.Gateway) (ac.Map, error) {
 
 			var claims ac.Claims
 			if jwt.Claims != nil {
-				c, diags := seetie.ExpToMap(conf.Context, jwt.Claims)
+				c, diags := seetie.ExpToMap(confCtx, jwt.Claims)
 				if diags.HasErrors() {
 					return nil, diags
 				}
@@ -431,20 +392,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.
@@ -459,22 +406,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
 	}
@@ -532,9 +488,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
-}
diff --git a/config/runtime/server_validation.go b/config/runtime/server_validation.go
new file mode 100644
index 00000000..4c54d841
--- /dev/null
+++ b/config/runtime/server_validation.go
@@ -0,0 +1,135 @@
+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}
+	}
+
+	// if origin contains fallback content with variables
+	origin = strings.ReplaceAll(strings.ReplaceAll(origin, "}", ""), "${", "")
+
+	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
+}
diff --git a/handler/context_options.go b/handler/context_options.go
index 80d4c81b..1e6dfe5d 100644
--- a/handler/context_options.go
+++ b/handler/context_options.go
@@ -3,24 +3,30 @@ package handler
 import (
 	"github.com/hashicorp/hcl/v2"
 
+	"github.com/avenga/couper/config"
 	"github.com/avenga/couper/internal/seetie"
 )
 
+const (
+	attrReqHeaders = "request_headers"
+	attrResHeaders = "response_headers"
+)
+
 type OptionsMap map[string][]string
 
 func NewCtxOptions(attrName string, evalCtx *hcl.EvalContext, body hcl.Body) (OptionsMap, error) {
 	var diags hcl.Diagnostics
 	var options OptionsMap
 
-	content, d := body.Content(headersAttributeSchema)
-	diags = append(diags, d...)
+	content, d := body.Content(config.Backend{}.Schema(true))
+	diags = append(diags, seetie.SetSeverityLevel(d)...)
 
 	for _, attr := range content.Attributes {
 		if attr.Name != attrName {
 			continue
 		}
 		o, d := NewOptionsMap(evalCtx, attr)
-		diags = append(diags, d...)
+		diags = append(diags, seetie.SetSeverityLevel(d)...)
 		options = o
 		break
 	}
@@ -49,19 +55,3 @@ func NewOptionsMap(evalCtx *hcl.EvalContext, attr *hcl.Attribute) (OptionsMap, h
 	}
 	return options, nil
 }
-
-const (
-	attrReqHeaders = "request_headers"
-	attrResHeaders = "response_headers"
-)
-
-var headersAttributeSchema = &hcl.BodySchema{
-	Attributes: []hcl.AttributeSchema{
-		{
-			Name: attrReqHeaders,
-		},
-		{
-			Name: attrResHeaders,
-		},
-	},
-}
diff --git a/handler/proxy.go b/handler/proxy.go
index 10eeff2b..f2457bbf 100644
--- a/handler/proxy.go
+++ b/handler/proxy.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"context"
 	"crypto/tls"
-	"errors"
 	"fmt"
 	"io"
 	"math"
@@ -13,6 +12,7 @@ import (
 	"net/url"
 	"strconv"
 	"strings"
+	"sync"
 	"time"
 
 	"github.com/hashicorp/hcl/v2"
@@ -34,8 +34,7 @@ var (
 	_ http.Handler   = &Proxy{}
 	_ server.Context = &Proxy{}
 
-	OriginRequiredError = errors.New("origin is required")
-	SchemeRequiredError = errors.New("backend origin must define a scheme")
+	transports sync.Map
 
 	// headerBlacklist lists all header keys which will be removed after
 	// context variable evaluation to ensure to not pass them upstream.
@@ -47,7 +46,6 @@ type Proxy struct {
 	evalContext  *hcl.EvalContext
 	log          *logrus.Entry
 	options      *ProxyOptions
-	originURL    *url.URL
 	srvOptions   *server.Options
 	transport    *http.Transport
 	upstreamLog  *logging.AccessLog
@@ -96,17 +94,6 @@ func (c *CORSOptions) AllowsOrigin(origin string) bool {
 }
 
 func NewProxy(options *ProxyOptions, log *logrus.Entry, srvOpts *server.Options, evalCtx *hcl.EvalContext) (http.Handler, error) {
-	if options.Origin == "" {
-		return nil, OriginRequiredError
-	}
-	originURL, err := url.Parse(options.Origin)
-	if err != nil {
-		return nil, fmt.Errorf("err parsing origin url: %w", err)
-	}
-	if originURL.Scheme != "http" && originURL.Scheme != "https" {
-		return nil, SchemeRequiredError
-	}
-
 	logConf := *logging.DefaultConfig
 	logConf.TypeFieldKey = "couper_backend"
 	env.DecodeWithPrefix(&logConf, "BACKEND_")
@@ -116,32 +103,43 @@ func NewProxy(options *ProxyOptions, log *logrus.Entry, srvOpts *server.Options,
 		evalContext:  evalCtx,
 		log:          log,
 		options:      options,
-		originURL:    originURL,
 		srvOptions:   srvOpts,
 		upstreamLog:  logging.NewAccessLog(&logConf, log.Logger),
 	}
 
-	var tlsConf *tls.Config
-	if options.Hostname != "" {
-		tlsConf = &tls.Config{
-			ServerName: options.Hostname,
-		}
-	}
+	return proxy, nil
+}
 
-	d := &net.Dialer{Timeout: options.ConnectTimeout}
-	proxy.transport = &http.Transport{
-		// DisableCompression: true,
-		DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
-			conn, err := d.DialContext(ctx, network, addr)
-			if err != nil {
-				return nil, fmt.Errorf("connecting to %s failed: %w", originURL.String(), err)
+func (p *Proxy) getTransport(scheme, origin, hostname string) *http.Transport {
+	key := scheme + "|" + origin + "|" + hostname
+	transport, ok := transports.Load(key)
+	if !ok {
+		var tlsConf *tls.Config
+		if origin != hostname {
+			tlsConf = &tls.Config{
+				ServerName: hostname,
 			}
-			return conn, nil
-		},
-		ResponseHeaderTimeout: proxy.options.TTFBTimeout,
-		TLSClientConfig:       tlsConf,
+		}
+
+		d := &net.Dialer{Timeout: p.options.ConnectTimeout}
+
+		transport = &http.Transport{
+			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+				conn, err := d.DialContext(ctx, network, addr)
+				if err != nil {
+					return nil, fmt.Errorf("connecting to %s %q failed: %w", p.options.BackendName, addr, err)
+				}
+				return conn, nil
+			},
+			ResponseHeaderTimeout: p.options.TTFBTimeout,
+			TLSClientConfig:       tlsConf,
+		}
+		transports.Store(key, transport)
 	}
-	return proxy, nil
+	if t, ok := transport.(*http.Transport); ok {
+		return t
+	}
+	return nil
 }
 
 func (p *Proxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
@@ -209,7 +207,7 @@ func (p *Proxy) roundtrip(rw http.ResponseWriter, req *http.Request) {
 		outreq.Header.Set("X-Forwarded-For", clientIP)
 	}
 
-	res, err := p.transport.RoundTrip(outreq)
+	res, err := p.getTransport(outreq.URL.Scheme, outreq.URL.Host, outreq.Host).RoundTrip(outreq)
 	roundtripInfo := req.Context().Value(request.RoundtripInfo).(*logging.RoundtripInfo)
 	roundtripInfo.BeReq, roundtripInfo.BeResp, roundtripInfo.Err = outreq, res, err
 	if err != nil {
@@ -280,23 +278,45 @@ func (p *Proxy) roundtrip(rw http.ResponseWriter, req *http.Request) {
 
 // Director request modification before roundtrip
 func (p *Proxy) Director(req *http.Request) error {
-	req.URL.Host = p.originURL.Host
-	req.URL.Scheme = p.originURL.Scheme
-	req.Host = p.originURL.Host
-	if p.options.Hostname != "" {
-		req.Host = p.options.Hostname
+	var origin, hostname, path string
+	schema := config.Backend{}.Schema(true)
+	evalContext := eval.NewHTTPContext(p.evalContext, p.bufferOption, req, nil, nil)
+	for _, hclContext := range p.options.Context { // context gets configured in order, last wins
+		content, _, _ := hclContext.PartialContent(schema)
+		if o := getAttribute(evalContext, "origin", content); o != "" {
+			origin = o
+		}
+		if h := getAttribute(evalContext, "hostname", content); h != "" {
+			hostname = h
+		}
+		if p := getAttribute(evalContext, "path", content); p != "" {
+			path = p
+		}
+	}
+
+	originURL, err := url.Parse(origin)
+	if err != nil {
+		return err
+	}
+
+	req.URL.Host = originURL.Host
+	req.URL.Scheme = originURL.Scheme
+	req.Host = originURL.Host
+
+	if hostname != "" {
+		req.Host = hostname
 	}
 
 	if pathMatch, ok := req.Context().
-		Value(request.Wildcard).(string); ok && strings.HasSuffix(p.options.Path, "/**") {
+		Value(request.Wildcard).(string); ok && strings.HasSuffix(path, "/**") {
 		if pathMatch == "" && req.URL.Path != "" { // wildcard "root" hit, take a look if the req has a trailing slash and apply
 			if req.URL.Path[len(req.URL.Path)-1] == '/' {
 				pathMatch = "/"
 			}
 		}
-		req.URL.Path = utils.JoinPath(strings.ReplaceAll(p.options.Path, "/**", "/"), pathMatch)
-	} else if p.options.Path != "" {
-		req.URL.Path = p.options.Path
+		req.URL.Path = utils.JoinPath("/", strings.ReplaceAll(path, "/**", "/"), pathMatch)
+	} else if path != "" {
+		req.URL.Path = utils.JoinPath("/", path)
 	}
 
 	if err := p.SetGetBody(req); err != nil {
@@ -558,6 +578,18 @@ func setHeaderFields(header http.Header, options OptionsMap) {
 	}
 }
 
+func getAttribute(ctx *hcl.EvalContext, name string, body *hcl.BodyContent) string {
+	attr := body.Attributes
+	if _, ok := attr[name]; !ok {
+		return ""
+	}
+	originValue, diags := attr[name].Expr.Value(ctx)
+	if diags != nil && diags.HasErrors() {
+		panic(diags.Error())
+	}
+	return seetie.ValueToString(originValue)
+}
+
 // switchProtocolCopier exists so goroutines proxying data back and
 // forth have nice names in stacks.
 type switchProtocolCopier struct {
diff --git a/handler/proxy_options.go b/handler/proxy_options.go
index 7c3db55c..899c9b70 100644
--- a/handler/proxy_options.go
+++ b/handler/proxy_options.go
@@ -14,7 +14,6 @@ type ProxyOptions struct {
 	ConnectTimeout, Timeout, TTFBTimeout time.Duration
 	Context                              []hcl.Body
 	BackendName                          string
-	Hostname, Origin, Path               string
 	CORS                                 *CORSOptions
 	RequestBodyLimit                     int64
 }
@@ -48,9 +47,6 @@ func NewProxyOptions(conf *config.Backend, corsOpts *CORSOptions, remainCtx []hc
 		CORS:             cors,
 		ConnectTimeout:   connectD,
 		Context:          remainCtx,
-		Hostname:         conf.Hostname,
-		Origin:           conf.Origin,
-		Path:             conf.Path,
 		RequestBodyLimit: bodyLimit,
 		TTFBTimeout:      ttfbD,
 		Timeout:          totalD,
@@ -78,18 +74,6 @@ func (po *ProxyOptions) Merge(o *ProxyOptions) *ProxyOptions {
 		po.CORS = o.CORS
 	}
 
-	if o.Hostname != "" {
-		po.Hostname = o.Hostname
-	}
-
-	if o.Origin != "" {
-		po.Origin = o.Origin
-	}
-
-	if o.Path != "" {
-		po.Path = o.Path
-	}
-
 	if o.RequestBodyLimit != po.RequestBodyLimit {
 		po.RequestBodyLimit = o.RequestBodyLimit
 	}
diff --git a/internal/seetie/convert.go b/internal/seetie/convert.go
index 1184b060..771334c5 100644
--- a/internal/seetie/convert.go
+++ b/internal/seetie/convert.go
@@ -165,7 +165,7 @@ var whitespaceRegex = regexp.MustCompile(`^\s*$`)
 // ValueToString explicitly drops all other (unknown) types and
 // converts non whitespace strings or numbers to its string representation.
 func ValueToString(v cty.Value) string {
-	if v.IsNull() {
+	if v.IsNull() || !v.IsKnown() {
 		return ""
 	}
 


Test patch:
diff --git a/config/backend_test.go b/config/backend_test.go
index 4cb784ba..2e977e41 100644
--- a/config/backend_test.go
+++ b/config/backend_test.go
@@ -20,39 +20,36 @@ func TestBackend_Merge(t *testing.T) {
 		{"nil", Backend{}, args{nil}, nil},
 		{"empty", Backend{}, args{&Backend{}}, &Backend{}},
 		{"left", Backend{
-			Hostname: "a", Name: "a", Origin: "a", Path: "a", Timeout: "s", ConnectTimeout: "s", Options: hcl.EmptyBody(),
+			Timeout: "s", ConnectTimeout: "s", Options: hcl.EmptyBody(),
 		}, args{&Backend{}}, &Backend{
-			Hostname: "a", Name: "a", Origin: "a", Path: "a", Timeout: "s", ConnectTimeout: "s", Options: hcl.EmptyBody(),
+			Timeout: "s", ConnectTimeout: "s", Options: hcl.EmptyBody(),
 		}},
 		{"right", Backend{}, args{&Backend{
-			Hostname: "a", Name: "a", Origin: "a", Path: "a", Timeout: "s", ConnectTimeout: "s", Options: hcl.EmptyBody(),
+			Timeout: "s", ConnectTimeout: "s", Options: hcl.EmptyBody(),
 		}}, &Backend{
-			Hostname: "a", Name: "a", Origin: "a", Path: "a", Timeout: "s", ConnectTimeout: "s", Options: hcl.EmptyBody(),
+			Timeout: "s", ConnectTimeout: "s", Options: hcl.EmptyBody(),
 		}},
 		{"override", Backend{
-			Hostname: "a", Name: "a", Origin: "a", Path: "a", Timeout: "s", ConnectTimeout: "s", RequestBodyLimit: "2M", TTFBTimeout: "t", Options: hcl.EmptyBody(),
+			Timeout: "s", ConnectTimeout: "s", RequestBodyLimit: "2M", TTFBTimeout: "t", Options: hcl.EmptyBody(),
 		}, args{&Backend{
-			Hostname: "b", Name: "b", Origin: "b", Path: "b", Timeout: "m", ConnectTimeout: "h", RequestBodyLimit: "20M", TTFBTimeout: "o", Options: hcl.EmptyBody(),
+			Timeout: "m", ConnectTimeout: "h", RequestBodyLimit: "20M", TTFBTimeout: "o", Options: hcl.EmptyBody(),
 		}}, &Backend{
-			Hostname: "b", Name: "b", Origin: "b", Path: "b", Timeout: "m", ConnectTimeout: "h", RequestBodyLimit: "20M", TTFBTimeout: "o", Options: hcl.EmptyBody(),
+			Timeout: "m", ConnectTimeout: "h", RequestBodyLimit: "20M", TTFBTimeout: "o", Options: hcl.EmptyBody(),
 		}},
 		{"partial override", Backend{
-			Hostname: "a", Name: "b", Origin: "c", Path: "d", Timeout: "e", ConnectTimeout: "f", TTFBTimeout: "t", Options: hcl.EmptyBody(),
+			Timeout: "e", ConnectTimeout: "f", TTFBTimeout: "t", Options: hcl.EmptyBody(),
 		}, args{&Backend{
-			Hostname: "c", ConnectTimeout: "d",
+			ConnectTimeout: "d",
 		}}, &Backend{
-			Hostname: "c", Name: "b", Origin: "c", Path: "d", Timeout: "e", ConnectTimeout: "d", TTFBTimeout: "t", Options: hcl.EmptyBody(),
+			Timeout: "e", ConnectTimeout: "d", TTFBTimeout: "t", Options: hcl.EmptyBody(),
 		}},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			b := &Backend{
 				ConnectTimeout:   tt.fields.ConnectTimeout,
-				Hostname:         tt.fields.Hostname,
 				Name:             tt.fields.Name,
 				Options:          tt.fields.Options,
-				Origin:           tt.fields.Origin,
-				Path:             tt.fields.Path,
 				RequestBodyLimit: tt.fields.RequestBodyLimit,
 				Timeout:          tt.fields.Timeout,
 				TTFBTimeout:      tt.fields.TTFBTimeout,
diff --git a/eval/context_test.go b/eval/context_test.go
index ec07063c..8ab041d1 100644
--- a/eval/context_test.go
+++ b/eval/context_test.go
@@ -113,7 +113,6 @@ func TestNewHTTPContext(t *testing.T) {
 			proxy, err := handler.NewProxy(&handler.ProxyOptions{
 				Context:          []hcl.Body{hclBody},
 				RequestBodyLimit: 256,
-				Origin:           req.URL.String(),
 				CORS:             &handler.CORSOptions{},
 			}, log.WithContext(nil), srvOpts, nil)
 			if err != nil {
diff --git a/handler/proxy_test.go b/handler/proxy_test.go
index 561d364b..408c485f 100644
--- a/handler/proxy_test.go
+++ b/handler/proxy_test.go
@@ -20,6 +20,7 @@ import (
 	"github.com/avenga/couper/errors"
 	"github.com/avenga/couper/eval"
 	"github.com/avenga/couper/handler"
+	"github.com/avenga/couper/internal/seetie"
 	"github.com/avenga/couper/internal/test"
 )
 
@@ -39,10 +40,10 @@ func TestProxy_ServeHTTP_Timings(t *testing.T) {
 		req            *http.Request
 		expectedStatus int
 	}{
-		{"with zero timings", &handler.ProxyOptions{Origin: origin.URL}, httptest.NewRequest(http.MethodGet, "http://1.2.3.4/", nil), http.StatusNoContent},
-		{"with overall timeout", &handler.ProxyOptions{Origin: "http://1.2.3.4/", Timeout: time.Second}, httptest.NewRequest(http.MethodGet, "http://1.2.3.4/", nil), http.StatusBadGateway},
-		{"with connect timeout", &handler.ProxyOptions{Origin: "http://blackhole.webpagetest.org/", ConnectTimeout: time.Second}, httptest.NewRequest(http.MethodGet, "http://1.2.3.4/", nil), http.StatusBadGateway},
-		{"with ttfb timeout", &handler.ProxyOptions{Origin: origin.URL, TTFBTimeout: time.Second}, httptest.NewRequest(http.MethodHead, "http://1.2.3.4/", nil), http.StatusBadGateway},
+		{"with zero timings", &handler.ProxyOptions{Context: test.NewRemainContext("origin", origin.URL)}, httptest.NewRequest(http.MethodGet, "http://1.2.3.4/", nil), http.StatusNoContent},
+		{"with overall timeout", &handler.ProxyOptions{Context: test.NewRemainContext("origin", "http://1.2.3.4/"), Timeout: time.Second}, httptest.NewRequest(http.MethodGet, "http://1.2.3.4/", nil), http.StatusBadGateway},
+		{"with connect timeout", &handler.ProxyOptions{Context: test.NewRemainContext("origin", "http://blackhole.webpagetest.org/"), ConnectTimeout: time.Second}, httptest.NewRequest(http.MethodGet, "http://1.2.3.4/", nil), http.StatusBadGateway},
+		{"with ttfb timeout", &handler.ProxyOptions{Context: test.NewRemainContext("origin", origin.URL), TTFBTimeout: time.Second}, httptest.NewRequest(http.MethodHead, "http://1.2.3.4/", nil), http.StatusBadGateway},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -180,7 +181,7 @@ func TestProxy_ServeHTTP_CORS_PFC(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			logger, hook := logrustest.NewNullLogger()
-			p, err := handler.NewProxy(&handler.ProxyOptions{Origin: origin.URL, CORS: tt.corsOptions}, logger.WithContext(nil), nil, eval.NewENVContext(nil))
+			p, err := handler.NewProxy(&handler.ProxyOptions{Context: test.NewRemainContext("origin", origin.URL), CORS: tt.corsOptions}, logger.WithContext(nil), nil, eval.NewENVContext(nil))
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -350,7 +351,7 @@ func TestProxy_ServeHTTP_CORS(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(subT *testing.T) {
 			logger, hook := logrustest.NewNullLogger()
-			p, err := handler.NewProxy(&handler.ProxyOptions{Origin: origin.URL, CORS: tt.corsOptions}, logger.WithContext(context.Background()), nil, eval.NewENVContext(nil))
+			p, err := handler.NewProxy(&handler.ProxyOptions{Context: test.NewRemainContext("origin", origin.URL), CORS: tt.corsOptions}, logger.WithContext(context.Background()), nil, eval.NewENVContext(nil))
 			if err != nil {
 				subT.Fatal(err)
 			}
@@ -387,34 +388,50 @@ func TestProxy_ServeHTTP_CORS(t *testing.T) {
 }
 
 func TestProxy_director(t *testing.T) {
+	helper := test.New(t)
+
 	log, _ := logrustest.NewNullLogger()
 	nullLog := log.WithContext(nil)
 
-	type fields struct {
-		log     *logrus.Entry
-		options *handler.ProxyOptions
-	}
-
-	emptyOptions := []hcl.Body{hcl.EmptyBody()}
 	bgCtx := context.Background()
 
 	tests := []struct {
-		name   string
-		fields fields
-		path   string
-		ctx    context.Context
-		expReq *http.Request
+		name      string
+		inlineCtx string
+		path      string
+		ctx       context.Context
+		expReq    *http.Request
 	}{
-		{"proxy url settings", fields{nullLog, &handler.ProxyOptions{Origin: "http://1.2.3.4", Context: emptyOptions}}, "", bgCtx, httptest.NewRequest("GET", "http://1.2.3.4", nil)},
-		{"proxy url settings w/hostname", fields{nullLog, &handler.ProxyOptions{Origin: "http://1.2.3.4", Hostname: "couper.io", Context: emptyOptions}}, "", bgCtx, httptest.NewRequest("GET", "http://couper.io", nil)},
-		{"proxy url settings w/wildcard ctx", fields{nullLog, &handler.ProxyOptions{Origin: "http://1.2.3.4", Hostname: "couper.io", Path: "/**", Context: emptyOptions}}, "/peter", context.WithValue(bgCtx, request.Wildcard, "/hans"), httptest.NewRequest("GET", "http://couper.io/hans", nil)},
-		{"proxy url settings w/wildcard ctx empty", fields{nullLog, &handler.ProxyOptions{Origin: "http://1.2.3.4", Hostname: "couper.io", Path: "/docs/**", Context: emptyOptions}}, "", context.WithValue(bgCtx, request.Wildcard, ""), httptest.NewRequest("GET", "http://couper.io/docs", nil)},
-		{"proxy url settings w/wildcard ctx empty /w trailing path slash", fields{nullLog, &handler.ProxyOptions{Origin: "http://1.2.3.4", Hostname: "couper.io", Path: "/docs/**", Context: emptyOptions}}, "/", context.WithValue(bgCtx, request.Wildcard, ""), httptest.NewRequest("GET", "http://couper.io/docs/", nil)},
+		{"proxy url settings", `origin = "http://1.2.3.4"`, "", bgCtx, httptest.NewRequest("GET", "http://1.2.3.4", nil)},
+		{"proxy url settings w/hostname", `
+			origin = "http://1.2.3.4"
+			hostname =  "couper.io"
+		`, "", bgCtx, httptest.NewRequest("GET", "http://couper.io", nil)},
+		{"proxy url settings w/wildcard ctx", `
+			origin = "http://1.2.3.4"
+			hostname =  "couper.io"
+			path = "/**"
+		`, "/peter", context.WithValue(bgCtx, request.Wildcard, "/hans"), httptest.NewRequest("GET", "http://couper.io/hans", nil)},
+		{"proxy url settings w/wildcard ctx empty", `
+			origin = "http://1.2.3.4"
+			hostname =  "couper.io"
+			path = "/docs/**"
+		`, "", context.WithValue(bgCtx, request.Wildcard, ""), httptest.NewRequest("GET", "http://couper.io/docs", nil)},
+		{"proxy url settings w/wildcard ctx empty /w trailing path slash", `
+			origin = "http://1.2.3.4"
+			hostname =  "couper.io"
+			path = "/docs/**"
+		`, "/", context.WithValue(bgCtx, request.Wildcard, ""), httptest.NewRequest("GET", "http://couper.io/docs/", nil)},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			p, err := handler.NewProxy(tt.fields.options, tt.fields.log, nil, eval.NewENVContext(nil))
+			hclContext := helper.NewProxyContext(tt.inlineCtx)
+
+			p, err := handler.NewProxy(&handler.ProxyOptions{
+				Context: hclContext,
+				CORS:    &handler.CORSOptions{},
+			}, nullLog, nil, eval.NewENVContext(nil))
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -428,10 +445,17 @@ func TestProxy_director(t *testing.T) {
 				t.Fatal(err)
 			}
 
-			if tt.fields.options.Hostname != "" && tt.fields.options.Hostname != tt.expReq.Host {
-				t.Errorf("expected same host value, want: %q, got: %q", tt.fields.options.Hostname, tt.expReq.Host)
-			} else if tt.fields.options.Hostname == "" && req.Host != tt.expReq.Host {
-				t.Error("expected a configured request host")
+			attr, _ := hclContext[0].JustAttributes()
+			hostnameExp, ok := attr["hostname"]
+
+			if !ok && tt.expReq.Host != req.Host {
+				t.Errorf("expected same host value, want: %q, got: %q", req.Host, tt.expReq.Host)
+			} else if ok {
+				hostVal, _ := hostnameExp.Expr.Value(eval.NewENVContext(nil))
+				hostname := seetie.ValueToString(hostVal)
+				if hostname != tt.expReq.Host {
+					t.Errorf("expected a configured request host: %q, got: %q", hostname, tt.expReq.Host)
+				}
 			}
 
 			if req.URL.Path != tt.expReq.URL.Path {
@@ -479,7 +503,7 @@ func TestProxy_ServeHTTP_Eval(t *testing.T) {
 
 	opts := &handler.ProxyOptions{
 		BackendName:      "test-origin",
-		Origin:           "http://" + origin.Listener.Addr().String(),
+		Context:          test.NewRemainContext("origin", "http://"+origin.Listener.Addr().String()),
 		CORS:             &handler.CORSOptions{},
 		RequestBodyLimit: 10,
 	}
@@ -633,6 +657,7 @@ func TestProxy_setRoundtripContext_Variables_json_body(t *testing.T) {
 		want      want
 	}{
 		{"method /w body", `
+		origin = "http://1.2.3.4/"
 		request_headers = {
 			x-test = req.json_body.foo
 		}`, []string{
@@ -646,10 +671,12 @@ func TestProxy_setRoundtripContext_Variables_json_body(t *testing.T) {
 			http.MethodOptions,
 		}, test.Header{"Content-Type": "application/json"}, `{"foo": "bar"}`, want{req: test.Header{"x-test": "bar"}}},
 		{"method /w body", `
+		origin = "http://1.2.3.4/"
 		request_headers = {
 			x-test = req.json_body.foo
 		}`, []string{http.MethodTrace}, test.Header{"Content-Type": "application/json"}, `{"foo": "bar"}`, want{req: test.Header{"x-test": ""}}},
 		{"method /wo body", `
+		origin = "http://1.2.3.4/"
 		request_headers = {
 			x-test = req.json_body.foo
 		}`, []string{
@@ -672,7 +699,6 @@ func TestProxy_setRoundtripContext_Variables_json_body(t *testing.T) {
 				proxy, _, _, closeFn := helper.NewProxy(&handler.ProxyOptions{
 					Context:          helper.NewProxyContext(tt.inlineCtx),
 					CORS:             &handler.CORSOptions{},
-					Origin:           "http://1.2.3.4/",
 					RequestBodyLimit: 64,
 				})
 
@@ -684,8 +710,7 @@ func TestProxy_setRoundtripContext_Variables_json_body(t *testing.T) {
 				}
 				req := httptest.NewRequest(method, "/", body)
 				tt.header.Set(req)
-				helper.Must(proxy.SetGetBody(req))
-				proxy.SetRoundtripContext(req, nil)
+				helper.Must(proxy.Director(req))
 
 				for k, v := range tt.want.req {
 					if req.Header.Get(k) != v {
diff --git a/internal/test/hcl.go b/internal/test/hcl.go
new file mode 100644
index 00000000..74e84256
--- /dev/null
+++ b/internal/test/hcl.go
@@ -0,0 +1,14 @@
+package test
+
+import (
+	"github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/hcltest"
+	"github.com/zclconf/go-cty/cty"
+)
+
+func NewRemainContext(name, value string) []hcl.Body {
+	expr := hcltest.MockExprLiteral(cty.StringVal(value))
+	return []hcl.Body{hcltest.MockBody(&hcl.BodyContent{Attributes: map[string]*hcl.Attribute{
+		name: {Name: name, Expr: expr},
+	}})}
+}
diff --git a/internal/test/helper_proxy.go b/internal/test/helper_proxy.go
index b6ba9ebd..85e620e0 100644
--- a/internal/test/helper_proxy.go
+++ b/internal/test/helper_proxy.go
@@ -22,8 +22,6 @@ func (h *Helper) NewProxy(opts *handler.ProxyOptions) (*handler.Proxy, *http.Cli
 	proxyOptions := &handler.ProxyOptions{
 		Context:     h.NewProxyContext(""),
 		BackendName: "HelperUpstream",
-		Hostname:    "upstream.helper",
-		Origin:      server.URL,
 		CORS:        &handler.CORSOptions{},
 	}
 	proxyOptions = proxyOptions.Merge(opts)
diff --git a/internal/test/test_backend.go b/internal/test/test_backend.go
index 0f50806c..f37acde0 100644
--- a/internal/test/test_backend.go
+++ b/internal/test/test_backend.go
@@ -43,6 +43,8 @@ func anything(rw http.ResponseWriter, req *http.Request) {
 	type anything struct {
 		Args                               url.Values
 		Headers                            http.Header
+		Host                               string
+		Path                               string
 		Method, RemoteAddr, Url, UserAgent string
 	}
 
@@ -51,7 +53,9 @@ func anything(rw http.ResponseWriter, req *http.Request) {
 	resp := &anything{
 		Args:       req.Form,
 		Headers:    req.Header.Clone(),
+		Host:       req.Host,
 		Method:     req.Method,
+		Path:       req.URL.Path,
 		RemoteAddr: req.RemoteAddr,
 		Url:        req.URL.String(),
 		UserAgent:  req.UserAgent(),
diff --git a/server/http_integration_test.go b/server/http_integration_test.go
index 9467386b..7767ef8b 100644
--- a/server/http_integration_test.go
+++ b/server/http_integration_test.go
@@ -3,6 +3,7 @@ package server_test
 import (
 	"bytes"
 	"context"
+	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"net"
@@ -14,6 +15,9 @@ import (
 	"testing"
 	"time"
 
+	"github.com/hashicorp/hcl/v2/hcltest"
+	"github.com/zclconf/go-cty/cty"
+
 	"github.com/sirupsen/logrus"
 	logrustest "github.com/sirupsen/logrus/hooks/test"
 
@@ -62,7 +66,20 @@ func newCouper(file string, helper *test.Helper) (func(), *logrustest.Hook) {
 	// TODO: limitation: no support for inline origin changes
 	if gatewayConf.Definitions != nil {
 		for _, backend := range gatewayConf.Definitions.Backend {
-			backend.Origin = testBackend.Addr()
+			backendSchema := config.Backend{}.Schema(false)
+			inlineSchema := config.Backend{}.Schema(true)
+			backendSchema.Attributes = append(backendSchema.Attributes, inlineSchema.Attributes...)
+			content, diags := backend.Options.Content(backendSchema)
+			if diags != nil && diags.HasErrors() {
+				helper.Must(diags)
+			}
+
+			if _, ok := content.Attributes["origin"]; !ok {
+				helper.Must(fmt.Errorf("backend requires an origin value"))
+			}
+
+			content.Attributes["origin"].Expr = hcltest.MockExprLiteral(cty.StringVal(testBackend.Addr()))
+			backend.Options = hcltest.MockBody(content)
 		}
 	}
 
@@ -93,6 +110,7 @@ func newClient() *http.Client {
 	dialer := &net.Dialer{}
 	return &http.Client{
 		Transport: &http.Transport{
+			DisableCompression: true,
 			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
 				_, port, _ := net.SplitHostPort(addr)
 				if port != "" {
@@ -439,3 +457,61 @@ func TestHTTPServer_XFHHeader(t *testing.T) {
 
 	cleanup(shutdown, t)
 }
+
+func TestHTTPServer_Endpoint_Evaluation(t *testing.T) {
+	client := newClient()
+
+	confPath := path.Join("testdata/integration/endpoint_eval/01_couper.hcl")
+	shutdown, _ := newCouper(confPath, test.New(t))
+
+	type expectation struct {
+		Host, Origin, Path string
+	}
+
+	type testCase struct {
+		reqPath string
+		exp     expectation
+	}
+
+	for _, tc := range []testCase{
+		{"/my-waffik/my.host.de/" + testBackend.Addr()[7:], expectation{
+			Host:   "my.host.de",
+			Origin: testBackend.Addr()[7:],
+			Path:   "/anything",
+		}},
+		{"/my-respo/my.host.com/" + testBackend.Addr()[7:], expectation{
+			Host:   "my.host.com",
+			Origin: testBackend.Addr()[7:],
+			Path:   "/anything",
+		}},
+	} {
+		t.Run("_"+tc.reqPath, func(subT *testing.T) {
+			helper := test.New(subT)
+
+			req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.reqPath, nil)
+			helper.Must(err)
+
+			res, err := client.Do(req)
+			helper.Must(err)
+
+			resBytes, err := ioutil.ReadAll(res.Body)
+			helper.Must(err)
+
+			_ = res.Body.Close()
+
+			var jsonResult expectation
+			err = json.Unmarshal(resBytes, &jsonResult)
+			if err != nil {
+				t.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes))
+			}
+
+			jsonResult.Origin = res.Header.Get("X-Origin")
+
+			if !reflect.DeepEqual(jsonResult, tc.exp) {
+				t.Errorf("want: %#v, got: %#v, payload:\n%s", tc.exp, jsonResult, string(resBytes))
+			}
+		})
+	}
+
+	cleanup(shutdown, t)
+}
diff --git a/server/http_test.go b/server/http_test.go
index a95d2947..6f4b8d68 100644
--- a/server/http_test.go
+++ b/server/http_test.go
@@ -60,7 +60,7 @@ func TestHTTPServer_ServeHTTP_Files(t *testing.T) {
 	httpConf := runtime.NewHTTPConfig(nil)
 	httpConf.ListenPort = 0 // random
 
-	conf, err := config.LoadBytes(confBytes.Bytes())
+	conf, err := config.LoadBytes(confBytes.Bytes(), "conf_test.hcl")
 	helper.Must(err)
 
 	srvConf, err := runtime.NewServerConfiguration(conf, httpConf, log.WithContext(nil))
@@ -152,7 +152,7 @@ func TestHTTPServer_ServeHTTP_Files2(t *testing.T) {
 	httpConf := runtime.NewHTTPConfig(nil)
 	httpConf.ListenPort = 0 // random
 
-	conf, err := config.LoadBytes(confBytes.Bytes())
+	conf, err := config.LoadBytes(confBytes.Bytes(), "conf_fileserving.hcl")
 	helper.Must(err)
 
 	error404Content := []byte("<html><body><h1>3001: Files route not found: My custom error template</h1></body></html>")
diff --git a/server/testdata/integration/endpoint_eval/01_couper.hcl b/server/testdata/integration/endpoint_eval/01_couper.hcl
new file mode 100644
index 00000000..a181ce51
--- /dev/null
+++ b/server/testdata/integration/endpoint_eval/01_couper.hcl
@@ -0,0 +1,27 @@
+server "api" {
+  error_file = "./../server_error.html"
+
+  api {
+    error_file = "./../api_error.json"
+
+    endpoint "/{path}/{hostname}/{origin}" {
+      path = "/unset/by/backend"
+      backend {
+        path = "/anything"
+        origin = "http://${req.path_param.origin}"
+        hostname = req.path_param.hostname
+        response_headers = {
+          x-origin = req.path_param.origin
+        }
+      }
+    }
+  }
+}
+
+definitions {
+  # backend origin within a definition block gets replaced with the integration test "anything" server.
+  backend "anything" {
+    path = "/anything"
+    origin = "http://anyserver/"
+  }
+}