diff options
author | Unknwon <u@gogs.io> | 2019-10-24 01:51:46 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-10-24 01:51:46 -0700 |
commit | 01c8df01ec0608f1f25b2f1444adabb98fa5ee8a (patch) | |
tree | f8a7e5dd8d2a8c51e1ce2cabb9d33571a93314dd /internal/tool | |
parent | 613139e7bef81d3573e7988a47eb6765f3de347a (diff) |
internal: move packages under this directory (#5836)
* Rename pkg -> internal
* Rename routes -> route
* Move route -> internal/route
* Rename models -> db
* Move db -> internal/db
* Fix route2 -> route
* Move cmd -> internal/cmd
* Bump version
Diffstat (limited to 'internal/tool')
-rw-r--r-- | internal/tool/file.go | 77 | ||||
-rw-r--r-- | internal/tool/path.go | 23 | ||||
-rw-r--r-- | internal/tool/path_test.go | 53 | ||||
-rw-r--r-- | internal/tool/tool.go | 465 |
4 files changed, 618 insertions, 0 deletions
diff --git a/internal/tool/file.go b/internal/tool/file.go new file mode 100644 index 00000000..6ca4136a --- /dev/null +++ b/internal/tool/file.go @@ -0,0 +1,77 @@ +// Copyright 2017 The Gogs 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 tool + +import ( + "fmt" + "math" + "net/http" + "strings" +) + +// IsTextFile returns true if file content format is plain text or empty. +func IsTextFile(data []byte) bool { + if len(data) == 0 { + return true + } + return strings.Contains(http.DetectContentType(data), "text/") +} + +func IsImageFile(data []byte) bool { + return strings.Contains(http.DetectContentType(data), "image/") +} + +func IsPDFFile(data []byte) bool { + return strings.Contains(http.DetectContentType(data), "application/pdf") +} + +func IsVideoFile(data []byte) bool { + return strings.Contains(http.DetectContentType(data), "video/") +} + +const ( + Byte = 1 + KByte = Byte * 1024 + MByte = KByte * 1024 + GByte = MByte * 1024 + TByte = GByte * 1024 + PByte = TByte * 1024 + EByte = PByte * 1024 +) + +var bytesSizeTable = map[string]uint64{ + "b": Byte, + "kb": KByte, + "mb": MByte, + "gb": GByte, + "tb": TByte, + "pb": PByte, + "eb": EByte, +} + +func logn(n, b float64) float64 { + return math.Log(n) / math.Log(b) +} + +func humanateBytes(s uint64, base float64, sizes []string) string { + if s < 10 { + return fmt.Sprintf("%d B", s) + } + e := math.Floor(logn(float64(s), base)) + suffix := sizes[int(e)] + val := float64(s) / math.Pow(base, math.Floor(e)) + f := "%.0f" + if val < 10 { + f = "%.1f" + } + + return fmt.Sprintf(f+" %s", val, suffix) +} + +// FileSize calculates the file size and generate user-friendly string. +func FileSize(s int64) string { + sizes := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"} + return humanateBytes(uint64(s), 1024, sizes) +} diff --git a/internal/tool/path.go b/internal/tool/path.go new file mode 100644 index 00000000..e95bba8b --- /dev/null +++ b/internal/tool/path.go @@ -0,0 +1,23 @@ +// Copyright 2018 The Gogs 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 tool + +import ( + "path/filepath" + "strings" +) + +// IsSameSiteURLPath returns true if the URL path belongs to the same site, false otherwise. +// False: //url, http://url, /\url +// True: /url +func IsSameSiteURLPath(url string) bool { + return len(url) >= 2 && url[0] == '/' && url[1] != '/' && url[1] != '\\' +} + +// IsMaliciousPath returns true if given path is an absolute path or contains malicious content +// which has potential to traverse upper level directories. +func IsMaliciousPath(path string) bool { + return filepath.IsAbs(path) || strings.Contains(path, "..") +} diff --git a/internal/tool/path_test.go b/internal/tool/path_test.go new file mode 100644 index 00000000..44ee975f --- /dev/null +++ b/internal/tool/path_test.go @@ -0,0 +1,53 @@ +// Copyright 2018 The Gogs 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 tool + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func Test_IsSameSiteURLPath(t *testing.T) { + Convey("Check if a path belongs to the same site", t, func() { + testCases := []struct { + url string + expect bool + }{ + {"//github.com", false}, + {"http://github.com", false}, + {"https://github.com", false}, + {"/\\github.com", false}, + + {"/admin", true}, + {"/user/repo", true}, + } + + for _, tc := range testCases { + So(IsSameSiteURLPath(tc.url), ShouldEqual, tc.expect) + } + }) +} + +func Test_IsMaliciousPath(t *testing.T) { + Convey("Detects malicious path", t, func() { + testCases := []struct { + path string + expect bool + }{ + {"../../../../../../../../../data/gogs/data/sessions/a/9/a9f0ab6c3ef63dd8", true}, + {"..\\/..\\/../data/gogs/data/sessions/a/9/a9f0ab6c3ef63dd8", true}, + {"data/gogs/../../../../../../../../../data/sessions/a/9/a9f0ab6c3ef63dd8", true}, + {"..\\..\\..\\..\\..\\..\\..\\..\\..\\data\\gogs\\data\\sessions\\a\\9\\a9f0ab6c3ef63dd8", true}, + {"data\\gogs\\..\\..\\..\\..\\..\\..\\..\\..\\..\\data\\sessions\\a\\9\\a9f0ab6c3ef63dd8", true}, + + {"data/sessions/a/9/a9f0ab6c3ef63dd8", false}, + {"data\\sessions\\a\\9\\a9f0ab6c3ef63dd8", false}, + } + for _, tc := range testCases { + So(IsMaliciousPath(tc.path), ShouldEqual, tc.expect) + } + }) +} diff --git a/internal/tool/tool.go b/internal/tool/tool.go new file mode 100644 index 00000000..d177af0b --- /dev/null +++ b/internal/tool/tool.go @@ -0,0 +1,465 @@ +// Copyright 2014 The Gogs 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 tool + +import ( + "crypto/md5" + "crypto/rand" + "crypto/sha1" + "encoding/base64" + "encoding/hex" + "fmt" + "html/template" + "math/big" + "strings" + "time" + "unicode" + "unicode/utf8" + + "github.com/unknwon/com" + "github.com/unknwon/i18n" + log "gopkg.in/clog.v1" + + "github.com/gogs/chardet" + + "gogs.io/gogs/internal/setting" +) + +// MD5Bytes encodes string to MD5 bytes. +func MD5Bytes(str string) []byte { + m := md5.New() + m.Write([]byte(str)) + return m.Sum(nil) +} + +// MD5 encodes string to MD5 hex value. +func MD5(str string) string { + return hex.EncodeToString(MD5Bytes(str)) +} + +// SHA1 encodes string to SHA1 hex value. +func SHA1(str string) string { + h := sha1.New() + h.Write([]byte(str)) + return hex.EncodeToString(h.Sum(nil)) +} + +// ShortSHA1 truncates SHA1 string length to at most 10. +func ShortSHA1(sha1 string) string { + if len(sha1) > 10 { + return sha1[:10] + } + return sha1 +} + +// DetectEncoding returns best guess of encoding of given content. +func DetectEncoding(content []byte) (string, error) { + if utf8.Valid(content) { + log.Trace("Detected encoding: UTF-8 (fast)") + return "UTF-8", nil + } + + result, err := chardet.NewTextDetector().DetectBest(content) + if result.Charset != "UTF-8" && len(setting.Repository.AnsiCharset) > 0 { + log.Trace("Using default AnsiCharset: %s", setting.Repository.AnsiCharset) + return setting.Repository.AnsiCharset, err + } + + log.Trace("Detected encoding: %s", result.Charset) + return result.Charset, err +} + +// BasicAuthDecode decodes username and password portions of HTTP Basic Authentication +// from encoded content. +func BasicAuthDecode(encoded string) (string, string, error) { + s, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return "", "", err + } + + auth := strings.SplitN(string(s), ":", 2) + return auth[0], auth[1], nil +} + +// BasicAuthEncode encodes username and password in HTTP Basic Authentication format. +func BasicAuthEncode(username, password string) string { + return base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) +} + +const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +// RandomString returns generated random string in given length of characters. +// It also returns possible error during generation. +func RandomString(n int) (string, error) { + buffer := make([]byte, n) + max := big.NewInt(int64(len(alphanum))) + + for i := 0; i < n; i++ { + index, err := randomInt(max) + if err != nil { + return "", err + } + + buffer[i] = alphanum[index] + } + + return string(buffer), nil +} + +func randomInt(max *big.Int) (int, error) { + rand, err := rand.Int(rand.Reader, max) + if err != nil { + return 0, err + } + + return int(rand.Int64()), nil +} + +// verify time limit code +func VerifyTimeLimitCode(data string, minutes int, code string) bool { + if len(code) <= 18 { + return false + } + + // split code + start := code[:12] + lives := code[12:18] + if d, err := com.StrTo(lives).Int(); err == nil { + minutes = d + } + + // right active code + retCode := CreateTimeLimitCode(data, minutes, start) + if retCode == code && minutes > 0 { + // check time is expired or not + before, _ := time.ParseInLocation("200601021504", start, time.Local) + now := time.Now() + if before.Add(time.Minute*time.Duration(minutes)).Unix() > now.Unix() { + return true + } + } + + return false +} + +const TIME_LIMIT_CODE_LENGTH = 12 + 6 + 40 + +// CreateTimeLimitCode generates a time limit code based on given input data. +// Format: 12 length date time string + 6 minutes string + 40 sha1 encoded string +func CreateTimeLimitCode(data string, minutes int, startInf interface{}) string { + format := "200601021504" + + var start, end time.Time + var startStr, endStr string + + if startInf == nil { + // Use now time create code + start = time.Now() + startStr = start.Format(format) + } else { + // use start string create code + startStr = startInf.(string) + start, _ = time.ParseInLocation(format, startStr, time.Local) + startStr = start.Format(format) + } + + end = start.Add(time.Minute * time.Duration(minutes)) + endStr = end.Format(format) + + // create sha1 encode string + sh := sha1.New() + sh.Write([]byte(data + setting.SecretKey + startStr + endStr + com.ToStr(minutes))) + encoded := hex.EncodeToString(sh.Sum(nil)) + + code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded) + return code +} + +// HashEmail hashes email address to MD5 string. +// https://en.gravatar.com/site/implement/hash/ +func HashEmail(email string) string { + email = strings.ToLower(strings.TrimSpace(email)) + h := md5.New() + h.Write([]byte(email)) + return hex.EncodeToString(h.Sum(nil)) +} + +// AvatarLink returns relative avatar link to the site domain by given email, +// which includes app sub-url as prefix. However, it is possible +// to return full URL if user enables Gravatar-like service. +func AvatarLink(email string) (url string) { + if setting.EnableFederatedAvatar && setting.LibravatarService != nil && + strings.Contains(email, "@") { + var err error + url, err = setting.LibravatarService.FromEmail(email) + if err != nil { + log.Warn("AvatarLink.LibravatarService.FromEmail [%s]: %v", email, err) + } + } + if len(url) == 0 && !setting.DisableGravatar { + url = setting.GravatarSource + HashEmail(email) + "?d=identicon" + } + if len(url) == 0 { + url = setting.AppSubURL + "/img/avatar_default.png" + } + return url +} + +// AppendAvatarSize appends avatar size query parameter to the URL in the correct format. +func AppendAvatarSize(url string, size int) string { + if strings.Contains(url, "?") { + return url + "&s=" + com.ToStr(size) + } + return url + "?s=" + com.ToStr(size) +} + +// Seconds-based time units +const ( + Minute = 60 + Hour = 60 * Minute + Day = 24 * Hour + Week = 7 * Day + Month = 30 * Day + Year = 12 * Month +) + +func computeTimeDiff(diff int64) (int64, string) { + diffStr := "" + switch { + case diff <= 0: + diff = 0 + diffStr = "now" + case diff < 2: + diff = 0 + diffStr = "1 second" + case diff < 1*Minute: + diffStr = fmt.Sprintf("%d seconds", diff) + diff = 0 + + case diff < 2*Minute: + diff -= 1 * Minute + diffStr = "1 minute" + case diff < 1*Hour: + diffStr = fmt.Sprintf("%d minutes", diff/Minute) + diff -= diff / Minute * Minute + + case diff < 2*Hour: + diff -= 1 * Hour + diffStr = "1 hour" + case diff < 1*Day: + diffStr = fmt.Sprintf("%d hours", diff/Hour) + diff -= diff / Hour * Hour + + case diff < 2*Day: + diff -= 1 * Day + diffStr = "1 day" + case diff < 1*Week: + diffStr = fmt.Sprintf("%d days", diff/Day) + diff -= diff / Day * Day + + case diff < 2*Week: + diff -= 1 * Week + diffStr = "1 week" + case diff < 1*Month: + diffStr = fmt.Sprintf("%d weeks", diff/Week) + diff -= diff / Week * Week + + case diff < 2*Month: + diff -= 1 * Month + diffStr = "1 month" + case diff < 1*Year: + diffStr = fmt.Sprintf("%d months", diff/Month) + diff -= diff / Month * Month + + case diff < 2*Year: + diff -= 1 * Year + diffStr = "1 year" + default: + diffStr = fmt.Sprintf("%d years", diff/Year) + diff = 0 + } + return diff, diffStr +} + +// TimeSincePro calculates the time interval and generate full user-friendly string. +func TimeSincePro(then time.Time) string { + now := time.Now() + diff := now.Unix() - then.Unix() + + if then.After(now) { + return "future" + } + + var timeStr, diffStr string + for { + if diff == 0 { + break + } + + diff, diffStr = computeTimeDiff(diff) + timeStr += ", " + diffStr + } + return strings.TrimPrefix(timeStr, ", ") +} + +func timeSince(then time.Time, lang string) string { + now := time.Now() + + lbl := i18n.Tr(lang, "tool.ago") + diff := now.Unix() - then.Unix() + if then.After(now) { + lbl = i18n.Tr(lang, "tool.from_now") + diff = then.Unix() - now.Unix() + } + + switch { + case diff <= 0: + return i18n.Tr(lang, "tool.now") + case diff <= 2: + return i18n.Tr(lang, "tool.1s", lbl) + case diff < 1*Minute: + return i18n.Tr(lang, "tool.seconds", diff, lbl) + + case diff < 2*Minute: + return i18n.Tr(lang, "tool.1m", lbl) + case diff < 1*Hour: + return i18n.Tr(lang, "tool.minutes", diff/Minute, lbl) + + case diff < 2*Hour: + return i18n.Tr(lang, "tool.1h", lbl) + case diff < 1*Day: + return i18n.Tr(lang, "tool.hours", diff/Hour, lbl) + + case diff < 2*Day: + return i18n.Tr(lang, "tool.1d", lbl) + case diff < 1*Week: + return i18n.Tr(lang, "tool.days", diff/Day, lbl) + + case diff < 2*Week: + return i18n.Tr(lang, "tool.1w", lbl) + case diff < 1*Month: + return i18n.Tr(lang, "tool.weeks", diff/Week, lbl) + + case diff < 2*Month: + return i18n.Tr(lang, "tool.1mon", lbl) + case diff < 1*Year: + return i18n.Tr(lang, "tool.months", diff/Month, lbl) + + case diff < 2*Year: + return i18n.Tr(lang, "tool.1y", lbl) + default: + return i18n.Tr(lang, "tool.years", diff/Year, lbl) + } +} + +func RawTimeSince(t time.Time, lang string) string { + return timeSince(t, lang) +} + +// TimeSince calculates the time interval and generate user-friendly string. +func TimeSince(t time.Time, lang string) template.HTML { + return template.HTML(fmt.Sprintf(`<span class="time-since" title="%s">%s</span>`, t.Format(setting.TimeFormat), timeSince(t, lang))) +} + +// Subtract deals with subtraction of all types of number. +func Subtract(left interface{}, right interface{}) interface{} { + var rleft, rright int64 + var fleft, fright float64 + var isInt bool = true + switch left.(type) { + case int: + rleft = int64(left.(int)) + case int8: + rleft = int64(left.(int8)) + case int16: + rleft = int64(left.(int16)) + case int32: + rleft = int64(left.(int32)) + case int64: + rleft = left.(int64) + case float32: + fleft = float64(left.(float32)) + isInt = false + case float64: + fleft = left.(float64) + isInt = false + } + + switch right.(type) { + case int: + rright = int64(right.(int)) + case int8: + rright = int64(right.(int8)) + case int16: + rright = int64(right.(int16)) + case int32: + rright = int64(right.(int32)) + case int64: + rright = right.(int64) + case float32: + fright = float64(left.(float32)) + isInt = false + case float64: + fleft = left.(float64) + isInt = false + } + + if isInt { + return rleft - rright + } else { + return fleft + float64(rleft) - (fright + float64(rright)) + } +} + +// EllipsisString returns a truncated short string, +// it appends '...' in the end of the length of string is too large. +func EllipsisString(str string, length int) string { + if len(str) < length { + return str + } + return str[:length-3] + "..." +} + +// TruncateString returns a truncated string with given limit, +// it returns input string if length is not reached limit. +func TruncateString(str string, limit int) string { + if len(str) < limit { + return str + } + return str[:limit] +} + +// StringsToInt64s converts a slice of string to a slice of int64. +func StringsToInt64s(strs []string) []int64 { + ints := make([]int64, len(strs)) + for i := range strs { + ints[i] = com.StrTo(strs[i]).MustInt64() + } + return ints +} + +// Int64sToStrings converts a slice of int64 to a slice of string. +func Int64sToStrings(ints []int64) []string { + strs := make([]string, len(ints)) + for i := range ints { + strs[i] = com.ToStr(ints[i]) + } + return strs +} + +// Int64sToMap converts a slice of int64 to a int64 map. +func Int64sToMap(ints []int64) map[int64]bool { + m := make(map[int64]bool) + for _, i := range ints { + m[i] = true + } + return m +} + +// IsLetter reports whether the rune is a letter (category L). +// https://github.com/golang/go/blob/master/src/go/scanner/scanner.go#L257 +func IsLetter(ch rune) bool { + return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= 0x80 && unicode.IsLetter(ch) +} |