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 }