repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

pico / pgs
Mac Chaffee · 13 Oct 24

redirect.go

  1package pgs
  2
  3import (
  4	"fmt"
  5	"net/http"
  6	"regexp"
  7	"strconv"
  8	"strings"
  9)
 10
 11type RedirectRule struct {
 12	From       string
 13	To         string
 14	Status     int
 15	Query      map[string]string
 16	Conditions map[string]string
 17	Force      bool
 18	Signed     bool
 19}
 20
 21var reSplitWhitespace = regexp.MustCompile(`\s+`)
 22
 23func isUrl(text string) bool {
 24	return strings.HasPrefix(text, "http://") || strings.HasPrefix(text, "https://")
 25}
 26
 27func isToPart(part string) bool {
 28	return strings.HasPrefix(part, "/") || isUrl(part)
 29}
 30
 31func hasStatusCode(part string) (int, bool) {
 32	status := 0
 33	forced := false
 34	pt := part
 35	if strings.HasSuffix(part, "!") {
 36		pt = strings.TrimSuffix(part, "!")
 37		forced = true
 38	}
 39
 40	status, err := strconv.Atoi(pt)
 41	if err != nil {
 42		return 0, forced
 43	}
 44	return status, forced
 45}
 46
 47func parsePairs(pairs []string) map[string]string {
 48	mapper := map[string]string{}
 49	for _, pair := range pairs {
 50		val := strings.SplitN(pair, "=", 1)
 51		if len(val) > 1 {
 52			mapper[val[0]] = val[1]
 53		}
 54	}
 55	return mapper
 56}
 57
 58/*
 59https://github.com/netlify/build/blob/main/packages/redirect-parser/src/line_parser.js#L9-L26
 60Parse `_redirects` file to an array of objects.
 61Each line in that file must be either:
 62  - An empty line
 63  - A comment starting with #
 64  - A redirect line, optionally ended with a comment
 65
 66Each redirect line has the following format:
 67
 68	from [query] [to] [status[!]] [conditions]
 69
 70The parts are:
 71  - "from": a path or a URL
 72  - "query": a whitespace-separated list of "key=value"
 73  - "to": a path or a URL
 74  - "status": an HTTP status integer
 75  - "!": an optional exclamation mark appended to "status" meant to indicate
 76    "forced"
 77  - "conditions": a whitespace-separated list of "key=value"
 78  - "Sign" is a special condition
 79*/
 80func parseRedirectText(text string) ([]*RedirectRule, error) {
 81	rules := []*RedirectRule{}
 82	origLines := strings.Split(text, "\n")
 83	for _, line := range origLines {
 84		trimmed := strings.TrimSpace(line)
 85		// ignore empty lines
 86		if trimmed == "" {
 87			continue
 88		}
 89
 90		// ignore comments
 91		if strings.HasPrefix(trimmed, "#") {
 92			continue
 93		}
 94
 95		parts := reSplitWhitespace.Split(trimmed, -1)
 96		if len(parts) < 2 {
 97			return rules, fmt.Errorf("missing destination path/URL")
 98		}
 99
100		from := parts[0]
101		rest := parts[1:]
102		status, forced := hasStatusCode(rest[0])
103		if status != 0 {
104			rules = append(rules, &RedirectRule{
105				Query:  map[string]string{},
106				Status: status,
107				Force:  forced,
108			})
109		} else {
110			toIndex := -1
111			for idx, part := range rest {
112				if isToPart(part) {
113					toIndex = idx
114				}
115			}
116
117			if toIndex == -1 {
118				return rules, fmt.Errorf("the destination path/URL must start with '/', 'http:' or 'https:'")
119			}
120
121			queryParts := rest[:toIndex]
122			to := rest[toIndex]
123			lastParts := rest[toIndex+1:]
124			conditions := map[string]string{}
125			sts := http.StatusOK
126			frcd := false
127			if len(lastParts) > 0 {
128				sts, frcd = hasStatusCode(lastParts[0])
129			}
130			if len(lastParts) > 1 {
131				conditions = parsePairs(lastParts[1:])
132			}
133
134			rules = append(rules, &RedirectRule{
135				To:         to,
136				From:       from,
137				Status:     sts,
138				Force:      frcd,
139				Query:      parsePairs(queryParts),
140				Conditions: conditions,
141			})
142		}
143	}
144
145	return rules, nil
146}