View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
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   * PathSpec for WebSocket @{@link ServerEndpoint} declarations with support for URI templates and @{@link PathParam} annotations
43   * 
44   * @see javax.websocket spec (JSR-356) Section 3.1.1 URI Mapping
45   * @see <a href="https://tools.ietf.org/html/rfc6570">URI Templates (Level 1)</a>
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      /** Reserved Symbols in URI Template variable */
53      private static final String VARIABLE_RESERVED = ":/?#[]@" + // gen-delims
54                                                      "!$&'()*+,;="; // sub-delims
55      /** Allowed Symboles in a URI Template variable */
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              // path specs must start with '/'
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         // split up into path segments (ignoring the first slash that will always be empty)
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                 // entire path segment is a variable.
126                 String variable = mat.group(1);
127                 if (varNames.contains(variable))
128                 {
129                     // duplicate variable names
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'; // variable
141                 // valid variable name
142                 varNames.add(variable);
143                 // build regex
144                 regex.append("/([^/]+)");
145             }
146             else if (mat.find(0))
147             {
148                 // variable exists as partial segment
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                 // variable is split with a path separator
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                 // glob segment
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                 // valid path segment
179                 segmentSignature[i] = 'e'; // exact
180                 // build regex
181                 regex.append('/');
182                 // escape regex special characters
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         // Handle trailing slash (which is not picked up during split)
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         // Convert signature to group
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      * Validate variable literal name, per RFC6570, Section 2.1 Literals
230      * @param variable
231      * @param pathParamSpec
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); // must not be zero length
240         
241         while (valid && i < len)
242         {
243             codepoint = variable.codePointAt(i);
244             i += Character.charCount(codepoint);
245 
246             // basic letters, digits, or symbols
247             if (isValidBasicLiteralCodepoint(codepoint))
248             {
249                 continue;
250             }
251 
252             // The ucschar and iprivate pieces
253             if (Character.isSupplementaryCodePoint(codepoint))
254             {
255                 continue;
256             }
257 
258             // pct-encoded
259             if (codepoint == '%')
260             {
261                 if (i + 2 > len)
262                 {
263                     // invalid percent encoding, missing extra 2 chars
264                     valid = false;
265                     continue;
266                 }
267                 codepoint = TypeUtil.convertHexDigit(variable.codePointAt(i++)) << 4;
268                 codepoint |= TypeUtil.convertHexDigit(variable.codePointAt(i++));
269 
270                 // validate basic literal
271                 if (isValidBasicLiteralCodepoint(codepoint))
272                 {
273                     continue;
274                 }
275             }
276             
277             valid = false;
278         }
279 
280         if (!valid)
281         {
282             // invalid variable name
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         // basic letters or digits
295         if((codepoint >= 'a' && codepoint <= 'z') ||
296            (codepoint >= 'A' && codepoint <= 'Z') ||
297            (codepoint >= '0' && codepoint <= '9'))
298         {
299             return true;
300         }
301         
302         // basic allowed symbols
303         if(VARIABLE_SYMBOLS.indexOf(codepoint) >= 0)
304         {
305             return true; // valid simple value
306         }
307         
308         // basic reserved symbols
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; // valid simple value
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 }