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