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