View Javadoc

1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   *
19   */
20  package org.apache.mina.proxy.utils;
21  
22  import java.io.ByteArrayOutputStream;
23  import java.io.UnsupportedEncodingException;
24  import java.util.ArrayList;
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Map;
28  
29  import javax.security.sasl.AuthenticationException;
30  import javax.security.sasl.SaslException;
31  
32  /**
33   * StringUtilities.java - Various methods to handle strings.
34   * 
35   * @author The Apache MINA Project (dev@mina.apache.org)
36   * @since MINA 2.0.0-M3
37   */
38  public class StringUtilities {
39  
40      /**
41       * A directive is a parameter of the digest authentication process.
42       * Returns the value of a directive from the map. If mandatory is true and the 
43       * value is null, then it throws an {@link AuthenticationException}.
44       *  
45       * @param directivesMap the directive's map 
46       * @param directive the name of the directive we want to retrieve
47       * @param mandatory is the directive mandatory
48       * @return the mandatory value as a String
49       * @throws AuthenticationException if mandatory is true and if 
50       * directivesMap.get(directive) == null
51       */
52      public static String getDirectiveValue(
53              HashMap<String, String> directivesMap, String directive,
54              boolean mandatory) throws AuthenticationException {
55          String value = directivesMap.get(directive);
56          if (value == null) {
57              if (mandatory) {
58                  throw new AuthenticationException("\"" + directive
59                          + "\" mandatory directive is missing");
60              }
61  
62              return "";
63          }
64  
65          return value;
66      }
67  
68      /**
69       * Copy the directive to the {@link StringBuilder} if not null.
70       * (A directive is a parameter of the digest authentication process.)
71       * 
72       * @param directives the directives map
73       * @param sb the output buffer
74       * @param directive the directive name to look for
75       */
76      public static void copyDirective(HashMap<String, String> directives,
77              StringBuilder sb, String directive) {
78          String directiveValue = directives.get(directive);
79          if (directiveValue != null) {
80              sb.append(directive).append(" = \"").append(directiveValue).append(
81                      "\", ");
82          }
83      }
84  
85      /**
86       * Copy the directive from the source map to the destination map, if it's
87       * value isn't null.
88       * (A directive is a parameter of the digest authentication process.)
89       * 
90       * @param src the source map
91       * @param dst the destination map
92       * @param directive the directive name
93       * @return the value of the copied directive
94       */
95      public static String copyDirective(HashMap<String, String> src,
96              HashMap<String, String> dst, String directive) {
97          String directiveValue = src.get(directive);
98          if (directiveValue != null) {
99              dst.put(directive, directiveValue);
100         }
101 
102         return directiveValue;
103     }
104 
105     /**
106      * Parses digest-challenge string, extracting each token and value(s). Each token
107      * is a directive.
108      *
109      * @param buf A non-null digest-challenge string.
110      * @throws UnsupportedEncodingException 
111      * @throws SaslException if the String cannot be parsed according to RFC 2831
112      */
113     public static HashMap<String, String> parseDirectives(byte[] buf)
114             throws SaslException {
115         HashMap<String, String> map = new HashMap<String, String>();
116         boolean gettingKey = true;
117         boolean gettingQuotedValue = false;
118         boolean expectSeparator = false;
119         byte bch;
120 
121         ByteArrayOutputStream key = new ByteArrayOutputStream(10);
122         ByteArrayOutputStream value = new ByteArrayOutputStream(10);
123 
124         int i = skipLws(buf, 0);
125         while (i < buf.length) {
126             bch = buf[i];
127 
128             if (gettingKey) {
129                 if (bch == ',') {
130                     if (key.size() != 0) {
131                         throw new SaslException("Directive key contains a ',':"
132                                 + key);
133                     }
134 
135                     // Empty element, skip separator and lws
136                     i = skipLws(buf, i + 1);
137                 } else if (bch == '=') {
138                     if (key.size() == 0) {
139                         throw new SaslException("Empty directive key");
140                     }
141 
142                     gettingKey = false; // Termination of key
143                     i = skipLws(buf, i + 1); // Skip to next non whitespace
144 
145                     // Check whether value is quoted
146                     if (i < buf.length) {
147                         if (buf[i] == '"') {
148                             gettingQuotedValue = true;
149                             ++i; // Skip quote
150                         }
151                     } else {
152                         throw new SaslException("Valueless directive found: "
153                                 + key.toString());
154                     }
155                 } else if (isLws(bch)) {
156                     // LWS that occurs after key
157                     i = skipLws(buf, i + 1);
158 
159                     // Expecting '='
160                     if (i < buf.length) {
161                         if (buf[i] != '=') {
162                             throw new SaslException("'=' expected after key: "
163                                     + key.toString());
164                         }
165                     } else {
166                         throw new SaslException("'=' expected after key: "
167                                 + key.toString());
168                     }
169                 } else {
170                     key.write(bch); // Append to key
171                     ++i; // Advance
172                 }
173             } else if (gettingQuotedValue) {
174                 // Getting a quoted value
175                 if (bch == '\\') {
176                     // quoted-pair = "\" CHAR ==> CHAR
177                     ++i; // Skip escape
178                     if (i < buf.length) {
179                         value.write(buf[i]);
180                         ++i; // Advance
181                     } else {
182                         // Trailing escape in a quoted value
183                         throw new SaslException(
184                                 "Unmatched quote found for directive: "
185                                         + key.toString() + " with value: "
186                                         + value.toString());
187                     }
188                 } else if (bch == '"') {
189                     // closing quote
190                     ++i; // Skip closing quote
191                     gettingQuotedValue = false;
192                     expectSeparator = true;
193                 } else {
194                     value.write(bch);
195                     ++i; // Advance
196                 }
197             } else if (isLws(bch) || bch == ',') {
198                 // Value terminated
199                 extractDirective(map, key.toString(), value.toString());
200                 key.reset();
201                 value.reset();
202                 gettingKey = true;
203                 gettingQuotedValue = expectSeparator = false;
204                 i = skipLws(buf, i + 1); // Skip separator and LWS
205             } else if (expectSeparator) {
206                 throw new SaslException(
207                         "Expecting comma or linear whitespace after quoted string: \""
208                                 + value.toString() + "\"");
209             } else {
210                 value.write(bch); // Unquoted value
211                 ++i; // Advance
212             }
213         }
214 
215         if (gettingQuotedValue) {
216             throw new SaslException("Unmatched quote found for directive: "
217                     + key.toString() + " with value: " + value.toString());
218         }
219 
220         // Get last pair
221         if (key.size() > 0) {
222             extractDirective(map, key.toString(), value.toString());
223         }
224 
225         return map;
226     }
227 
228     /**
229      * Processes directive/value pairs from the digest-challenge and
230      * fill out the provided map.
231      * 
232      * @param key A non-null String challenge token name.
233      * @param value A non-null String token value.
234      * @throws SaslException if either the key or the value is null or
235      * if the key already has a value. 
236      */
237     private static void extractDirective(HashMap<String, String> map,
238             String key, String value) throws SaslException {
239         if (map.get(key) != null) {
240             throw new SaslException("Peer sent more than one " + key
241                     + " directive");
242         }
243 
244         map.put(key, value);
245     }
246 
247     /**
248      * Is character a linear white space ?
249      * LWS            = [CRLF] 1*( SP | HT )
250      * Note that we're checking individual bytes instead of CRLF
251      * 
252      * @param b the byte to check
253      * @return <code>true</code> if it's a linear white space
254      */
255     public static boolean isLws(byte b) {
256         switch (b) {
257         case 13: // US-ASCII CR, carriage return
258         case 10: // US-ASCII LF, line feed
259         case 32: // US-ASCII SP, space
260         case 9: // US-ASCII HT, horizontal-tab
261             return true;
262         }
263 
264         return false;
265     }
266 
267     /**
268      * Skip all linear white spaces
269      * 
270      * @param buf the buf which is being scanned for lws
271      * @param start the offset to start at
272      * @return the next position in buf which isn't a lws character
273      */
274     private static int skipLws(byte[] buf, int start) {
275         int i;
276 
277         for (i = start; i < buf.length; i++) {
278             if (!isLws(buf[i])) {
279                 return i;
280             }
281         }
282 
283         return i;
284     }
285 
286     /**
287      * Used to convert username-value, passwd or realm to 8859_1 encoding
288      * if all chars in string are within the 8859_1 (Latin 1) encoding range.
289      * 
290      * @param str a non-null String
291      * @return a non-null String containing the 8859_1 encoded string
292      * @throws AuthenticationException 
293      */
294     public static String stringTo8859_1(String str)
295             throws UnsupportedEncodingException {
296         if (str == null) {
297             return "";
298         }
299 
300         return new String(str.getBytes("UTF8"), "8859_1");
301     }
302 
303     /**
304      * Returns the value of the named header. If it has multiple values
305      * then an {@link IllegalArgumentException} is thrown
306      * 
307      * @param headers the http headers map
308      * @param key the key of the header 
309      * @return the value of the http header
310      */
311     public static String getSingleValuedHeader(
312             Map<String, List<String>> headers, String key) {
313         List<String> values = headers.get(key);
314 
315         if (values == null) {
316             return null;
317         }
318 
319         if (values.size() > 1) {
320             throw new IllegalArgumentException("Header with key [\"" + key
321                     + "\"] isn't single valued !");
322         }
323 
324         return values.get(0);
325     }
326 
327     /**
328      * Adds an header to the provided map of headers.
329      * 
330      * @param headers the http headers map
331      * @param key the name of the new header to add
332      * @param value the value of the added header
333      * @param singleValued if true and the map already contains one value
334      * then it is replaced by the new value. Otherwise it simply adds a new
335      * value to this multi-valued header.
336      */
337     public static void addValueToHeader(Map<String, List<String>> headers,
338             String key, String value, boolean singleValued) {
339         List<String> values = headers.get(key);
340 
341         if (values == null) {
342             values = new ArrayList<String>(1);
343             headers.put(key, values);
344         }
345 
346         if (singleValued && values.size() == 1) {
347             values.set(0, value);
348         } else {
349             values.add(value);
350         }
351     }
352 }