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