View Javadoc

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