/* * JBoss, Home of Professional Open Source. * Copyright 2014 Red Hat, Inc., and individual contributors * as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.undertow.util; import java.io.UnsupportedEncodingException; import io.undertow.UndertowMessages; import io.undertow.server.HttpServerExchange; /** * Utilities for dealing with URLs * * @author Stuart Douglas */ public class URLUtils { private static final char PATH_SEPARATOR = '/'; private static final QueryStringParser QUERY_STRING_PARSER = new QueryStringParser() { @Override void handle(HttpServerExchange exchange, String key, String value) { exchange.addQueryParam(key, value); } }; private static final QueryStringParser PATH_PARAM_PARSER = new QueryStringParser() { @Override void handle(HttpServerExchange exchange, String key, String value) { exchange.addPathParam(key, value); } }; private URLUtils() { } public static void parseQueryString(final String string, final HttpServerExchange exchange, final String charset, final boolean doDecode, int maxParameters) throws ParameterLimitException { QUERY_STRING_PARSER.parse(string, exchange, charset, doDecode, maxParameters); } public static void parsePathParms(final String string, final HttpServerExchange exchange, final String charset, final boolean doDecode, int maxParameters) throws ParameterLimitException { PATH_PARAM_PARSER.parse(string, exchange, charset, doDecode, maxParameters); } /** * Decodes a URL. If the decoding fails for any reason then an IllegalArgumentException will be thrown. * * @param s The string to decode * @param enc The encoding * @param decodeSlash If slash characters should be decoded * @param buffer The string builder to use as a buffer. * @return The decoded URL */ public static String decode(String s, String enc, boolean decodeSlash, StringBuilder buffer) { buffer.setLength(0); boolean needToChange = false; int numChars = s.length(); int i = 0; char c; byte[] bytes = null; while (i < numChars) { c = s.charAt(i); switch (c) { case '+': buffer.append(' '); i++; needToChange = true; break; case '%': /* * Starting with this instance of %, process all * consecutive substrings of the form %xy. Each * substring %xy will yield a byte. Convert all * consecutive bytes obtained this way to whatever * character(s) they represent in the provided * encoding. * * Note that we need to decode the whole rest of the value, we can't just decode * three characters. For multi code point characters there if the code point can be * represented as an alphanumeric */ try { // (numChars-i) is an upper bound for the number // of remaining bytes if (bytes == null) { bytes = new byte[numChars - i + 1]; } int pos = 0; while ((i< numChars)) { if (c == '%') { char p1 = Character.toLowerCase(s.charAt(i + 1)); char p2 = Character.toLowerCase(s.charAt(i + 2)); if (!decodeSlash && ((p1 == '2' && p2 == 'f') || (p1 == '5' && p2 == 'c'))) { bytes[pos++] = (byte) c; // should be copied with preserved upper/lower case bytes[pos++] = (byte) s.charAt(i + 1); bytes[pos++] = (byte) s.charAt(i + 2); i += 3; if (i < numChars) { c = s.charAt(i); } continue; } int v = 0; if (p1 >= '0' && p1 <= '9') { v = (p1 - '0') << 4; } else if (p1 >= 'a' && p1 <= 'f') { v = (p1 - 'a' + 10) << 4; } else { throw UndertowMessages.MESSAGES.failedToDecodeURL(s, enc, null); } if (p2 >= '0' && p2 <= '9') { v += (p2 - '0'); } else if (p2 >= 'a' && p2 <= 'f') { v += (p2 - 'a' + 10); } else { throw UndertowMessages.MESSAGES.failedToDecodeURL(s, enc, null); } if (v < 0) { throw UndertowMessages.MESSAGES.failedToDecodeURL(s, enc, null); } bytes[pos++] = (byte) v; i += 3; if (i < numChars) { c = s.charAt(i); } }else if(c == '+') { bytes[pos++] = (byte) ' '; ++i; if (i < numChars) { c = s.charAt(i); } } else { bytes[pos++] = (byte) c; ++i; if (i < numChars) { c = s.charAt(i); } } } String decoded = new String(bytes, 0, pos, enc); buffer.append(decoded); } catch (NumberFormatException e) { throw UndertowMessages.MESSAGES.failedToDecodeURL(s, enc, e); } catch (UnsupportedEncodingException e) { throw UndertowMessages.MESSAGES.failedToDecodeURL(s, enc, e); } needToChange = true; break; default: buffer.append(c); i++; if(c > 127 && !needToChange) { //we have non-ascii data in our URL, which sucks //its hard to know exactly what to do with this, but we assume that because this data //has not been properly encoded none of the other data is either try { char[] carray = s.toCharArray(); byte[] buf = new byte[carray.length]; for(int l = 0;l < buf.length; ++l) { buf[l] = (byte) carray[l]; } return new String(buf, enc); } catch (UnsupportedEncodingException e) { throw UndertowMessages.MESSAGES.failedToDecodeURL(s, enc, e); } } break; } } return (needToChange ? buffer.toString() : s); } private abstract static class QueryStringParser { void parse(final String string, final HttpServerExchange exchange, final String charset, final boolean doDecode, int max) throws ParameterLimitException { int count = 0; try { int stringStart = 0; String attrName = null; for (int i = 0; i < string.length(); ++i) { char c = string.charAt(i); if (c == '=' && attrName == null) { attrName = string.substring(stringStart, i); stringStart = i + 1; } else if (c == '&') { if (attrName != null) { handle(exchange, decode(charset, attrName, doDecode), decode(charset, string.substring(stringStart, i), doDecode)); if(++count > max) { throw UndertowMessages.MESSAGES.tooManyParameters(max); } } else { handle(exchange, decode(charset, string.substring(stringStart, i), doDecode), ""); if(++count > max) { throw UndertowMessages.MESSAGES.tooManyParameters(max); } } stringStart = i + 1; attrName = null; } } if (attrName != null) { handle(exchange, decode(charset, attrName, doDecode), decode(charset, string.substring(stringStart, string.length()), doDecode)); if(++count > max) { throw UndertowMessages.MESSAGES.tooManyParameters(max); } } else if (string.length() != stringStart) { handle(exchange, decode(charset, string.substring(stringStart, string.length()), doDecode), ""); if(++count > max) { throw UndertowMessages.MESSAGES.tooManyParameters(max); } } } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } private String decode(String charset, String attrName, final boolean doDecode) throws UnsupportedEncodingException { if (doDecode) { return URLUtils.decode(attrName, charset, true, new StringBuilder()); } return attrName; } abstract void handle(final HttpServerExchange exchange, final String key, final String value); } /** * Adds a '/' prefix to the beginning of a path if one isn't present * and removes trailing slashes if any are present. * * @param path the path to normalize * @return a normalized (with respect to slashes) result */ public static String normalizeSlashes(final String path) { // prepare final StringBuilder builder = new StringBuilder(path); boolean modified = false; // remove all trailing '/'s except the first one while (builder.length() > 0 && builder.length() != 1 && PATH_SEPARATOR == builder.charAt(builder.length() - 1)) { builder.deleteCharAt(builder.length() - 1); modified = true; } // add a slash at the beginning if one isn't present if (builder.length() == 0 || PATH_SEPARATOR != builder.charAt(0)) { builder.insert(0, PATH_SEPARATOR); modified = true; } // only create string when it was modified if (modified) { return builder.toString(); } return path; } }