| // |
| // ======================================================================== |
| // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| package org.eclipse.jetty.websocket.jsr356.server.pathmap; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.eclipse.jetty.util.TypeUtil; |
| import org.eclipse.jetty.util.log.Log; |
| import org.eclipse.jetty.util.log.Logger; |
| import org.eclipse.jetty.websocket.server.pathmap.PathSpecGroup; |
| import org.eclipse.jetty.websocket.server.pathmap.RegexPathSpec; |
| |
| /** |
| * PathSpec for WebSocket @{@link ServerEndpoint} declarations with support for URI templates and @{@link PathParam} annotations |
| * |
| * @see javax.websocket spec (JSR-356) Section 3.1.1 URI Mapping |
| * @see <a href="https://tools.ietf.org/html/rfc6570">URI Templates (Level 1)</a> |
| */ |
| public class WebSocketPathSpec extends RegexPathSpec |
| { |
| private static final Logger LOG = Log.getLogger(WebSocketPathSpec.class); |
| |
| private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{(.*)\\}"); |
| /** Reserved Symbols in URI Template variable */ |
| private static final String VARIABLE_RESERVED = ":/?#[]@" + // gen-delims |
| "!$&'()*+,;="; // sub-delims |
| /** Allowed Symboles in a URI Template variable */ |
| private static final String VARIABLE_SYMBOLS="-._"; |
| private static final Set<String> FORBIDDEN_SEGMENTS; |
| |
| static |
| { |
| FORBIDDEN_SEGMENTS = new HashSet<>(); |
| FORBIDDEN_SEGMENTS.add("/./"); |
| FORBIDDEN_SEGMENTS.add("/../"); |
| FORBIDDEN_SEGMENTS.add("//"); |
| } |
| |
| private String variables[]; |
| |
| public WebSocketPathSpec(String pathParamSpec) |
| { |
| super(); |
| Objects.requireNonNull(pathParamSpec,"Path Param Spec cannot be null"); |
| |
| if ("".equals(pathParamSpec) || "/".equals(pathParamSpec)) |
| { |
| super.pathSpec = "/"; |
| super.pattern = Pattern.compile("^/$"); |
| super.pathDepth = 1; |
| this.specLength = 1; |
| this.variables = new String[0]; |
| this.group = PathSpecGroup.EXACT; |
| return; |
| } |
| |
| if (pathParamSpec.charAt(0) != '/') |
| { |
| // path specs must start with '/' |
| StringBuilder err = new StringBuilder(); |
| err.append("Syntax Error: path spec \""); |
| err.append(pathParamSpec); |
| err.append("\" must start with '/'"); |
| throw new IllegalArgumentException(err.toString()); |
| } |
| |
| for (String forbidden : FORBIDDEN_SEGMENTS) |
| { |
| if (pathParamSpec.contains(forbidden)) |
| { |
| StringBuilder err = new StringBuilder(); |
| err.append("Syntax Error: segment "); |
| err.append(forbidden); |
| err.append(" is forbidden in path spec: "); |
| err.append(pathParamSpec); |
| throw new IllegalArgumentException(err.toString()); |
| } |
| } |
| |
| this.pathSpec = pathParamSpec; |
| |
| StringBuilder regex = new StringBuilder(); |
| regex.append('^'); |
| |
| List<String> varNames = new ArrayList<>(); |
| // split up into path segments (ignoring the first slash that will always be empty) |
| String segments[] = pathParamSpec.substring(1).split("/"); |
| char segmentSignature[] = new char[segments.length]; |
| this.pathDepth = segments.length; |
| for (int i = 0; i < segments.length; i++) |
| { |
| String segment = segments[i]; |
| Matcher mat = VARIABLE_PATTERN.matcher(segment); |
| |
| if (mat.matches()) |
| { |
| // entire path segment is a variable. |
| String variable = mat.group(1); |
| if (varNames.contains(variable)) |
| { |
| // duplicate variable names |
| StringBuilder err = new StringBuilder(); |
| err.append("Syntax Error: variable "); |
| err.append(variable); |
| err.append(" is duplicated in path spec: "); |
| err.append(pathParamSpec); |
| throw new IllegalArgumentException(err.toString()); |
| } |
| |
| assertIsValidVariableLiteral(variable); |
| |
| segmentSignature[i] = 'v'; // variable |
| // valid variable name |
| varNames.add(variable); |
| // build regex |
| regex.append("/([^/]+)"); |
| } |
| else if (mat.find(0)) |
| { |
| // variable exists as partial segment |
| StringBuilder err = new StringBuilder(); |
| err.append("Syntax Error: variable "); |
| err.append(mat.group()); |
| err.append(" must exist as entire path segment: "); |
| err.append(pathParamSpec); |
| throw new IllegalArgumentException(err.toString()); |
| } |
| else if ((segment.indexOf('{') >= 0) || (segment.indexOf('}') >= 0)) |
| { |
| // variable is split with a path separator |
| StringBuilder err = new StringBuilder(); |
| err.append("Syntax Error: invalid path segment /"); |
| err.append(segment); |
| err.append("/ variable declaration incomplete: "); |
| err.append(pathParamSpec); |
| throw new IllegalArgumentException(err.toString()); |
| } |
| else if (segment.indexOf('*') >= 0) |
| { |
| // glob segment |
| StringBuilder err = new StringBuilder(); |
| err.append("Syntax Error: path segment /"); |
| err.append(segment); |
| err.append("/ contains a wildcard symbol (not supported by javax.websocket): "); |
| err.append(pathParamSpec); |
| throw new IllegalArgumentException(err.toString()); |
| } |
| else |
| { |
| // valid path segment |
| segmentSignature[i] = 'e'; // exact |
| // build regex |
| regex.append('/'); |
| // escape regex special characters |
| for (char c : segment.toCharArray()) |
| { |
| if ((c == '.') || (c == '[') || (c == ']') || (c == '\\')) |
| { |
| regex.append('\\'); |
| } |
| regex.append(c); |
| } |
| } |
| } |
| |
| // Handle trailing slash (which is not picked up during split) |
| if(pathParamSpec.charAt(pathParamSpec.length()-1) == '/') |
| { |
| regex.append('/'); |
| } |
| |
| regex.append('$'); |
| |
| this.pattern = Pattern.compile(regex.toString()); |
| |
| int varcount = varNames.size(); |
| this.variables = varNames.toArray(new String[varcount]); |
| |
| // Convert signature to group |
| String sig = String.valueOf(segmentSignature); |
| |
| if (Pattern.matches("^e*$",sig)) |
| { |
| this.group = PathSpecGroup.EXACT; |
| } |
| else if (Pattern.matches("^e*v+",sig)) |
| { |
| this.group = PathSpecGroup.PREFIX_GLOB; |
| } |
| else if (Pattern.matches("^v+e+",sig)) |
| { |
| this.group = PathSpecGroup.SUFFIX_GLOB; |
| } |
| else |
| { |
| this.group = PathSpecGroup.MIDDLE_GLOB; |
| } |
| } |
| |
| /** |
| * Validate variable literal name, per RFC6570, Section 2.1 Literals |
| * @param variable |
| * @param pathParamSpec |
| */ |
| private void assertIsValidVariableLiteral(String variable) |
| { |
| int len = variable.length(); |
| |
| int i = 0; |
| int codepoint; |
| boolean valid = (len > 0); // must not be zero length |
| |
| while (valid && i < len) |
| { |
| codepoint = variable.codePointAt(i); |
| i += Character.charCount(codepoint); |
| |
| // basic letters, digits, or symbols |
| if (isValidBasicLiteralCodepoint(codepoint)) |
| { |
| continue; |
| } |
| |
| // The ucschar and iprivate pieces |
| if (Character.isSupplementaryCodePoint(codepoint)) |
| { |
| continue; |
| } |
| |
| // pct-encoded |
| if (codepoint == '%') |
| { |
| if (i + 2 > len) |
| { |
| // invalid percent encoding, missing extra 2 chars |
| valid = false; |
| continue; |
| } |
| codepoint = TypeUtil.convertHexDigit(variable.codePointAt(i++)) << 4; |
| codepoint |= TypeUtil.convertHexDigit(variable.codePointAt(i++)); |
| |
| // validate basic literal |
| if (isValidBasicLiteralCodepoint(codepoint)) |
| { |
| continue; |
| } |
| } |
| |
| valid = false; |
| } |
| |
| if (!valid) |
| { |
| // invalid variable name |
| StringBuilder err = new StringBuilder(); |
| err.append("Syntax Error: variable {"); |
| err.append(variable); |
| err.append("} an invalid variable name: "); |
| err.append(pathSpec); |
| throw new IllegalArgumentException(err.toString()); |
| } |
| } |
| |
| private boolean isValidBasicLiteralCodepoint(int codepoint) |
| { |
| // basic letters or digits |
| if((codepoint >= 'a' && codepoint <= 'z') || |
| (codepoint >= 'A' && codepoint <= 'Z') || |
| (codepoint >= '0' && codepoint <= '9')) |
| { |
| return true; |
| } |
| |
| // basic allowed symbols |
| if(VARIABLE_SYMBOLS.indexOf(codepoint) >= 0) |
| { |
| return true; // valid simple value |
| } |
| |
| // basic reserved symbols |
| if(VARIABLE_RESERVED.indexOf(codepoint) >= 0) |
| { |
| LOG.warn("Detected URI Template reserved symbol [{}] in path spec \"{}\"",(char)codepoint,pathSpec); |
| return false; // valid simple value |
| } |
| |
| return false; |
| } |
| |
| public Map<String, String> getPathParams(String path) |
| { |
| Matcher matcher = getMatcher(path); |
| if (matcher.matches()) |
| { |
| if (group == PathSpecGroup.EXACT) |
| { |
| return Collections.emptyMap(); |
| } |
| Map<String, String> ret = new HashMap<>(); |
| int groupCount = matcher.groupCount(); |
| for (int i = 1; i <= groupCount; i++) |
| { |
| ret.put(this.variables[i - 1],matcher.group(i)); |
| } |
| return ret; |
| } |
| return null; |
| } |
| |
| public int getVariableCount() |
| { |
| return variables.length; |
| } |
| |
| public String[] getVariables() |
| { |
| return this.variables; |
| } |
| } |