org.eclipse.jgit.transport.HttpAuthMethod.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.jgit.transport.HttpAuthMethod.java

Source

/*
 * Copyright (C) 2010, 2013, Google Inc.
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.eclipse.jgit.transport;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.util.HttpSupport.HDR_AUTHORIZATION;
import static org.eclipse.jgit.util.HttpSupport.HDR_WWW_AUTHENTICATE;

import java.io.IOException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;

import org.eclipse.jgit.transport.http.HttpConnection;
import org.eclipse.jgit.util.Base64;
import org.eclipse.jgit.util.GSSManagerFactory;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;

/**
 * Support class to populate user authentication data on a connection.
 * <p>
 * Instances of an HttpAuthMethod are not thread-safe, as some implementations
 * may need to maintain per-connection state information.
 */
abstract class HttpAuthMethod {
    /**
     * Enum listing the http authentication method types supported by jgit. They
     * are sorted by priority order!!!
     */
    public enum Type {
        NONE {
            @Override
            public HttpAuthMethod method(String hdr) {
                return None.INSTANCE;
            }

            @Override
            public String getSchemeName() {
                return "None"; //$NON-NLS-1$
            }
        },
        BASIC {
            @Override
            public HttpAuthMethod method(String hdr) {
                return new Basic();
            }

            @Override
            public String getSchemeName() {
                return "Basic"; //$NON-NLS-1$
            }
        },
        DIGEST {
            @Override
            public HttpAuthMethod method(String hdr) {
                return new Digest(hdr);
            }

            @Override
            public String getSchemeName() {
                return "Digest"; //$NON-NLS-1$
            }
        },
        NEGOTIATE {
            @Override
            public HttpAuthMethod method(String hdr) {
                return new Negotiate(hdr);
            }

            @Override
            public String getSchemeName() {
                return "Negotiate"; //$NON-NLS-1$
            }
        };
        /**
         * Creates a HttpAuthMethod instance configured with the provided HTTP
         * WWW-Authenticate header.
         *
         * @param hdr the http header
         * @return a configured HttpAuthMethod instance
         */
        public abstract HttpAuthMethod method(String hdr);

        /**
         * @return the name of the authentication scheme in the form to be used
         *         in HTTP authentication headers as specified in RFC2617 and
         *         RFC4559
         */
        public abstract String getSchemeName();
    }

    static final String EMPTY_STRING = ""; //$NON-NLS-1$
    static final String SCHEMA_NAME_SEPARATOR = " "; //$NON-NLS-1$

    /**
     * Handle an authentication failure and possibly return a new response.
     *
     * @param conn
     *            the connection that failed.
     * @param ignoreTypes
     *            authentication types to be ignored.
     * @return new authentication method to try.
     */
    static HttpAuthMethod scanResponse(final HttpConnection conn, Collection<Type> ignoreTypes) {
        final Map<String, List<String>> headers = conn.getHeaderFields();
        HttpAuthMethod authentication = Type.NONE.method(EMPTY_STRING);

        for (Entry<String, List<String>> entry : headers.entrySet()) {
            if (HDR_WWW_AUTHENTICATE.equalsIgnoreCase(entry.getKey())) {
                if (entry.getValue() != null) {
                    for (String value : entry.getValue()) {
                        if (value != null && value.length() != 0) {
                            final String[] valuePart = value.split(SCHEMA_NAME_SEPARATOR, 2);

                            try {
                                Type methodType = Type.valueOf(valuePart[0].toUpperCase(Locale.ROOT));

                                if ((ignoreTypes != null) && (ignoreTypes.contains(methodType))) {
                                    continue;
                                }

                                if (authentication.getType().compareTo(methodType) >= 0) {
                                    continue;
                                }

                                final String param;
                                if (valuePart.length == 1)
                                    param = EMPTY_STRING;
                                else
                                    param = valuePart[1];

                                authentication = methodType.method(param);
                            } catch (IllegalArgumentException e) {
                                // This auth method is not supported
                            }
                        }
                    }
                }
                break;
            }
        }

        return authentication;
    }

    protected final Type type;

    /**
     * Constructor for HttpAuthMethod.
     *
     * @param type
     *            authentication method type
     */
    protected HttpAuthMethod(Type type) {
        this.type = type;
    }

    /**
     * Update this method with the credentials from the URIish.
     *
     * @param uri
     *            the URI used to create the connection.
     * @param credentialsProvider
     *            the credentials provider, or null. If provided,
     *            {@link URIish#getPass() credentials in the URI} are ignored.
     *
     * @return true if the authentication method is able to provide
     *         authorization for the given URI
     */
    boolean authorize(URIish uri, CredentialsProvider credentialsProvider) {
        String username;
        String password;

        if (credentialsProvider != null) {
            CredentialItem.Username u = new CredentialItem.Username();
            CredentialItem.Password p = new CredentialItem.Password();

            if (credentialsProvider.supports(u, p) && credentialsProvider.get(uri, u, p)) {
                username = u.getValue();
                char[] v = p.getValue();
                password = (v == null) ? null : new String(p.getValue());
                p.clear();
            } else
                return false;
        } else {
            username = uri.getUser();
            password = uri.getPass();
        }
        if (username != null) {
            authorize(username, password);
            return true;
        }
        return false;
    }

