fork of whitequark.org/git-pages with mods for tangled
at main 5.5 kB view raw
1package git_pages 2 3import ( 4 "fmt" 5 "net/http" 6 "net/url" 7 "slices" 8 "strings" 9 10 "github.com/tj/go-redirects" 11 "google.golang.org/protobuf/proto" 12) 13 14const RedirectsFileName string = "_redirects" 15 16func unparseRule(rule redirects.Rule) string { 17 var statusPart string 18 if rule.Force { 19 statusPart = fmt.Sprintf("%d!", rule.Status) 20 } else { 21 statusPart = fmt.Sprintf("%d", rule.Status) 22 } 23 parts := []string{ 24 rule.From, 25 rule.To, 26 statusPart, 27 } 28 for name, value := range rule.Params { 29 parts = append(parts, fmt.Sprintf("%s=%s", name, value)) 30 } 31 return strings.Join(parts, " ") 32} 33 34var validRedirectHTTPStatuses []int = []int{ 35 http.StatusOK, 36 http.StatusMovedPermanently, 37 http.StatusFound, 38 http.StatusSeeOther, 39 http.StatusTemporaryRedirect, 40 http.StatusPermanentRedirect, 41 http.StatusForbidden, 42 http.StatusNotFound, 43 http.StatusGone, 44 http.StatusTeapot, 45 http.StatusUnavailableForLegalReasons, 46} 47 48func Is3xxHTTPStatus(status int) bool { 49 return status >= 300 && status <= 399 50} 51 52func validateRedirectRule(rule redirects.Rule) error { 53 if len(rule.Params) > 0 { 54 return fmt.Errorf("rules with parameters are not supported") 55 } 56 if !slices.Contains(validRedirectHTTPStatuses, rule.Status) { 57 return fmt.Errorf("rule cannot use status %d: must be %v", 58 rule.Status, validRedirectHTTPStatuses) 59 } 60 fromURL, err := url.Parse(rule.From) 61 if err != nil { 62 return fmt.Errorf("malformed 'from' URL") 63 } 64 if fromURL.Scheme != "" { 65 return fmt.Errorf("'from' URL path must not contain a scheme") 66 } 67 if !strings.HasPrefix(fromURL.Path, "/") { 68 return fmt.Errorf("'from' URL path must start with a /") 69 } 70 if strings.Contains(fromURL.Path, "*") && !strings.HasSuffix(fromURL.Path, "/*") { 71 return fmt.Errorf("splat * must be its own final segment of the path") 72 } 73 toURL, err := url.Parse(rule.To) 74 if err != nil { 75 return fmt.Errorf("malformed 'to' URL") 76 } 77 if !Is3xxHTTPStatus(rule.Status) { 78 if !strings.HasPrefix(toURL.Path, "/") { 79 return fmt.Errorf("'to' URL path must start with a / for non-3xx status rules") 80 } 81 if toURL.Host != "" { 82 return fmt.Errorf("'to' URL may only include a hostname for 3xx status rules") 83 } 84 } 85 return nil 86} 87 88// Parses redirects file and injects rules into the manifest. 89func ProcessRedirectsFile(manifest *Manifest) error { 90 redirectsEntry := manifest.Contents[RedirectsFileName] 91 delete(manifest.Contents, RedirectsFileName) 92 if redirectsEntry == nil { 93 return nil 94 } else if redirectsEntry.GetType() != Type_InlineFile { 95 return AddProblem(manifest, RedirectsFileName, 96 "not a regular file") 97 } 98 99 rules, err := redirects.ParseString(string(redirectsEntry.GetData())) 100 if err != nil { 101 return AddProblem(manifest, RedirectsFileName, 102 "syntax error: %s", err) 103 } 104 105 for index, rule := range rules { 106 if err := validateRedirectRule(rule); err != nil { 107 AddProblem(manifest, RedirectsFileName, 108 "rule #%d %q: %s", index+1, unparseRule(rule), err) 109 continue 110 } 111 manifest.Redirects = append(manifest.Redirects, &RedirectRule{ 112 From: proto.String(rule.From), 113 To: proto.String(rule.To), 114 Status: proto.Uint32(uint32(rule.Status)), 115 Force: proto.Bool(rule.Force), 116 }) 117 } 118 return nil 119} 120 121func CollectRedirectsFile(manifest *Manifest) string { 122 var rules []string 123 for _, rule := range manifest.GetRedirects() { 124 rules = append(rules, unparseRule(redirects.Rule{ 125 From: rule.GetFrom(), 126 To: rule.GetTo(), 127 Status: int(rule.GetStatus()), 128 Force: rule.GetForce(), 129 })+"\n") 130 } 131 return strings.Join(rules, "") 132} 133 134func pathSegments(path string) []string { 135 return strings.Split(strings.TrimPrefix(path, "/"), "/") 136} 137 138func toOrFromComponent(to, from string) string { 139 if to == "" { 140 return from 141 } else { 142 return to 143 } 144} 145 146type RedirectKind int 147 148const ( 149 RedirectAny RedirectKind = iota 150 RedirectForce 151) 152 153func ApplyRedirectRules( 154 manifest *Manifest, fromURL *url.URL, kind RedirectKind, 155) ( 156 toURL *url.URL, status int, 157) { 158 fromSegments := pathSegments(fromURL.Path) 159next: 160 for _, rule := range manifest.Redirects { 161 if kind == RedirectForce && !*rule.Force { 162 continue 163 } 164 // check if the rule matches fromURL 165 ruleFromURL, _ := url.Parse(*rule.From) // pre-validated in `validateRedirectRule` 166 if ruleFromURL.Scheme != "" && fromURL.Scheme != ruleFromURL.Scheme { 167 continue 168 } 169 if ruleFromURL.Host != "" && fromURL.Hostname() != ruleFromURL.Host { 170 continue 171 } 172 ruleFromSegments := pathSegments(ruleFromURL.Path) 173 splatSegments := []string{} 174 if ruleFromSegments[len(ruleFromSegments)-1] != "*" { 175 if len(ruleFromSegments) < len(fromSegments) { 176 continue 177 } 178 } 179 for index, ruleFromSegment := range ruleFromSegments { 180 if ruleFromSegment == "*" { 181 splatSegments = fromSegments[index:] 182 break 183 } 184 if len(fromSegments) <= index { 185 continue next 186 } 187 if fromSegments[index] != ruleFromSegment { 188 continue next 189 } 190 } 191 // the rule has matched fromURL, figure out where to redirect 192 ruleToURL, _ := url.Parse(*rule.To) // pre-validated in `validateRule` 193 toSegments := []string{} 194 for _, ruleToSegment := range pathSegments(ruleToURL.Path) { 195 if ruleToSegment == ":splat" { 196 toSegments = append(toSegments, splatSegments...) 197 } else { 198 toSegments = append(toSegments, ruleToSegment) 199 } 200 } 201 toURL = &url.URL{ 202 Scheme: toOrFromComponent(ruleToURL.Scheme, fromURL.Scheme), 203 Host: toOrFromComponent(ruleToURL.Host, fromURL.Host), 204 Path: "/" + strings.Join(toSegments, "/"), 205 RawQuery: fromURL.RawQuery, 206 } 207 status = int(*rule.Status) 208 break 209 } 210 // no redirect found 211 return 212}