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 <a href="http://mina.apache.org">Apache MINA Project</a>
47   * @since MINA 2.0.0-M3
48   */
49  public class HttpDigestAuthLogicHandler extends AbstractAuthLogicHandler {
50  
51      private final static Logger logger = LoggerFactory.getLogger(HttpDigestAuthLogicHandler.class);
52  
53      /**
54       * The challenge directives provided by the server.
55       */
56      private HashMap<String, String> directives = null;
57  
58      /**
59       * The response received to the last request.
60       */
61      private HttpProxyResponse response;
62  
63      private static SecureRandom rnd;
64  
65      static {
66          // Initialize secure random generator 
67          try {
68              rnd = SecureRandom.getInstance("SHA1PRNG");
69          } catch (NoSuchAlgorithmException e) {
70              throw new RuntimeException(e);
71          }
72      }
73  
74      public HttpDigestAuthLogicHandler(final ProxyIoSession proxyIoSession) throws ProxyAuthException {
75          super(proxyIoSession);
76  
77          ((HttpProxyRequest) request).checkRequiredProperties(HttpProxyConstants.USER_PROPERTY,
78                  HttpProxyConstants.PWD_PROPERTY);
79      }
80  
81      @Override
82      public void doHandshake(NextFilter nextFilter) throws ProxyAuthException {
83          logger.debug(" doHandshake()");
84  
85          if (step > 0 && directives == null) {
86              throw new ProxyAuthException("Authentication challenge not received");
87          }
88  
89          HttpProxyRequest req = (HttpProxyRequest) request;
90          Map<String, List<String>> headers = req.getHeaders() != null ? req.getHeaders()
91                  : new HashMap<String, List<String>>();
92  
93          if (step > 0) {
94              logger.debug("  sending DIGEST challenge response");
95  
96              // Build a challenge response
97              HashMap<String, String> map = new HashMap<String, String>();
98              map.put("username", req.getProperties().get(HttpProxyConstants.USER_PROPERTY));
99              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 (String key : map.keySet()) {
164 
165                 if (addSeparator) {
166                     sb.append(", ");
167                 } else {
168                     addSeparator = true;
169                 }
170 
171                 boolean quotedValue = !"qop".equals(key) && !"nc".equals(key);
172                 sb.append(key);
173                 if (quotedValue) {
174                     sb.append("=\"").append(map.get(key)).append('\"');
175                 } else {
176                     sb.append('=').append(map.get(key));
177                 }
178             }
179 
180             StringUtilities.addValueToHeader(headers, "Proxy-Authorization", sb.toString(), true);
181         }
182 
183         addKeepAliveHeaders(headers);
184         req.setHeaders(headers);
185 
186         writeRequest(nextFilter, req);
187         step++;
188     }
189 
190     @Override
191     public void handleResponse(final HttpProxyResponse response) throws ProxyAuthException {
192         this.response = response;
193 
194         if (step == 0) {
195             if (response.getStatusCode() != 401 && response.getStatusCode() != 407) {
196                 throw new ProxyAuthException("Received unexpected response code (" + response.getStatusLine() + ").");
197             }
198 
199             // Header should look like this
200             // Proxy-Authenticate: Digest still_some_more_stuff
201             List<String> values = response.getHeaders().get("Proxy-Authenticate");
202             String challengeResponse = null;
203 
204             for (String s : values) {
205                 if (s.startsWith("Digest")) {
206                     challengeResponse = s;
207                     break;
208                 }
209             }
210 
211             if (challengeResponse == null) {
212                 throw new ProxyAuthException("Server doesn't support digest authentication method !");
213             }
214 
215             try {
216                 directives = StringUtilities.parseDirectives(challengeResponse.substring(7).getBytes(
217                         proxyIoSession.getCharsetName()));
218             } catch (Exception e) {
219                 throw new ProxyAuthException("Parsing of server digest directives failed", e);
220             }
221             step = 1;
222         } else {
223             throw new ProxyAuthException("Received unexpected response code (" + response.getStatusLine() + ").");
224         }
225     }
226 }