* 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/setting" | |||
| "code.gitea.io/gitea/modules/templates" | |||
| "code.gitea.io/gitea/modules/validation" | |||
| "code.gitea.io/gitea/routers" | |||
| "code.gitea.io/gitea/routers/admin" | |||
| 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}) | |||
| bindIgnErr := binding.BindIgnErr | |||
| validation.AddBindingRules() | |||
| m.Use(user.GetNotificationCount) | |||
| @@ -32,7 +32,7 @@ type AdminEditUserForm struct { | |||
| FullName string `binding:"MaxSize(100)"` | |||
| Email string `binding:"Required;Email;MaxSize(254)"` | |||
| Password string `binding:"MaxSize(255)"` | |||
| Website string `binding:"Url;MaxSize(255)"` | |||
| Website string `binding:"ValidUrl;MaxSize(255)"` | |||
| Location string `binding:"MaxSize(50)"` | |||
| MaxRepoCreation int | |||
| Active bool | |||
| @@ -19,6 +19,7 @@ import ( | |||
| "code.gitea.io/gitea/modules/base" | |||
| "code.gitea.io/gitea/modules/log" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| "code.gitea.io/gitea/modules/validation" | |||
| ) | |||
| // 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") | |||
| case binding.ERR_ALPHA_DASH_DOT: | |||
| 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: | |||
| data["ErrorMsg"] = trName + l.Tr("form.size_error", GetSize(field)) | |||
| case binding.ERR_MIN_SIZE: | |||
| @@ -31,7 +31,7 @@ type UpdateOrgSettingForm struct { | |||
| Name string `binding:"Required;AlphaDashDot;MaxSize(35)" locale:"org.org_name_holder"` | |||
| FullName string `binding:"MaxSize(100)"` | |||
| Description string `binding:"MaxSize(255)"` | |||
| Website string `binding:"Url;MaxSize(255)"` | |||
| Website string `binding:"ValidUrl;MaxSize(255)"` | |||
| Location string `binding:"MaxSize(50)"` | |||
| MaxRepoCreation int | |||
| } | |||
| @@ -87,7 +87,7 @@ func (f MigrateRepoForm) ParseRemoteAddr(user *models.User) (string, error) { | |||
| type RepoSettingForm struct { | |||
| RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"` | |||
| Description string `binding:"MaxSize(255)"` | |||
| Website string `binding:"Url;MaxSize(255)"` | |||
| Website string `binding:"ValidUrl;MaxSize(255)"` | |||
| Interval string | |||
| MirrorAddress string | |||
| Private bool | |||
| @@ -143,7 +143,7 @@ func (f WebhookForm) ChooseEvents() bool { | |||
| // NewWebhookForm form for creating web hook | |||
| type NewWebhookForm struct { | |||
| PayloadURL string `binding:"Required;Url"` | |||
| PayloadURL string `binding:"Required;ValidUrl"` | |||
| ContentType int `binding:"Required"` | |||
| Secret string | |||
| WebhookForm | |||
| @@ -156,7 +156,7 @@ func (f *NewWebhookForm) Validate(ctx *macaron.Context, errs binding.Errors) bin | |||
| // NewSlackHookForm form for creating slack hook | |||
| type NewSlackHookForm struct { | |||
| PayloadURL string `binding:"Required;Url"` | |||
| PayloadURL string `binding:"Required;ValidUrl"` | |||
| Channel string `binding:"Required"` | |||
| Username string | |||
| IconURL string | |||
| @@ -323,7 +323,7 @@ type EditRepoFileForm struct { | |||
| CommitSummary string `binding:"MaxSize(100)"` | |||
| CommitMessage string | |||
| CommitChoice string `binding:"Required;MaxSize(50)"` | |||
| NewBranchName string `binding:"AlphaDashDot;MaxSize(100)"` | |||
| NewBranchName string `binding:"GitRefName;MaxSize(100)"` | |||
| LastCommit string | |||
| } | |||
| @@ -356,7 +356,7 @@ type UploadRepoFileForm struct { | |||
| CommitSummary string `binding:"MaxSize(100)"` | |||
| CommitMessage string | |||
| CommitChoice string `binding:"Required;MaxSize(50)"` | |||
| NewBranchName string `binding:"AlphaDashDot;MaxSize(100)"` | |||
| NewBranchName string `binding:"GitRefName;MaxSize(100)"` | |||
| Files []string | |||
| } | |||
| @@ -387,7 +387,7 @@ type DeleteRepoFileForm struct { | |||
| CommitSummary string `binding:"MaxSize(100)"` | |||
| CommitMessage string | |||
| CommitChoice string `binding:"Required;MaxSize(50)"` | |||
| NewBranchName string `binding:"AlphaDashDot;MaxSize(100)"` | |||
| NewBranchName string `binding:"GitRefName;MaxSize(100)"` | |||
| } | |||
| // Validate validates the fields | |||
| @@ -103,7 +103,7 @@ type UpdateProfileForm struct { | |||
| FullName string `binding:"MaxSize(100)"` | |||
| Email string `binding:"Required;Email;MaxSize(254)"` | |||
| KeepEmailPrivate bool | |||
| Website string `binding:"Url;MaxSize(255)"` | |||
| Website string `binding:"ValidUrl;MaxSize(255)"` | |||
| 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.` | |||
| alpha_dash_error = ` must be valid alphanumeric or dash(-_) 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.` | |||
| min_size_error = ` must contain at least %s characters.` | |||
| max_size_error = ` must contain at most %s characters.` | |||