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