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