001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.mina.proxy.handlers.http.digest;
021
022import java.io.UnsupportedEncodingException;
023import java.security.NoSuchAlgorithmException;
024import java.security.SecureRandom;
025import java.util.Arrays;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.StringTokenizer;
030
031import org.apache.mina.core.filterchain.IoFilter.NextFilter;
032import org.apache.mina.proxy.ProxyAuthException;
033import org.apache.mina.proxy.handlers.http.AbstractAuthLogicHandler;
034import org.apache.mina.proxy.handlers.http.HttpProxyConstants;
035import org.apache.mina.proxy.handlers.http.HttpProxyRequest;
036import org.apache.mina.proxy.handlers.http.HttpProxyResponse;
037import org.apache.mina.proxy.session.ProxyIoSession;
038import org.apache.mina.proxy.utils.StringUtilities;
039import org.apache.mina.util.Base64;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043/**
044 * HttpDigestAuthLogicHandler.java - HTTP Digest authentication mechanism logic handler. 
045 * 
046 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
047 * @since MINA 2.0.0-M3
048 */
049public class HttpDigestAuthLogicHandler extends AbstractAuthLogicHandler {
050
051    private final static Logger logger = LoggerFactory.getLogger(HttpDigestAuthLogicHandler.class);
052
053    /**
054     * The challenge directives provided by the server.
055     */
056    private HashMap<String, String> directives = null;
057
058    /**
059     * The response received to the last request.
060     */
061    private HttpProxyResponse response;
062
063    private static SecureRandom rnd;
064
065    static {
066        // Initialize secure random generator 
067        try {
068            rnd = SecureRandom.getInstance("SHA1PRNG");
069        } catch (NoSuchAlgorithmException e) {
070            throw new RuntimeException(e);
071        }
072    }
073
074    public HttpDigestAuthLogicHandler(final ProxyIoSession proxyIoSession) throws ProxyAuthException {
075        super(proxyIoSession);
076
077        ((HttpProxyRequest) request).checkRequiredProperties(HttpProxyConstants.USER_PROPERTY,
078                HttpProxyConstants.PWD_PROPERTY);
079    }
080
081    @Override
082    public void doHandshake(NextFilter nextFilter) throws ProxyAuthException {
083        logger.debug(" doHandshake()");
084
085        if (step > 0 && directives == null) {
086            throw new ProxyAuthException("Authentication challenge not received");
087        }
088
089        HttpProxyRequest req = (HttpProxyRequest) request;
090        Map<String, List<String>> headers = req.getHeaders() != null ? req.getHeaders()
091                : new HashMap<String, List<String>>();
092
093        if (step > 0) {
094            logger.debug("  sending DIGEST challenge response");
095
096            // Build a challenge response
097            HashMap<String, String> map = new HashMap<String, String>();
098            map.put("username", req.getProperties().get(HttpProxyConstants.USER_PROPERTY));
099            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 ( Map.Entry<String, String> entry : map.entrySet()) {
164                String key = entry.getKey();
165
166                if (addSeparator) {
167                    sb.append(", ");
168                } else {
169                    addSeparator = true;
170                }
171
172                boolean quotedValue = !"qop".equals(key) && !"nc".equals(key);
173                sb.append(key);
174                
175                if (quotedValue) {
176                    sb.append("=\"").append(entry.getValue()).append('\"');
177                } else {
178                    sb.append('=').append(entry.getValue());
179                }
180            }
181
182            StringUtilities.addValueToHeader(headers, "Proxy-Authorization", sb.toString(), true);
183        }
184
185        addKeepAliveHeaders(headers);
186        req.setHeaders(headers);
187
188        writeRequest(nextFilter, req);
189        step++;
190    }
191
192    @Override
193    public void handleResponse(final HttpProxyResponse response) throws ProxyAuthException {
194        this.response = response;
195
196        if (step == 0) {
197            if (response.getStatusCode() != 401 && response.getStatusCode() != 407) {
198                throw new ProxyAuthException("Received unexpected response code (" + response.getStatusLine() + ").");
199            }
200
201            // Header should look like this
202            // Proxy-Authenticate: Digest still_some_more_stuff
203            List<String> values = response.getHeaders().get("Proxy-Authenticate");
204            String challengeResponse = null;
205
206            for (String s : values) {
207                if (s.startsWith("Digest")) {
208                    challengeResponse = s;
209                    break;
210                }
211            }
212
213            if (challengeResponse == null) {
214                throw new ProxyAuthException("Server doesn't support digest authentication method !");
215            }
216
217            try {
218                directives = StringUtilities.parseDirectives(challengeResponse.substring(7).getBytes(
219                        proxyIoSession.getCharsetName()));
220            } catch (Exception e) {
221                throw new ProxyAuthException("Parsing of server digest directives failed", e);
222            }
223            step = 1;
224        } else {
225            throw new ProxyAuthException("Received unexpected response code (" + response.getStatusLine() + ").");
226        }
227    }
228}