Antonio Mika
·
2026-10-09
1package pgs
2
3import (
4 "fmt"
5 "log/slog"
6 "regexp"
7 "slices"
8 "strings"
9)
10
11type HeaderRule struct {
12 Path string
13 Headers []*HeaderLine
14}
15
16type HeaderLine struct {
17 Path string
18 Name string
19 Value string
20}
21
22var headerDenyList = []string{
23 "accept-ranges",
24 "age",
25 "allow",
26 "alt-svc",
27 "connection",
28 "content-encoding",
29 "content-length",
30 "content-range",
31 "date",
32 "location",
33 "server",
34 "trailer",
35 "transfer-encoding",
36 "upgrade",
37}
38
39// from https://github.com/netlify/build/tree/main/packages/headers-parser
40func parseHeaderText(text string) ([]*HeaderRule, error) {
41 rules := []*HeaderRule{}
42 parsed := []*HeaderLine{}
43 lines := strings.Split(text, "\n")
44 for _, line := range lines {
45 parsedLine, err := parseLine(strings.TrimSpace(line))
46 if parsedLine == nil {
47 continue
48 }
49 if err != nil {
50 return rules, err
51 }
52 parsed = append(parsed, parsedLine)
53 }
54
55 var prevPath *HeaderRule
56 for _, rule := range parsed {
57 if rule.Path != "" {
58 if prevPath != nil {
59 if len(prevPath.Headers) > 0 {
60 rules = append(rules, prevPath)
61 }
62 }
63
64 prevPath = &HeaderRule{
65 Path: rule.Path,
66 }
67 } else if prevPath != nil {
68 // do not add headers in deny list
69 if slices.Contains(headerDenyList, rule.Name) {
70 continue
71 }
72 prevPath.Headers = append(
73 prevPath.Headers,
74 &HeaderLine{Name: rule.Name, Value: rule.Value},
75 )
76 }
77 }
78
79 // cleanup
80 if prevPath != nil && len(prevPath.Headers) > 0 {
81 rules = append(rules, prevPath)
82 }
83
84 return rules, nil
85}
86
87// headerMatcher is a _headers rule with its path compiled.
88type headerMatcher struct {
89 path *regexp.Regexp
90 headers []*HeaderLine
91}
92
93// compileHeaderRules compiles each rule's path once, when the rules are
94// cached, and drops rules whose path is not a valid pattern.
95func compileHeaderRules(rules []*HeaderRule, logger *slog.Logger) []headerMatcher {
96 matchers := make([]headerMatcher, 0, len(rules))
97 for _, rule := range rules {
98 re, err := regexp.Compile(rule.Path)
99 if err != nil {
100 logger.Error("skipping _headers rule with invalid path", "path", rule.Path, "err", err)
101 continue
102 }
103 matchers = append(matchers, headerMatcher{path: re, headers: rule.Headers})
104 }
105 return matchers
106}
107
108func parseLine(line string) (*HeaderLine, error) {
109 rule := &HeaderLine{}
110
111 if isPathLine(line) {
112 rule.Path = line
113 return rule, nil
114 }
115
116 if isEmpty(line) {
117 return nil, nil
118 }
119
120 if isComment(line) {
121 return nil, nil
122 }
123
124 if !strings.Contains(line, ":") {
125 return nil, nil
126 }
127
128 results := strings.SplitN(line, ":", 2)
129 name := strings.ToLower(strings.TrimSpace(results[0]))
130 value := strings.TrimSpace(results[1])
131
132 if name == "" {
133 return nil, fmt.Errorf("header name cannot be empty")
134 }
135
136 if value == "" {
137 return nil, fmt.Errorf("header value cannot be empty")
138 }
139
140 rule.Name = name
141 rule.Value = value
142 return rule, nil
143}
144
145func isComment(line string) bool {
146 return strings.HasPrefix(line, "#")
147}
148
149func isEmpty(line string) bool {
150 return line == ""
151}
152
153func isPathLine(line string) bool {
154 return strings.HasPrefix(line, "/")
155}