blob: e7a8f60cd493858cc20c9596ad643672a0d337ed [file] [log] [blame]
//
// ========================================================================
// 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;
}
}