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 }