001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.mina.proxy.utils;
021
022import java.io.ByteArrayOutputStream;
023import java.io.UnsupportedEncodingException;
024import java.util.ArrayList;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028
029import javax.security.sasl.AuthenticationException;
030import javax.security.sasl.SaslException;
031
032/**
033 * StringUtilities.java - Various methods to handle strings.
034 * 
035 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
036 * @since MINA 2.0.0-M3
037 */
038public class StringUtilities {
039
040    /**
041     * A directive is a parameter of the digest authentication process.
042     * Returns the value of a directive from the map. If mandatory is true and the 
043     * value is null, then it throws an {@link AuthenticationException}.
044     *  
045     * @param directivesMap the directive's map 
046     * @param directive the name of the directive we want to retrieve
047     * @param mandatory is the directive mandatory
048     * @return the mandatory value as a String
049     * @throws AuthenticationException if mandatory is true and if 
050     * directivesMap.get(directive) == null
051     */
052    public static String getDirectiveValue(HashMap<String, String> directivesMap, String directive, boolean mandatory)
053            throws AuthenticationException {
054        String value = directivesMap.get(directive);
055        if (value == null) {
056            if (mandatory) {
057                throw new AuthenticationException("\"" + directive + "\" mandatory directive is missing");
058            }
059
060            return "";
061        }
062
063        return value;
064    }
065
066    /**
067     * Copy the directive to the {@link StringBuilder} if not null.
068     * (A directive is a parameter of the digest authentication process.)
069     * 
070     * @param directives the directives map
071     * @param sb the output buffer
072     * @param directive the directive name to look for
073     */
074    public static void copyDirective(HashMap<String, String> directives, StringBuilder sb, String directive) {
075        String directiveValue = directives.get(directive);
076        if (directiveValue != null) {
077            sb.append(directive).append(" = \"").append(directiveValue).append("\", ");
078        }
079    }
080
081    /**
082     * Copy the directive from the source map to the destination map, if it's
083     * value isn't null.
084     * (A directive is a parameter of the digest authentication process.)
085     * 
086     * @param src the source map
087     * @param dst the destination map
088     * @param directive the directive name
089     * @return the value of the copied directive
090     */
091    public static String copyDirective(HashMap<String, String> src, HashMap<String, String> dst, String directive) {
092        String directiveValue = src.get(directive);
093        if (directiveValue != null) {
094            dst.put(directive, directiveValue);
095        }
096
097        return directiveValue;
098    }
099
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     * @return A Map containing the aprsed directives
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 <tt>true</tt> 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 UnsupportedEncodingException if we weren't able to decode using the ISO 8859_1 encoding
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}