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