    /**
     * Update this method with the given username and password pair.
     *
     * @param user
     * @param pass
     */
    abstract void authorize(String user, String pass);

    /**
     * Update connection properties based on this authentication method.
     *
     * @param conn
     * @throws IOException
     */
    abstract void configureRequest(HttpConnection conn) throws IOException;

    /**
     * Gives the method type associated to this http auth method
     *
     * @return the method type
     */
    public Type getType() {
        return type;
    }

    /** Performs no user authentication. */
    private static class None extends HttpAuthMethod {
        static final None INSTANCE = new None();

        public None() {
            super(Type.NONE);
        }

        @Override
        void authorize(String user, String pass) {
            // Do nothing when no authentication is enabled.
        }

        @Override
        void configureRequest(HttpConnection conn) throws IOException {
            // Do nothing when no authentication is enabled.
        }
    }

    /** Performs HTTP basic authentication (plaintext username/password). */
    private static class Basic extends HttpAuthMethod {
        private String user;

        private String pass;

        public Basic() {
            super(Type.BASIC);
        }

        @Override
        void authorize(String username, String password) {
            this.user = username;
            this.pass = password;
        }

        @Override
        void configureRequest(HttpConnection conn) throws IOException {
            String ident = user + ":" + pass; //$NON-NLS-1$
            String enc = Base64.encodeBytes(ident.getBytes(UTF_8));
            conn.setRequestProperty(HDR_AUTHORIZATION, type.getSchemeName() + " " + enc); //$NON-NLS-1$
        }
    }

    /** Performs HTTP digest authentication. */
    private static class Digest extends HttpAuthMethod {
        private static final SecureRandom PRNG = new SecureRandom();

        private final Map<String, String> params;

        private int requestCount;

        private String user;

        private String pass;

        Digest(String hdr) {
            super(Type.DIGEST);
            params = parse(hdr);

            final String qop = params.get("qop"); //$NON-NLS-1$
            if ("auth".equals(qop)) { //$NON-NLS-1$
                final byte[] bin = new byte[8];
                PRNG.nextBytes(bin);
                params.put("cnonce", Base64.encodeBytes(bin)); //$NON-NLS-1$
            }
        }

        @Override
        void authorize(String username, String password) {
            this.user = username;
            this.pass = password;
        }

        @SuppressWarnings("boxing")
        @Override
        void configureRequest(HttpConnection conn) throws IOException {
            final Map<String, String> r = new LinkedHashMap<>();

            final String realm = params.get("realm"); //$NON-NLS-1$
            final String nonce = params.get("nonce"); //$NON-NLS-1$
            final String cnonce = params.get("cnonce"); //$NON-NLS-1$
            final String uri = uri(conn.getURL());
            final String qop = params.get("qop"); //$NON-NLS-1$
            final String method = conn.getRequestMethod();

            final String A1 = user + ":" + realm + ":" + pass; //$NON-NLS-1$ //$NON-NLS-2$
            final String A2 = method + ":" + uri; //$NON-NLS-1$

            r.put("username", user); //$NON-NLS-1$
            r.put("realm", realm); //$NON-NLS-1$
            r.put("nonce", nonce); //$NON-NLS-1$
            r.put("uri", uri); //$NON-NLS-1$

            final String response, nc;
            if ("auth".equals(qop)) { //$NON-NLS-1$
                nc = String.format("%08x", ++requestCount); //$NON-NLS-1$
                response = KD(H(A1), nonce + ":" + nc + ":" + cnonce + ":" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                        + qop + ":" //$NON-NLS-1$
                        + H(A2));
            } else {
                nc = null;
                response = KD(H(A1), nonce + ":" + H(A2)); //$NON-NLS-1$
            }
            r.put("response", response); //$NON-NLS-1$
            if (params.containsKey("algorithm")) //$NON-NLS-1$
                r.put("algorithm", "MD5"); //$NON-NLS-1$ //$NON-NLS-2$
            if (cnonce != null && qop != null)
                r.put("cnonce", cnonce); //$NON-NLS-1$
            if (params.containsKey("opaque")) //$NON-NLS-1$
                r.put("opaque", params.get("opaque")); //$NON-NLS-1$ //$NON-NLS-2$
            if (qop != null)
                r.put("qop", qop); //$NON-NLS-1$
            if (nc != null)
                r.put("nc", nc); //$NON-NLS-1$

            StringBuilder v = new StringBuilder();
            for (Map.Entry<String, String> e : r.entrySet()) {
                if (v.length() > 0)
                    v.append(", "); //$NON-NLS-1$
                v.append(e.getKey());
                v.append('=');
                v.append('"');
                v.append(e.getValue());
                v.append('"');
            }
            conn.setRequestProperty(HDR_AUTHORIZATION, type.getSchemeName() + " " + v); //$NON-NLS-1$
        }

