// // ======================================================================== // 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.api.util; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.NoSuchElementException; /** * Provide some consistent Http header value and Extension configuration parameter quoting support. * <p> * While QuotedStringTokenizer exists in jetty-util, and works great with http header values, using it in websocket-api is undesired. * <ul> * <li>Using QuotedStringTokenizer would introduce a dependency to jetty-util that would need to be exposed via the WebAppContext classloader</li> * <li>ABNF defined extension parameter parsing requirements of RFC-6455 (WebSocket) ABNF, is slightly different than the ABNF parsing defined in RFC-2616 * (HTTP/1.1).</li> * <li>Future HTTPbis ABNF changes for parsing will impact QuotedStringTokenizer</li> * </ul> * It was decided to keep this implementation separate for the above reasons. */ public class QuoteUtil { private static class DeQuotingStringIterator implements Iterator<String> { private enum State { START, TOKEN, QUOTE_SINGLE, QUOTE_DOUBLE } private final String input; private final String delims; private StringBuilder token; private boolean hasToken = false; private int i = 0; public DeQuotingStringIterator(String input, String delims) { this.input = input; this.delims = delims; int len = input.length(); token = new StringBuilder(len > 1024?512:len / 2); } private void appendToken(char c) { if (hasToken) { token.append(c); } else { if (Character.isWhitespace(c)) { return; // skip whitespace at start of token. } else { token.append(c); hasToken = true; } } } @Override public boolean hasNext() { // already found a token if (hasToken) { return true; } State state = State.START; boolean escape = false; int inputLen = input.length(); while (i < inputLen) { char c = input.charAt(i++); switch (state) { case START: { if (c == '\'') { state = State.QUOTE_SINGLE; appendToken(c); } else if (c == '\"') { state = State.QUOTE_DOUBLE; appendToken(c); } else { appendToken(c); state = State.TOKEN; } break; } case TOKEN: { if (delims.indexOf(c) >= 0) { // System.out.printf("hasNext/t: %b [%s]%n",hasToken,token); return hasToken; } else if (c == '\'') { state = State.QUOTE_SINGLE; } else if (c == '\"') { state = State.QUOTE_DOUBLE; } appendToken(c); break; } case QUOTE_SINGLE: { if (escape) { escape = false; appendToken(c); } else if (c == '\'') { appendToken(c); state = State.TOKEN; } else if (c == '\\') { escape = true; } else { appendToken(c); } break; } case QUOTE_DOUBLE: { if (escape) { escape = false; appendToken(c); } else if (c == '\"') { appendToken(c); state = State.TOKEN; } else if (c == '\\') { escape = true; } else { appendToken(c); } break; } } // System.out.printf("%s <%s> : [%s]%n",state,c,token); } // System.out.printf("hasNext/e: %b [%s]%n",hasToken,token); return hasToken; } @Override public String next() { if (!hasNext()) { throw new NoSuchElementException(); } String ret = token.toString(); token.setLength(0); hasToken = false; return QuoteUtil.dequote(ret.trim()); } @Override public void remove() { throw new UnsupportedOperationException("Remove not supported with this iterator"); } } /** * ABNF from RFC 2616, RFC 822, and RFC 6455 specified characters requiring quoting. */ public static final String ABNF_REQUIRED_QUOTING = "\"'\\\n\r\t\f\b%+ ;="; private static final char UNICODE_TAG = 0xFFFF; private static final char[] escapes = new char[32]; static { Arrays.fill(escapes,UNICODE_TAG); // non-unicode escapes['\b'] = 'b'; escapes['\t'] = 't'; escapes['\n'] = 'n'; escapes['\f'] = 'f'; escapes['\r'] = 'r'; } private static int dehex(byte b) { if ((b >= '0') && (b <= '9')) { return (byte)(b - '0'); } if ((b >= 'a') && (b <= 'f')) { return (byte)((b - 'a') + 10); } if ((b >= 'A') && (b <= 'F')) { return (byte)((b - 'A') + 10); } throw new IllegalArgumentException("!hex:" + Integer.toHexString(0xff & b)); } /** * Remove quotes from a string, only if the input string start with and end with the same quote character. * * @param str * the string to remove surrounding quotes from * @return the de-quoted string */ public static String dequote(String str) { char start = str.charAt(0); if ((start == '\'') || (start == '\"')) { // possibly quoted char end = str.charAt(str.length() - 1); if (start == end) { // dequote return str.substring(1,str.length() - 1); } } return str; } public static void escape(StringBuilder buf, String str) { for (char c : str.toCharArray()) { if (c >= 32) { // non special character if ((c == '"') || (c == '\\')) { buf.append('\\'); } buf.append(c); } else { // special characters, requiring escaping char escaped = escapes[c]; // is this a unicode escape? if (escaped == UNICODE_TAG) { buf.append("\\u00"); if (c < 0x10) { buf.append('0'); } buf.append(Integer.toString(c,16)); // hex } else { // normal escape buf.append('\\').append(escaped); } } } } /** * Simple quote of a string, escaping where needed. * * @param buf * the StringBuilder to append to * @param str * the string to quote */ public static void quote(StringBuilder buf, String str) { buf.append('"'); escape(buf,str); buf.append('"'); } /** * Append into buf the provided string, adding quotes if needed. * <p> * Quoting is determined if any of the characters in the <code>delim</code> are found in the input <code>str</code>. * * @param buf * the buffer to append to * @param str * the string to possibly quote * @param delim * the delimiter characters that will trigger automatic quoting */ public static void quoteIfNeeded(StringBuilder buf, String str, String delim) { if (str == null) { return; } // check for delimiters in input string int len = str.length(); if (len == 0) { return; } int ch; for (int i = 0; i < len; i++) { ch = str.codePointAt(i); if (delim.indexOf(ch) >= 0) { // found a delimiter codepoint. we need to quote it. quote(buf,str); return; } } // no special delimiters used, no quote needed. buf.append(str); } /** * Create an iterator of the input string, breaking apart the string at the provided delimiters, removing quotes and triming the parts of the string as * needed. * * @param str * the input string to split apart * @param delims * the delimiter characters to split the string on * @return the iterator of the parts of the string, trimmed, with quotes around the string part removed, and unescaped */ public static Iterator<String> splitAt(String str, String delims) { return new DeQuotingStringIterator(str.trim(),delims); } public static String unescape(String str) { if (str == null) { // nothing there return null; } int len = str.length(); if (len <= 1) { // impossible to be escaped return str; } StringBuilder ret = new StringBuilder(len - 2); boolean escaped = false; char c; for (int i = 0; i < len; i++) { c = str.charAt(i); if (escaped) { escaped = false; switch (c) { case 'n': ret.append('\n'); break; case 'r': ret.append('\r'); break; case 't': ret.append('\t'); break; case 'f': ret.append('\f'); break; case 'b': ret.append('\b'); break; case '\\': ret.append('\\'); break; case '/': ret.append('/'); break; case '"': ret.append('"'); break; case 'u': ret.append((char)((dehex((byte)str.charAt(i++)) << 24) + (dehex((byte)str.charAt(i++)) << 16) + (dehex((byte)str.charAt(i++)) << 8) + (dehex((byte)str .charAt(i++))))); break; default: ret.append(c); } } else if (c == '\\') { escaped = true; } else { ret.append(c); } } return ret.toString(); } public static String join(Object[] objs, String delim) { if (objs == null) { return ""; } StringBuilder ret = new StringBuilder(); int len = objs.length; for (int i = 0; i < len; i++) { if (i > 0) { ret.append(delim); } if (objs[i] instanceof String) { ret.append('"').append(objs[i]).append('"'); } else { ret.append(objs[i]); } } return ret.toString(); } public static String join(Collection<?> objs, String delim) { if (objs == null) { return ""; } StringBuilder ret = new StringBuilder(); boolean needDelim = false; for (Object obj : objs) { if (needDelim) { ret.append(delim); } if (obj instanceof String) { ret.append('"').append(obj).append('"'); } else { ret.append(obj); } needDelim = true; } return ret.toString(); } }