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.handlers.http.digest; 021 022import java.io.UnsupportedEncodingException; 023import java.security.NoSuchAlgorithmException; 024import java.security.SecureRandom; 025import java.util.Arrays; 026import java.util.HashMap; 027import java.util.List; 028import java.util.Map; 029import java.util.StringTokenizer; 030 031import org.apache.mina.core.filterchain.IoFilter.NextFilter; 032import org.apache.mina.proxy.ProxyAuthException; 033import org.apache.mina.proxy.handlers.http.AbstractAuthLogicHandler; 034import org.apache.mina.proxy.handlers.http.HttpProxyConstants; 035import org.apache.mina.proxy.handlers.http.HttpProxyRequest; 036import org.apache.mina.proxy.handlers.http.HttpProxyResponse; 037import org.apache.mina.proxy.session.ProxyIoSession; 038import org.apache.mina.proxy.utils.StringUtilities; 039import org.apache.mina.util.Base64; 040import org.slf4j.Logger; 041import org.slf4j.LoggerFactory; 042 043/** 044 * HttpDigestAuthLogicHandler.java - HTTP Digest authentication mechanism logic handler. 045 * 046 * @author <a href="http://mina.apache.org">Apache MINA Project</a> 047 * @since MINA 2.0.0-M3 048 */ 049public class HttpDigestAuthLogicHandler extends AbstractAuthLogicHandler { 050 051 private final static Logger logger = LoggerFactory.getLogger(HttpDigestAuthLogicHandler.class); 052 053 /** 054 * The challenge directives provided by the server. 055 */ 056 private HashMap<String, String> directives = null; 057 058 /** 059 * The response received to the last request. 060 */ 061 private HttpProxyResponse response; 062 063 private static SecureRandom rnd; 064 065 static { 066 // Initialize secure random generator 067 try { 068 rnd = SecureRandom.getInstance("SHA1PRNG"); 069 } catch (NoSuchAlgorithmException e) { 070 throw new RuntimeException(e); 071 } 072 } 073 074 public HttpDigestAuthLogicHandler(final ProxyIoSession proxyIoSession) throws ProxyAuthException { 075 super(proxyIoSession); 076 077 ((HttpProxyRequest) request).checkRequiredProperties(HttpProxyConstants.USER_PROPERTY, 078 HttpProxyConstants.PWD_PROPERTY); 079 } 080 081 @Override 082 public void doHandshake(NextFilter nextFilter) throws ProxyAuthException { 083 logger.debug(" doHandshake()"); 084 085 if (step > 0 && directives == null) { 086 throw new ProxyAuthException("Authentication challenge not received"); 087 } 088 089 HttpProxyRequest req = (HttpProxyRequest) request; 090 Map<String, List<String>> headers = req.getHeaders() != null ? req.getHeaders() 091 : new HashMap<String, List<String>>(); 092 093 if (step > 0) { 094 logger.debug(" sending DIGEST challenge response"); 095 096 // Build a challenge response 097 HashMap<String, String> map = new HashMap<String, String>(); 098 map.put("username", req.getProperties().get(HttpProxyConstants.USER_PROPERTY)); 099 StringUtilities.copyDirective(directives, map, "realm"); 100 StringUtilities.copyDirective(directives, map, "uri"); 101 StringUtilities.copyDirective(directives, map, "opaque"); 102 StringUtilities.copyDirective(directives, map, "nonce"); 103 String algorithm = StringUtilities.copyDirective(directives, map, "algorithm"); 104 105 // Check for a supported algorithm 106 if (algorithm != null && !"md5".equalsIgnoreCase(algorithm) && !"md5-sess".equalsIgnoreCase(algorithm)) { 107 throw new ProxyAuthException("Unknown algorithm required by server"); 108 } 109 110 // Check for a supported qop 111 String qop = directives.get("qop"); 112 if (qop != null) { 113 StringTokenizer st = new StringTokenizer(qop, ","); 114 String token = null; 115 116 while (st.hasMoreTokens()) { 117 String tk = st.nextToken(); 118 if ("auth".equalsIgnoreCase(token)) { 119 break; 120 } 121 122 int pos = Arrays.binarySearch(DigestUtilities.SUPPORTED_QOPS, tk); 123 if (pos > -1) { 124 token = tk; 125 } 126 } 127 128 if (token != null) { 129 map.put("qop", token); 130 131 byte[] nonce = new byte[8]; 132 rnd.nextBytes(nonce); 133 134 try { 135 String cnonce = new String(Base64.encodeBase64(nonce), proxyIoSession.getCharsetName()); 136 map.put("cnonce", cnonce); 137 } catch (UnsupportedEncodingException e) { 138 throw new ProxyAuthException("Unable to encode cnonce", e); 139 } 140 } else { 141 throw new ProxyAuthException("No supported qop option available"); 142 } 143 } 144 145 map.put("nc", "00000001"); 146 map.put("uri", req.getHttpURI()); 147 148 // Compute the response 149 try { 150 map.put("response", DigestUtilities.computeResponseValue(proxyIoSession.getSession(), map, req 151 .getHttpVerb().toUpperCase(), req.getProperties().get(HttpProxyConstants.PWD_PROPERTY), 152 proxyIoSession.getCharsetName(), response.getBody())); 153 154 } catch (Exception e) { 155 throw new ProxyAuthException("Digest response computing failed", e); 156 } 157 158 // Prepare the challenge response header and add it to the 159 // request we will send 160 StringBuilder sb = new StringBuilder("Digest "); 161 boolean addSeparator = false; 162 163 for ( Map.Entry<String, String> entry : map.entrySet()) { 164 String key = entry.getKey(); 165 166 if (addSeparator) { 167 sb.append(", "); 168 } else { 169 addSeparator = true; 170 } 171 172 boolean quotedValue = !"qop".equals(key) && !"nc".equals(key); 173 sb.append(key); 174 175 if (quotedValue) { 176 sb.append("=\"").append(entry.getValue()).append('\"'); 177 } else { 178 sb.append('=').append(entry.getValue()); 179 } 180 } 181 182 StringUtilities.addValueToHeader(headers, "Proxy-Authorization", sb.toString(), true); 183 } 184 185 addKeepAliveHeaders(headers); 186 req.setHeaders(headers); 187 188 writeRequest(nextFilter, req); 189 step++; 190 } 191 192 @Override 193 public void handleResponse(final HttpProxyResponse response) throws ProxyAuthException { 194 this.response = response; 195 196 if (step == 0) { 197 if (response.getStatusCode() != 401 && response.getStatusCode() != 407) { 198 throw new ProxyAuthException("Received unexpected response code (" + response.getStatusLine() + ")."); 199 } 200 201 // Header should look like this 202 // Proxy-Authenticate: Digest still_some_more_stuff 203 List<String> values = response.getHeaders().get("Proxy-Authenticate"); 204 String challengeResponse = null; 205 206 for (String s : values) { 207 if (s.startsWith("Digest")) { 208 challengeResponse = s; 209 break; 210 } 211 } 212 213 if (challengeResponse == null) { 214 throw new ProxyAuthException("Server doesn't support digest authentication method !"); 215 } 216 217 try { 218 directives = StringUtilities.parseDirectives(challengeResponse.substring(7).getBytes( 219 proxyIoSession.getCharsetName())); 220 } catch (Exception e) { 221 throw new ProxyAuthException("Parsing of server digest directives failed", e); 222 } 223 step = 1; 224 } else { 225 throw new ProxyAuthException("Received unexpected response code (" + response.getStatusLine() + ")."); 226 } 227 } 228}