package internal import ( "bytes" "context" "crypto/rand" "encoding/base32" "errors" "fmt" "html/template" "math" "net/http" "net/url" "os" "os/exec" "strconv" "strings" "syscall" "time" // Blank import so we can handle image/jpeg _ "image/gif" _ "image/jpeg" "git.mills.io/prologic/spyda" "github.com/goware/urlx" log "github.com/sirupsen/logrus" "github.com/writeas/slug" "golang.org/x/crypto/blake2b" ) const ( CacheDir = "cache" requestTimeout = time.Second * 30 DayAgo = time.Hour * 24 WeekAgo = DayAgo * 7 MonthAgo = DayAgo * 30 YearAgo = MonthAgo * 12 ) var ( ErrBadRequest = errors.New("error: request failed with non-200 response") ) func GenerateRandomToken() string { b := make([]byte, 16) rand.Read(b) return fmt.Sprintf("%x", b) } func FastHash(s string) string { sum := blake2b.Sum256([]byte(s)) // Base32 is URL-safe, unlike Base64, and shorter than hex. encoding := base32.StdEncoding.WithPadding(base32.NoPadding) hash := strings.ToLower(encoding.EncodeToString(sum[:])) return hash } func IntPow(x, y int) int { return int(math.Pow(float64(x), float64(y))) } func Slugify(uri string) string { u, err := url.Parse(uri) if err != nil { log.WithError(err).Warnf("Slugify(): error parsing uri: %s", uri) return "" } return slug.Make(fmt.Sprintf("%s/%s", u.Hostname(), u.Path)) } func Request(conf *Config, method, url string, headers http.Header) (*http.Response, error) { req, err := http.NewRequest(method, url, nil) if err != nil { log.WithError(err).Errorf("%s: http.NewRequest fail: %s", url, err) return nil, err } if headers == nil { headers = make(http.Header) } // Set a default User-Agent (if none set) if headers.Get("User-Agent") == "" { headers.Set( "User-Agent", fmt.Sprintf( "spyda/%s (%s Support: %s)", spyda.FullVersion(), conf.Name, URLForPage(conf.BaseURL, "support"), ), ) } req.Header = headers client := http.Client{ Timeout: requestTimeout, } res, err := client.Do(req) if err != nil { log.WithError(err).Errorf("%s: client.Do fail: %s", url, err) return nil, err } return res, nil } func ResourceExists(conf *Config, url string) bool { res, err := Request(conf, http.MethodHead, url, nil) if err != nil { log.WithError(err).Errorf("error checking if %s exists", url) return false } defer res.Body.Close() return res.StatusCode/100 == 2 } func FileExists(name string) bool { if _, err := os.Stat(name); err != nil { if os.IsNotExist(err) { return false } } return true } // CmdExists ... func CmdExists(cmd string) bool { _, err := exec.LookPath(cmd) return err == nil } // RunCmd ... func RunCmd(timeout time.Duration, command string, args ...string) error { var ( ctx context.Context cancel context.CancelFunc ) if timeout > 0 { ctx, cancel = context.WithTimeout(context.Background(), timeout) } else { ctx, cancel = context.WithCancel(context.Background()) } defer cancel() cmd := exec.CommandContext(ctx, command, args...) out, err := cmd.CombinedOutput() if err != nil { if exitError, ok := err.(*exec.ExitError); ok { if ws, ok := exitError.Sys().(syscall.WaitStatus); ok && ws.Signal() == syscall.SIGKILL { err = &ErrCommandKilled{Err: err, Signal: ws.Signal()} } else { err = &ErrCommandFailed{Err: err, Status: exitError.ExitCode()} } } log. WithError(err). WithField("out", string(out)). Errorf("error running command") return err } return nil } // RenderString ... func RenderString(tpl string, ctx *Context) (string, error) { t := template.Must(template.New("tpl").Parse(tpl)) buf := bytes.NewBuffer([]byte{}) err := t.Execute(buf, ctx) if err != nil { return "", err } return buf.String(), nil } func IsLocalURLFactory(conf *Config) func(url string) bool { return func(url string) bool { if NormalizeURL(url) == "" { return false } return strings.HasPrefix(NormalizeURL(url), NormalizeURL(conf.BaseURL)) } } func StringKeys(kv map[string]string) []string { var res []string for k := range kv { res = append(res, k) } return res } func StringValues(kv map[string]string) []string { var res []string for _, v := range kv { res = append(res, v) } return res } func MapStrings(xs []string, f func(s string) string) []string { var res []string for _, x := range xs { res = append(res, f(x)) } return res } func HasString(a []string, x string) bool { for _, n := range a { if x == n { return true } } return false } func UniqStrings(xs []string) []string { set := make(map[string]bool) for _, x := range xs { if _, ok := set[x]; !ok { set[x] = true } } res := []string{} for k := range set { res = append(res, k) } return res } func RemoveString(xs []string, e string) []string { res := []string{} for _, x := range xs { if x == e { continue } res = append(res, x) } return res } type URI struct { Type string Path string } func (u URI) IsZero() bool { return u.Type == "" && u.Path == "" } func (u URI) String() string { return fmt.Sprintf("%s://%s", u.Type, u.Path) } func ParseURI(uri string) (*URI, error) { parts := strings.Split(uri, "://") if len(parts) == 2 { return &URI{Type: strings.ToLower(parts[0]), Path: parts[1]}, nil } return nil, fmt.Errorf("invalid uri: %s", uri) } func NormalizeUsername(username string) string { return strings.TrimSpace(strings.ToLower(username)) } func NormalizeURL(url string) string { if url == "" { return "" } u, err := urlx.Parse(url) if err != nil { log.WithError(err).Errorf("error parsing url %s", url) return "" } if u.Scheme == "http" && strings.HasSuffix(u.Host, ":80") { u.Host = strings.TrimSuffix(u.Host, ":80") } if u.Scheme == "https" && strings.HasSuffix(u.Host, ":443") { u.Host = strings.TrimSuffix(u.Host, ":443") } u.User = nil u.Path = strings.TrimSuffix(u.Path, "/") norm, err := urlx.Normalize(u) if err != nil { log.WithError(err).Errorf("error normalizing url %s", url) return "" } return norm } // RedirectRefererURL constructs a Redirect URL from the given Request URL // and possibly Referer, if the Referer's Base URL matches the Pod's Base URL // will return the Referer URL otherwise the defaultURL. This is primarily used // to redirect a user from a successful /login back to the page they were on. func RedirectRefererURL(r *http.Request, conf *Config, defaultURL string) string { referer := NormalizeURL(r.Header.Get("Referer")) if referer != "" && strings.HasPrefix(referer, conf.BaseURL) { return referer } return defaultURL } func HostnameFromURL(uri string) string { u, err := url.Parse(uri) if err != nil { log.WithError(err).Warnf("HostnameFromURL(): error parsing url: %s", uri) return uri } return u.Hostname() } func PrettyURL(uri string) string { u, err := url.Parse(uri) if err != nil { log.WithError(err).Warnf("PrettyURL(): error parsing url: %s", uri) return uri } return fmt.Sprintf("%s/%s", u.Hostname(), strings.TrimPrefix(u.EscapedPath(), "/")) } // IsAdminUserFactory returns a function that returns true if the user provided // is the configured pod administrator, false otherwise. func IsAdminUserFactory(conf *Config) func(user *User) bool { return func(user *User) bool { return NormalizeUsername(conf.AdminUser) == NormalizeUsername(user.Username) } } func URLForPage(baseURL, page string) string { return fmt.Sprintf( "%s/%s", strings.TrimSuffix(baseURL, "/"), page, ) } func URLForCached(baseURL, hash string) string { return fmt.Sprintf( "%s/cached/%s", strings.TrimSuffix(baseURL, "/"), hash, ) } // SafeParseInt ... func SafeParseInt(s string, d int) int { n, e := strconv.Atoi(s) if e != nil { return d } return n } func FormatForDateTime(t time.Time) string { var format string dt := time.Since(t) if dt > YearAgo { format = "Mon, Jan 2 3:04PM 2006" } else if dt > MonthAgo { format = "Mon, Jan 2 3:04PM" } else if dt > WeekAgo { format = "Mon, Jan 2 3:04PM" } else if dt > DayAgo { format = "Mon 2, 3:04PM" } else { format = "3:04PM" } return format } // FormatRequest generates ascii representation of a request func FormatRequest(r *http.Request) string { return fmt.Sprintf( "%s %v %s%v %v (%s)", r.RemoteAddr, r.Method, r.Host, r.URL, r.Proto, r.UserAgent(), ) }