diff --git a/README.md b/README.md index 720df00..31dfacb 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Inspired by [django-forms](https://docs.djangoproject.com/en/dev/topics/forms/) ## Install ``` -go get github.com/bluele/gforms +go get github.com/votezilla/gforms ``` ## Examples diff --git a/common_passwords.txt b/common_passwords.txt new file mode 100644 index 0000000..e69de29 diff --git a/password_utility.go b/password_utility.go new file mode 100644 index 0000000..bf8e709 --- /dev/null +++ b/password_utility.go @@ -0,0 +1,226 @@ +// Taken from https://github.com/briandowns/GoPasswordUtilities/edit/master/password_utility.go +// +// Copyright 2014 Brian J. Downs +// +// 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. + +// Simple library for working with passwords in Go. +// All generated passwords are going to be a minimum of 8 +// characters in length. +package gforms//GoPasswordUtilities + +import ( + "bufio" + "bytes" + "crypto/md5" + "crypto/rand" + "crypto/sha256" + "crypto/sha512" + "fmt" + "log" + "os" + "regexp" +) + +const ( + characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+,.?/:;{}[]~" + wordsLocation = "common_passwords.txt" // https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/10_million_password_list_top_1000000.txt +) + +var ( + commonPasswords = map[string]bool{} + passwordScores = map[int]string{ + 0: "Horrible", + 1: "Weak", + 2: "Medium", + 3: "Strong", + 4: "Very Strong"} +) + +type Password struct { + Pass string + Length int + Score int + ContainsUpper bool + ContainsLower bool + ContainsNumber bool + ContainsSpecial bool + CommonPassword bool +} + +type SaltConf struct { + Length int +} + +// New is used when a user enters a password as well as the +// being called from the GeneratePassword function. +func New(password string) *Password { + return &Password{Pass: password, Length: len(password)} +} + +// GeneratePassword will generate and return a password as a string and as a +// byte slice of the given length. +func GeneratePassword(length int) *Password { + passwordBuffer := new(bytes.Buffer) + randBytes := make([]byte, length) + + if _, err := rand.Read(randBytes); err == nil { + for j := 0; j < length; j++ { + tmpIndex := int(randBytes[j]) % len(characters) + char := characters[tmpIndex] + passwordBuffer.WriteString(string(char)) + } + } + return New(passwordBuffer.String()) +} + +// GenerateVeryStrongPassword will generate a "Very Strong" password. +func GenerateVeryStrongPassword(length int) *Password { + for { + p := GeneratePassword(length) + p.ProcessPassword() + if p.Score == 4 { + return p + } + } +} + +// getRandomBytes will generate random bytes. This is for internal +// use in the library itself. +func getRandomBytes(length int) []byte { + randomData := make([]byte, length) + if _, err := rand.Read(randomData); err != nil { + log.Fatalf("%v\n", err) + } + return randomData +} + +// MD5 sum for the given password. If a SaltConf +// pointer is given as a parameter a salt with the given +// length will be returned with it included in the hash. +func (p *Password) MD5(saltConf ...*SaltConf) ([16]byte, []byte) { + if len(saltConf) > 0 { + var saltLength int + + for _, i := range saltConf[0:] { + saltLength = i.Length + } + + salt := getRandomBytes(saltLength) + return md5.Sum([]byte(fmt.Sprintf("%s%x", p.Pass, salt))), salt + } + return md5.Sum([]byte(p.Pass)), nil +} + +// SHA256 sum for the given password. If a SaltConf +// pointer is given as a parameter a salt with the given +// length will be returned with it included in the hash. +func (p *Password) SHA256(saltConf ...*SaltConf) ([32]byte, []byte) { + if len(saltConf) > 0 { + var saltLength int + + for _, i := range saltConf[0:] { + saltLength = i.Length + } + + salt := getRandomBytes(saltLength) + return sha256.Sum256([]byte(fmt.Sprintf("%s%x", p.Pass, salt))), salt + } + return sha256.Sum256([]byte(p.Pass)), nil +} + +// SHA512 sum for the given password. If a SaltConf +// pointer is given as a parameter a salt with the given +// length will be returned with it included in the hash. +func (p *Password) SHA512(saltConf ...*SaltConf) ([64]byte, []byte) { + if len(saltConf) > 0 { + var saltLength int + + for _, i := range saltConf[0:] { + saltLength = i.Length + } + + salt := getRandomBytes(saltLength) + return sha512.Sum512([]byte(fmt.Sprintf("%s%x", p.Pass, salt))), salt + } + return sha512.Sum512([]byte(p.Pass)), nil +} + +// GetLength will provide the length of the password. This method is +// being put on the password struct in case someone decides not to +// do a complexity check. +func (p *Password) GetLength() int { + return p.Length +} + +// ProcessPassword will parse the password and populate the Password struct attributes. +func (p *Password) ProcessPassword() { + matchLower := regexp.MustCompile(`[a-z]`) + matchUpper := regexp.MustCompile(`[A-Z]`) + matchNumber := regexp.MustCompile(`[0-9]`) + matchSpecial := regexp.MustCompile(`[\!\@\#\$\%\^\&\*\(\\\)\-_\=\+\,\.\?\/\:\;\{\}\[\]~]`) + + if p.Length < 8 { + p.Score = 0 + return + } + + if matchLower.MatchString(p.Pass) { + p.ContainsLower = true + p.Score++ + } + if matchUpper.MatchString(p.Pass) { + p.ContainsUpper = true + p.Score++ + } + if matchNumber.MatchString(p.Pass) { + p.ContainsNumber = true + p.Score++ + } + if matchSpecial.MatchString(p.Pass) { + p.ContainsSpecial = true + p.Score++ + } + if searchCommonPasswords(p.Pass) { + p.CommonPassword = true + p.Score = 0 + } +} + +func readCommonPasswordFile() { + file, err := os.Open(wordsLocation) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + commonPasswords[scanner.Text()] = true // does this work or do we need a .Set() ? + } +} + +// searchCommonPasswords will search for match with exact list of common passwords +// to be installed. +func searchCommonPasswords(password string) bool { + return commonPasswords[password] +} + +// ComplexityRating provides the rating for the password. +func (p *Password) ComplexityRating() string { + return passwordScores[p.Score] +} + +func init() { + readCommonPasswordFile() +} \ No newline at end of file diff --git a/selectwidget.go b/selectwidget.go index 10c57d4..470ce49 100644 --- a/selectwidget.go +++ b/selectwidget.go @@ -103,6 +103,19 @@ func SelectWidget(attrs map[string]string, mk SelectOptionsMaker) *selectWidget return wg } +// SelectWidget constructor which is easier use. +func SelectWidgetEasy(selectOptions [][2]string) *selectWidget { + return SelectWidget( + map[string]string{}, // attrs + // type SelectOptionsMaker func() SelectOptions + SelectOptionsMaker(func() SelectOptions { + ret := make(StringSelectOptions, len(selectOptions)) + for i, option := range selectOptions { + ret[i] = []string{option[0], option[1], "false", "false"} + } + return ret +}))} + // Generate select-multiple and options field: func SelectMultipleWidget(attrs map[string]string, mk SelectOptionsMaker) *selectWidget { wg := SelectWidget(attrs, mk) diff --git a/templates.go b/templates.go index c24c5bf..5b372b1 100644 --- a/templates.go +++ b/templates.go @@ -10,11 +10,14 @@ const defaultTemplates = ` {{define "BooleanTypeField"}}{{end}} {{define "SimpleWidget"}}{{end}} {{define "SelectWidget"}}{{end}} {{define "RadioWidget"}}{{$name := .Field.GetName}}{{range $idx, $val := .Options}}{{$val.Label | html}} {{end}}{{end}} -{{define "CheckboxMultipleWidget"}}{{$name := .Field.GetName}}{{range $idx, $val := .Options}}{{$val.Label | html}} +{{define "CheckboxMultipleWidget"}}{{$name := .Field.GetName}}{{range $idx, $val := .Options}}
{{$val.Label | html}}
{{end}}{{end}} ` diff --git a/validators.go b/validators.go index 0919faa..87886d6 100644 --- a/validators.go +++ b/validators.go @@ -191,22 +191,115 @@ func RegexpValidator(regex string, message ...string) regexpValidator { return vl } -// An EmailValidator that ensures a value looks like an email address. +// An EmailValidator that ensures a value looks like an international email address. func EmailValidator(message ...string) regexpValidator { - regex := `(?i)^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$` - if len(message) > 0 { - return RegexpValidator(regex, message[0]) - } else { - return RegexpValidator(regex, "Enter a valid email address.") - } + regex := `^.+@.+$` // international email can include UTF8 characters. Better to have false positives than false negatives. + if len(message) > 0 { + return RegexpValidator(regex, message[0]) + } else { + return RegexpValidator(regex, "Enter a valid email address.") + } +} + +// A FullNameValidator that ensures that we have a full name (e.g. 'John Doe'). +func FullNameValidator(message ...string) regexpValidator { + regex := `^[\p{L}]+( [\p{L}]+)+$` + if len(message) > 0 { + return RegexpValidator(regex, message[0]) + } else { + return RegexpValidator(regex, "Enter a valid full name (i.e. 'John Doe').") + } } // An URLValidator that ensures a value looks like an url. func URLValidator(message ...string) regexpValidator { - regex := `^(https?|ftp)(:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+)$` - if len(message) > 0 { - return RegexpValidator(regex, message[0]) - } else { - return RegexpValidator(regex, "Enter a valid url.") + regex := `^(https?|ftp)(:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+)$` + if len(message) > 0 { + return RegexpValidator(regex, message[0]) + } else { + return RegexpValidator(regex, "Enter a valid url.") + } +} + +type passwordStrengthValidator struct { + RequiredStrength int + Message string + Validator +} +// A PasswordStrengthValidator that ensures that the password is complex enough. +// requiredStrength: +// 0: Horrible +// 1: Weak +// 2: Medium +// 3: Strong +// 4: Very Strong +func PasswordStrengthValidator(requiredStrength int, message ...string) passwordStrengthValidator { + vl := passwordStrengthValidator{} + vl.RequiredStrength = requiredStrength + if len(message) > 0 { + vl.Message = message[0] + } else { + vl.Message = "Password isn't strong enough. Add a mix of uppers, lowers, numbers, and special characters." + } + return vl +} +func (vl passwordStrengthValidator) Validate(fi *FieldInstance, fo *FormInstance) error { + v := fi.V + if v.IsNil || v.Kind != reflect.String { + return nil + } + sv := v.Value.(string) + + password := New(sv) + password.ProcessPassword() + if password.CommonPassword { + return errors.New("Your password is a common password. Try making it harder to guess.") } + if password.Score < vl.RequiredStrength { + return errors.New(vl.Message) + } + return nil +} + +type fieldMatchValidator struct { + FieldMatchName string + Message string + Validator +} +// A FieldMatchValidator ensures that this field matches the field [FieldMatchName]. +func FieldMatchValidator(fieldMatchName string, message ...string) fieldMatchValidator { + vl := fieldMatchValidator{} + vl.FieldMatchName = fieldMatchName + if len(message) > 0 { + vl.Message = message[0] + } else { + vl.Message = fieldMatchName + " fields must match" + } + return vl +} +func (vl fieldMatchValidator) Validate(fi *FieldInstance, fo *FormInstance) error { + v := fi.V + if v.IsNil || v.Kind != reflect.String { + return nil + } + sv := v.Value.(string) + + if sv != fo.Data[vl.FieldMatchName].RawStr { + return errors.New(vl.Message) + } + return nil +} + +type fnValidator struct { + ValidationFn func(fi *FieldInstance, fo *FormInstance) error + Validator +} +// A FieldMatchValidator ensures that this field matches the field [FieldMatchName]. +func FnValidator(validationFn func(fi *FieldInstance, fo *FormInstance) error) fnValidator { + vl := fnValidator{} + vl.ValidationFn = validationFn + return vl +} +func (vl fnValidator) Validate(fi *FieldInstance, fo *FormInstance) error { + return vl.ValidationFn(fi, fo) } diff --git a/validators_test.go b/validators_test.go index 2c8314b..e0f77b3 100644 --- a/validators_test.go +++ b/validators_test.go @@ -128,7 +128,7 @@ func TestRegexpValidator(t *testing.T) { } } -func TestEmailValidator(t *testing.T) { +func testEmail(t *testing.T, email string, expectValid bool) { Form := DefineForm(NewFields( NewTextField( "email", @@ -137,20 +137,52 @@ func TestEmailValidator(t *testing.T) { }, ), )) - - req1, _ := http.NewRequest("POST", "/", strings.NewReader(url.Values{"email": {"junkxdev@gmail.com"}}.Encode())) - req1.Header.Add("Content-Type", "application/x-www-form-urlencoded") - form1 := Form(req1) - if !form1.IsValid() { - t.Error("Not expected: validation error.") + + req, _ := http.NewRequest("POST", "/", strings.NewReader(url.Values{"email": {email}}.Encode())) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + form := Form(req) + if expectValid { + if !form.IsValid() { + t.Error("Not expected: validation error for email " + email) + } + } else { + if form.IsValid() { + t.Error("Expected: validation error for email " + email) + } } +} - req2, _ := http.NewRequest("POST", "/", strings.NewReader(url.Values{"email": {"abc"}}.Encode())) - req2.Header.Add("Content-Type", "application/x-www-form-urlencoded") - form2 := Form(req2) - if form2.IsValid() { - t.Error("Expected: validation error.") - } +func TestEmailValidator(t *testing.T) { + Form := DefineForm(NewFields( + NewTextField( + "email", + Validators{ + EmailValidator(), + }, + ), + )) + + // Normal email + testEmail(t, "junkxdev@gmail.com", true) + + // Normal email with special characters + testEmail(t, "user.1234!#$%&'*+-/=?^_`{|}~ ()@gmail.com", true) + + // Email missing '.' after '@' - rare and not encouraged but still legal. + testEmail(t, "junkxdev@gmail", true) + + // Bad email - missing '@' + testEmail(t, "junkxdevgmail.com", false) + testEmail(t, "@gmail.com", false) + + // Valid UTF8 emails + testEmail(t, "闪闪发光@闪闪发光.com", true) + testEmail(t, "Pelé@example.com", true) // Latin alphabet (with diacritics) + testEmail(t, "δοκιμή@παράδειγμα.δοκιμή", true) // Greek alphabet + testEmail(t, "我買@屋企.香港", true) // Traditional Chinese characters + testEmail(t, "甲斐@黒川.日本", true) // Japanese characters + testEmail(t, "чебурашка@ящик-с-апельсинами.рф", true) // Cyrillic characters + testEmail(t, "संपर्क@डाटामेल.भारत", true) // Hindi email address req3, _ := http.NewRequest("POST", "/", strings.NewReader(url.Values{}.Encode())) req3.Header.Add("Content-Type", "application/x-www-form-urlencoded")