View Javadoc

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