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(
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 }