* Add correct git branch name validation * Change git refname validation error constant name * Implement URL validation based on GoLang url.Parse method * Backward compatibility with older Go compiler * Add git reference name validation unit tests * Remove unused variable in unit test * Implement URL validation based on GoLang url.Parse method * Backward compatibility with older Go compiler * Add url validation unit teststags/v1.21.12.1
| @@ -23,6 +23,7 @@ import ( | |||||
| "code.gitea.io/gitea/modules/public" | "code.gitea.io/gitea/modules/public" | ||||
| "code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
| "code.gitea.io/gitea/modules/templates" | "code.gitea.io/gitea/modules/templates" | ||||
| "code.gitea.io/gitea/modules/validation" | |||||
| "code.gitea.io/gitea/routers" | "code.gitea.io/gitea/routers" | ||||
| "code.gitea.io/gitea/routers/admin" | "code.gitea.io/gitea/routers/admin" | ||||
| apiv1 "code.gitea.io/gitea/routers/api/v1" | apiv1 "code.gitea.io/gitea/routers/api/v1" | ||||
| @@ -177,6 +178,7 @@ func runWeb(ctx *cli.Context) error { | |||||
| reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true}) | reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true}) | ||||
| bindIgnErr := binding.BindIgnErr | bindIgnErr := binding.BindIgnErr | ||||
| validation.AddBindingRules() | |||||
| m.Use(user.GetNotificationCount) | m.Use(user.GetNotificationCount) | ||||
| @@ -32,7 +32,7 @@ type AdminEditUserForm struct { | |||||
| FullName string `binding:"MaxSize(100)"` | FullName string `binding:"MaxSize(100)"` | ||||
| Email string `binding:"Required;Email;MaxSize(254)"` | Email string `binding:"Required;Email;MaxSize(254)"` | ||||
| Password string `binding:"MaxSize(255)"` | Password string `binding:"MaxSize(255)"` | ||||
| Website string `binding:"Url;MaxSize(255)"` | |||||
| Website string `binding:"ValidUrl;MaxSize(255)"` | |||||
| Location string `binding:"MaxSize(50)"` | Location string `binding:"MaxSize(50)"` | ||||
| MaxRepoCreation int | MaxRepoCreation int | ||||
| Active bool | Active bool | ||||
| @@ -19,6 +19,7 @@ import ( | |||||
| "code.gitea.io/gitea/modules/base" | "code.gitea.io/gitea/modules/base" | ||||
| "code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
| "code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
| "code.gitea.io/gitea/modules/validation" | |||||
| ) | ) | ||||
| // IsAPIPath if URL is an api path | // IsAPIPath if URL is an api path | ||||
| @@ -253,6 +254,8 @@ func validate(errs binding.Errors, data map[string]interface{}, f Form, l macaro | |||||
| data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_error") | data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_error") | ||||
| case binding.ERR_ALPHA_DASH_DOT: | case binding.ERR_ALPHA_DASH_DOT: | ||||
| data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_dot_error") | data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_dot_error") | ||||
| case validation.ErrGitRefName: | |||||
| data["ErrorMsg"] = trName + l.Tr("form.git_ref_name_error") | |||||
| case binding.ERR_SIZE: | case binding.ERR_SIZE: | ||||
| data["ErrorMsg"] = trName + l.Tr("form.size_error", GetSize(field)) | data["ErrorMsg"] = trName + l.Tr("form.size_error", GetSize(field)) | ||||
| case binding.ERR_MIN_SIZE: | case binding.ERR_MIN_SIZE: | ||||
| @@ -31,7 +31,7 @@ type UpdateOrgSettingForm struct { | |||||
| Name string `binding:"Required;AlphaDashDot;MaxSize(35)" locale:"org.org_name_holder"` | Name string `binding:"Required;AlphaDashDot;MaxSize(35)" locale:"org.org_name_holder"` | ||||
| FullName string `binding:"MaxSize(100)"` | FullName string `binding:"MaxSize(100)"` | ||||
| Description string `binding:"MaxSize(255)"` | Description string `binding:"MaxSize(255)"` | ||||
| Website string `binding:"Url;MaxSize(255)"` | |||||
| Website string `binding:"ValidUrl;MaxSize(255)"` | |||||
| Location string `binding:"MaxSize(50)"` | Location string `binding:"MaxSize(50)"` | ||||
| MaxRepoCreation int | MaxRepoCreation int | ||||
| } | } | ||||
| @@ -87,7 +87,7 @@ func (f MigrateRepoForm) ParseRemoteAddr(user *models.User) (string, error) { | |||||
| type RepoSettingForm struct { | type RepoSettingForm struct { | ||||
| RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"` | RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"` | ||||
| Description string `binding:"MaxSize(255)"` | Description string `binding:"MaxSize(255)"` | ||||
| Website string `binding:"Url;MaxSize(255)"` | |||||
| Website string `binding:"ValidUrl;MaxSize(255)"` | |||||
| Interval string | Interval string | ||||
| MirrorAddress string | MirrorAddress string | ||||
| Private bool | Private bool | ||||
| @@ -143,7 +143,7 @@ func (f WebhookForm) ChooseEvents() bool { | |||||
| // NewWebhookForm form for creating web hook | // NewWebhookForm form for creating web hook | ||||
| type NewWebhookForm struct { | type NewWebhookForm struct { | ||||
| PayloadURL string `binding:"Required;Url"` | |||||
| PayloadURL string `binding:"Required;ValidUrl"` | |||||
| ContentType int `binding:"Required"` | ContentType int `binding:"Required"` | ||||
| Secret string | Secret string | ||||
| WebhookForm | WebhookForm | ||||
| @@ -156,7 +156,7 @@ func (f *NewWebhookForm) Validate(ctx *macaron.Context, errs binding.Errors) bin | |||||
| // NewSlackHookForm form for creating slack hook | // NewSlackHookForm form for creating slack hook | ||||
| type NewSlackHookForm struct { | type NewSlackHookForm struct { | ||||
| PayloadURL string `binding:"Required;Url"` | |||||
| PayloadURL string `binding:"Required;ValidUrl"` | |||||
| Channel string `binding:"Required"` | Channel string `binding:"Required"` | ||||
| Username string | Username string | ||||
| IconURL string | IconURL string | ||||
| @@ -323,7 +323,7 @@ type EditRepoFileForm struct { | |||||
| CommitSummary string `binding:"MaxSize(100)"` | CommitSummary string `binding:"MaxSize(100)"` | ||||
| CommitMessage string | CommitMessage string | ||||
| CommitChoice string `binding:"Required;MaxSize(50)"` | CommitChoice string `binding:"Required;MaxSize(50)"` | ||||
| NewBranchName string `binding:"AlphaDashDot;MaxSize(100)"` | |||||
| NewBranchName string `binding:"GitRefName;MaxSize(100)"` | |||||
| LastCommit string | LastCommit string | ||||
| } | } | ||||
| @@ -356,7 +356,7 @@ type UploadRepoFileForm struct { | |||||
| CommitSummary string `binding:"MaxSize(100)"` | CommitSummary string `binding:"MaxSize(100)"` | ||||
| CommitMessage string | CommitMessage string | ||||
| CommitChoice string `binding:"Required;MaxSize(50)"` | CommitChoice string `binding:"Required;MaxSize(50)"` | ||||
| NewBranchName string `binding:"AlphaDashDot;MaxSize(100)"` | |||||
| NewBranchName string `binding:"GitRefName;MaxSize(100)"` | |||||
| Files []string | Files []string | ||||
| } | } | ||||
| @@ -387,7 +387,7 @@ type DeleteRepoFileForm struct { | |||||
| CommitSummary string `binding:"MaxSize(100)"` | CommitSummary string `binding:"MaxSize(100)"` | ||||
| CommitMessage string | CommitMessage string | ||||
| CommitChoice string `binding:"Required;MaxSize(50)"` | CommitChoice string `binding:"Required;MaxSize(50)"` | ||||
| NewBranchName string `binding:"AlphaDashDot;MaxSize(100)"` | |||||
| NewBranchName string `binding:"GitRefName;MaxSize(100)"` | |||||
| } | } | ||||
| // Validate validates the fields | // Validate validates the fields | ||||
| @@ -103,7 +103,7 @@ type UpdateProfileForm struct { | |||||
| FullName string `binding:"MaxSize(100)"` | FullName string `binding:"MaxSize(100)"` | ||||
| Email string `binding:"Required;Email;MaxSize(254)"` | Email string `binding:"Required;Email;MaxSize(254)"` | ||||
| KeepEmailPrivate bool | KeepEmailPrivate bool | ||||
| Website string `binding:"Url;MaxSize(255)"` | |||||
| Website string `binding:"ValidUrl;MaxSize(255)"` | |||||
| Location string `binding:"MaxSize(50)"` | Location string `binding:"MaxSize(50)"` | ||||
| } | } | ||||
| @@ -0,0 +1,102 @@ | |||||
| // Copyright 2017 The Gitea Authors. All rights reserved. | |||||
| // Use of this source code is governed by a MIT-style | |||||
| // license that can be found in the LICENSE file. | |||||
| package validation | |||||
| import ( | |||||
| "fmt" | |||||
| "net/url" | |||||
| "regexp" | |||||
| "strings" | |||||
| "github.com/go-macaron/binding" | |||||
| ) | |||||
| const ( | |||||
| // ErrGitRefName is git reference name error | |||||
| ErrGitRefName = "GitRefNameError" | |||||
| ) | |||||
| var ( | |||||
| // GitRefNamePattern is regular expression wirh unallowed characters in git reference name | |||||
| GitRefNamePattern = regexp.MustCompile("[^\\d\\w-_\\./]") | |||||
| ) | |||||
| // AddBindingRules adds additional binding rules | |||||
| func AddBindingRules() { | |||||
| addGitRefNameBindingRule() | |||||
| addValidURLBindingRule() | |||||
| } | |||||
| func addGitRefNameBindingRule() { | |||||
| // Git refname validation rule | |||||
| binding.AddRule(&binding.Rule{ | |||||
| IsMatch: func(rule string) bool { | |||||
| return strings.HasPrefix(rule, "GitRefName") | |||||
| }, | |||||
| IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { | |||||
| str := fmt.Sprintf("%v", val) | |||||
| if GitRefNamePattern.MatchString(str) { | |||||
| errs.Add([]string{name}, ErrGitRefName, "GitRefName") | |||||
| return false, errs | |||||
| } | |||||
| // Additional rules as described at https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html | |||||
| if strings.HasPrefix(str, "/") || strings.HasSuffix(str, "/") || | |||||
| strings.HasPrefix(str, ".") || strings.HasSuffix(str, ".") || | |||||
| strings.HasSuffix(str, ".lock") || | |||||
| strings.Contains(str, "..") || strings.Contains(str, "//") { | |||||
| errs.Add([]string{name}, ErrGitRefName, "GitRefName") | |||||
| return false, errs | |||||
| } | |||||
| return true, errs | |||||
| }, | |||||
| }) | |||||
| } | |||||
| func addValidURLBindingRule() { | |||||
| // URL validation rule | |||||
| binding.AddRule(&binding.Rule{ | |||||
| IsMatch: func(rule string) bool { | |||||
| return strings.HasPrefix(rule, "ValidUrl") | |||||
| }, | |||||
| IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { | |||||
| str := fmt.Sprintf("%v", val) | |||||
| if len(str) != 0 { | |||||
| if u, err := url.ParseRequestURI(str); err != nil || | |||||
| (u.Scheme != "http" && u.Scheme != "https") || | |||||
| !validPort(portOnly(u.Host)) { | |||||
| errs.Add([]string{name}, binding.ERR_URL, "Url") | |||||
| return false, errs | |||||
| } | |||||
| } | |||||
| return true, errs | |||||
| }, | |||||
| }) | |||||
| } | |||||
| func portOnly(hostport string) string { | |||||
| colon := strings.IndexByte(hostport, ':') | |||||
| if colon == -1 { | |||||
| return "" | |||||
| } | |||||
| if i := strings.Index(hostport, "]:"); i != -1 { | |||||
| return hostport[i+len("]:"):] | |||||
| } | |||||
| if strings.Contains(hostport, "]") { | |||||
| return "" | |||||
| } | |||||
| return hostport[colon+len(":"):] | |||||
| } | |||||
| func validPort(p string) bool { | |||||
| for _, r := range []byte(p) { | |||||
| if r < '0' || r > '9' { | |||||
| return false | |||||
| } | |||||
| } | |||||
| return true | |||||
| } | |||||
| @@ -0,0 +1,62 @@ | |||||
| // Copyright 2017 The Gitea Authors. All rights reserved. | |||||
| // Use of this source code is governed by a MIT-style | |||||
| // license that can be found in the LICENSE file. | |||||
| package validation | |||||
| import ( | |||||
| "fmt" | |||||
| "net/http" | |||||
| "net/http/httptest" | |||||
| "testing" | |||||
| "github.com/go-macaron/binding" | |||||
| "github.com/stretchr/testify/assert" | |||||
| "gopkg.in/macaron.v1" | |||||
| ) | |||||
| const ( | |||||
| testRoute = "/test" | |||||
| ) | |||||
| type ( | |||||
| validationTestCase struct { | |||||
| description string | |||||
| data interface{} | |||||
| expectedErrors binding.Errors | |||||
| } | |||||
| handlerFunc func(interface{}, ...interface{}) macaron.Handler | |||||
| modeler interface { | |||||
| Model() string | |||||
| } | |||||
| TestForm struct { | |||||
| BranchName string `form:"BranchName" binding:"GitRefName"` | |||||
| URL string `form:"ValidUrl" binding:"ValidUrl"` | |||||
| } | |||||
| ) | |||||
| func performValidationTest(t *testing.T, testCase validationTestCase) { | |||||
| httpRecorder := httptest.NewRecorder() | |||||
| m := macaron.Classic() | |||||
| m.Post(testRoute, binding.Validate(testCase.data), func(actual binding.Errors) { | |||||
| assert.Equal(t, fmt.Sprintf("%+v", testCase.expectedErrors), fmt.Sprintf("%+v", actual)) | |||||
| }) | |||||
| req, err := http.NewRequest("POST", testRoute, nil) | |||||
| if err != nil { | |||||
| panic(err) | |||||
| } | |||||
| m.ServeHTTP(httpRecorder, req) | |||||
| switch httpRecorder.Code { | |||||
| case http.StatusNotFound: | |||||
| panic("Routing is messed up in test fixture (got 404): check methods and paths") | |||||
| case http.StatusInternalServerError: | |||||
| panic("Something bad happened on '" + testCase.description + "'") | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,142 @@ | |||||
| // Copyright 2017 The Gitea Authors. All rights reserved. | |||||
| // Use of this source code is governed by a MIT-style | |||||
| // license that can be found in the LICENSE file. | |||||
| package validation | |||||
| import ( | |||||
| "testing" | |||||
| "github.com/go-macaron/binding" | |||||
| ) | |||||
| var gitRefNameValidationTestCases = []validationTestCase{ | |||||
| { | |||||
| description: "Referece contains only characters", | |||||
| data: TestForm{ | |||||
| BranchName: "test", | |||||
| }, | |||||
| expectedErrors: binding.Errors{}, | |||||
| }, | |||||
| { | |||||
| description: "Reference name contains single slash", | |||||
| data: TestForm{ | |||||
| BranchName: "feature/test", | |||||
| }, | |||||
| expectedErrors: binding.Errors{}, | |||||
| }, | |||||
| { | |||||
| description: "Reference name contains backslash", | |||||
| data: TestForm{ | |||||
| BranchName: "feature\\test", | |||||
| }, | |||||
| expectedErrors: binding.Errors{ | |||||
| binding.Error{ | |||||
| FieldNames: []string{"BranchName"}, | |||||
| Classification: ErrGitRefName, | |||||
| Message: "GitRefName", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| { | |||||
| description: "Reference name starts with dot", | |||||
| data: TestForm{ | |||||
| BranchName: ".test", | |||||
| }, | |||||
| expectedErrors: binding.Errors{ | |||||
| binding.Error{ | |||||
| FieldNames: []string{"BranchName"}, | |||||
| Classification: ErrGitRefName, | |||||
| Message: "GitRefName", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| { | |||||
| description: "Reference name ends with dot", | |||||
| data: TestForm{ | |||||
| BranchName: "test.", | |||||
| }, | |||||
| expectedErrors: binding.Errors{ | |||||
| binding.Error{ | |||||
| FieldNames: []string{"BranchName"}, | |||||
| Classification: ErrGitRefName, | |||||
| Message: "GitRefName", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| { | |||||
| description: "Reference name starts with slash", | |||||
| data: TestForm{ | |||||
| BranchName: "/test", | |||||
| }, | |||||
| expectedErrors: binding.Errors{ | |||||
| binding.Error{ | |||||
| FieldNames: []string{"BranchName"}, | |||||
| Classification: ErrGitRefName, | |||||
| Message: "GitRefName", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| { | |||||
| description: "Reference name ends with slash", | |||||
| data: TestForm{ | |||||
| BranchName: "test/", | |||||
| }, | |||||
| expectedErrors: binding.Errors{ | |||||
| binding.Error{ | |||||
| FieldNames: []string{"BranchName"}, | |||||
| Classification: ErrGitRefName, | |||||
| Message: "GitRefName", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| { | |||||
| description: "Reference name ends with .lock", | |||||
| data: TestForm{ | |||||
| BranchName: "test.lock", | |||||
| }, | |||||
| expectedErrors: binding.Errors{ | |||||
| binding.Error{ | |||||
| FieldNames: []string{"BranchName"}, | |||||
| Classification: ErrGitRefName, | |||||
| Message: "GitRefName", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| { | |||||
| description: "Reference name contains multiple consecutive dots", | |||||
| data: TestForm{ | |||||
| BranchName: "te..st", | |||||
| }, | |||||
| expectedErrors: binding.Errors{ | |||||
| binding.Error{ | |||||
| FieldNames: []string{"BranchName"}, | |||||
| Classification: ErrGitRefName, | |||||
| Message: "GitRefName", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| { | |||||
| description: "Reference name contains multiple consecutive slashes", | |||||
| data: TestForm{ | |||||
| BranchName: "te//st", | |||||
| }, | |||||
| expectedErrors: binding.Errors{ | |||||
| binding.Error{ | |||||
| FieldNames: []string{"BranchName"}, | |||||
| Classification: ErrGitRefName, | |||||
| Message: "GitRefName", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| } | |||||
| func Test_GitRefNameValidation(t *testing.T) { | |||||
| AddBindingRules() | |||||
| for _, testCase := range gitRefNameValidationTestCases { | |||||
| t.Run(testCase.description, func(t *testing.T) { | |||||
| performValidationTest(t, testCase) | |||||
| }) | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,111 @@ | |||||
| // Copyright 2017 The Gitea Authors. All rights reserved. | |||||
| // Use of this source code is governed by a MIT-style | |||||
| // license that can be found in the LICENSE file. | |||||
| package validation | |||||
| import ( | |||||
| "testing" | |||||
| "github.com/go-macaron/binding" | |||||
| ) | |||||
| var urlValidationTestCases = []validationTestCase{ | |||||
| { | |||||
| description: "Empty URL", | |||||
| data: TestForm{ | |||||
| URL: "", | |||||
| }, | |||||
| expectedErrors: binding.Errors{}, | |||||
| }, | |||||
| { | |||||
| description: "URL without port", | |||||
| data: TestForm{ | |||||
| URL: "http://test.lan/", | |||||
| }, | |||||
| expectedErrors: binding.Errors{}, | |||||
| }, | |||||
| { | |||||
| description: "URL with port", | |||||
| data: TestForm{ | |||||
| URL: "http://test.lan:3000/", | |||||
| }, | |||||
| expectedErrors: binding.Errors{}, | |||||
| }, | |||||
| { | |||||
| description: "URL with IPv6 address without port", | |||||
| data: TestForm{ | |||||
| URL: "http://[::1]/", | |||||
| }, | |||||
| expectedErrors: binding.Errors{}, | |||||
| }, | |||||
| { | |||||
| description: "URL with IPv6 address with port", | |||||
| data: TestForm{ | |||||
| URL: "http://[::1]:3000/", | |||||
| }, | |||||
| expectedErrors: binding.Errors{}, | |||||
| }, | |||||
| { | |||||
| description: "Invalid URL", | |||||
| data: TestForm{ | |||||
| URL: "http//test.lan/", | |||||
| }, | |||||
| expectedErrors: binding.Errors{ | |||||
| binding.Error{ | |||||
| FieldNames: []string{"URL"}, | |||||
| Classification: binding.ERR_URL, | |||||
| Message: "Url", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| { | |||||
| description: "Invalid schema", | |||||
| data: TestForm{ | |||||
| URL: "ftp://test.lan/", | |||||
| }, | |||||
| expectedErrors: binding.Errors{ | |||||
| binding.Error{ | |||||
| FieldNames: []string{"URL"}, | |||||
| Classification: binding.ERR_URL, | |||||
| Message: "Url", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| { | |||||
| description: "Invalid port", | |||||
| data: TestForm{ | |||||
| URL: "http://test.lan:3x4/", | |||||
| }, | |||||
| expectedErrors: binding.Errors{ | |||||
| binding.Error{ | |||||
| FieldNames: []string{"URL"}, | |||||
| Classification: binding.ERR_URL, | |||||
| Message: "Url", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| { | |||||
| description: "Invalid port with IPv6 address", | |||||
| data: TestForm{ | |||||
| URL: "http://[::1]:3x4/", | |||||
| }, | |||||
| expectedErrors: binding.Errors{ | |||||
| binding.Error{ | |||||
| FieldNames: []string{"URL"}, | |||||
| Classification: binding.ERR_URL, | |||||
| Message: "Url", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| } | |||||
| func Test_ValidURLValidation(t *testing.T) { | |||||
| AddBindingRules() | |||||
| for _, testCase := range urlValidationTestCases { | |||||
| t.Run(testCase.description, func(t *testing.T) { | |||||
| performValidationTest(t, testCase) | |||||
| }) | |||||
| } | |||||
| } | |||||
| @@ -233,6 +233,7 @@ Content = Content | |||||
| require_error = ` cannot be empty.` | require_error = ` cannot be empty.` | ||||
| alpha_dash_error = ` must be valid alphanumeric or dash(-_) characters.` | alpha_dash_error = ` must be valid alphanumeric or dash(-_) characters.` | ||||
| alpha_dash_dot_error = ` must be valid alphanumeric, dash(-_) or dot characters.` | alpha_dash_dot_error = ` must be valid alphanumeric, dash(-_) or dot characters.` | ||||
| git_ref_name_error = ` must be well formed git reference name.` | |||||
| size_error = ` must be size %s.` | size_error = ` must be size %s.` | ||||
| min_size_error = ` must contain at least %s characters.` | min_size_error = ` must contain at least %s characters.` | ||||
| max_size_error = ` must contain at most %s characters.` | max_size_error = ` must contain at most %s characters.` | ||||