6
6
"fmt"
7
7
"io"
8
8
"net/url"
9
+ "regexp"
9
10
"strconv"
10
11
"strings"
11
12
@@ -21,6 +22,12 @@ type Rule struct {
21
22
// From is the path which is matched to perform the rule.
22
23
From string
23
24
25
+ // FromQuery is the set of required query parameters which
26
+ // must be present to perform the rule.
27
+ // A string without a preceding colon requires that query parameter is this exact value.
28
+ // A string with a preceding colon will match any value, and provide it as a placeholder.
29
+ FromQuery map [string ]string
30
+
24
31
// To is the destination which may be relative, or absolute
25
32
// in order to proxy the request to another URL.
26
33
To string
@@ -51,37 +58,70 @@ func (r *Rule) IsProxy() bool {
51
58
52
59
// MatchAndExpandPlaceholders expands placeholders in `r.To` and returns true if the provided path matches.
53
60
// Otherwise it returns false.
54
- func (r * Rule ) MatchAndExpandPlaceholders (urlPath string ) bool {
61
+ func (r * Rule ) MatchAndExpandPlaceholders (urlPath string , urlParams url. Values ) bool {
55
62
// get rule.From, trim trailing slash, ...
56
63
fromPath := urlpath .New (strings .TrimSuffix (r .From , "/" ))
57
64
match , ok := fromPath .Match (urlPath )
58
-
59
65
if ! ok {
60
66
return false
61
67
}
62
68
63
- // We have a match! Perform substitution and return the updated rule
69
+ placeholders := match .Params
70
+ placeholders ["splat" ] = match .Trailing
71
+ if ! matchParams (r .FromQuery , urlParams , placeholders ) {
72
+ return false
73
+ }
74
+
75
+ // We have a match! Perform substitution and return the updated rule
64
76
toPath := r .To
65
- toPath = replacePlaceholders (toPath , match )
66
- toPath = replaceSplat (toPath , match )
77
+ toPath = replacePlaceholders (toPath , placeholders )
78
+
79
+ // There's a placeholder unsupplied somewhere
80
+ if strings .Contains (toPath , ":" ) {
81
+ return false
82
+ }
67
83
68
84
r .To = toPath
69
85
70
86
return true
71
87
}
72
88
73
- func replacePlaceholders (to string , match urlpath.Match ) string {
74
- if len (match .Params ) > 0 {
75
- for key , value := range match .Params {
76
- to = strings .ReplaceAll (to , ":" + key , value )
77
- }
89
+ func replacePlaceholders (to string , placeholders map [string ]string ) string {
90
+ if len (placeholders ) == 0 {
91
+ return to
92
+ }
93
+
94
+ for key , value := range placeholders {
95
+ to = strings .ReplaceAll (to , ":" + key , value )
78
96
}
79
97
80
98
return to
81
99
}
82
100
83
- func replaceSplat (to string , match urlpath.Match ) string {
84
- return strings .ReplaceAll (to , ":splat" , match .Trailing )
101
+ func replaceSplat (to string , splat string ) string {
102
+ return strings .ReplaceAll (to , ":splat" , splat )
103
+ }
104
+
105
+ func matchParams (fromQuery map [string ]string , urlParams url.Values , placeholders map [string ]string ) bool {
106
+ for neededK , neededV := range fromQuery {
107
+ haveVs , ok := urlParams [neededK ]
108
+ if ! ok {
109
+ return false
110
+ }
111
+
112
+ if isPlaceholder (neededV ) {
113
+ if _ , ok := placeholders [neededV [1 :]]; ! ok {
114
+ placeholders [neededV [1 :]] = haveVs [0 ]
115
+ }
116
+ continue
117
+ }
118
+
119
+ if ! contains (haveVs , neededV ) {
120
+ return false
121
+ }
122
+ }
123
+
124
+ return true
85
125
}
86
126
87
127
// Must parse utility.
@@ -124,10 +164,6 @@ func Parse(r io.Reader) (rules []Rule, err error) {
124
164
return nil , fmt .Errorf ("missing 'to' path" )
125
165
}
126
166
127
- if len (fields ) > 3 {
128
- return nil , fmt .Errorf ("must match format 'from to [status]'" )
129
- }
130
-
131
167
// implicit status
132
168
rule := Rule {Status : 301 }
133
169
@@ -138,23 +174,42 @@ func Parse(r io.Reader) (rules []Rule, err error) {
138
174
}
139
175
rule .From = from
140
176
177
+ hasStatus := isLikelyStatusCode (fields [len (fields )- 1 ])
178
+ toIndex := len (fields ) - 1
179
+ if hasStatus {
180
+ toIndex = len (fields ) - 2
181
+ }
182
+
141
183
// to (must parse as an absolute path or an URL)
142
- to , err := parseTo (fields [1 ])
184
+ to , err := parseTo (fields [toIndex ])
143
185
if err != nil {
144
186
return nil , errors .Wrapf (err , "parsing 'to'" )
145
187
}
146
188
rule .To = to
147
189
148
190
// status
149
- if len ( fields ) > 2 {
150
- code , err := parseStatus (fields [2 ])
191
+ if hasStatus {
192
+ code , err := parseStatus (fields [len ( fields ) - 1 ])
151
193
if err != nil {
152
194
return nil , errors .Wrapf (err , "parsing status %q" , fields [2 ])
153
195
}
154
196
155
197
rule .Status = code
156
198
}
157
199
200
+ // from query
201
+ if toIndex > 1 {
202
+ rule .FromQuery = make (map [string ]string )
203
+
204
+ for i := 1 ; i < toIndex ; i ++ {
205
+ key , value , err := parseFromQuery (fields [i ])
206
+ if err != nil {
207
+ return nil , errors .Wrapf (err , "parsing 'fromQuery'" )
208
+ }
209
+ rule .FromQuery [key ] = value
210
+ }
211
+ }
212
+
158
213
rules = append (rules , rule )
159
214
}
160
215
@@ -194,6 +249,46 @@ func parseFrom(s string) (string, error) {
194
249
return s , nil
195
250
}
196
251
252
+ func parseFromQuery (s string ) (string , string , error ) {
253
+ params , err := url .ParseQuery (s )
254
+ if err != nil {
255
+ return "" , "" , err
256
+ }
257
+ if len (params ) != 1 {
258
+ return "" , "" , fmt .Errorf ("separate different fromQuery arguments with a space" )
259
+ }
260
+
261
+ var key string
262
+ var val []string
263
+ // We know there's only 1, but we don't know the key to access it
264
+ for k , v := range params {
265
+ key = k
266
+ val = v
267
+ }
268
+
269
+ if url .QueryEscape (key ) != key {
270
+ return "" , "" , fmt .Errorf ("fromQuery key must be URL encoded" )
271
+ }
272
+
273
+ if len (val ) > 1 {
274
+ return "" , "" , fmt .Errorf ("separate different fromQuery arguments with a space" )
275
+ }
276
+
277
+ ignorePlaceholders := val [0 ]
278
+ if isPlaceholder (val [0 ]) {
279
+ ignorePlaceholders = ignorePlaceholders [1 :]
280
+ }
281
+
282
+ if url .QueryEscape (ignorePlaceholders ) != ignorePlaceholders {
283
+ return "" , "" , fmt .Errorf ("fromQuery val must be URL encoded" )
284
+ }
285
+ return key , val [0 ], nil
286
+ }
287
+
288
+ func isPlaceholder (s string ) bool {
289
+ return strings .HasPrefix (s , ":" )
290
+ }
291
+
197
292
func parseTo (s string ) (string , error ) {
198
293
// confirm value is within URL path spec
199
294
u , err := url .Parse (s )
@@ -211,6 +306,13 @@ func parseTo(s string) (string, error) {
211
306
return s , nil
212
307
}
213
308
309
+ var likeStatusCode = regexp .MustCompile (`^\d{1,3}!?$` )
310
+
311
+ // isLikelyStatusCode returns true if the given string is likely to be a status code.
312
+ func isLikelyStatusCode (s string ) bool {
313
+ return likeStatusCode .MatchString (s )
314
+ }
315
+
214
316
// parseStatus returns the status code.
215
317
func parseStatus (s string ) (code int , err error ) {
216
318
if strings .HasSuffix (s , "!" ) {
@@ -237,3 +339,12 @@ func isValidStatusCode(status int) bool {
237
339
}
238
340
return false
239
341
}
342
+
343
+ func contains (arr []string , s string ) bool {
344
+ for _ , a := range arr {
345
+ if a == s {
346
+ return true
347
+ }
348
+ }
349
+ return false
350
+ }
0 commit comments