Files
spyda/internal/utils.go
2021-01-30 14:05:04 +10:00

401 lines
8.1 KiB
Go

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 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(),
)
}