Skip to content

Commit

Permalink
Merge branch 'master' into log-fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcel Ludwig authored Apr 6, 2021
2 parents 9ece79e + b0c54e2 commit c8146ae
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 28 deletions.
2 changes: 2 additions & 0 deletions config/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ type API struct {
DisableAccessControl []string `hcl:"disable_access_control,optional"`
Endpoints Endpoints `hcl:"endpoint,block"`
ErrorFile string `hcl:"error_file,optional"`
// internally used
CatchAllEndpoint *Endpoint
}

// APIs represents a list of <API> objects.
Expand Down
23 changes: 23 additions & 0 deletions config/configload/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package configload
import (
"fmt"
"io/ioutil"
"net/http"
"path/filepath"
"regexp"

Expand Down Expand Up @@ -141,6 +142,8 @@ func LoadConfig(body hcl.Body, src []byte, filename string) (*config.Couper, err
if err != nil {
return nil, err
}

apiBlock.CatchAllEndpoint = createCatchAllEndpoint()
}

// standalone endpoints
Expand Down Expand Up @@ -522,6 +525,26 @@ func newBackend(definedBackends Backends, inlineConfig config.Inline) (hcl.Body,
return bend, nil
}

func createCatchAllEndpoint() *config.Endpoint {
responseBody := hclbody.New(&hcl.BodyContent{
Attributes: map[string]*hcl.Attribute{
"status": {
Name: "status",
Expr: &hclsyntax.LiteralValueExpr{
Val: cty.NumberIntVal(http.StatusNotFound),
},
},
},
})

return &config.Endpoint{
Remain: hclbody.New(&hcl.BodyContent{}),
Response: &config.Response{
Remain: responseBody,
},
}
}

func newOAuthBackend(definedBackends Backends, parent hcl.Body) (hcl.Body, error) {
innerContent, err := contentByType(oauth2, parent)
if err != nil {
Expand Down
54 changes: 48 additions & 6 deletions config/runtime/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,12 @@ func NewServerConfiguration(
}

endpointsPatterns := make(map[string]bool)
endpointsMap, err := newEndpointMap(srvConf, serverOptions)
if err != nil {
return nil, err
}

for endpointConf, parentAPI := range newEndpointMap(srvConf) {
for endpointConf, parentAPI := range endpointsMap {
var basePath string
var corsOptions *middleware.CORSOptions
var errTpl *errors.Template
Expand Down Expand Up @@ -247,10 +251,10 @@ func NewServerConfiguration(

// setACHandlerFn individual wrap for access_control configuration per endpoint
setACHandlerFn := func(protectedHandler http.Handler) {
accessControl := config.NewAccessControl(srvConf.AccessControl, srvConf.DisableAccessControl)
accessControl := newAC(srvConf, parentAPI)

if parentAPI != nil {
accessControl = accessControl.Merge(config.NewAccessControl(parentAPI.AccessControl, parentAPI.DisableAccessControl))
if parentAPI != nil && parentAPI.CatchAllEndpoint == endpointConf {
protectedHandler = errTpl.ServeError(errors.APIRouteNotFound)
}

endpointHandlers[endpointConf] = configureProtectedHandler(accessControls, errTpl, accessControl,
Expand Down Expand Up @@ -627,20 +631,48 @@ func getPortsHostsList(hosts []string, defaultPort int) (Ports, error) {
return portsHosts, nil
}

func newEndpointMap(srvConf *config.Server) endpointMap {
func newEndpointMap(srvConf *config.Server, serverOptions *server.Options) (endpointMap, error) {
endpoints := make(endpointMap)

apiBasePaths := make(map[string]struct{})

for _, api := range srvConf.APIs {
basePath := serverOptions.APIBasePaths[api]

var filesBasePath, spaBasePath string
if serverOptions.FilesBasePath != "" {
filesBasePath = serverOptions.FilesBasePath
}
if serverOptions.SPABasePath != "" {
spaBasePath = serverOptions.SPABasePath
}

isAPIBasePathUniqueToFilesAndSPA := basePath != filesBasePath && basePath != spaBasePath

if _, ok := apiBasePaths[basePath]; ok {
return nil, fmt.Errorf("API paths must be unique")
}

apiBasePaths[basePath] = struct{}{}

for _, endpoint := range api.Endpoints {
endpoints[endpoint] = api

if endpoint.Pattern == "/**" {
isAPIBasePathUniqueToFilesAndSPA = false
}
}

if isAPIBasePathUniqueToFilesAndSPA && len(newAC(srvConf, api).List()) > 0 {
endpoints[api.CatchAllEndpoint] = api
}
}

for _, endpoint := range srvConf.Endpoints {
endpoints[endpoint] = nil
}

return endpoints
return endpoints, nil
}

// parseDuration sets the target value if the given duration string is not empty.
Expand All @@ -661,3 +693,13 @@ func parseBodyLimit(limit string) (int64, error) {
}
return units.FromHumanSize(requestBodyLimit)
}

func newAC(srvConf *config.Server, api *config.API) config.AccessControl {
accessControl := config.NewAccessControl(srvConf.AccessControl, srvConf.DisableAccessControl)

if api != nil {
accessControl = accessControl.Merge(config.NewAccessControl(api.AccessControl, api.DisableAccessControl))
}

return accessControl
}
4 changes: 3 additions & 1 deletion config/runtime/server_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/avenga/couper/config"
"github.com/avenga/couper/config/runtime/server"
"github.com/avenga/couper/eval"
)

Expand Down Expand Up @@ -53,7 +54,8 @@ func TestServer_getEndpointsList(t *testing.T) {
},
}

endpoints := newEndpointMap(srvConf)
serverOptions, _ := server.NewServerOptions(nil)
endpoints, _ := newEndpointMap(srvConf, serverOptions)
if l := len(endpoints); l != 4 {
t.Fatalf("Expected 4 endpointes, given %d", l)
}
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ as json error with an error body payload. This can be customized via `error_file
| [Endpoint Block(s)](#endpoint-block) | Configures specific endpoint(s) for current `API Block` context. |
| [CORS Block](#cors-block) | Configures CORS behavior for the current `API Block` context. Overrides the CORS behavior of the parent [Server Block](#server-block). |
| **Attributes** | **Description** |
| `base_path` | <ul><li>Optional.</li><li>Configures the path prefix for all requests.</li><li>*Example:* `base_path = "/v1"`</li></ul> |
| `base_path` | <ul><li>Optional.</li><li>Configures the path prefix for all requests.</li><li>Must be unique if multiple API Blocks are defined.</li><li>*Example:* `base_path = "/v1"`</li></ul> |
| `error_file` | <ul><li>Optional.</li><li>Location of the error file template.</li><li>*Example:* `error_file = "./my_error_body.json"`</li></ul> |
| `access_control` | <ul><li>Optional.</li><li>Sets predefined [Access Control](#access-control) for current `API Block` context.</li><li>*Example:* `access_control = ["foo"]`</li><li>&#9888; Inherited by nested blocks.</li></ul> |

Expand Down
7 changes: 6 additions & 1 deletion handler/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ func (f *File) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}

func (f *File) serveDirectory(reqPath string, rw http.ResponseWriter, req *http.Request) {
if !f.HasResponse(req) {
f.srvOptions.FilesErrTpl.ServeError(errors.FilesRouteNotFound).ServeHTTP(rw, req)
return
}

if !strings.HasSuffix(reqPath, "/") {
rw.Header().Set("Location", utils.JoinPath(req.URL.Path, "/"))
rw.WriteHeader(http.StatusFound)
Expand Down Expand Up @@ -116,7 +121,7 @@ func (f *File) HasResponse(req *http.Request) bool {
if info.IsDir() {
reqPath = path.Join(reqPath, "/", dirIndexFile)

file, info, err := f.openDocRootFile(reqPath)
file, info, err = f.openDocRootFile(reqPath)
if err != nil {
return false
}
Expand Down
63 changes: 63 additions & 0 deletions server/http_endpoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"
"net/http/httptest"
"path"
"reflect"
"strings"
"testing"
"time"
Expand All @@ -17,6 +18,68 @@ import (

const testdataPath = "testdata/endpoints"

func TestEndpoints_Protected404(t *testing.T) {
client := newClient()

type expectation struct {
Code int
ResponseStatus int
}

type testCase struct {
auth string
path string
exp expectation
}

shutdown, _ := newCouper("testdata/endpoints/07_couper.hcl", test.New(t))
defer shutdown()

for _, tc := range []testCase{
{"", "/v1/anything", expectation{
Code: 5002, ResponseStatus: 0,
}},
{"secret", "/v1/anything", expectation{
Code: 0, ResponseStatus: 200,
}},
{"", "/v1/xxx", expectation{
Code: 5002, ResponseStatus: 0,
}},
{"secret", "/v1/xxx", expectation{
Code: 0, ResponseStatus: 404,
}},
} {
t.Run(tc.path, func(subT *testing.T) {
helper := test.New(subT)

req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil)
helper.Must(err)

if tc.auth != "" {
req.SetBasicAuth("", tc.auth)
}

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))
}

if !reflect.DeepEqual(jsonResult, tc.exp) {
t.Errorf("\nwant: \n%#v\ngot: \n%#v\npayload:\n%s", tc.exp, jsonResult, string(resBytes))
}
})
}
}

func TestEndpoints_ProxyReqRes(t *testing.T) {
client := newClient()
helper := test.New(t)
Expand Down
37 changes: 18 additions & 19 deletions server/http_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -702,20 +702,20 @@ func TestHTTPServer_PathPrefix(t *testing.T) {
}

for _, tc := range []testCase{
{"/", expectation{
Path: "/xxx/xxx/",
{"/v1", expectation{
Path: "/xxx/xxx/v1",
}},
{"/uuu/foo", expectation{
{"/v1/uuu/foo", expectation{
Path: "/xxx/xxx/api/foo",
}},
{"/vvv/foo", expectation{
{"/v1/vvv/foo", expectation{
Path: "/xxx/xxx/api/foo",
}},
{"/yyy", expectation{
Path: "/yyy",
{"/v2/yyy", expectation{
Path: "/v2/yyy",
}},
{"/zzz", expectation{
Path: "/zzz/zzz",
{"/v3/zzz", expectation{
Path: "/zzz/v3/zzz",
}},
} {
t.Run("_"+tc.path, func(subT *testing.T) {
Expand Down Expand Up @@ -2165,11 +2165,10 @@ func TestAccessControl_Files_SPA(t *testing.T) {
}

for _, tc := range []testCase{
// FIXME: https://github.com/avenga/couper/issues/143
// {"/favicon.ico", "", http.StatusUnauthorized},
// {"/robots.txt", "", http.StatusUnauthorized},
// {"/app", "", http.StatusUnauthorized},
// {"/app/1", "", http.StatusUnauthorized},
{"/favicon.ico", "", http.StatusUnauthorized},
{"/robots.txt", "", http.StatusUnauthorized},
{"/app", "", http.StatusUnauthorized},
{"/app/1", "", http.StatusUnauthorized},
{"/favicon.ico", "hans", http.StatusNotFound},
{"/robots.txt", "hans", http.StatusOK},
{"/app", "hans", http.StatusOK},
Expand Down Expand Up @@ -2211,14 +2210,14 @@ func TestHTTPServer_MultiAPI(t *testing.T) {
defer shutdown()

for _, tc := range []testCase{
{"/xxx", expectation{
Path: "/xxx",
{"/v1/xxx", expectation{
Path: "/v1/xxx",
}},
{"/yyy", expectation{
Path: "/yyy",
{"/v2/yyy", expectation{
Path: "/v2/yyy",
}},
{"/zzz", expectation{
Path: "/zzz",
{"/v3/zzz", expectation{
Path: "/v3/zzz",
}},
} {
t.Run(tc.path, func(subT *testing.T) {
Expand Down
4 changes: 4 additions & 0 deletions server/mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ func (m *Mux) FindHandler(req *http.Request) http.Handler {
node, paramValues = m.match(m.spaRoot, req)

if node == nil {
if fileHandler != nil {
return fileHandler
}

if isConfigured(m.opts.ServerOptions.FilesBasePath) && matchesPath(m.opts.ServerOptions.FilesBasePath, req.URL.Path) {
return m.opts.ServerOptions.FilesErrTpl.ServeError(errors.FilesRouteNotFound)
}
Expand Down
22 changes: 22 additions & 0 deletions server/testdata/endpoints/07_couper.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
server "protected" {
api {
base_path = "/v1"
error_file = "./../integration/api_error.json"
access_control = ["BA"]

endpoint "/{path}" {
proxy {
backend {
path = req.path_params.path
origin = env.COUPER_TEST_BACKEND_ADDR
}
}
}
}
}

definitions {
basic_auth "BA" {
password = "secret"
}
}
3 changes: 3 additions & 0 deletions server/testdata/integration/api/05_couper.hcl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
server "multi-api" {
api {
base_path = "/v1"
endpoint "/xxx" {
proxy {
backend {
Expand All @@ -10,6 +11,7 @@ server "multi-api" {
}

api {
base_path = "/v2"
endpoint "/yyy" {
proxy {
backend {
Expand All @@ -20,6 +22,7 @@ server "multi-api" {
}

api {
base_path = "/v3"
endpoint "/zzz" {
proxy {
backend {
Expand Down
Loading

0 comments on commit c8146ae

Please sign in to comment.