        private static String uri(URL u) {
            StringBuilder r = new StringBuilder();
            r.append(u.getProtocol());
            r.append("://"); //$NON-NLS-1$
            r.append(u.getHost());
            if (0 < u.getPort()) {
                if (u.getPort() == 80 && "http".equals(u.getProtocol())) { //$NON-NLS-1$
                    /* nothing */
                } else if (u.getPort() == 443 && "https".equals(u.getProtocol())) { //$NON-NLS-1$
                    /* nothing */
                } else {
                    r.append(':').append(u.getPort());
                }
            }
            r.append(u.getPath());
            if (u.getQuery() != null)
                r.append('?').append(u.getQuery());
            return r.toString();
        }

        private static String H(String data) {
            MessageDigest md = newMD5();
            md.update(data.getBytes(UTF_8));
            return LHEX(md.digest());
        }

        private static String KD(String secret, String data) {
            MessageDigest md = newMD5();
            md.update(secret.getBytes(UTF_8));
            md.update((byte) ':');
            md.update(data.getBytes(UTF_8));
            return LHEX(md.digest());
        }

        private static MessageDigest newMD5() {
            try {
                return MessageDigest.getInstance("MD5"); //$NON-NLS-1$
            } catch (NoSuchAlgorithmException e) {
                throw new RuntimeException("No MD5 available", e); //$NON-NLS-1$
            }
        }

        private static final char[] LHEX = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', //
                'a', 'b', 'c', 'd', 'e', 'f' };

        private static String LHEX(byte[] bin) {
            StringBuilder r = new StringBuilder(bin.length * 2);
            for (int i = 0; i < bin.length; i++) {
                byte b = bin[i];
                r.append(LHEX[(b >>> 4) & 0x0f]);
                r.append(LHEX[b & 0x0f]);
            }
            return r.toString();
        }

        private static Map<String, String> parse(String auth) {
            Map<String, String> p = new HashMap<>();
            int next = 0;
            while (next < auth.length()) {
                if (next < auth.length() && auth.charAt(next) == ',') {
                    next++;
                }
                while (next < auth.length() && Character.isWhitespace(auth.charAt(next))) {
                    next++;
                }

                int eq = auth.indexOf('=', next);
                if (eq < 0 || eq + 1 == auth.length()) {
                    return Collections.emptyMap();
                }

                final String name = auth.substring(next, eq);
                final String value;
                if (auth.charAt(eq + 1) == '"') {
                    int dq = auth.indexOf('"', eq + 2);
                    if (dq < 0) {
                        return Collections.emptyMap();
                    }
                    value = auth.substring(eq + 2, dq);
                    next = dq + 1;

                } else {
                    int space = auth.indexOf(' ', eq + 1);
                    int comma = auth.indexOf(',', eq + 1);
                    if (space < 0)
                        space = auth.length();
                    if (comma < 0)
                        comma = auth.length();

                    final int e = Math.min(space, comma);
                    value = auth.substring(eq + 1, e);
                    next = e + 1;
                }
                p.put(name, value);
            }
            return p;
        }
    }

    private static class Negotiate extends HttpAuthMethod {
        private static final GSSManagerFactory GSS_MANAGER_FACTORY = GSSManagerFactory.detect();

        private static final Oid OID;
        static {
            try {
                // OID for SPNEGO
                OID = new Oid("1.3.6.1.5.5.2"); //$NON-NLS-1$
            } catch (GSSException e) {
                throw new Error("Cannot create NEGOTIATE oid.", e); //$NON-NLS-1$
            }
        }

        private final byte[] prevToken;

        public Negotiate(String hdr) {
            super(Type.NEGOTIATE);
            prevToken = Base64.decode(hdr);
        }

        @Override
        void authorize(String user, String pass) {
            // not used
        }

        @Override
        void configureRequest(HttpConnection conn) throws IOException {
            GSSManager gssManager = GSS_MANAGER_FACTORY.newInstance(conn.getURL());
            String host = conn.getURL().getHost();
            String peerName = "HTTP@" + host.toLowerCase(Locale.ROOT); //$NON-NLS-1$
            try {
                GSSName gssName = gssManager.createName(peerName, GSSName.NT_HOSTBASED_SERVICE);
                GSSContext context = gssManager.createContext(gssName, OID, null, GSSContext.DEFAULT_LIFETIME);
                // Respect delegation policy in HTTP/SPNEGO.
                context.requestCredDeleg(true);

                byte[] token = context.initSecContext(prevToken, 0, prevToken.length);

                conn.setRequestProperty(HDR_AUTHORIZATION,
                        getType().getSchemeName() + " " + Base64.encodeBytes(token)); //$NON-NLS-1$
            } catch (GSSException e) {
                throw new IOException(e);
            }
        }
    }
}