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