diff options
author | Chris Aniszczyk <caniszczyk@gmail.com> | 2010-10-07 12:25:15 -0400 |
---|---|---|
committer | Code Review <codereview-daemon@eclipse.org> | 2010-10-07 12:25:15 -0400 |
commit | 7a6efe1dfcfbf0ddf4fa83e0d9d10fc9d4a3b649 (patch) | |
tree | 44a8f8896247c0e6e9bf85510c5508c8a2d1d18c | |
parent | 0a2b4c14559b1c94f9600394ccb79de1985c04d8 (diff) | |
parent | 858b2c92e8ffecbde9f80dd9d705518b8e6085f6 (diff) | |
download | jgit-7a6efe1dfcfbf0ddf4fa83e0d9d10fc9d4a3b649.tar.gz jgit-7a6efe1dfcfbf0ddf4fa83e0d9d10fc9d4a3b649.zip |
Merge "Support HTTP basic and digest authentication"
3 files changed, 373 insertions, 24 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpAuthMethod.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpAuthMethod.java new file mode 100644 index 0000000000..64c8bf0c74 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpAuthMethod.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2010, 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 org.eclipse.jgit.util.HttpSupport.HDR_AUTHORIZATION; +import static org.eclipse.jgit.util.HttpSupport.HDR_WWW_AUTHENTICATE; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +import org.eclipse.jgit.util.Base64; + +/** + * 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 { + /** No authentication is configured. */ + static final HttpAuthMethod NONE = new None(); + + /** + * Handle an authentication failure and possibly return a new response. + * + * @param conn + * the connection that failed. + * @return new authentication method to try. + */ + static HttpAuthMethod scanResponse(HttpURLConnection conn) { + String hdr = conn.getHeaderField(HDR_WWW_AUTHENTICATE); + if (hdr == null || hdr.length() == 0) + return NONE; + + int sp = hdr.indexOf(' '); + if (sp < 0) + return NONE; + + String type = hdr.substring(0, sp); + if (Basic.NAME.equals(type)) + return new Basic(); + else if (Digest.NAME.equals(type)) + return new Digest(hdr.substring(sp + 1)); + else + return NONE; + } + + /** + * Update this method with the credentials from the URIish. + * + * @param uri + * the URI used to create the connection. + */ + void authorize(URIish uri) { + authorize(uri.getUser(), uri.getPass()); + } + + /** + * 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(HttpURLConnection conn) throws IOException; + + /** Performs no user authentication. */ + private static class None extends HttpAuthMethod { + @Override + void authorize(String user, String pass) { + // Do nothing when no authentication is enabled. + } + + @Override + void configureRequest(HttpURLConnection conn) throws IOException { + // Do nothing when no authentication is enabled. + } + } + + /** Performs HTTP basic authentication (plaintext username/password). */ + private static class Basic extends HttpAuthMethod { + static final String NAME = "Basic"; + + private String user; + + private String pass; + + @Override + void authorize(final String username, final String password) { + this.user = username; + this.pass = password; + } + + @Override + void configureRequest(final HttpURLConnection conn) throws IOException { + String ident = user + ":" + pass; + String enc = Base64.encodeBytes(ident.getBytes("UTF-8")); + conn.setRequestProperty(HDR_AUTHORIZATION, NAME + " " + enc); + } + } + + /** Performs HTTP digest authentication. */ + private static class Digest extends HttpAuthMethod { + static final String NAME = "Digest"; + + private static final Random PRNG = new Random(); + + private final Map<String, String> params; + + private int requestCount; + + private String user; + + private String pass; + + Digest(String hdr) { + params = parse(hdr); + + final String qop = params.get("qop"); + if ("auth".equals(qop)) { + final byte[] bin = new byte[8]; + PRNG.nextBytes(bin); + params.put("cnonce", Base64.encodeBytes(bin)); + } + } + + @Override + void authorize(final String username, final String password) { + this.user = username; + this.pass = password; + } + + @SuppressWarnings("boxing") + @Override + void configureRequest(final HttpURLConnection conn) throws IOException { + final Map<String, String> p = new HashMap<String, String>(params); + p.put("username", user); + + final String realm = p.get("realm"); + final String nonce = p.get("nonce"); + final String uri = p.get("uri"); + final String qop = p.get("qop"); + final String method = conn.getRequestMethod(); + + final String A1 = user + ":" + realm + ":" + pass; + final String A2 = method + ":" + uri; + + final String expect; + if ("auth".equals(qop)) { + final String c = p.get("cnonce"); + final String nc = String.format("%8.8x", ++requestCount); + p.put("nc", nc); + expect = KD(H(A1), nonce + ":" + nc + ":" + c + ":" + qop + ":" + + H(A2)); + } else { + expect = KD(H(A1), nonce + ":" + H(A2)); + } + p.put("response", expect); + + StringBuilder v = new StringBuilder(); + for (Map.Entry<String, String> e : p.entrySet()) { + if (v.length() > 0) { + v.append(", "); + } + v.append(e.getKey()); + v.append('='); + v.append('"'); + v.append(e.getValue()); + v.append('"'); + } + conn.setRequestProperty(HDR_AUTHORIZATION, NAME + " " + v); + } + + private static String H(String data) { + try { + MessageDigest md = newMD5(); + md.update(data.getBytes("UTF-8")); + return LHEX(md.digest()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not available", e); + } + } + + private static String KD(String secret, String data) { + try { + MessageDigest md = newMD5(); + md.update(secret.getBytes("UTF-8")); + md.update((byte) ':'); + md.update(data.getBytes("UTF-8")); + return LHEX(md.digest()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not available", e); + } + } + + private static MessageDigest newMD5() { + try { + return MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("No MD5 available", e); + } + } + + 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<String, String>(); + 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; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java index 333f91bf09..dae7d0b995 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java @@ -51,6 +51,7 @@ import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_ENCODING; import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_TYPE; import static org.eclipse.jgit.util.HttpSupport.HDR_PRAGMA; import static org.eclipse.jgit.util.HttpSupport.HDR_USER_AGENT; +import static org.eclipse.jgit.util.HttpSupport.METHOD_GET; import static org.eclipse.jgit.util.HttpSupport.METHOD_POST; import java.io.BufferedReader; @@ -162,6 +163,8 @@ public class TransportHttp extends HttpTransport implements WalkTransport, private boolean useSmartHttp = true; + private HttpAuthMethod authMethod = HttpAuthMethod.NONE; + TransportHttp(final Repository local, final URIish uri) throws NotSupportedException { super(local, uri); @@ -341,27 +344,42 @@ public class TransportHttp extends HttpTransport implements WalkTransport, } try { - final HttpURLConnection conn = httpOpen(u); - if (useSmartHttp) { - String expType = "application/x-" + service + "-advertisement"; - conn.setRequestProperty(HDR_ACCEPT, expType + ", */*"); - } else { - conn.setRequestProperty(HDR_ACCEPT, "*/*"); - } - final int status = HttpSupport.response(conn); - switch (status) { - case HttpURLConnection.HTTP_OK: - return conn; - - case HttpURLConnection.HTTP_NOT_FOUND: - throw new NoRemoteRepositoryException(uri, MessageFormat.format(JGitText.get().URLNotFound, u)); - - case HttpURLConnection.HTTP_FORBIDDEN: - throw new TransportException(uri, MessageFormat.format(JGitText.get().serviceNotPermitted, service)); - - default: - String err = status + " " + conn.getResponseMessage(); - throw new TransportException(uri, err); + int authAttempts = 1; + for (;;) { + final HttpURLConnection conn = httpOpen(u); + if (useSmartHttp) { + String exp = "application/x-" + service + "-advertisement"; + conn.setRequestProperty(HDR_ACCEPT, exp + ", */*"); + } else { + conn.setRequestProperty(HDR_ACCEPT, "*/*"); + } + final int status = HttpSupport.response(conn); + switch (status) { + case HttpURLConnection.HTTP_OK: + return conn; + + case HttpURLConnection.HTTP_NOT_FOUND: + throw new NoRemoteRepositoryException(uri, u + " not found"); + + case HttpURLConnection.HTTP_UNAUTHORIZED: + authMethod = HttpAuthMethod.scanResponse(conn); + if (authMethod == HttpAuthMethod.NONE) + throw new TransportException(uri, + "authentication not supported"); + if (1 < authAttempts || uri.getUser() == null) + throw new TransportException(uri, "not authorized"); + authMethod.authorize(uri); + authAttempts++; + continue; + + case HttpURLConnection.HTTP_FORBIDDEN: + throw new TransportException(uri, service + + " not permitted"); + + default: + String err = status + " " + conn.getResponseMessage(); + throw new TransportException(uri, err); + } } } catch (NotSupportedException e) { throw e; @@ -372,15 +390,21 @@ public class TransportHttp extends HttpTransport implements WalkTransport, } } - final HttpURLConnection httpOpen(final URL u) throws IOException { + final HttpURLConnection httpOpen(URL u) throws IOException { + return httpOpen(METHOD_GET, u); + } + + final HttpURLConnection httpOpen(String method, URL u) throws IOException { final Proxy proxy = HttpSupport.proxyFor(proxySelector, u); HttpURLConnection conn = (HttpURLConnection) u.openConnection(proxy); + conn.setRequestMethod(method); conn.setUseCaches(false); conn.setRequestProperty(HDR_ACCEPT_ENCODING, ENCODING_GZIP); conn.setRequestProperty(HDR_PRAGMA, "no-cache");//$NON-NLS-1$ conn.setRequestProperty(HDR_USER_AGENT, userAgent); conn.setConnectTimeout(getTimeout() * 1000); conn.setReadTimeout(getTimeout() * 1000); + authMethod.configureRequest(conn); return conn; } @@ -652,8 +676,7 @@ public class TransportHttp extends HttpTransport implements WalkTransport, } void openStream() throws IOException { - conn = httpOpen(new URL(baseUrl, serviceName)); - conn.setRequestMethod(METHOD_POST); + conn = httpOpen(METHOD_POST, new URL(baseUrl, serviceName)); conn.setInstanceFollowRedirects(false); conn.setDoOutput(true); conn.setRequestProperty(HDR_CONTENT_TYPE, requestType); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java index d3e1f60035..5ac9552ed6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java @@ -59,6 +59,9 @@ import org.eclipse.jgit.JGitText; /** Extra utilities to support usage of HTTP. */ public class HttpSupport { + /** The {@code GET} HTTP method. */ + public static final String METHOD_GET = "GET"; + /** The {@code POST} HTTP method. */ public static final String METHOD_POST = "POST"; @@ -122,6 +125,12 @@ public class HttpSupport { /** The standard {@code text/plain} MIME type. */ public static final String TEXT_PLAIN = "text/plain"; + /** The {@code Authorization} header. */ + public static final String HDR_AUTHORIZATION = "Authorization"; + + /** The {@code WWW-Authenticate} header. */ + public static final String HDR_WWW_AUTHENTICATE = "WWW-Authenticate"; + /** * URL encode a value string into an output buffer. * |