2020-04-29 17:44:29 -07:00
|
|
|
package gcutil
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/md5"
|
2023-05-23 08:47:27 -07:00
|
|
|
"crypto/sha1" // skipcq GSC-G505
|
2020-04-29 17:44:29 -07:00
|
|
|
"encoding/json"
|
2020-06-06 09:28:45 -07:00
|
|
|
"errors"
|
2020-04-29 17:44:29 -07:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"math/rand"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
2023-12-28 00:36:24 -08:00
|
|
|
"net/netip"
|
2020-04-29 17:44:29 -07:00
|
|
|
"os"
|
2021-12-15 23:42:07 -08:00
|
|
|
"path"
|
2020-04-29 17:44:29 -07:00
|
|
|
"path/filepath"
|
|
|
|
"regexp"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/aquilax/tripcode"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
2022-01-29 23:47:13 -08:00
|
|
|
x_html "golang.org/x/net/html"
|
2020-04-29 17:44:29 -07:00
|
|
|
)
|
|
|
|
|
2021-03-26 11:10:05 -07:00
|
|
|
const (
|
2022-06-18 12:49:12 -07:00
|
|
|
// DefaultMaxAge is used for cookies that have an invalid or unset max age (default is 1 month)
|
2021-03-26 11:10:05 -07:00
|
|
|
DefaultMaxAge = 60 * 60 * 24 * 31
|
|
|
|
)
|
|
|
|
|
2020-04-29 17:44:29 -07:00
|
|
|
var (
|
2021-12-15 23:42:07 -08:00
|
|
|
// ErrNotImplemented should be used for unimplemented functionality when necessary, not for bugs
|
2023-08-30 11:20:24 -07:00
|
|
|
ErrNotImplemented = errors.New("not implemented")
|
2020-04-29 17:44:29 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
// BcryptSum generates and returns a checksum using the bcrypt hashing function
|
|
|
|
func BcryptSum(str string) string {
|
2023-12-19 14:33:40 -08:00
|
|
|
digest, err := bcrypt.GenerateFromPassword([]byte(str), 10)
|
2020-04-29 17:44:29 -07:00
|
|
|
if err == nil {
|
|
|
|
return string(digest)
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// Md5Sum generates and returns a checksum using the MD5 hashing function
|
|
|
|
func Md5Sum(str string) string {
|
2023-01-04 23:40:35 -08:00
|
|
|
hash := md5.New() // skipcq: GSC-G401
|
2020-04-29 17:44:29 -07:00
|
|
|
io.WriteString(hash, str)
|
|
|
|
return fmt.Sprintf("%x", hash.Sum(nil))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sha1Sum generates and returns a checksum using the SHA-1 hashing function
|
|
|
|
func Sha1Sum(str string) string {
|
2023-01-04 23:40:35 -08:00
|
|
|
hash := sha1.New() // skipcq: GSC-G401
|
2020-04-29 17:44:29 -07:00
|
|
|
io.WriteString(hash, str)
|
|
|
|
return fmt.Sprintf("%x", hash.Sum(nil))
|
|
|
|
}
|
|
|
|
|
|
|
|
// CloseHandle closes the given closer object only if it is non-nil
|
|
|
|
func CloseHandle(handle io.Closer) {
|
|
|
|
if handle != nil {
|
|
|
|
handle.Close()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteMatchingFiles deletes files in a folder (root) that match a given regular expression.
|
|
|
|
// Returns the number of files that were deleted, and any error encountered.
|
|
|
|
func DeleteMatchingFiles(root, match string) (filesDeleted int, err error) {
|
2022-09-08 15:45:29 -07:00
|
|
|
files, err := os.ReadDir(root)
|
2020-04-29 17:44:29 -07:00
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
for _, f := range files {
|
|
|
|
match, _ := regexp.MatchString(match, f.Name())
|
|
|
|
if match {
|
|
|
|
os.Remove(filepath.Join(root, f.Name()))
|
|
|
|
filesDeleted++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return filesDeleted, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// FindResource searches for a file in the given paths and returns the first one it finds
|
|
|
|
// or a blank string if none of the paths exist
|
|
|
|
func FindResource(paths ...string) string {
|
|
|
|
var err error
|
|
|
|
for _, filepath := range paths {
|
|
|
|
if _, err = os.Stat(filepath); err == nil {
|
|
|
|
return filepath
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2021-12-15 23:42:07 -08:00
|
|
|
// GetFileParts returns the base filename, the filename sans-extension, and the extension
|
|
|
|
// sans-filename
|
|
|
|
func GetFileParts(filename string) (string, string, string) {
|
|
|
|
base := path.Base(filename)
|
|
|
|
var noExt string
|
|
|
|
var ext string
|
|
|
|
lastIndex := strings.LastIndex(base, ".")
|
|
|
|
if lastIndex > -1 {
|
|
|
|
noExt = base[:strings.LastIndex(base, ".")]
|
|
|
|
ext = path.Ext(base)[1:]
|
2020-04-29 17:44:29 -07:00
|
|
|
}
|
2021-12-15 23:42:07 -08:00
|
|
|
return base, noExt, ext
|
|
|
|
}
|
2020-04-29 17:44:29 -07:00
|
|
|
|
|
|
|
// GetFormattedFilesize returns a human readable filesize
|
|
|
|
func GetFormattedFilesize(size float64) string {
|
|
|
|
if size < 1000 {
|
|
|
|
return fmt.Sprintf("%dB", int(size))
|
|
|
|
} else if size <= 100000 {
|
|
|
|
return fmt.Sprintf("%fKB", size/1024)
|
|
|
|
} else if size <= 100000000 {
|
|
|
|
return fmt.Sprintf("%fMB", size/1024.0/1024.0)
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("%0.2fGB", size/1024.0/1024.0/1024.0)
|
|
|
|
}
|
|
|
|
|
2024-01-12 15:00:32 -08:00
|
|
|
// GetRealIP checks the GC_TESTIP environment variable as well as HTTP_CF_CONNCTING_IP
|
|
|
|
// and X-Forwarded-For HTTP headers to get a potentially obfuscated IP address, before
|
|
|
|
// getting the request's reported remote address
|
2020-04-29 17:44:29 -07:00
|
|
|
func GetRealIP(request *http.Request) string {
|
2024-01-12 15:00:32 -08:00
|
|
|
if ip, ok := os.LookupEnv("GC_TESTIP"); ok {
|
|
|
|
return ip
|
|
|
|
}
|
2020-04-29 17:44:29 -07:00
|
|
|
if request.Header.Get("HTTP_CF_CONNECTING_IP") != "" {
|
|
|
|
return request.Header.Get("HTTP_CF_CONNECTING_IP")
|
|
|
|
}
|
|
|
|
if request.Header.Get("X-Forwarded-For") != "" {
|
|
|
|
return request.Header.Get("X-Forwarded-For")
|
|
|
|
}
|
|
|
|
remoteHost, _, err := net.SplitHostPort(request.RemoteAddr)
|
|
|
|
if err != nil {
|
|
|
|
return request.RemoteAddr
|
|
|
|
}
|
|
|
|
return remoteHost
|
|
|
|
}
|
|
|
|
|
|
|
|
// HackyStringToInt parses a string to an int, or 0 if error
|
|
|
|
func HackyStringToInt(text string) int {
|
|
|
|
value, _ := strconv.Atoi(text)
|
|
|
|
return value
|
|
|
|
}
|
|
|
|
|
|
|
|
// MarshalJSON creates a JSON string with the given data and returns the string and any errors
|
|
|
|
func MarshalJSON(data interface{}, indent bool) (string, error) {
|
|
|
|
var jsonBytes []byte
|
|
|
|
var err error
|
|
|
|
|
|
|
|
if indent {
|
|
|
|
jsonBytes, err = json.MarshalIndent(data, "", " ")
|
|
|
|
} else {
|
|
|
|
jsonBytes, err = json.Marshal(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
jsonBytes, _ = json.Marshal(map[string]string{"error": err.Error()})
|
|
|
|
}
|
|
|
|
return string(jsonBytes), err
|
|
|
|
}
|
|
|
|
|
2023-12-28 00:36:24 -08:00
|
|
|
// ParseIPRange takes a single IP address or an IP range of the form "networkIP/netmaskbits" and
|
|
|
|
// gives the starting IP and ending IP in the subnet
|
|
|
|
//
|
|
|
|
// More info: https://en.wikipedia.org/wiki/Subnet
|
|
|
|
func ParseIPRange(ipOrCIDR string) (string, string, error) {
|
|
|
|
var ipStart netip.Addr
|
|
|
|
|
|
|
|
if strings.ContainsRune(ipOrCIDR, '/') {
|
|
|
|
var ipEnd netip.Addr
|
|
|
|
// CIDR range
|
|
|
|
prefix, err := netip.ParsePrefix(ipOrCIDR)
|
|
|
|
if err != nil {
|
|
|
|
return "", "", err
|
|
|
|
}
|
|
|
|
ipStart = prefix.Addr()
|
|
|
|
ipEnd = prefix.Addr()
|
|
|
|
var tmp netip.Addr
|
|
|
|
for {
|
|
|
|
tmp = ipEnd.Next()
|
|
|
|
if !prefix.Contains(tmp) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
ipEnd = tmp
|
|
|
|
}
|
|
|
|
return ipStart.String(), ipEnd.String(), nil
|
|
|
|
}
|
|
|
|
// single IP
|
|
|
|
var err error
|
|
|
|
if ipStart, err = netip.ParseAddr(ipOrCIDR); err != nil {
|
|
|
|
return "", "", err
|
|
|
|
}
|
|
|
|
return ipStart.String(), ipStart.String(), nil
|
|
|
|
}
|
|
|
|
|
2023-12-28 23:06:44 -08:00
|
|
|
// GetIPRangeSubnet returns the smallest subnet that contains the start and end
|
|
|
|
// IP addresses, and any errors that occured
|
|
|
|
func GetIPRangeSubnet(start string, end string) (*net.IPNet, error) {
|
2023-12-28 00:36:24 -08:00
|
|
|
startIP := net.ParseIP(start)
|
|
|
|
endIP := net.ParseIP(end)
|
|
|
|
if startIP == nil {
|
2023-12-28 23:06:44 -08:00
|
|
|
return nil, fmt.Errorf("invalid IP address %s", start)
|
2023-12-28 00:36:24 -08:00
|
|
|
}
|
|
|
|
if endIP == nil {
|
2023-12-28 23:06:44 -08:00
|
|
|
return nil, fmt.Errorf("invalid IP address %s", end)
|
2023-12-28 00:36:24 -08:00
|
|
|
}
|
|
|
|
if len(startIP) != len(endIP) {
|
2023-12-28 23:06:44 -08:00
|
|
|
return nil, errors.New("ip addresses must both be IPv4 or IPv6")
|
2023-12-28 00:36:24 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
if startIP.To4() != nil {
|
|
|
|
startIP = startIP.To4()
|
|
|
|
endIP = endIP.To4()
|
|
|
|
}
|
|
|
|
|
|
|
|
bits := 0
|
2023-12-28 23:06:44 -08:00
|
|
|
var ipn *net.IPNet
|
2023-12-28 00:36:24 -08:00
|
|
|
for b := range startIP {
|
|
|
|
if startIP[b] == endIP[b] {
|
|
|
|
bits += 8
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
for i := 7; i >= 0; i-- {
|
|
|
|
if startIP[b]&(1<<i) == endIP[b]&(1<<i) {
|
|
|
|
bits++
|
|
|
|
continue
|
|
|
|
}
|
2023-12-28 23:06:44 -08:00
|
|
|
ipn = &net.IPNet{IP: startIP, Mask: net.CIDRMask(bits, len(startIP)*8)}
|
|
|
|
return ipn, nil
|
2023-12-28 00:36:24 -08:00
|
|
|
}
|
|
|
|
}
|
2023-12-28 23:06:44 -08:00
|
|
|
return &net.IPNet{IP: startIP, Mask: net.CIDRMask(bits, len(startIP)*8)}, nil
|
2023-12-28 00:36:24 -08:00
|
|
|
}
|
|
|
|
|
2020-04-29 17:44:29 -07:00
|
|
|
// ParseName takes a name string from a request object and returns the name and tripcode parts
|
2023-04-26 13:10:38 -07:00
|
|
|
func ParseName(name string) (string, string) {
|
|
|
|
var namePart string
|
|
|
|
var tripcodePart string
|
2020-04-29 17:44:29 -07:00
|
|
|
if !strings.Contains(name, "#") {
|
2023-04-26 13:10:38 -07:00
|
|
|
namePart = name
|
2020-04-29 17:44:29 -07:00
|
|
|
} else if strings.Index(name, "#") == 0 {
|
2023-04-26 13:10:38 -07:00
|
|
|
tripcodePart = tripcode.Tripcode(name[1:])
|
2020-04-29 17:44:29 -07:00
|
|
|
} else if strings.Index(name, "#") > 0 {
|
|
|
|
postNameArr := strings.SplitN(name, "#", 2)
|
2023-04-26 13:10:38 -07:00
|
|
|
namePart = postNameArr[0]
|
|
|
|
tripcodePart = tripcode.Tripcode(postNameArr[1])
|
2020-04-29 17:44:29 -07:00
|
|
|
}
|
2023-04-26 13:10:38 -07:00
|
|
|
return namePart, tripcodePart
|
2020-04-29 17:44:29 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// RandomString returns a randomly generated string of the given length
|
|
|
|
func RandomString(length int) string {
|
|
|
|
var str string
|
|
|
|
for i := 0; i < length; i++ {
|
2023-12-19 14:33:40 -08:00
|
|
|
num := rand.Intn(127) // skipcq: GSC-G404
|
2020-04-29 17:44:29 -07:00
|
|
|
if num < 32 {
|
|
|
|
num += 32
|
|
|
|
}
|
2020-10-23 19:13:06 -07:00
|
|
|
str += fmt.Sprintf("%c", num)
|
2020-04-29 17:44:29 -07:00
|
|
|
}
|
|
|
|
return str
|
|
|
|
}
|
2021-04-21 17:03:00 -07:00
|
|
|
|
2022-01-29 23:47:13 -08:00
|
|
|
func StripHTML(htmlIn string) string {
|
|
|
|
dom := x_html.NewTokenizer(strings.NewReader(htmlIn))
|
|
|
|
for tokenType := dom.Next(); tokenType != x_html.ErrorToken; {
|
|
|
|
if tokenType != x_html.TextToken {
|
|
|
|
tokenType = dom.Next()
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
txtContent := strings.TrimSpace(x_html.UnescapeString(string(dom.Text())))
|
|
|
|
if len(txtContent) > 0 {
|
|
|
|
return x_html.EscapeString(txtContent)
|
|
|
|
}
|
|
|
|
tokenType = dom.Next()
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|