1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.jetty.http.pathmap;
20
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Objects;
28 import java.util.Set;
29 import java.util.regex.Matcher;
30 import java.util.regex.Pattern;
31
32 import org.eclipse.jetty.util.TypeUtil;
33 import org.eclipse.jetty.util.log.Log;
34 import org.eclipse.jetty.util.log.Logger;
35
36
37
38
39
40
41 public class UriTemplatePathSpec extends RegexPathSpec
42 {
43 private static final Logger LOG = Log.getLogger(UriTemplatePathSpec.class);
44
45 private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{(.*)\\}");
46
47 private static final String VARIABLE_RESERVED = ":/?#[]@" +
48 "!$&'()*+,;=";
49
50 private static final String VARIABLE_SYMBOLS="-._";
51 private static final Set<String> FORBIDDEN_SEGMENTS;
52
53 static
54 {
55 FORBIDDEN_SEGMENTS = new HashSet<>();
56 FORBIDDEN_SEGMENTS.add("/./");
57 FORBIDDEN_SEGMENTS.add("/../");
58 FORBIDDEN_SEGMENTS.add("//");
59 }
60
61 private String variables[];
62
63 public UriTemplatePathSpec(String rawSpec)
64 {
65 super();
66 Objects.requireNonNull(rawSpec,"Path Param Spec cannot be null");
67
68 if ("".equals(rawSpec) || "/".equals(rawSpec))
69 {
70 super.pathSpec = "/";
71 super.pattern = Pattern.compile("^/$");
72 super.pathDepth = 1;
73 this.specLength = 1;
74 this.variables = new String[0];
75 this.group = PathSpecGroup.EXACT;
76 return;
77 }
78
79 if (rawSpec.charAt(0) != '/')
80 {
81
82 StringBuilder err = new StringBuilder();
83 err.append("Syntax Error: path spec \"");
84 err.append(rawSpec);
85 err.append("\" must start with '/'");
86 throw new IllegalArgumentException(err.toString());
87 }
88
89 for (String forbidden : FORBIDDEN_SEGMENTS)
90 {
91 if (rawSpec.contains(forbidden))
92 {
93 StringBuilder err = new StringBuilder();
94 err.append("Syntax Error: segment ");
95 err.append(forbidden);
96 err.append(" is forbidden in path spec: ");
97 err.append(rawSpec);
98 throw new IllegalArgumentException(err.toString());
99 }
100 }
101
102 this.pathSpec = rawSpec;
103
104 StringBuilder regex = new StringBuilder();
105 regex.append('^');
106
107 List<String> varNames = new ArrayList<>();
108
109 String segments[] = rawSpec.substring(1).split("/");
110 char segmentSignature[] = new char[segments.length];
111 this.pathDepth = segments.length;
112 for (int i = 0; i < segments.length; i++)
113 {
114 String segment = segments[i];
115 Matcher mat = VARIABLE_PATTERN.matcher(segment);
116
117 if (mat.matches())
118 {
119
120 String variable = mat.group(1);
121 if (varNames.contains(variable))
122 {
123
124 StringBuilder err = new StringBuilder();
125 err.append("Syntax Error: variable ");
126 err.append(variable);
127 err.append(" is duplicated in path spec: ");
128 err.append(rawSpec);
129 throw new IllegalArgumentException(err.toString());
130 }
131
132 assertIsValidVariableLiteral(variable);
133
134 segmentSignature[i] = 'v';
135
136 varNames.add(variable);
137
138 regex.append("/([^/]+)");
139 }
140 else if (mat.find(0))
141 {
142
143 StringBuilder err = new StringBuilder();
144 err.append("Syntax Error: variable ");
145 err.append(mat.group());
146 err.append(" must exist as entire path segment: ");
147 err.append(rawSpec);
148 throw new IllegalArgumentException(err.toString());
149 }
150 else if ((segment.indexOf('{') >= 0) || (segment.indexOf('}') >= 0))
151 {
152
153 StringBuilder err = new StringBuilder();
154 err.append("Syntax Error: invalid path segment /");
155 err.append(segment);
156 err.append("/ variable declaration incomplete: ");
157 err.append(rawSpec);
158 throw new IllegalArgumentException(err.toString());
159 }
160 else if (segment.indexOf('*') >= 0)
161 {
162
163 StringBuilder err = new StringBuilder();
164 err.append("Syntax Error: path segment /");
165 err.append(segment);
166 err.append("/ contains a wildcard symbol (not supported by this uri-template implementation): ");
167 err.append(rawSpec);
168 throw new IllegalArgumentException(err.toString());
169 }
170 else
171 {
172
173 segmentSignature[i] = 'e';
174
175 regex.append('/');
176
177 for (char c : segment.toCharArray())
178 {
179 if ((c == '.') || (c == '[') || (c == ']') || (c == '\\'))
180 {
181 regex.append('\\');
182 }
183 regex.append(c);
184 }
185 }
186 }
187
188
189 if(rawSpec.charAt(rawSpec.length()-1) == '/')
190 {
191 regex.append('/');
192 }
193
194 regex.append('$');
195
196 this.pattern = Pattern.compile(regex.toString());
197
198 int varcount = varNames.size();
199 this.variables = varNames.toArray(new String[varcount]);
200
201
202 String sig = String.valueOf(segmentSignature);
203
204 if (Pattern.matches("^e*$",sig))
205 {
206 this.group = PathSpecGroup.EXACT;
207 }
208 else if (Pattern.matches("^e*v+",sig))
209 {
210 this.group = PathSpecGroup.PREFIX_GLOB;
211 }
212 else if (Pattern.matches("^v+e+",sig))
213 {
214 this.group = PathSpecGroup.SUFFIX_GLOB;
215 }
216 else
217 {
218 this.group = PathSpecGroup.MIDDLE_GLOB;
219 }
220 }
221
222
223
224
225
226
227 private void assertIsValidVariableLiteral(String variable)
228 {
229 int len = variable.length();
230
231 int i = 0;
232 int codepoint;
233 boolean valid = (len > 0);
234
235 while (valid && i < len)
236 {
237 codepoint = variable.codePointAt(i);
238 i += Character.charCount(codepoint);
239
240
241 if (isValidBasicLiteralCodepoint(codepoint))
242 {
243 continue;
244 }
245
246
247 if (Character.isSupplementaryCodePoint(codepoint))
248 {
249 continue;
250 }
251
252
253 if (codepoint == '%')
254 {
255 if (i + 2 > len)
256 {
257
258 valid = false;
259 continue;
260 }
261 codepoint = TypeUtil.convertHexDigit(variable.codePointAt(i++)) << 4;
262 codepoint |= TypeUtil.convertHexDigit(variable.codePointAt(i++));
263
264
265 if (isValidBasicLiteralCodepoint(codepoint))
266 {
267 continue;
268 }
269 }
270
271 valid = false;
272 }
273
274 if (!valid)
275 {
276
277 StringBuilder err = new StringBuilder();
278 err.append("Syntax Error: variable {");
279 err.append(variable);
280 err.append("} an invalid variable name: ");
281 err.append(pathSpec);
282 throw new IllegalArgumentException(err.toString());
283 }
284 }
285
286 private boolean isValidBasicLiteralCodepoint(int codepoint)
287 {
288
289 if((codepoint >= 'a' && codepoint <= 'z') ||
290 (codepoint >= 'A' && codepoint <= 'Z') ||
291 (codepoint >= '0' && codepoint <= '9'))
292 {
293 return true;
294 }
295
296
297 if(VARIABLE_SYMBOLS.indexOf(codepoint) >= 0)
298 {
299 return true;
300 }
301
302
303 if(VARIABLE_RESERVED.indexOf(codepoint) >= 0)
304 {
305 LOG.warn("Detected URI Template reserved symbol [{}] in path spec \"{}\"",(char)codepoint,pathSpec);
306 return false;
307 }
308
309 return false;
310 }
311
312 public Map<String, String> getPathParams(String path)
313 {
314 Matcher matcher = getMatcher(path);
315 if (matcher.matches())
316 {
317 if (group == PathSpecGroup.EXACT)
318 {
319 return Collections.emptyMap();
320 }
321 Map<String, String> ret = new HashMap<>();
322 int groupCount = matcher.groupCount();
323 for (int i = 1; i <= groupCount; i++)
324 {
325 ret.put(this.variables[i - 1],matcher.group(i));
326 }
327 return ret;
328 }
329 return null;
330 }
331
332 public int getVariableCount()
333 {
334 return variables.length;
335 }
336
337 public String[] getVariables()
338 {
339 return this.variables;
340 }
341 }