util.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. // Package util provides some common utility methods
  2. package util
  3. import (
  4. "bytes"
  5. "crypto/ecdsa"
  6. "crypto/ed25519"
  7. "crypto/elliptic"
  8. "crypto/rand"
  9. "crypto/rsa"
  10. "crypto/tls"
  11. "crypto/x509"
  12. "encoding/pem"
  13. "errors"
  14. "fmt"
  15. "html/template"
  16. "io"
  17. "io/fs"
  18. "net"
  19. "net/http"
  20. "net/url"
  21. "os"
  22. "path"
  23. "path/filepath"
  24. "regexp"
  25. "runtime"
  26. "strings"
  27. "time"
  28. "github.com/google/uuid"
  29. "github.com/lithammer/shortuuid/v3"
  30. "github.com/rs/xid"
  31. "golang.org/x/crypto/ssh"
  32. "github.com/drakkan/sftpgo/v2/logger"
  33. )
  34. const (
  35. logSender = "util"
  36. osWindows = "windows"
  37. )
  38. var (
  39. xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For")
  40. xRealIP = http.CanonicalHeaderKey("X-Real-IP")
  41. cfConnectingIP = http.CanonicalHeaderKey("CF-Connecting-IP")
  42. trueClientIP = http.CanonicalHeaderKey("True-Client-IP")
  43. emailRegex = regexp.MustCompile("^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$")
  44. )
  45. // Contains reports whether v is present in elems.
  46. func Contains[T comparable](elems []T, v T) bool {
  47. for _, s := range elems {
  48. if v == s {
  49. return true
  50. }
  51. }
  52. return false
  53. }
  54. // IsStringPrefixInSlice searches a string prefix in a slice and returns true
  55. // if a matching prefix is found
  56. func IsStringPrefixInSlice(obj string, list []string) bool {
  57. for i := 0; i < len(list); i++ {
  58. if strings.HasPrefix(obj, list[i]) {
  59. return true
  60. }
  61. }
  62. return false
  63. }
  64. // RemoveDuplicates returns a new slice removing any duplicate element from the initial one
  65. func RemoveDuplicates(obj []string) []string {
  66. if len(obj) == 0 {
  67. return obj
  68. }
  69. seen := make(map[string]bool)
  70. validIdx := 0
  71. for _, item := range obj {
  72. if !seen[item] {
  73. seen[item] = true
  74. obj[validIdx] = item
  75. validIdx++
  76. }
  77. }
  78. return obj[:validIdx]
  79. }
  80. // GetTimeAsMsSinceEpoch returns unix timestamp as milliseconds from a time struct
  81. func GetTimeAsMsSinceEpoch(t time.Time) int64 {
  82. return t.UnixMilli()
  83. }
  84. // GetTimeFromMsecSinceEpoch return a time struct from a unix timestamp with millisecond precision
  85. func GetTimeFromMsecSinceEpoch(msec int64) time.Time {
  86. return time.Unix(0, msec*1000000)
  87. }
  88. // GetDurationAsString returns a string representation for a time.Duration
  89. func GetDurationAsString(d time.Duration) string {
  90. d = d.Round(time.Second)
  91. h := d / time.Hour
  92. d -= h * time.Hour
  93. m := d / time.Minute
  94. d -= m * time.Minute
  95. s := d / time.Second
  96. if h > 0 {
  97. return fmt.Sprintf("%02d:%02d:%02d", h, m, s)
  98. }
  99. return fmt.Sprintf("%02d:%02d", m, s)
  100. }
  101. // ByteCountSI returns humanized size in SI (decimal) format
  102. func ByteCountSI(b int64) string {
  103. return byteCount(b, 1000)
  104. }
  105. // ByteCountIEC returns humanized size in IEC (binary) format
  106. func ByteCountIEC(b int64) string {
  107. return byteCount(b, 1024)
  108. }
  109. func byteCount(b int64, unit int64) string {
  110. if b < unit {
  111. return fmt.Sprintf("%d B", b)
  112. }
  113. div, exp := unit, 0
  114. for n := b / unit; n >= unit; n /= unit {
  115. div *= unit
  116. exp++
  117. }
  118. if unit == 1000 {
  119. return fmt.Sprintf("%.1f %cB",
  120. float64(b)/float64(div), "KMGTPE"[exp])
  121. }
  122. return fmt.Sprintf("%.1f %ciB",
  123. float64(b)/float64(div), "KMGTPE"[exp])
  124. }
  125. // GetIPFromRemoteAddress returns the IP from the remote address.
  126. // If the given remote address cannot be parsed it will be returned unchanged
  127. func GetIPFromRemoteAddress(remoteAddress string) string {
  128. ip, _, err := net.SplitHostPort(remoteAddress)
  129. if err == nil {
  130. return ip
  131. }
  132. return remoteAddress
  133. }
  134. // NilIfEmpty returns nil if the input string is empty
  135. func NilIfEmpty(s string) *string {
  136. if s == "" {
  137. return nil
  138. }
  139. return &s
  140. }
  141. // GetStringFromPointer returns the string value or empty if nil
  142. func GetStringFromPointer(val *string) string {
  143. if val == nil {
  144. return ""
  145. }
  146. return *val
  147. }
  148. // GetIntFromPointer returns the int value or zero
  149. func GetIntFromPointer(val *int64) int64 {
  150. if val == nil {
  151. return 0
  152. }
  153. return *val
  154. }
  155. // GetTimeFromPointer returns the time value or now
  156. func GetTimeFromPointer(val *time.Time) time.Time {
  157. if val == nil {
  158. return time.Now()
  159. }
  160. return *val
  161. }
  162. // GenerateRSAKeys generate rsa private and public keys and write the
  163. // private key to specified file and the public key to the specified
  164. // file adding the .pub suffix
  165. func GenerateRSAKeys(file string) error {
  166. if err := createDirPathIfMissing(file, 0700); err != nil {
  167. return err
  168. }
  169. key, err := rsa.GenerateKey(rand.Reader, 4096)
  170. if err != nil {
  171. return err
  172. }
  173. o, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
  174. if err != nil {
  175. return err
  176. }
  177. defer o.Close()
  178. priv := &pem.Block{
  179. Type: "RSA PRIVATE KEY",
  180. Bytes: x509.MarshalPKCS1PrivateKey(key),
  181. }
  182. if err := pem.Encode(o, priv); err != nil {
  183. return err
  184. }
  185. pub, err := ssh.NewPublicKey(&key.PublicKey)
  186. if err != nil {
  187. return err
  188. }
  189. return os.WriteFile(file+".pub", ssh.MarshalAuthorizedKey(pub), 0600)
  190. }
  191. // GenerateECDSAKeys generate ecdsa private and public keys and write the
  192. // private key to specified file and the public key to the specified
  193. // file adding the .pub suffix
  194. func GenerateECDSAKeys(file string) error {
  195. if err := createDirPathIfMissing(file, 0700); err != nil {
  196. return err
  197. }
  198. key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
  199. if err != nil {
  200. return err
  201. }
  202. keyBytes, err := x509.MarshalECPrivateKey(key)
  203. if err != nil {
  204. return err
  205. }
  206. priv := &pem.Block{
  207. Type: "EC PRIVATE KEY",
  208. Bytes: keyBytes,
  209. }
  210. o, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
  211. if err != nil {
  212. return err
  213. }
  214. defer o.Close()
  215. if err := pem.Encode(o, priv); err != nil {
  216. return err
  217. }
  218. pub, err := ssh.NewPublicKey(&key.PublicKey)
  219. if err != nil {
  220. return err
  221. }
  222. return os.WriteFile(file+".pub", ssh.MarshalAuthorizedKey(pub), 0600)
  223. }
  224. // GenerateEd25519Keys generate ed25519 private and public keys and write the
  225. // private key to specified file and the public key to the specified
  226. // file adding the .pub suffix
  227. func GenerateEd25519Keys(file string) error {
  228. pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
  229. if err != nil {
  230. return err
  231. }
  232. keyBytes, err := x509.MarshalPKCS8PrivateKey(privKey)
  233. if err != nil {
  234. return err
  235. }
  236. priv := &pem.Block{
  237. Type: "PRIVATE KEY",
  238. Bytes: keyBytes,
  239. }
  240. o, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
  241. if err != nil {
  242. return err
  243. }
  244. defer o.Close()
  245. if err := pem.Encode(o, priv); err != nil {
  246. return err
  247. }
  248. pub, err := ssh.NewPublicKey(pubKey)
  249. if err != nil {
  250. return err
  251. }
  252. return os.WriteFile(file+".pub", ssh.MarshalAuthorizedKey(pub), 0600)
  253. }
  254. // GetDirsForVirtualPath returns all the directory for the given path in reverse order
  255. // for example if the path is: /1/2/3/4 it returns:
  256. // [ "/1/2/3/4", "/1/2/3", "/1/2", "/1", "/" ]
  257. func GetDirsForVirtualPath(virtualPath string) []string {
  258. if virtualPath == "" || virtualPath == "." {
  259. virtualPath = "/"
  260. } else {
  261. if !path.IsAbs(virtualPath) {
  262. virtualPath = CleanPath(virtualPath)
  263. }
  264. }
  265. dirsForPath := []string{virtualPath}
  266. for {
  267. if virtualPath == "/" {
  268. break
  269. }
  270. virtualPath = path.Dir(virtualPath)
  271. dirsForPath = append(dirsForPath, virtualPath)
  272. }
  273. return dirsForPath
  274. }
  275. // CleanPath returns a clean POSIX (/) absolute path to work with
  276. func CleanPath(p string) string {
  277. p = filepath.ToSlash(p)
  278. if !path.IsAbs(p) {
  279. p = "/" + p
  280. }
  281. return path.Clean(p)
  282. }
  283. // LoadTemplate parses the given template paths.
  284. // It behaves like template.Must but it writes a log before exiting.
  285. // You can optionally provide a base template (e.g. to define some custom functions)
  286. func LoadTemplate(base *template.Template, paths ...string) *template.Template {
  287. var t *template.Template
  288. var err error
  289. if base != nil {
  290. base, err = base.Clone()
  291. if err == nil {
  292. t, err = base.ParseFiles(paths...)
  293. }
  294. } else {
  295. t, err = template.ParseFiles(paths...)
  296. }
  297. if err != nil {
  298. logger.ErrorToConsole("error loading required template: %v", err)
  299. logger.Error(logSender, "", "error loading required template: %v", err)
  300. panic(err)
  301. }
  302. return t
  303. }
  304. // IsFileInputValid returns true this is a valid file name.
  305. // This method must be used before joining a file name, generally provided as
  306. // user input, with a directory
  307. func IsFileInputValid(fileInput string) bool {
  308. cleanInput := filepath.Clean(fileInput)
  309. if cleanInput == "." || cleanInput == ".." {
  310. return false
  311. }
  312. return true
  313. }
  314. // FindSharedDataPath searches for the specified directory name in searchDir
  315. // and in system-wide shared data directories.
  316. // If name is an absolute path it is returned unmodified.
  317. func FindSharedDataPath(name, searchDir string) string {
  318. if !IsFileInputValid(name) {
  319. return ""
  320. }
  321. if name != "" && !filepath.IsAbs(name) {
  322. searchList := []string{searchDir}
  323. if runtime.GOOS != osWindows {
  324. searchList = append(searchList, "/usr/share/sftpgo")
  325. searchList = append(searchList, "/usr/local/share/sftpgo")
  326. }
  327. for _, basePath := range searchList {
  328. res := filepath.Join(basePath, name)
  329. _, err := os.Stat(res)
  330. if err == nil {
  331. logger.Debug(logSender, "", "found share data path for name %#v: %#v", name, res)
  332. return res
  333. }
  334. }
  335. return filepath.Join(searchDir, name)
  336. }
  337. return name
  338. }
  339. // CleanDirInput sanitizes user input for directories.
  340. // On Windows it removes any trailing `"`.
  341. // We try to help windows users that set an invalid path such as "C:\ProgramData\SFTPGO\".
  342. // This will only help if the invalid path is the last argument, for example in this command:
  343. // sftpgo.exe serve -c "C:\ProgramData\SFTPGO\" -l "sftpgo.log"
  344. // the -l flag will be ignored and the -c flag will get the value `C:\ProgramData\SFTPGO" -l sftpgo.log`
  345. // since the backslash after SFTPGO escape the double quote. This is definitely a bad user input
  346. func CleanDirInput(dirInput string) string {
  347. if runtime.GOOS == osWindows {
  348. for strings.HasSuffix(dirInput, "\"") {
  349. dirInput = strings.TrimSuffix(dirInput, "\"")
  350. }
  351. }
  352. return filepath.Clean(dirInput)
  353. }
  354. func createDirPathIfMissing(file string, perm os.FileMode) error {
  355. dirPath := filepath.Dir(file)
  356. if _, err := os.Stat(dirPath); errors.Is(err, fs.ErrNotExist) {
  357. err = os.MkdirAll(dirPath, perm)
  358. if err != nil {
  359. return err
  360. }
  361. }
  362. return nil
  363. }
  364. // GenerateRandomBytes generates the secret to use for JWT auth
  365. func GenerateRandomBytes(length int) []byte {
  366. b := make([]byte, length)
  367. _, err := io.ReadFull(rand.Reader, b)
  368. if err == nil {
  369. return b
  370. }
  371. b = xid.New().Bytes()
  372. for len(b) < length {
  373. b = append(b, xid.New().Bytes()...)
  374. }
  375. return b[:length]
  376. }
  377. // GenerateUniqueID retuens an unique ID
  378. func GenerateUniqueID() string {
  379. u, err := uuid.NewRandom()
  380. if err != nil {
  381. return xid.New().String()
  382. }
  383. return shortuuid.DefaultEncoder.Encode(u)
  384. }
  385. // HTTPListenAndServe is a wrapper for ListenAndServe that support both tcp
  386. // and Unix-domain sockets
  387. func HTTPListenAndServe(srv *http.Server, address string, port int, isTLS bool, logSender string) error {
  388. var listener net.Listener
  389. var err error
  390. if filepath.IsAbs(address) && runtime.GOOS != osWindows {
  391. if !IsFileInputValid(address) {
  392. return fmt.Errorf("invalid socket address %#v", address)
  393. }
  394. err = createDirPathIfMissing(address, os.ModePerm)
  395. if err != nil {
  396. logger.ErrorToConsole("error creating Unix-domain socket parent dir: %v", err)
  397. logger.Error(logSender, "", "error creating Unix-domain socket parent dir: %v", err)
  398. }
  399. os.Remove(address)
  400. listener, err = newListener("unix", address, srv.ReadTimeout, srv.WriteTimeout)
  401. } else {
  402. CheckTCP4Port(port)
  403. listener, err = newListener("tcp", fmt.Sprintf("%s:%d", address, port), srv.ReadTimeout, srv.WriteTimeout)
  404. }
  405. if err != nil {
  406. return err
  407. }
  408. logger.Info(logSender, "", "server listener registered, address: %v TLS enabled: %v", listener.Addr().String(), isTLS)
  409. defer listener.Close()
  410. if isTLS {
  411. return srv.ServeTLS(listener, "", "")
  412. }
  413. return srv.Serve(listener)
  414. }
  415. // GetTLSCiphersFromNames returns the TLS ciphers from the specified names
  416. func GetTLSCiphersFromNames(cipherNames []string) []uint16 {
  417. var ciphers []uint16
  418. for _, name := range RemoveDuplicates(cipherNames) {
  419. for _, c := range tls.CipherSuites() {
  420. if c.Name == strings.TrimSpace(name) {
  421. ciphers = append(ciphers, c.ID)
  422. }
  423. }
  424. }
  425. return ciphers
  426. }
  427. // EncodeTLSCertToPem returns the specified certificate PEM encoded.
  428. // This can be verified using openssl x509 -in cert.crt -text -noout
  429. func EncodeTLSCertToPem(tlsCert *x509.Certificate) (string, error) {
  430. if len(tlsCert.Raw) == 0 {
  431. return "", errors.New("invalid x509 certificate, no der contents")
  432. }
  433. publicKeyBlock := pem.Block{
  434. Type: "CERTIFICATE",
  435. Bytes: tlsCert.Raw,
  436. }
  437. return string(pem.EncodeToMemory(&publicKeyBlock)), nil
  438. }
  439. // CheckTCP4Port quits the app if bind on the given IPv4 port fails.
  440. // This is a ugly hack to avoid to bind on an already used port.
  441. // It is required on Windows only. Upstream does not consider this
  442. // behaviour a bug:
  443. // https://github.com/golang/go/issues/45150
  444. func CheckTCP4Port(port int) {
  445. if runtime.GOOS != osWindows {
  446. return
  447. }
  448. listener, err := net.Listen("tcp4", fmt.Sprintf(":%d", port))
  449. if err != nil {
  450. logger.ErrorToConsole("unable to bind on tcp4 address: %v", err)
  451. logger.Error(logSender, "", "unable to bind on tcp4 address: %v", err)
  452. os.Exit(1)
  453. }
  454. listener.Close()
  455. }
  456. // IsByteArrayEmpty return true if the byte array is empty or a new line
  457. func IsByteArrayEmpty(b []byte) bool {
  458. if len(b) == 0 {
  459. return true
  460. }
  461. if bytes.Equal(b, []byte("\n")) {
  462. return true
  463. }
  464. if bytes.Equal(b, []byte("\r\n")) {
  465. return true
  466. }
  467. return false
  468. }
  469. // GetSSHPublicKeyAsString returns an SSH public key serialized as string
  470. func GetSSHPublicKeyAsString(pubKey []byte) (string, error) {
  471. if len(pubKey) == 0 {
  472. return "", nil
  473. }
  474. k, err := ssh.ParsePublicKey(pubKey)
  475. if err != nil {
  476. return "", err
  477. }
  478. return string(ssh.MarshalAuthorizedKey(k)), nil
  479. }
  480. // GetRealIP returns the ip address as result of parsing either the
  481. // X-Real-IP header or the X-Forwarded-For header
  482. func GetRealIP(r *http.Request) string {
  483. var ip string
  484. if clientIP := r.Header.Get(trueClientIP); clientIP != "" {
  485. ip = clientIP
  486. } else if xrip := r.Header.Get(xRealIP); xrip != "" {
  487. ip = xrip
  488. } else if clientIP := r.Header.Get(cfConnectingIP); clientIP != "" {
  489. ip = clientIP
  490. } else if xff := r.Header.Get(xForwardedFor); xff != "" {
  491. i := strings.Index(xff, ",")
  492. if i == -1 {
  493. i = len(xff)
  494. }
  495. ip = strings.TrimSpace(xff[:i])
  496. }
  497. if ip == "" || net.ParseIP(ip) == nil {
  498. return ""
  499. }
  500. return ip
  501. }
  502. // GetHTTPLocalAddress returns the local address for an http.Request
  503. // or empty if it cannot be determined
  504. func GetHTTPLocalAddress(r *http.Request) string {
  505. if r == nil {
  506. return ""
  507. }
  508. localAddr, ok := r.Context().Value(http.LocalAddrContextKey).(net.Addr)
  509. if ok {
  510. return localAddr.String()
  511. }
  512. return ""
  513. }
  514. // ParseAllowedIPAndRanges returns a list of functions that allow to find if an
  515. // IP is equal or is contained within the allowed list
  516. func ParseAllowedIPAndRanges(allowed []string) ([]func(net.IP) bool, error) {
  517. res := make([]func(net.IP) bool, len(allowed))
  518. for i, allowFrom := range allowed {
  519. if strings.LastIndex(allowFrom, "/") > 0 {
  520. _, ipRange, err := net.ParseCIDR(allowFrom)
  521. if err != nil {
  522. return nil, fmt.Errorf("given string %q is not a valid IP range: %v", allowFrom, err)
  523. }
  524. res[i] = ipRange.Contains
  525. } else {
  526. allowed := net.ParseIP(allowFrom)
  527. if allowed == nil {
  528. return nil, fmt.Errorf("given string %q is not a valid IP address", allowFrom)
  529. }
  530. res[i] = allowed.Equal
  531. }
  532. }
  533. return res, nil
  534. }
  535. // GetRedactedURL returns the url redacting the password if any
  536. func GetRedactedURL(rawurl string) string {
  537. if !strings.HasPrefix(rawurl, "http") {
  538. return rawurl
  539. }
  540. u, err := url.Parse(rawurl)
  541. if err != nil {
  542. return rawurl
  543. }
  544. return u.Redacted()
  545. }
  546. // PrependFileInfo prepends a file info to a slice in an efficient way.
  547. // We, optimistically, assume that the slice has enough capacity
  548. func PrependFileInfo(files []os.FileInfo, info os.FileInfo) []os.FileInfo {
  549. files = append(files, nil)
  550. copy(files[1:], files)
  551. files[0] = info
  552. return files
  553. }
  554. // GetTLSVersion returns the TLS version for integer:
  555. // - 12 means TLS 1.2
  556. // - 13 means TLS 1.3
  557. // default is TLS 1.2
  558. func GetTLSVersion(val int) uint16 {
  559. switch val {
  560. case 13:
  561. return tls.VersionTLS13
  562. default:
  563. return tls.VersionTLS12
  564. }
  565. }
  566. // IsEmailValid returns true if the specified email address is valid
  567. func IsEmailValid(email string) bool {
  568. return emailRegex.MatchString(email)
  569. }