Skip to content

Commit

Permalink
Merge branch 'master' into TT-2539/transaction_logs
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffy-mathew authored Sep 12, 2024
2 parents 72c2a42 + 89bcc57 commit b1dc70c
Show file tree
Hide file tree
Showing 32 changed files with 1,419 additions and 415 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ jobs:
if: ${{ github.event_name == 'pull_request' }}
run: git fetch origin ${{ github.base_ref }}

- name: Print CPU info
run: grep '^model name' /proc/cpuinfo

- name: Print Go env
run: go env

- name: Run Gateway Tests
id: ci-tests
run: |
Expand Down
2 changes: 1 addition & 1 deletion .taskfiles/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ tasks:
package:
sh: go mod edit -json | jq .Module.Path -r
packages:
sh: go list ./... | sed -e 's|{{.package}}|.|g'
sh: go list ./... | tail -n +2 | sed -e 's|{{.package}}|.|g'
cmds:
- defer: { task: services:down }
- rm -rf coverage && mkdir -p coverage
Expand Down
6 changes: 6 additions & 0 deletions cli/linter/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,12 @@
"enable_strict_routes": {
"type": "boolean"
},
"enable_path_prefix_matching": {
"type": "boolean"
},
"enable_path_suffix_matching": {
"type": "boolean"
},
"flush_interval": {
"type": "integer"
},
Expand Down
43 changes: 43 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,49 @@ type HttpServerOptionsConfig struct {
// Regular expressions and parameterized routes will be left alone regardless of this setting.
EnableStrictRoutes bool `json:"enable_strict_routes"`

// EnablePathPrefixMatching changes the URL matching from wildcard mode to prefix mode.
// For example, `/json` matches `*/json*` by current default behaviour.
// If prefix matching is enabled, the match will be performed as a prefix match (`/json*`).
//
// The `/json` url would be matched as `^/json` against the following paths:
//
// - Full listen path and versioning URL (`/listen-path/v4/json`)
// - Stripped listen path URL (`/v4/json`)
// - Stripped version information (`/json`) - match.
//
// If versioning is disabled then the following URLs are considered:
//
// - Full listen path and endpoint (`/listen-path/json`)
// - Stripped listen path (`/json`) - match.
//
// For inputs that start with `/`, a prefix match is ensured by
// prepending the start of string `^` caret.
//
// For all other cases, the pattern remains unmodified.
//
// Combine this option with `enable_path_suffix_matching` to achieve
// exact url matching with `/json` being evaluated as `^/json$`.
EnablePathPrefixMatching bool `json:"enable_path_prefix_matching"`

// EnablePathSuffixMatching changes the URL matching to match as a suffix.
// For example: `/json` is matched as `/json$` against the following paths:
//
// - Full listen path and versioning URL (`/listen-path/v4/json`)
// - Stripped listen path URL (`/v4/json`)
// - Stripped version information (`/json`) - match.
//
// If versioning is disabled then the following URLs are considered:
//
// - Full listen path and endpoint (`/listen-path/json`)
// - Stripped listen path (`/json`) - match.
//
// If the input pattern already ends with a `$` (`/json$`)
// then the pattern remains unmodified.
//
// Combine this option with `enable_path_prefix_matching` to achieve
// exact url matching with `/json` being evaluated as `^/json$`.
EnablePathSuffixMatching bool `json:"enable_path_suffix_matching"`

// Disable TLS verification. Required if you are using self-signed certificates.
SSLInsecureSkipVerify bool `json:"ssl_insecure_skip_verify"`

Expand Down
4 changes: 3 additions & 1 deletion docker/services/httpbin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ services:
image: tykio/ci-tools:latest
networks:
- proxy
ports:
- 3123:3123
volumes:
- ./logs:/logs:rw
entrypoint:
- /usr/local/bin/httpbin-logserver
command:
- '-addr'
- ':80'
- ':3123'
- '-output'
- '/logs/service.json'
56 changes: 44 additions & 12 deletions gateway/api_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ const (
// path is on any of the white, black or ignored lists. This is generated as part of the
// configuration init
type URLSpec struct {
Spec *regexp.Regexp
spec *regexp.Regexp

Status URLStatus
MethodActions map[string]apidef.EndpointMethodMeta
Whitelist apidef.EndPointMeta
Expand Down Expand Up @@ -817,21 +818,28 @@ func (a APIDefinitionLoader) getPathSpecs(apiVersionDef apidef.VersionInfo, conf
return combinedPath, len(whiteListPaths) > 0
}

// match mux tags, `{id}`.
var apiLangIDsRegex = regexp.MustCompile(`{([^}]+)}`)

func (a APIDefinitionLoader) generateRegex(stringSpec string, newSpec *URLSpec, specType URLStatus, conf config.Config) {
// replace mux named parameters with regex path match
asRegexStr := apiLangIDsRegex.ReplaceAllString(stringSpec, `([^/]+)`)
var (
pattern string
err error
)
// Hook per-api settings here via newSpec *URLSpec
isPrefixMatch := conf.HttpServerOptions.EnablePathPrefixMatching
isSuffixMatch := conf.HttpServerOptions.EnablePathSuffixMatching
isIgnoreCase := newSpec.IgnoreCase || conf.IgnoreEndpointCase

pattern = httputil.PreparePathRegexp(stringSpec, isPrefixMatch, isSuffixMatch)

// Case insensitive match
if newSpec.IgnoreCase || conf.IgnoreEndpointCase {
asRegexStr = "(?i)" + asRegexStr
if isIgnoreCase {
pattern = "(?i)" + pattern
}

asRegex, _ := regexp.Compile(asRegexStr)
asRegex, err := regexp.Compile(pattern)
log.WithError(err).Debugf("URLSpec: %s => %s type=%d", stringSpec, pattern, specType)

newSpec.Status = specType
newSpec.Spec = asRegex
newSpec.spec = asRegex
}

func (a APIDefinitionLoader) compilePathSpec(paths []string, specType URLStatus, conf config.Config) []URLSpec {
Expand Down Expand Up @@ -1493,7 +1501,7 @@ func (a *APISpec) getURLStatus(stat URLStatus) RequestStatus {
// URLAllowedAndIgnored checks if a url is allowed and ignored.
func (a *APISpec) URLAllowedAndIgnored(r *http.Request, rxPaths []URLSpec, whiteListStatus bool) (RequestStatus, interface{}) {
for i := range rxPaths {
if !rxPaths[i].Spec.MatchString(r.URL.Path) {
if !rxPaths[i].matchesPath(r.URL.Path, a) {
continue
}

Expand All @@ -1504,7 +1512,7 @@ func (a *APISpec) URLAllowedAndIgnored(r *http.Request, rxPaths []URLSpec, white

// Check if ignored
for i := range rxPaths {
if !rxPaths[i].Spec.MatchString(r.URL.Path) {
if !rxPaths[i].matchesPath(r.URL.Path, a) {
continue
}

Expand Down Expand Up @@ -1776,10 +1784,34 @@ func (a *APISpec) Version(r *http.Request) (*apidef.VersionInfo, RequestStatus)
return &version, StatusOk
}

// StripListenPath will strip the listen path from the URL, keeping version in tact.
func (a *APISpec) StripListenPath(reqPath string) string {
return httputil.StripListenPath(a.Proxy.ListenPath, reqPath)
}

// StripVersionPath will strip the version from the URL. The input URL
// should already have listen path stripped.
func (a *APISpec) StripVersionPath(reqPath string) string {
// First part of the url is the version fragment
part := strings.Split(strings.Trim(reqPath, "/"), "/")[0]

matchesUrlVersioningPattern := true
if a.VersionDefinition.UrlVersioningPattern != "" {
re, err := regexp.Compile(a.VersionDefinition.UrlVersioningPattern)
if err != nil {
log.Error("Error compiling versioning pattern: ", err)
} else {
matchesUrlVersioningPattern = re.Match([]byte(part))
}
}

if (a.VersionDefinition.StripVersioningData || a.VersionDefinition.StripPath) && matchesUrlVersioningPattern {
return strings.Replace(reqPath, "/"+part+"/", "/", 1)
}

return reqPath
}

func (a *APISpec) SanitizeProxyPaths(r *http.Request) {
if !a.Proxy.StripListenPath {
return
Expand Down
23 changes: 11 additions & 12 deletions gateway/api_definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,13 +412,15 @@ func TestConflictingPaths(t *testing.T) {

ts.Run(t, []test.TestCase{
// Should ignore auth check
{Method: "POST", Path: "/customer-servicing/documents/metadata/purge", Code: http.StatusOK},
{Method: "GET", Path: "/customer-servicing/documents/metadata/{id}", Code: http.StatusOK},
{Method: "POST", Path: "/metadata/purge", Code: http.StatusOK},
{Method: "GET", Path: "/metadata/{id}", Code: http.StatusOK},
}...)
}

func TestIgnored(t *testing.T) {
ts := StartTest(nil)
ts := StartTest(func(c *config.Config) {
c.HttpServerOptions.EnablePathPrefixMatching = true
})
defer ts.Close()

t.Run("Extended Paths", func(t *testing.T) {
Expand All @@ -445,14 +447,13 @@ func TestIgnored(t *testing.T) {
{Path: "/ignored/literal", Code: http.StatusOK},
{Path: "/ignored/123/test", Code: http.StatusOK},
// Only GET is ignored
{Method: "POST", Path: "/ext/ignored/literal", Code: 401},
{Method: "POST", Path: "/ext/ignored/literal", Code: http.StatusUnauthorized},

{Path: "/", Code: 401},
{Path: "/", Code: http.StatusUnauthorized},
}...)
})

t.Run("Simple Paths", func(t *testing.T) {

ts.Gw.BuildAndLoadAPI(func(spec *APISpec) {
UpdateAPIVersion(spec, "v1", func(v *apidef.VersionInfo) {
v.Paths.Ignored = []string{"/ignored/literal", "/ignored/{id}/test"}
Expand All @@ -467,15 +468,14 @@ func TestIgnored(t *testing.T) {
// Should ignore auth check
{Path: "/ignored/literal", Code: http.StatusOK},
{Path: "/ignored/123/test", Code: http.StatusOK},
// All methods ignored
{Method: "POST", Path: "/ext/ignored/literal", Code: http.StatusOK},

{Path: "/", Code: 401},
{Method: "POST", Path: "/ext/ignored/literal", Code: http.StatusUnauthorized},

{Path: "/", Code: http.StatusUnauthorized},
}...)
})

t.Run("With URL rewrite", func(t *testing.T) {

ts.Gw.BuildAndLoadAPI(func(spec *APISpec) {
UpdateAPIVersion(spec, "v1", func(v *apidef.VersionInfo) {
v.ExtendedPaths.URLRewrite = []apidef.URLRewriteMeta{{
Expand Down Expand Up @@ -510,7 +510,6 @@ func TestIgnored(t *testing.T) {
})

t.Run("Case Sensitivity", func(t *testing.T) {

spec := BuildAPI(func(spec *APISpec) {
UpdateAPIVersion(spec, "v1", func(v *apidef.VersionInfo) {
v.ExtendedPaths.Ignored = []apidef.EndPointMeta{{Path: "/Foo", IgnoreCase: false}, {Path: "/bar", IgnoreCase: true}}
Expand Down Expand Up @@ -1032,7 +1031,7 @@ func (ts *Test) testPrepareDefaultVersion() (string, *APISpec) {
func TestGetVersionFromRequest(t *testing.T) {

versionInfo := apidef.VersionInfo{}
versionInfo.Paths.WhiteList = []string{"/foo"}
versionInfo.Paths.WhiteList = []string{"/foo", "/v3/foo"}
versionInfo.Paths.BlackList = []string{"/bar"}

t.Run("Header location", func(t *testing.T) {
Expand Down
14 changes: 11 additions & 3 deletions gateway/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ func TestApiHandlerPostDupPath(t *testing.T) {
}

func TestKeyHandler(t *testing.T) {
test.Exclusive(t) // Uses DeleteAllKeys, need to limit parallelism.

ts := StartTest(nil)
defer ts.Close()

Expand Down Expand Up @@ -338,7 +340,7 @@ func TestKeyHandler(t *testing.T) {
},
}...)

ts.Gw.GlobalSessionManager.Store().DeleteAllKeys()
ts.Gw.GlobalSessionManager.Store().DeleteAllKeys() // exclusive
})

_, knownKey := ts.CreateSession(func(s *user.SessionState) {
Expand Down Expand Up @@ -644,6 +646,8 @@ func BenchmarkKeyHandler_CreateKeyHandler(b *testing.B) {
}

func TestKeyHandler_DeleteKeyWithQuota(t *testing.T) {
test.Exclusive(t) // Uses quota, need to limit parallelism due to DeleteAllKeys.

const testAPIID = "testAPIID"
const orgId = "default"

Expand Down Expand Up @@ -826,6 +830,8 @@ func TestUpdateKeyWithCert(t *testing.T) {
}

func TestKeyHandler_CheckKeysNotDuplicateOnUpdate(t *testing.T) {
test.Exclusive(t) // Uses DeleteAllKeys, need to limit parallelism.

ts := StartTest(nil)
defer ts.Close()

Expand Down Expand Up @@ -878,7 +884,7 @@ func TestKeyHandler_CheckKeysNotDuplicateOnUpdate(t *testing.T) {

for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
ts.Gw.GlobalSessionManager.Store().DeleteAllKeys()
ts.Gw.GlobalSessionManager.Store().DeleteAllKeys() // exclusive
session := CreateStandardSession()
session.AccessRights = map[string]user.AccessDefinition{"test": {
APIID: "test", Versions: []string{"v1"},
Expand Down Expand Up @@ -906,6 +912,8 @@ func TestKeyHandler_CheckKeysNotDuplicateOnUpdate(t *testing.T) {
}

func TestHashKeyHandler(t *testing.T) {
test.Exclusive(t) // Uses DeleteAllKeys, need to limit parallelism.

conf := func(globalConf *config.Config) {
// make it to use hashes for Redis keys
globalConf.HashKeys = true
Expand All @@ -932,7 +940,7 @@ func TestHashKeyHandler(t *testing.T) {
gwConf := ts.Gw.GetConfig()
gwConf.HashKeyFunction = tc.hashFunction
ts.Gw.SetConfig(gwConf)
ok := ts.Gw.GlobalSessionManager.Store().DeleteAllKeys()
ok := ts.Gw.GlobalSessionManager.Store().DeleteAllKeys() // exclusive
assert.True(t, ok)

t.Run(fmt.Sprintf("%sHash fn: %s", tc.desc, tc.hashFunction), func(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions gateway/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@ func TestSkipTargetPassEscapingOffWithSkipURLCleaningTrue(t *testing.T) {
}

func TestQuota(t *testing.T) {
test.Exclusive(t) // Uses quota, need to limit parallelism due to DeleteAllKeys.

ts := StartTest(nil)
defer ts.Close()

Expand Down
Loading

0 comments on commit b1dc70c

Please sign in to comment.