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 <a href="http://mina.apache.org">Apache MINA Project</a>
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(HashMap<String, String> directivesMap, String directive, boolean mandatory)
53              throws AuthenticationException {
54          String value = directivesMap.get(directive);
55          if (value == null) {
56              if (mandatory) {
57                  throw new AuthenticationException("\"" + directive + "\" mandatory directive is missing");
58              }
59  
60              return "";
61          }
62  
63          return value;
64      }
65  
66      /**
67       * Copy the directive to the {@link StringBuilder} if not null.
68       * (A directive is a parameter of the digest authentication process.)
69       * 
70       * @param directives the directives map
71       * @param sb the output buffer
72       * @param directive the directive name to look for
73       */
74      public static void copyDirective(HashMap<String, String> directives, StringBuilder sb, String directive) {
75          String directiveValue = directives.get(directive);
76          if (directiveValue != null) {
77              sb.append(directive).append(" = \"").append(directiveValue).append("\", ");
78          }
79      }
80  
81      /**
82       * Copy the directive from the source map to the destination map, if it's
83       * value isn't null.
84       * (A directive is a parameter of the digest authentication process.)
85       * 
86       * @param src the source map
87       * @param dst the destination map
88       * @param directive the directive name
89       * @return the value of the copied directive
90       */
91      public static String copyDirective(HashMap<String, String> src, HashMap<String, String> dst, String directive) {
92          String directiveValue = src.get(directive);
93          if (directiveValue != null) {
94              dst.put(directive, directiveValue);
95          }
96  
97          return directiveValue;
98      }
99  
100     /**
101      * Parses digest-challenge string, extracting each token and value(s). Each token
102      * is a directive.
103      *
104      * @param buf A non-null digest-challenge string.
105      * @throws UnsupportedEncodingException 
106      * @throws SaslException if the String cannot be parsed according to RFC 2831
107      */
108     public static HashMap<String, String> parseDirectives(byte[] buf) throws SaslException {
109         HashMap<String, String> map = new HashMap<String, String>();
110         boolean gettingKey = true;
111         boolean gettingQuotedValue = false;
112         boolean expectSeparator = false;
113         byte bch;
114 
115         ByteArrayOutputStream key = new ByteArrayOutputStream(10);
116         ByteArrayOutputStream value = new ByteArrayOutputStream(10);
117 
118         int i = skipLws(buf, 0);
119         while (i < buf.length) {
120             bch = buf[i];
121 
122             if (gettingKey) {
123                 if (bch == ',') {
124                     if (key.size() != 0) {
125                         throw new SaslException("Directive key contains a ',':" + key);
126                     }
127 
128                     // Empty element, skip separator and lws
129                     i = skipLws(buf, i + 1);
130                 } else if (bch == '=') {
131                     if (key.size() == 0) {
132                         throw new SaslException("Empty directive key");
133                     }
134 
135                     gettingKey = false; // Termination of key
136                     i = skipLws(buf, i + 1); // Skip to next non whitespace
137 
138                     // Check whether value is quoted
139                     if (i < buf.length) {
140                         if (buf[i] == '"') {
141                             gettingQuotedValue = true;
142                             ++i; // Skip quote
143                         }
144                     } else {
145                         throw new SaslException("Valueless directive found: " + key.toString());
146                     }
147                 } else if (isLws(bch)) {
148                     // LWS that occurs after key
149                     i = skipLws(buf, i + 1);
150 
151                     // Expecting '='
152                     if (i < buf.length) {
153                         if (buf[i] != '=') {
154                             throw new SaslException("'=' expected after key: " + key.toString());
155                         }
156                     } else {
157                         throw new SaslException("'=' expected after key: " + key.toString());
158                     }
159                 } else {
160                     key.write(bch); // Append to key
161                     ++i; // Advance
162                 }
163             } else if (gettingQuotedValue) {
164                 // Getting a quoted value
165                 if (bch == '\\') {
166                     // quoted-pair = "\" CHAR ==> CHAR
167                     ++i; // Skip escape
168                     if (i < buf.length) {
169                         value.write(buf[i]);
170                         ++i; // Advance
171                     } else {
172                         // Trailing escape in a quoted value
173                         throw new SaslException("Unmatched quote found for directive: " + key.toString()
174                                 + " with value: " + value.toString());
175                     }
176                 } else if (bch == '"') {
177                     // closing quote
178                     ++i; // Skip closing quote
179                     gettingQuotedValue = false;
180                     expectSeparator = true;
181                 } else {
182                     value.write(bch);
183                     ++i; // Advance
184                 }
185             } else if (isLws(bch) || bch == ',') {
186                 // Value terminated
187                 extractDirective(map, key.toString(), value.toString());
188                 key.reset();
189                 value.reset();
190                 gettingKey = true;
191                 gettingQuotedValue = expectSeparator = false;
192                 i = skipLws(buf, i + 1); // Skip separator and LWS
193             } else if (expectSeparator) {
194                 throw new SaslException("Expecting comma or linear whitespace after quoted string: \""
195                         + value.toString() + "\"");
196             } else {
197                 value.write(bch); // Unquoted value
198                 ++i; // Advance
199             }
200         }
201 
202         if (gettingQuotedValue) {
203             throw new SaslException("Unmatched quote found for directive: " + key.toString() + " with value: "
204                     + value.toString());
205         }
206 
207         // Get last pair
208         if (key.size() > 0) {
209             extractDirective(map, key.toString(), value.toString());
210         }
211 
212         return map;
213     }
214 
215     /**
216      * Processes directive/value pairs from the digest-challenge and
217      * fill out the provided map.
218      * 
219      * @param key A non-null String challenge token name.
220      * @param value A non-null String token value.
221      * @throws SaslException if either the key or the value is null or
222      * if the key already has a value. 
223      */
224     private static void extractDirective(HashMap<String, String> map, String key, String value) throws SaslException {
225         if (map.get(key) != null) {
226             throw new SaslException("Peer sent more than one " + key + " directive");
227         }
228 
229         map.put(key, value);
230     }
231 
232     /**
233      * Is character a linear white space ?
234      * LWS            = [CRLF] 1*( SP | HT )
235      * Note that we're checking individual bytes instead of CRLF
236      * 
237      * @param b the byte to check
238      * @return <code>true</code> if it's a linear white space
239      */
240     public static boolean isLws(byte b) {
241         switch (b) {
242         case 13: // US-ASCII CR, carriage return
243         case 10: // US-ASCII LF, line feed
244         case 32: // US-ASCII SP, space
245         case 9: // US-ASCII HT, horizontal-tab
246             return true;
247         }
248 
249         return false;
250     }
251 
252     /**
253      * Skip all linear white spaces
254      * 
255      * @param buf the buf which is being scanned for lws
256      * @param start the offset to start at
257      * @return the next position in buf which isn't a lws character
258      */
259     private static int skipLws(byte[] buf, int start) {
260         int i;
261 
262         for (i = start; i < buf.length; i++) {
263             if (!isLws(buf[i])) {
264                 return i;
265             }
266         }
267 
268         return i;
269     }
270 
271     /**
272      * Used to convert username-value, passwd or realm to 8859_1 encoding
273      * if all chars in string are within the 8859_1 (Latin 1) encoding range.
274      * 
275      * @param str a non-null String
276      * @return a non-null String containing the 8859_1 encoded string
277      * @throws AuthenticationException 
278      */
279     public static String stringTo8859_1(String str) throws UnsupportedEncodingException {
280         if (str == null) {
281             return "";
282         }
283 
284         return new String(str.getBytes("UTF8"), "8859_1");
285     }
286 
287     /**
288      * Returns the value of the named header. If it has multiple values
289      * then an {@link IllegalArgumentException} is thrown
290      * 
291      * @param headers the http headers map
292      * @param key the key of the header 
293      * @return the value of the http header
294      */
295     public static String getSingleValuedHeader(Map<String, List<String>> headers, String key) {
296         List<String> values = headers.get(key);
297 
298         if (values == null) {
299             return null;
300         }
301 
302         if (values.size() > 1) {
303             throw new IllegalArgumentException("Header with key [\"" + key + "\"] isn't single valued !");
304         }
305 
306         return values.get(0);
307     }
308 
309     /**
310      * Adds an header to the provided map of headers.
311      * 
312      * @param headers the http headers map
313      * @param key the name of the new header to add
314      * @param value the value of the added header
315      * @param singleValued if true and the map already contains one value
316      * then it is replaced by the new value. Otherwise it simply adds a new
317      * value to this multi-valued header.
318      */
319     public static void addValueToHeader(Map<String, List<String>> headers, String key, String value,
320             boolean singleValued) {
321         List<String> values = headers.get(key);
322 
323         if (values == null) {
324             values = new ArrayList<String>(1);
325             headers.put(key, values);
326         }
327 
328         if (singleValued && values.size() == 1) {
329             values.set(0, value);
330         } else {
331             values.add(value);
332         }
333     }
334 }