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.handlers.http.digest;
21  
22  import java.io.UnsupportedEncodingException;
23  import java.security.NoSuchAlgorithmException;
24  import java.security.SecureRandom;
25  import java.util.Arrays;
26  import java.util.HashMap;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.StringTokenizer;
30  
31  import org.apache.mina.core.filterchain.IoFilter.NextFilter;
32  import org.apache.mina.proxy.ProxyAuthException;
33  import org.apache.mina.proxy.handlers.http.AbstractAuthLogicHandler;
34  import org.apache.mina.proxy.handlers.http.HttpProxyConstants;
35  import org.apache.mina.proxy.handlers.http.HttpProxyRequest;
36  import org.apache.mina.proxy.handlers.http.HttpProxyResponse;
37  import org.apache.mina.proxy.session.ProxyIoSession;
38  import org.apache.mina.proxy.utils.StringUtilities;
39  import org.apache.mina.util.Base64;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  
43  /**
44   * HttpDigestAuthLogicHandler.java - HTTP Digest authentication mechanism logic handler. 
45   * 
46   * @author The Apache MINA Project (dev@mina.apache.org)
47   * @version $Rev: 685703 $, $Date: 2008-08-14 00:14:47 +0200 (Thu, 14 Aug 2008) $
48   * @since MINA 2.0.0-M3
49   */
50  public class HttpDigestAuthLogicHandler extends AbstractAuthLogicHandler {
51  
52      private final static Logger logger = LoggerFactory
53              .getLogger(HttpDigestAuthLogicHandler.class);
54  
55      /**
56       * The challenge directives provided by the server.
57       */
58      private HashMap<String, String> directives = null;
59  
60      /**
61       * The response received to the last request.
62       */
63      private HttpProxyResponse response;
64  
65      private static SecureRandom rnd;
66  
67      static {
68          // Initialize secure random generator 
69          try {
70              rnd = SecureRandom.getInstance("SHA1PRNG");
71          } catch (NoSuchAlgorithmException e) {
72              throw new RuntimeException(e);
73          }
74      }
75  
76      public HttpDigestAuthLogicHandler(final ProxyIoSession proxyIoSession)
77              throws ProxyAuthException {
78          super(proxyIoSession);
79  
80          if (request == null || !(request instanceof HttpProxyRequest)) {
81              throw new IllegalArgumentException(
82                      "request parameter should be a non null HttpProxyRequest instance");
83          }
84  
85          HttpProxyRequest req = (HttpProxyRequest) request;
86          req.checkRequiredProperty(HttpProxyConstants.USER_PROPERTY);
87          req.checkRequiredProperty(HttpProxyConstants.PWD_PROPERTY);
88      }
89  
90      @Override
91      public void doHandshake(NextFilter nextFilter) throws ProxyAuthException {
92          logger.debug(" doHandshake()");
93  
94          if (step > 0 && directives == null) {
95              throw new ProxyAuthException(
96                      "Authentication challenge not received");
97          } else {
98              HttpProxyRequest req = (HttpProxyRequest) request;
99              Map<String, List<String>> headers = req.getHeaders() != null ? req
100                     .getHeaders() : new HashMap<String, List<String>>();
101 
102             if (step > 0) {
103                 logger.debug("  sending DIGEST challenge response");
104 
105                 HashMap<String, String> map = new HashMap<String, String>();
106                 map.put("username", req.getProperties().get(
107                         HttpProxyConstants.USER_PROPERTY));
108                 StringUtilities.copyDirective(directives, map, "realm");
109                 StringUtilities.copyDirective(directives, map, "uri");
110                 StringUtilities.copyDirective(directives, map, "opaque");
111                 StringUtilities.copyDirective(directives, map, "nonce");
112                 String algorithm = StringUtilities.copyDirective(directives,
113                         map, "algorithm");
114 
115                 // Check for a supported algorithm
116                 if (algorithm != null && !"md5".equalsIgnoreCase(algorithm)
117                         && !"md5-sess".equalsIgnoreCase(algorithm)) {
118                     throw new ProxyAuthException(
119                             "Unknown algorithm required by server");
120                 }
121 
122                 // Check for a supported qop
123                 String qop = directives.get("qop");
124                 if (qop != null) {
125                     StringTokenizer st = new StringTokenizer(qop, ",");
126                     String token = null;
127 
128                     while (st.hasMoreTokens()) {
129                         String tk = st.nextToken();
130                         if ("auth".equalsIgnoreCase(token)) {
131                             break;
132                         } else {
133                             int pos = Arrays.binarySearch(
134                                     DigestUtilities.SUPPORTED_QOPS, tk);
135                             if (pos > -1) {
136                                 token = tk;
137                             }
138                         }
139                     }
140 
141                     if (token != null) {
142                         map.put("qop", token);
143 
144                         byte[] nonce = new byte[8];
145                         rnd.nextBytes(nonce);
146 
147                         try {
148                             String cnonce = new String(Base64
149                                     .encodeBase64(nonce), proxyIoSession
150                                     .getCharsetName());
151                             map.put("cnonce", cnonce);
152                         } catch (UnsupportedEncodingException e) {
153                             throw new ProxyAuthException(
154                                     "Unable to encode cnonce", e);
155                         }
156                     } else {
157                         throw new ProxyAuthException(
158                                 "No supported qop option available");
159                     }
160                 }
161 
162                 map.put("nc", "00000001");
163                 map.put("uri", req.getHttpURI());
164 
165                 // Compute the response
166                 try {
167                     map.put("response", DigestUtilities
168                             .computeResponseValue(proxyIoSession.getSession(),
169                                     map, req.getHttpVerb().toUpperCase(),
170                                     req.getProperties().get(
171                                             HttpProxyConstants.PWD_PROPERTY),
172                                     proxyIoSession.getCharsetName(), response
173                                             .getBody()));
174 
175                 } catch (Exception e) {
176                     throw new ProxyAuthException(
177                             "Digest response computing failed", e);
178                 }
179 
180                 // Prepare the challenge response header and add it to the request we will send
181                 StringBuilder sb = new StringBuilder("Digest ");
182                 boolean addSeparator = false;
183 
184                 for (String key : map.keySet()) {
185 
186                     if (addSeparator) {
187                         sb.append(", ");
188                     } else {
189                         addSeparator = true;
190                     }
191 
192                     boolean quotedValue = !"qop".equals(key)
193                             && !"nc".equals(key);
194                     sb.append(key);
195                     if (quotedValue) {
196                         sb.append("=\"").append(map.get(key)).append('\"');
197                     } else {
198                         sb.append('=').append(map.get(key));
199                     }
200                 }
201 
202                 StringUtilities.addValueToHeader(headers,
203                         "Proxy-Authorization", sb.toString(), true);
204             }
205 
206             StringUtilities.addValueToHeader(headers, "Keep-Alive",
207                     HttpProxyConstants.DEFAULT_KEEP_ALIVE_TIME, true);
208             StringUtilities.addValueToHeader(headers, "Proxy-Connection",
209                     "keep-Alive", true);
210             req.setHeaders(headers);
211 
212             writeRequest(nextFilter, req);
213             step++;
214         }
215     }
216 
217     @Override
218     public void handleResponse(final HttpProxyResponse response)
219             throws ProxyAuthException {
220         this.response = response;
221 
222         if (step == 0) {
223             if (response.getStatusCode() != 401
224                     && response.getStatusCode() != 407) {
225                 throw new ProxyAuthException(
226                         "Received unexpected response code ("
227                                 + response.getStatusLine() + ").");
228             }
229 
230             // Header should be like this
231             // Proxy-Authenticate: Digest still_some_more_stuff
232             List<String> values = response.getHeaders().get(
233                     "Proxy-Authenticate");
234             String challengeResponse = null;
235 
236             for (String s : values) {
237                 if (s.startsWith("Digest")) {
238                     challengeResponse = s;
239                     break;
240                 }
241             }
242 
243             if (challengeResponse == null) {
244                 throw new ProxyAuthException(
245                         "Server doesn't support digest authentication method !");
246             }
247 
248             try {
249                 directives = StringUtilities.parseDirectives(challengeResponse
250                         .substring(7).getBytes(proxyIoSession.getCharsetName()));
251             } catch (Exception e) {
252                 throw new ProxyAuthException(
253                         "Parsing of server digest directives failed", e);
254             }
255             step = 1;
256         } else {
257             throw new ProxyAuthException("Received unexpected response code ("
258                     + response.getStatusLine() + ").");
259         }
260     }
261 }