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.api.util;
20  
21  import java.util.Arrays;
22  import java.util.Collection;
23  import java.util.Iterator;
24  import java.util.NoSuchElementException;
25  
26  /**
27   * Provide some consistent Http header value and Extension configuration parameter quoting support.
28   * <p>
29   * While QuotedStringTokenizer exists in jetty-util, and works great with http header values, using it in websocket-api is undesired.
30   * <p>
31   * <ul>
32   * <li>Using QuotedStringTokenizer would introduce a dependency to jetty-util that would need to be exposed via the WebAppContext classloader</li>
33   * <li>ABNF defined extension parameter parsing requirements of RFC-6455 (WebSocket) ABNF, is slightly different than the ABNF parsing defined in RFC-2616
34   * (HTTP/1.1).</li>
35   * <li>Future HTTPbis ABNF changes for parsing will impact QuotedStringTokenizer</li>
36   * </ul>
37   * It was decided to keep this implementation separate for the above reasons.
38   */
39  public class QuoteUtil
40  {
41      private static class DeQuotingStringIterator implements Iterator<String>
42      {
43          private enum State
44          {
45              START,
46              TOKEN,
47              QUOTE_SINGLE,
48              QUOTE_DOUBLE
49          }
50  
51          private static final boolean DEBUG = false;
52  
53          private final String input;
54          private final String delims;
55          private StringBuilder token;
56          private boolean hasToken = false;
57          private int i = 0;
58  
59          public DeQuotingStringIterator(String input, String delims)
60          {
61              this.input = input;
62              this.delims = delims;
63              int len = input.length();
64              token = new StringBuilder(len > 1024?512:len / 2);
65          }
66  
67          private void appendToken(char c)
68          {
69              if (hasToken)
70              {
71                  token.append(c);
72              }
73              else
74              {
75                  if (Character.isWhitespace(c))
76                  {
77                      return; // skip whitespace at start of token.
78                  }
79                  else
80                  {
81                      token.append(c);
82                      hasToken = true;
83                  }
84              }
85          }
86  
87          private void debug(String format, Object... args)
88          {
89              if (DEBUG)
90              {
91                  System.out.printf(format,args);
92              }
93          }
94  
95          @Override
96          public boolean hasNext()
97          {
98              // already found a token
99              if (hasToken)
100             {
101                 return true;
102             }
103 
104             State state = State.START;
105             boolean escape = false;
106             int inputLen = input.length();
107 
108             while (i < inputLen)
109             {
110                 char c = input.charAt(i++);
111 
112                 switch (state)
113                 {
114                     case START:
115                     {
116                         if (c == '\'')
117                         {
118                             state = State.QUOTE_SINGLE;
119                             appendToken(c);
120                         }
121                         else if (c == '\"')
122                         {
123                             state = State.QUOTE_DOUBLE;
124                             appendToken(c);
125                         }
126                         else
127                         {
128                             appendToken(c);
129                             state = State.TOKEN;
130                         }
131                         break;
132                     }
133                     case TOKEN:
134                     {
135                         if (delims.indexOf(c) >= 0)
136                         {
137                             debug("hasNext/t: %b [%s]%n",hasToken,token);
138                             return hasToken;
139                         }
140                         else if (c == '\'')
141                         {
142                             state = State.QUOTE_SINGLE;
143                         }
144                         else if (c == '\"')
145                         {
146                             state = State.QUOTE_DOUBLE;
147                         }
148                         appendToken(c);
149                         break;
150                     }
151                     case QUOTE_SINGLE:
152                     {
153                         if (escape)
154                         {
155                             escape = false;
156                             appendToken(c);
157                         }
158                         else if (c == '\'')
159                         {
160                             appendToken(c);
161                             state = State.TOKEN;
162                         }
163                         else if (c == '\\')
164                         {
165                             escape = true;
166                         }
167                         else
168                         {
169                             appendToken(c);
170                         }
171                         break;
172                     }
173                     case QUOTE_DOUBLE:
174                     {
175                         if (escape)
176                         {
177                             escape = false;
178                             appendToken(c);
179                         }
180                         else if (c == '\"')
181                         {
182                             appendToken(c);
183                             state = State.TOKEN;
184                         }
185                         else if (c == '\\')
186                         {
187                             escape = true;
188                         }
189                         else
190                         {
191                             appendToken(c);
192                         }
193                         break;
194                     }
195                 }
196                 debug("%s <%s> : [%s]%n",state,c,token);
197             }
198 
199             debug("hasNext/e: %b [%s]%n",hasToken,token);
200             return hasToken;
201         }
202 
203         @Override
204         public String next()
205         {
206             if (!hasNext())
207             {
208                 throw new NoSuchElementException();
209             }
210             String ret = token.toString();
211             token.setLength(0);
212             hasToken = false;
213             return QuoteUtil.dequote(ret.trim());
214         }
215 
216         @Override
217         public void remove()
218         {
219             throw new UnsupportedOperationException("Remove not supported with this iterator");
220         }
221     }
222 
223     /**
224      * ABNF from RFC 2616, RFC 822, and RFC 6455 specified characters requiring quoting.
225      */
226     public static final String ABNF_REQUIRED_QUOTING = "\"'\\\n\r\t\f\b%+ ;=";
227 
228     private static final char UNICODE_TAG = 0xFFFF;
229     private static final char[] escapes = new char[32];
230 
231     static
232     {
233         Arrays.fill(escapes,UNICODE_TAG);
234         // non-unicode
235         escapes['\b'] = 'b';
236         escapes['\t'] = 't';
237         escapes['\n'] = 'n';
238         escapes['\f'] = 'f';
239         escapes['\r'] = 'r';
240     }
241 
242     private static int dehex(byte b)
243     {
244         if ((b >= '0') && (b <= '9'))
245         {
246             return (byte)(b - '0');
247         }
248         if ((b >= 'a') && (b <= 'f'))
249         {
250             return (byte)((b - 'a') + 10);
251         }
252         if ((b >= 'A') && (b <= 'F'))
253         {
254             return (byte)((b - 'A') + 10);
255         }
256         throw new IllegalArgumentException("!hex:" + Integer.toHexString(0xff & b));
257     }
258 
259     /**
260      * Remove quotes from a string, only if the input string start with and end with the same quote character.
261      * 
262      * @param str
263      *            the string to remove surrounding quotes from
264      * @return the de-quoted string
265      */
266     public static String dequote(String str)
267     {
268         char start = str.charAt(0);
269         if ((start == '\'') || (start == '\"'))
270         {
271             // possibly quoted
272             char end = str.charAt(str.length() - 1);
273             if (start == end)
274             {
275                 // dequote
276                 return str.substring(1,str.length() - 1);
277             }
278         }
279         return str;
280     }
281 
282     public static void escape(StringBuilder buf, String str)
283     {
284         for (char c : str.toCharArray())
285         {
286             if (c >= 32)
287             {
288                 // non special character
289                 if ((c == '"') || (c == '\\'))
290                 {
291                     buf.append('\\');
292                 }
293                 buf.append(c);
294             }
295             else
296             {
297                 // special characters, requiring escaping
298                 char escaped = escapes[c];
299 
300                 // is this a unicode escape?
301                 if (escaped == UNICODE_TAG)
302                 {
303                     buf.append("\\u00");
304                     if (c < 0x10)
305                     {
306                         buf.append('0');
307                     }
308                     buf.append(Integer.toString(c,16)); // hex
309                 }
310                 else
311                 {
312                     // normal escape
313                     buf.append('\\').append(escaped);
314                 }
315             }
316         }
317     }
318 
319     /**
320      * Simple quote of a string, escaping where needed.
321      * 
322      * @param buf
323      *            the StringBuilder to append to
324      * @param str
325      *            the string to quote
326      */
327     public static void quote(StringBuilder buf, String str)
328     {
329         buf.append('"');
330         escape(buf,str);
331         buf.append('"');
332     }
333 
334     /**
335      * Append into buf the provided string, adding quotes if needed.
336      * <p>
337      * Quoting is determined if any of the characters in the <code>delim</code> are found in the input <code>str</code>.
338      * 
339      * @param buf
340      *            the buffer to append to
341      * @param str
342      *            the string to possibly quote
343      * @param delim
344      *            the delimiter characters that will trigger automatic quoting
345      */
346     public static void quoteIfNeeded(StringBuilder buf, String str, String delim)
347     {
348         if (str == null)
349         {
350             return;
351         }
352         // check for delimiters in input string
353         int len = str.length();
354         if (len == 0)
355         {
356             return;
357         }
358         int ch;
359         for (int i = 0; i < len; i++)
360         {
361             ch = str.codePointAt(i);
362             if (delim.indexOf(ch) >= 0)
363             {
364                 // found a delimiter codepoint. we need to quote it.
365                 quote(buf,str);
366                 return;
367             }
368         }
369 
370         // no special delimiters used, no quote needed.
371         buf.append(str);
372     }
373 
374     /**
375      * 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
376      * needed.
377      * 
378      * @param str
379      *            the input string to split apart
380      * @param delims
381      *            the delimiter characters to split the string on
382      * @return the iterator of the parts of the string, trimmed, with quotes around the string part removed, and unescaped
383      */
384     public static Iterator<String> splitAt(String str, String delims)
385     {
386         return new DeQuotingStringIterator(str.trim(),delims);
387     }
388 
389     public static String unescape(String str)
390     {
391         if (str == null)
392         {
393             // nothing there
394             return null;
395         }
396 
397         int len = str.length();
398         if (len <= 1)
399         {
400             // impossible to be escaped
401             return str;
402         }
403 
404         StringBuilder ret = new StringBuilder(len - 2);
405         boolean escaped = false;
406         char c;
407         for (int i = 0; i < len; i++)
408         {
409             c = str.charAt(i);
410             if (escaped)
411             {
412                 escaped = false;
413                 switch (c)
414                 {
415                     case 'n':
416                         ret.append('\n');
417                         break;
418                     case 'r':
419                         ret.append('\r');
420                         break;
421                     case 't':
422                         ret.append('\t');
423                         break;
424                     case 'f':
425                         ret.append('\f');
426                         break;
427                     case 'b':
428                         ret.append('\b');
429                         break;
430                     case '\\':
431                         ret.append('\\');
432                         break;
433                     case '/':
434                         ret.append('/');
435                         break;
436                     case '"':
437                         ret.append('"');
438                         break;
439                     case 'u':
440                         ret.append((char)((dehex((byte)str.charAt(i++)) << 24) + (dehex((byte)str.charAt(i++)) << 16) + (dehex((byte)str.charAt(i++)) << 8) + (dehex((byte)str
441                                 .charAt(i++)))));
442                         break;
443                     default:
444                         ret.append(c);
445                 }
446             }
447             else if (c == '\\')
448             {
449                 escaped = true;
450             }
451             else
452             {
453                 ret.append(c);
454             }
455         }
456         return ret.toString();
457     }
458 
459     public static String join(Object[] objs, String delim)
460     {
461         if (objs == null)
462         {
463             return "";
464         }
465         StringBuilder ret = new StringBuilder();
466         int len = objs.length;
467         for (int i = 0; i < len; i++)
468         {
469             if (i > 0)
470             {
471                 ret.append(delim);
472             }
473             if (objs[i] instanceof String)
474             {
475                 ret.append('"').append(objs[i]).append('"');
476             }
477             else
478             {
479                 ret.append(objs[i]);
480             }
481         }
482         return ret.toString();
483     }
484 
485     public static String join(Collection<?> objs, String delim)
486     {
487         if (objs == null)
488         {
489             return "";
490         }
491         StringBuilder ret = new StringBuilder();
492         boolean needDelim = false;
493         for (Object obj : objs)
494         {
495             if (needDelim)
496             {
497                 ret.append(delim);
498             }
499             if (obj instanceof String)
500             {
501                 ret.append('"').append(obj).append('"');
502             }
503             else
504             {
505                 ret.append(obj);
506             }
507             needDelim = true;
508         }
509         return ret.toString();
510     }
511 }