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