diff --git a/pkg/redirects/redirect.go b/pkg/redirects/redirect.go index a55d7d8..da0af58 100644 --- a/pkg/redirects/redirect.go +++ b/pkg/redirects/redirect.go @@ -16,7 +16,6 @@ package redirects import ( "fmt" - "net/url" "regexp" "strings" ) @@ -45,6 +44,20 @@ type RedirectURIMatchConfig struct { domainRegex *regexp.Regexp } +// NewRedirectURIMatchConfig return an instance of *RedirectURIMatchConfig. +func NewRedirectURIMatchConfig(domainMatchType, domain, pathMatchType, path string) (*RedirectURIMatchConfig, error) { + c := &RedirectURIMatchConfig{ + PathMatchType: pathMatchType, + Path: path, + DomainMatchType: domainMatchType, + Domain: domain, + } + if err := c.Validate(); err != nil { + return nil, err + } + return c, nil +} + // Validate validates RedirectURIMatchConfig. func (c *RedirectURIMatchConfig) Validate() error { switch c.PathMatchType { @@ -110,73 +123,3 @@ func (c *RedirectURIMatchConfig) Validate() error { return nil } - -// Match matches HTTP URL to the bypass configuration. -func Match(u *url.URL, cfgs []*RedirectURIMatchConfig) bool { - pathMatched := false - domainMatched := false - - for _, cfg := range cfgs { - switch cfg.pathMatch { - case matchExact: - if cfg.Path == u.Path { - pathMatched = true - } - case matchPartial: - if strings.Contains(u.Path, cfg.Path) { - pathMatched = true - } - case matchPrefix: - if strings.HasPrefix(u.Path, cfg.Path) { - pathMatched = true - } - case matchSuffix: - if strings.HasSuffix(u.Path, cfg.Path) { - pathMatched = true - } - case matchRegex: - if cfg.pathRegex.MatchString(u.Path) { - pathMatched = true - } - } - if pathMatched { - break - } - } - if !pathMatched { - return false - } - - for _, cfg := range cfgs { - switch cfg.domainMatch { - case matchExact: - if cfg.Domain == u.Host { - domainMatched = true - } - case matchPartial: - if strings.Contains(u.Host, cfg.Domain) { - domainMatched = true - } - case matchPrefix: - if strings.HasPrefix(u.Host, cfg.Domain) { - domainMatched = true - } - case matchSuffix: - if strings.HasSuffix(u.Host, cfg.Domain) { - domainMatched = true - } - case matchRegex: - if cfg.domainRegex.MatchString(u.Host) { - domainMatched = true - } - } - if domainMatched { - break - } - } - if !domainMatched { - return false - } - - return false -} diff --git a/pkg/redirects/redirect_match.go b/pkg/redirects/redirect_match.go new file mode 100644 index 0000000..e1fa1d0 --- /dev/null +++ b/pkg/redirects/redirect_match.go @@ -0,0 +1,88 @@ +// Copyright 2024 Paul Greenberg greenpau@outlook.com +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package redirects + +import ( + "net/url" + "strings" +) + +// Match matches HTTP URL to the bypass configuration. +func Match(u *url.URL, cfgs []*RedirectURIMatchConfig) bool { + pathMatched := false + domainMatched := false + + for _, cfg := range cfgs { + switch cfg.pathMatch { + case matchExact: + if cfg.Path == u.Path { + pathMatched = true + } + case matchPartial: + if strings.Contains(u.Path, cfg.Path) { + pathMatched = true + } + case matchPrefix: + if strings.HasPrefix(u.Path, cfg.Path) { + pathMatched = true + } + case matchSuffix: + if strings.HasSuffix(u.Path, cfg.Path) { + pathMatched = true + } + case matchRegex: + if cfg.pathRegex.MatchString(u.Path) { + pathMatched = true + } + } + if pathMatched { + break + } + } + if !pathMatched { + return false + } + for _, cfg := range cfgs { + switch cfg.domainMatch { + case matchExact: + if cfg.Domain == u.Host { + domainMatched = true + } + case matchPartial: + if strings.Contains(u.Host, cfg.Domain) { + domainMatched = true + } + case matchPrefix: + if strings.HasPrefix(u.Host, cfg.Domain) { + domainMatched = true + } + case matchSuffix: + if strings.HasSuffix(u.Host, cfg.Domain) { + domainMatched = true + } + case matchRegex: + if cfg.domainRegex.MatchString(u.Host) { + domainMatched = true + } + } + if domainMatched { + break + } + } + if !domainMatched { + return false + } + return true +} diff --git a/pkg/redirects/redirect_match_test.go b/pkg/redirects/redirect_match_test.go new file mode 100644 index 0000000..4464a2b --- /dev/null +++ b/pkg/redirects/redirect_match_test.go @@ -0,0 +1,103 @@ +// Copyright 2024 Paul Greenberg greenpau@outlook.com +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package redirects + +import ( + "fmt" + + "testing" + + "github.com/greenpau/go-authcrunch/internal/tests" +) + +func TestMatch(t *testing.T) { + + testInput1 := "https://authcrunch.com/?redirect_uri=https://authcrunch.com/path/to/login" + + testcases := []struct { + name string + config []string + input string + want map[string]interface{} + shouldErr bool + err error + }{ + { + name: "test matched exact domain and exact path match", + config: []string{"exact", "authcrunch.com", "exact", "/path/to/login"}, + input: testInput1, + want: map[string]interface{}{ + "match": true, + "domain": "authcrunch.com", + "path": "/path/to/login", + }, + }, + // { + // name: "text exact domain and partial path match", + // config: []string{"exact", "authcrunch.com", "partial", "/to/"}, + // input: testInput1, + // want: map[string]interface{}{ + // "match": true, + // "domain": "authcrunch.com", + // "path": "/path/to/login", + // }, + // }, + // { + // name: "text exact domain and prefix path match", + // config: []string{"exact", "authcrunch.com", "prefix", "/path/to"}, + // input: testInput1, + // want: map[string]interface{}{ + // "match": true, + // "domain": "authcrunch.com", + // "path": "/path/to/login", + // }, + // }, + /* + { + name: "test invalid config", + shouldErr: true, + err: fmt.Errorf("TBD"), + }, + */ + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + got := make(map[string]interface{}) + msgs := []string{fmt.Sprintf("test name: %s", tc.name)} + msgs = append(msgs, fmt.Sprintf("input:\n%s", tc.input)) + + c, err := NewRedirectURIMatchConfig(tc.config[0], tc.config[1], tc.config[2], tc.config[3]) + if err != nil { + t.Fatal(err) + } + + redirURI := ParseRedirectURI(tc.input) + if redirURI == nil { + t.Fatalf("redirect uri not found in the input: %s", tc.input) + } + + got["match"] = Match(redirURI, []*RedirectURIMatchConfig{c}) + got["domain"] = redirURI.Host + got["path"] = redirURI.Path + + // got, err := Parse(tc.input) + if tests.EvalErrWithLog(t, err, "Match", tc.shouldErr, tc.err, msgs) { + return + } + + tests.EvalObjectsWithLog(t, "Output", tc.want, got, msgs) + }) + } +} diff --git a/pkg/redirects/redirect_parser.go b/pkg/redirects/redirect_parser.go new file mode 100644 index 0000000..469e02f --- /dev/null +++ b/pkg/redirects/redirect_parser.go @@ -0,0 +1,38 @@ +// Copyright 2024 Paul Greenberg greenpau@outlook.com +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package redirects + +import ( + "net/url" + "strings" +) + +// ParseRedirectURI parses redirect_uri from URL string. +func ParseRedirectURI(s string) *url.URL { + parsedURL, err := url.Parse(s) + if err != nil { + return nil + } + queryParams := parsedURL.Query() + rawRedirectURI := queryParams.Get("redirect_uri") + if strings.HasPrefix(rawRedirectURI, "/") { + return nil + } + parsedRedirectURI, err := url.Parse(rawRedirectURI) + if err != nil { + return nil + } + return parsedRedirectURI +} diff --git a/pkg/redirects/redirect_parser_test.go b/pkg/redirects/redirect_parser_test.go new file mode 100644 index 0000000..b87a94f --- /dev/null +++ b/pkg/redirects/redirect_parser_test.go @@ -0,0 +1,72 @@ +// Copyright 2024 Paul Greenberg greenpau@outlook.com +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package redirects + +import ( + "fmt" + + "testing" + + "github.com/greenpau/go-authcrunch/internal/tests" +) + +func TestParseRedirectURI(t *testing.T) { + + testInput1 := "https://authcrunch.com/?redirect_uri=https://authcrunch.com/path/to/login" + + testcases := []struct { + name string + input string + want map[string]interface{} + shouldErr bool + err error + }{ + { + name: "text valid redirect uri", + input: testInput1, + want: map[string]interface{}{ + "redirect_uri": "https://authcrunch.com/path/to/login", + }, + }, + /* + { + name: "test invalid config", + shouldErr: true, + err: fmt.Errorf("TBD"), + }, + */ + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + var err error + got := make(map[string]interface{}) + msgs := []string{fmt.Sprintf("test name: %s", tc.name)} + msgs = append(msgs, fmt.Sprintf("input:\n%s", tc.input)) + + redirURI := ParseRedirectURI(tc.input) + if redirURI == nil { + err = fmt.Errorf("redirect uri not found") + } + + if tests.EvalErrWithLog(t, err, "Match", tc.shouldErr, tc.err, msgs) { + return + } + + got["redirect_uri"] = redirURI.String() + + tests.EvalObjectsWithLog(t, "RedirectURI", tc.want, got, msgs) + }) + } +} diff --git a/pkg/redirects/redirect_test.go b/pkg/redirects/redirect_test.go new file mode 100644 index 0000000..342b921 --- /dev/null +++ b/pkg/redirects/redirect_test.go @@ -0,0 +1,74 @@ +// Copyright 2024 Paul Greenberg greenpau@outlook.com +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package redirects + +import ( + "fmt" + + "testing" + + "github.com/greenpau/go-authcrunch/internal/tests" +) + +func TestNewRedirectURIMatchConfig(t *testing.T) { + testcases := []struct { + name string + config []string + want map[string]interface{} + shouldErr bool + err error + }{ + { + name: "text exact domain and exact path match", + config: []string{"exact", "authcrunch.com", "exact", "/path/to"}, + want: map[string]interface{}{ + "domain_match_type": "exact", + "domain": "authcrunch.com", + "path_match_type": "exact", + "path": "/path/to", + }, + }, + /* + { + name: "test invalid config", + shouldErr: true, + err: fmt.Errorf("TBD"), + }, + */ + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + got := make(map[string]interface{}) + msgs := []string{fmt.Sprintf("test name: %s", tc.name)} + msgs = append(msgs, fmt.Sprintf("config:\n%v", tc.config)) + + c, err := NewRedirectURIMatchConfig(tc.config[0], tc.config[1], tc.config[2], tc.config[3]) + if err != nil { + t.Fatal(err) + } + + if tests.EvalErrWithLog(t, err, "Match", tc.shouldErr, tc.err, msgs) { + return + } + + got["domain_match_type"] = c.DomainMatchType + got["domain"] = c.Domain + got["path_match_type"] = c.PathMatchType + got["path"] = c.Path + + tests.EvalObjectsWithLog(t, "Output", tc.want, got, msgs) + }) + } +}