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}