401 lines
8.1 KiB
Go
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(),
|
|
)
|
|
}
|