// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved. // Revel Framework source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package revel import ( "errors" "fmt" "html" "net" "net/url" "reflect" "regexp" "strconv" "strings" "unicode/utf8" ) type Validator interface { IsSatisfied(interface{}) bool DefaultMessage() string } type Required struct{} func ValidRequired() Required { return Required{} } func (r Required) IsSatisfied(obj interface{}) bool { if obj == nil { return false } switch v := reflect.ValueOf(obj); v.Kind() { case reflect.Array, reflect.Slice, reflect.Map, reflect.String, reflect.Chan: if v.Len() == 0 { return false } case reflect.Ptr: return r.IsSatisfied(reflect.Indirect(v).Interface()) } return !reflect.DeepEqual(obj, reflect.Zero(reflect.TypeOf(obj)).Interface()) } func (r Required) DefaultMessage() string { return fmt.Sprintln("Required") } type Min struct { Min float64 } func ValidMin(min int) Min { return ValidMinFloat(float64(min)) } func ValidMinFloat(min float64) Min { return Min{min} } func (m Min) IsSatisfied(obj interface{}) bool { var ( num float64 ok bool ) switch reflect.TypeOf(obj).Kind() { case reflect.Float64: num, ok = obj.(float64) case reflect.Float32: ok = true num = float64(obj.(float32)) case reflect.Int: ok = true num = float64(obj.(int)) } if ok { return num >= m.Min } return false } func (m Min) DefaultMessage() string { return fmt.Sprintln("Minimum is", m.Min) } type Max struct { Max float64 } func ValidMax(max int) Max { return ValidMaxFloat(float64(max)) } func ValidMaxFloat(max float64) Max { return Max{max} } func (m Max) IsSatisfied(obj interface{}) bool { var ( num float64 ok bool ) switch reflect.TypeOf(obj).Kind() { case reflect.Float64: num, ok = obj.(float64) case reflect.Float32: ok = true num = float64(obj.(float32)) case reflect.Int: ok = true num = float64(obj.(int)) } if ok { return num <= m.Max } return false } func (m Max) DefaultMessage() string { return fmt.Sprintln("Maximum is", m.Max) } // Range requires an integer to be within Min, Max inclusive. type Range struct { Min Max } func ValidRange(min, max int) Range { return ValidRangeFloat(float64(min), float64(max)) } func ValidRangeFloat(min, max float64) Range { return Range{Min{min}, Max{max}} } func (r Range) IsSatisfied(obj interface{}) bool { return r.Min.IsSatisfied(obj) && r.Max.IsSatisfied(obj) } func (r Range) DefaultMessage() string { return fmt.Sprintln("Range is", r.Min.Min, "to", r.Max.Max) } // MinSize requires an array or string to be at least a given length. type MinSize struct { Min int } func ValidMinSize(min int) MinSize { return MinSize{min} } func (m MinSize) IsSatisfied(obj interface{}) bool { if str, ok := obj.(string); ok { return utf8.RuneCountInString(str) >= m.Min } v := reflect.ValueOf(obj) if v.Kind() == reflect.Slice { return v.Len() >= m.Min } return false } func (m MinSize) DefaultMessage() string { return fmt.Sprintln("Minimum size is", m.Min) } // MaxSize requires an array or string to be at most a given length. type MaxSize struct { Max int } func ValidMaxSize(max int) MaxSize { return MaxSize{max} } func (m MaxSize) IsSatisfied(obj interface{}) bool { if str, ok := obj.(string); ok { return utf8.RuneCountInString(str) <= m.Max } v := reflect.ValueOf(obj) if v.Kind() == reflect.Slice { return v.Len() <= m.Max } return false } func (m MaxSize) DefaultMessage() string { return fmt.Sprintln("Maximum size is", m.Max) } // Length requires an array or string to be exactly a given length. type Length struct { N int } func ValidLength(n int) Length { return Length{n} } func (s Length) IsSatisfied(obj interface{}) bool { if str, ok := obj.(string); ok { return utf8.RuneCountInString(str) == s.N } v := reflect.ValueOf(obj) if v.Kind() == reflect.Slice { return v.Len() == s.N } return false } func (s Length) DefaultMessage() string { return fmt.Sprintln("Required length is", s.N) } // Match requires a string to match a given regex. type Match struct { Regexp *regexp.Regexp } func ValidMatch(regex *regexp.Regexp) Match { return Match{regex} } func (m Match) IsSatisfied(obj interface{}) bool { str := obj.(string) return m.Regexp.MatchString(str) } func (m Match) DefaultMessage() string { return fmt.Sprintln("Must match", m.Regexp) } var emailPattern = regexp.MustCompile("^[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?$") type Email struct { Match } func ValidEmail() Email { return Email{Match{emailPattern}} } func (e Email) DefaultMessage() string { return fmt.Sprintln("Must be a valid email address") } const ( None = 0 IPAny = 1 IPv4 = 32 // IPv4 (32 chars) IPv6 = 39 // IPv6(39 chars) IPv4MappedIPv6 = 45 // IP4-mapped IPv6 (45 chars) , Ex) ::FFFF:129.144.52.38 IPv4CIDR = IPv4 + 3 IPv6CIDR = IPv6 + 3 IPv4MappedIPv6CIDR = IPv4MappedIPv6 + 3 ) // Requires a string(IP Address) to be within IP Pattern type inclusive. type IPAddr struct { Vaildtypes []int } // Requires an IP Address string to be exactly a given validation type (IPv4, IPv6, IPv4MappedIPv6, IPv4CIDR, IPv6CIDR, IPv4MappedIPv6CIDR OR IPAny) func ValidIPAddr(cktypes ...int) IPAddr { for _, cktype := range cktypes { if cktype != IPAny && cktype != IPv4 && cktype != IPv6 && cktype != IPv4MappedIPv6 && cktype != IPv4CIDR && cktype != IPv6CIDR && cktype != IPv4MappedIPv6CIDR { return IPAddr{Vaildtypes: []int{None}} } } return IPAddr{Vaildtypes: cktypes} } func isWithCIDR(str string, l int) bool { if str[l-3] == '/' || str[l-2] == '/' { cidr_bit := strings.Split(str, "/") if 2 == len(cidr_bit) { bit, err := strconv.Atoi(cidr_bit[1]) //IPv4 : 0~32, IPv6 : 0 ~ 128 if err == nil && bit >= 0 && bit <= 128 { return true } } } return false } func getIPType(str string, l int) int { if l < 3 { //least 3 chars (::F) return None } has_dot := strings.Index(str[2:], ".") has_colon := strings.Index(str[2:], ":") switch { case has_dot > -1 && has_colon == -1 && l >= 7 && l <= IPv4CIDR: if isWithCIDR(str, l) == true { return IPv4CIDR } else { return IPv4 } case has_dot == -1 && has_colon > -1 && l >= 6 && l <= IPv6CIDR: if isWithCIDR(str, l) == true { return IPv6CIDR } else { return IPv6 } case has_dot > -1 && has_colon > -1 && l >= 14 && l <= IPv4MappedIPv6: if isWithCIDR(str, l) == true { return IPv4MappedIPv6CIDR } else { return IPv4MappedIPv6 } } return None } func (i IPAddr) IsSatisfied(obj interface{}) bool { if str, ok := obj.(string); ok { l := len(str) ret := getIPType(str, l) for _, ck := range i.Vaildtypes { if ret != None && (ck == ret || ck == IPAny) { switch ret { case IPv4, IPv6, IPv4MappedIPv6: ip := net.ParseIP(str) if ip != nil { return true } case IPv4CIDR, IPv6CIDR, IPv4MappedIPv6CIDR: _, _, err := net.ParseCIDR(str) if err == nil { return true } } } } } return false } func (i IPAddr) DefaultMessage() string { return fmt.Sprintln("Must be a vaild IP address") } // Requires a MAC Address string to be exactly type MacAddr struct{} func ValidMacAddr() MacAddr { return MacAddr{} } func (m MacAddr) IsSatisfied(obj interface{}) bool { if str, ok := obj.(string); ok { if _, err := net.ParseMAC(str); err == nil { return true } } return false } func (m MacAddr) DefaultMessage() string { return fmt.Sprintln("Must be a vaild MAC address") } var domainPattern = regexp.MustCompile(`^(([a-zA-Z0-9-\p{L}]{1,63}\.)?(xn--)?[a-zA-Z0-9\p{L}]+(-[a-zA-Z0-9\p{L}]+)*\.)+[a-zA-Z\p{L}]{2,63}$`) // Requires a Domain string to be exactly type Domain struct { Regexp *regexp.Regexp } func ValidDomain() Domain { return Domain{domainPattern} } func (d Domain) IsSatisfied(obj interface{}) bool { if str, ok := obj.(string); ok { l := len(str) //can't exceed 253 chars. if l > 253 { return false } //first and last char must be alphanumeric if str[l-1] == 46 || str[0] == 46 { return false } return domainPattern.MatchString(str) } return false } func (d Domain) DefaultMessage() string { return fmt.Sprintln("Must be a vaild domain address") } var urlPattern = regexp.MustCompile(`^((((https?|ftps?|gopher|telnet|nntp)://)|(mailto:|news:))(%[0-9A-Fa-f]{2}|[-()_.!~*';/?:@#&=+$,A-Za-z0-9\p{L}])+)([).!';/?:,][[:blank:]])?$`) type URL struct { Domain } func ValidURL() URL { return URL{Domain: ValidDomain()} } func (u URL) IsSatisfied(obj interface{}) bool { if str, ok := obj.(string); ok { // TODO : Required lot of testing return urlPattern.MatchString(str) } return false } func (u URL) DefaultMessage() string { return fmt.Sprintln("Must be a vaild URL address") } /* NORMAL BenchmarkRegex-8 2000000000 0.24 ns/op STRICT BenchmarkLoop-8 2000000000 0.01 ns/op */ const ( NORMAL = 0 STRICT = 4 ) // Requires a string to be without invisible characters type PureText struct { Mode int } func ValidPureText(m int) PureText { if m != NORMAL && m != STRICT { // Q:required fatal error m = STRICT } return PureText{m} } func isPureTextStrict(str string) (bool, error) { l := len(str) for i := 0; i < l; i++ { c := str[i] // deny : control char (00-31 without 9(TAB) and Single 10(LF),13(CR) if c >= 0 && c <= 31 && c != 9 && c != 10 && c != 13 { return false, errors.New("detect control character") } // deny : control char (DEL) if c == 127 { return false, errors.New("detect control character (DEL)") } //deny : html tag (< ~ >) if c == 60 { ds := 0 for n := i; n < l; n++ { // 60 (<) , 47(/) | 33(!) | 63(?) if str[n] == 60 && n+1 <= l && (str[n+1] == 47 || str[n+1] == 33 || str[n+1] == 63) { ds = 1 n += 3 //jump to next char } // 62 (>) if ds == 1 && str[n] == 62 { return false, errors.New("detect tag (<[!|?]~>)") } } } //deby : html encoded tag (&xxx;) if c == 38 && i+1 <= l && str[i+1] != 35 { max := i + 64 if max > l { max = l } for n := i; n < max; n++ { if str[n] == 59 { return false, errors.New("detect html encoded ta (&XXX;)") } } } } return true, nil } // Requires a string to match a given html tag elements regex pattern // referrer : http://www.w3schools.com/Tags/ var elementPattern = regexp.MustCompile(`(?im)<(?P(/*\s*|\?*|\!*)(figcaption|expression|blockquote|plaintext|textarea|progress|optgroup|noscript|noframes|menuitem|frameset|fieldset|!DOCTYPE|datalist|colgroup|behavior|basefont|summary|section|isindex|details|caption|bgsound|article|address|acronym|strong|strike|source|select|script|output|option|object|legend|keygen|ilayer|iframe|header|footer|figure|dialog|center|canvas|button|applet|video|track|title|thead|tfoot|tbody|table|style|small|param|meter|layer|label|input|frame|embed|blink|audio|aside|alert|time|span|samp|ruby|meta|menu|mark|main|link|html|head|form|font|code|cite|body|base|area|abbr|xss|xml|wbr|var|svg|sup|sub|pre|nav|map|kbd|ins|img|div|dir|dfn|del|col|big|bdo|bdi|!--|ul|tt|tr|th|td|rt|rp|ol|li|hr|em|dt|dl|dd|br|u|s|q|p|i|b|a|(h[0-9]+)))([^><]*)([><]*)`) // Requires a string to match a given urlencoded regex pattern var urlencodedPattern = regexp.MustCompile(`(?im)(\%[0-9a-fA-F]{1,})`) // Requires a string to match a given control characters regex pattern (ASCII : 00-08, 11, 12, 14, 15-31) var controlcharPattern = regexp.MustCompile(`(?im)([\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+)`) func isPureTextNormal(str string) (bool, error) { decoded_str := html.UnescapeString(str) matched_urlencoded := urlencodedPattern.MatchString(decoded_str) if matched_urlencoded == true { temp_buf, err := url.QueryUnescape(decoded_str) if err == nil { decoded_str = temp_buf } } matched_element := elementPattern.MatchString(decoded_str) if matched_element == true { return false, errors.New("detect html element") } matched_cc := controlcharPattern.MatchString(decoded_str) if matched_cc == true { return false, errors.New("detect control character") } return true, nil } func (p PureText) IsSatisfied(obj interface{}) bool { if str, ok := obj.(string); ok { var ret bool switch p.Mode { case STRICT: ret, _ = isPureTextStrict(str) case NORMAL: ret, _ = isPureTextStrict(str) } return ret } return false } func (p PureText) DefaultMessage() string { return fmt.Sprintln("Must be a vaild Text") } const ( ONLY_FILENAME = 0 ALLOW_RELATIVE_PATH = 1 ) const regexDenyFileNameCharList = `[\x00-\x1f|\x21-\x2c|\x3b-\x40|\x5b-\x5e|\x60|\x7b-\x7f]+` const regexDenyFileName = `|\x2e\x2e\x2f+` var checkAllowRelativePath = regexp.MustCompile(`(?m)(` + regexDenyFileNameCharList + `)`) var checkDenyRelativePath = regexp.MustCompile(`(?m)(` + regexDenyFileNameCharList + regexDenyFileName + `)`) // Requires an string to be sanitary file path type FilePath struct { Mode int } func ValidFilePath(m int) FilePath { if m != ONLY_FILENAME && m != ALLOW_RELATIVE_PATH { m = ONLY_FILENAME } return FilePath{m} } func (f FilePath) IsSatisfied(obj interface{}) bool { if str, ok := obj.(string); ok { var ret bool switch f.Mode { case ALLOW_RELATIVE_PATH: ret = checkAllowRelativePath.MatchString(str) if ret == false { return true } default: //ONLY_FILENAME ret = checkDenyRelativePath.MatchString(str) if ret == false { return true } } } return false } func (f FilePath) DefaultMessage() string { return fmt.Sprintln("Must be a unsanitary string") }