Permalink
Fetching contributors…
Cannot retrieve contributors at this time
644 lines (505 sloc) 13.5 KB
// 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"
"time"
"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
}
if str, ok := obj.(string); ok {
return utf8.RuneCountInString(str) > 0
}
if b, ok := obj.(bool); ok {
return b
}
if i, ok := obj.(int); ok {
return i != 0
}
if t, ok := obj.(time.Time); ok {
return !t.IsZero()
}
v := reflect.ValueOf(obj)
if v.Kind() == reflect.Slice {
return v.Len() > 0
}
return true
}
func (r Required) DefaultMessage() string {
return "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<tag>(/*\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")
}