Mercurial > hg4j
diff src/org/tmatesoft/hg/internal/remote/HttpConnector.java @ 687:9859fcea475d
Towards ssh remote repositories: refactor HgRemoteRepository - move http related code to HttpConnector
author | Artem Tikhomirov <tikhomirov.artem@gmail.com> |
---|---|
date | Sat, 27 Jul 2013 18:34:14 +0200 |
parents | |
children | 24f4efedc9d5 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/remote/HttpConnector.java Sat Jul 27 18:34:14 2013 +0200 @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2013 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.internal.remote; + +import static org.tmatesoft.hg.util.LogFacility.Severity.Info; + +import java.io.BufferedReader; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.tmatesoft.hg.core.HgRemoteConnectionException; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.core.SessionContext; +import org.tmatesoft.hg.internal.PropertyMarshal; +import org.tmatesoft.hg.repo.HgRemoteRepository.Range; +import org.tmatesoft.hg.repo.HgRuntimeException; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HttpConnector implements Connector { + private URL url; + private SSLContext sslContext; + private String authInfo; + private boolean debug; + private SessionContext sessionCtx; + // + private HttpURLConnection conn; + + public void init(URL url, SessionContext sessionContext, Object globalConfig) throws HgRuntimeException { + this.url = url; + sessionCtx = sessionContext; + debug = new PropertyMarshal(sessionCtx).getBoolean("hg4j.remote.debug", false); + if (url.getUserInfo() != null) { + String ai = null; + try { + // Hack to get Base64-encoded credentials + Preferences tempNode = Preferences.userRoot().node("xxx"); + tempNode.putByteArray("xxx", url.getUserInfo().getBytes()); + ai = tempNode.get("xxx", null); + tempNode.removeNode(); + } catch (BackingStoreException ex) { + sessionContext.getLog().dump(getClass(), Info, ex, null); + // IGNORE + } + authInfo = ai; + } else { + authInfo = null; + } + } + + public void connect() throws HgRemoteConnectionException, HgRuntimeException { + if ("https".equals(url.getProtocol())) { + try { + sslContext = SSLContext.getInstance("SSL"); + class TrustEveryone implements X509TrustManager { + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + if (debug) { + System.out.println("checkClientTrusted:" + authType); + } + } + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + if (debug) { + System.out.println("checkServerTrusted:" + authType); + } + } + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + sslContext.init(null, new TrustManager[] { new TrustEveryone() }, null); + } catch (Exception ex) { + throw new HgRemoteConnectionException("Can't initialize secure connection", ex); + } + } else { + sslContext = null; + } + } + + public void disconnect() throws HgRemoteConnectionException, HgRuntimeException { + // TODO Auto-generated method stub + + } + + public void sessionBegin() throws HgRemoteConnectionException, HgRuntimeException { + // TODO Auto-generated method stub + + } + + public void sessionEnd() throws HgRemoteConnectionException, HgRuntimeException { + if (conn != null) { + conn.disconnect(); + conn = null; + } + } + + public String getServerLocation() { + if (url.getUserInfo() == null) { + return url.toExternalForm(); + } + if (url.getPort() != -1) { + return String.format("%s://%s:%d%s", url.getProtocol(), url.getHost(), url.getPort(), url.getPath()); + } else { + return String.format("%s://%s%s", url.getProtocol(), url.getHost(), url.getPath()); + } + } + + public String getCapabilities() throws HgRemoteConnectionException { + // say hello to server, check response + try { + URL u = new URL(url, url.getPath() + "?cmd=hello"); + HttpURLConnection c = setupConnection(u.openConnection()); + c.connect(); + if (debug) { + dumpResponseHeader(u); + } + BufferedReader r = new BufferedReader(new InputStreamReader(c.getInputStream(), "US-ASCII")); + String line = r.readLine(); + c.disconnect(); + final String capsPrefix = CMD_CAPABILITIES + ':'; + if (line != null && line.startsWith(capsPrefix)) { + return line.substring(capsPrefix.length()).trim(); + } + // for whatever reason, some servers do not respond to hello command (e.g. svnkit) + // but respond to 'capabilities' instead. Try it. + // TODO [post-1.0] tests needed + u = new URL(url, url.getPath() + "?cmd=capabilities"); + c = setupConnection(u.openConnection()); + c.connect(); + if (debug) { + dumpResponseHeader(u); + } + r = new BufferedReader(new InputStreamReader(c.getInputStream(), "US-ASCII")); + line = r.readLine(); + c.disconnect(); + if (line != null && line.startsWith(capsPrefix)) { + return line.substring(capsPrefix.length()).trim(); + } + return new String(); + } catch (MalformedURLException ex) { + throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand(CMD_HELLO).setServerInfo(getServerLocation()); + } catch (IOException ex) { + throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_HELLO).setServerInfo(getServerLocation()); + } + } + + public InputStream heads() throws HgRemoteConnectionException, HgRuntimeException { + try { + URL u = new URL(url, url.getPath() + "?cmd=heads"); + conn = setupConnection(u.openConnection()); + conn.connect(); + if (debug) { + dumpResponseHeader(u); + } + return conn.getInputStream(); + } catch (MalformedURLException ex) { + throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand(CMD_HEADS).setServerInfo(getServerLocation()); + } catch (IOException ex) { + throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_HEADS).setServerInfo(getServerLocation()); + } + } + + public InputStream between(Collection<Range> ranges) throws HgRemoteConnectionException, HgRuntimeException { + StringBuilder sb = new StringBuilder(20 + ranges.size() * 82); + sb.append("pairs="); + for (Range r : ranges) { + r.append(sb); + sb.append('+'); + } + if (sb.charAt(sb.length() - 1) == '+') { + // strip last space + sb.setLength(sb.length() - 1); + } + try { + boolean usePOST = ranges.size() > 3; + URL u = new URL(url, url.getPath() + "?cmd=between" + (usePOST ? "" : '&' + sb.toString())); + conn = setupConnection(u.openConnection()); + if (usePOST) { + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Length", String.valueOf(sb.length()/*nodeids are ASCII, bytes == characters */)); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setDoOutput(true); + conn.connect(); + OutputStream os = conn.getOutputStream(); + os.write(sb.toString().getBytes()); + os.flush(); + os.close(); + } else { + conn.connect(); + } + if (debug) { + System.out.printf("%d ranges, method:%s \n", ranges.size(), conn.getRequestMethod()); + dumpResponseHeader(u); + } + return conn.getInputStream(); + } catch (MalformedURLException ex) { + throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand(CMD_BETWEEN).setServerInfo(getServerLocation()); + } catch (IOException ex) { + throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_BETWEEN).setServerInfo(getServerLocation()); + } + } + + public InputStream branches(List<Nodeid> nodes) throws HgRemoteConnectionException, HgRuntimeException { + StringBuilder sb = appendNodeidListArgument("nodes", nodes, null); + try { + URL u = new URL(url, url.getPath() + "?cmd=branches&" + sb.toString()); + conn = setupConnection(u.openConnection()); + conn.connect(); + if (debug) { + dumpResponseHeader(u); + } + return conn.getInputStream(); + } catch (MalformedURLException ex) { + throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand(CMD_BRANCHES).setServerInfo(getServerLocation()); + } catch (IOException ex) { + throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_BRANCHES).setServerInfo(getServerLocation()); + } + } + + public InputStream changegroup(List<Nodeid> roots) throws HgRemoteConnectionException, HgRuntimeException { + StringBuilder sb = appendNodeidListArgument("roots", roots, null); + try { + URL u = new URL(url, url.getPath() + "?cmd=changegroup&" + sb.toString()); + conn = setupConnection(u.openConnection()); + conn.connect(); + if (debug) { + dumpResponseHeader(u); + } + return conn.getInputStream(); + } catch (MalformedURLException ex) { // XXX in fact, this exception might be better to be re-thrown as RuntimeEx, + // as there's little user can do about this issue (URLs are constructed by our code) + throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("changegroup").setServerInfo(getServerLocation()); + } catch (IOException ex) { + throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("changegroup").setServerInfo(getServerLocation()); + } + } + + // + // FIXME consider HttpURLConnection#setChunkedStreamingMode() as described at + // http://stackoverflow.com/questions/2793150/how-to-use-java-net-urlconnection-to-fire-and-handle-http-requests + public OutputStream unbundle(long outputLen, List<Nodeid> remoteHeads) throws HgRemoteConnectionException, HgRuntimeException { + StringBuilder sb = appendNodeidListArgument(CMD_HEADS, remoteHeads, null); + try { + final URL u = new URL(url, url.getPath() + "?cmd=unbundle&" + sb.toString()); + conn = setupConnection(u.openConnection()); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "application/mercurial-0.1"); + conn.setRequestProperty("Content-Length", String.valueOf(outputLen)); + conn.connect(); + return new FilterOutputStream(conn.getOutputStream()) { + public void close() throws IOException { + super.close(); + if (debug) { + dumpResponseHeader(u); + dumpResponse(); + } + try { + checkResponseOk("Push", CMD_UNBUNDLE); + } catch (HgRemoteConnectionException ex) { + IOException e = new IOException(ex.getMessage()); + // not e.initCause(ex); as HgRemoteConnectionException is just a message holder + e.setStackTrace(ex.getStackTrace()); + throw e; + } + } + }; + } catch (MalformedURLException ex) { + throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand(CMD_UNBUNDLE).setServerInfo(getServerLocation()); + } catch (IOException ex) { + // FIXME consume c.getErrorStream as http://docs.oracle.com/javase/6/docs/technotes/guides/net/http-keepalive.html suggests + throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_UNBUNDLE).setServerInfo(getServerLocation()); + } + } + + public InputStream pushkey(String opName, String namespace, String key, String oldValue, String newValue) throws HgRemoteConnectionException, HgRuntimeException { + try { + final String p = String.format("%s?cmd=pushkey&namespace=%s&key=%s&old=%s&new=%s", url.getPath(), namespace, key, oldValue, newValue); + URL u = new URL(url, p); + conn = setupConnection(u.openConnection()); + conn.setRequestMethod("POST"); + conn.connect(); + if (debug) { + dumpResponseHeader(u); + } + checkResponseOk(opName, "pushkey"); + return conn.getInputStream(); + } catch (MalformedURLException ex) { + throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("pushkey").setServerInfo(getServerLocation()); + } catch (IOException ex) { + throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("pushkey").setServerInfo(getServerLocation()); + } + } + + public InputStream listkeys(String namespace, String actionName) throws HgRemoteConnectionException, HgRuntimeException { + try { + URL u = new URL(url, url.getPath() + "?cmd=listkeys&namespace=" + namespace); + conn = setupConnection(u.openConnection()); + conn.connect(); + if (debug) { + dumpResponseHeader(u); + } + checkResponseOk(actionName, "listkeys"); + return conn.getInputStream(); + } catch (MalformedURLException ex) { + throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand(CMD_LISTKEYS).setServerInfo(getServerLocation()); + } catch (IOException ex) { + throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_LISTKEYS).setServerInfo(getServerLocation()); + } + } + + private void checkResponseOk(String opName, String remoteCmd) throws HgRemoteConnectionException, IOException { + if (conn.getResponseCode() != 200) { + String m = conn.getResponseMessage() == null ? "unknown reason" : conn.getResponseMessage(); + String em = String.format("%s failed: %s (HTTP error:%d)", opName, m, conn.getResponseCode()); + throw new HgRemoteConnectionException(em).setRemoteCommand(remoteCmd).setServerInfo(getServerLocation()); + } + } + + private HttpURLConnection setupConnection(URLConnection urlConnection) { + urlConnection.setRequestProperty("User-Agent", "hg4j/1.0.0"); + urlConnection.addRequestProperty("Accept", "application/mercurial-0.1"); + if (authInfo != null) { + urlConnection.addRequestProperty("Authorization", "Basic " + authInfo); + } + if (sslContext != null) { + ((HttpsURLConnection) urlConnection).setSSLSocketFactory(sslContext.getSocketFactory()); + } + return (HttpURLConnection) urlConnection; + } + + private StringBuilder appendNodeidListArgument(String key, List<Nodeid> values, StringBuilder sb) { + if (sb == null) { + sb = new StringBuilder(20 + values.size() * 41); + } + sb.append(key); + sb.append('='); + for (Nodeid n : values) { + sb.append(n.toString()); + sb.append('+'); + } + if (sb.charAt(sb.length() - 1) == '+') { + // strip last space + sb.setLength(sb.length() - 1); + } + return sb; + } + + private void dumpResponseHeader(URL u) { + System.out.printf("Query (%d bytes):%s\n", u.getQuery().length(), u.getQuery()); + System.out.println("Response headers:"); + final Map<String, List<String>> headerFields = conn.getHeaderFields(); + for (String s : headerFields.keySet()) { + System.out.printf("%s: %s\n", s, conn.getHeaderField(s)); + } + } + + private void dumpResponse() throws IOException { + if (conn.getContentLength() > 0) { + final Object content = conn.getContent(); + System.out.println(content); + } + } +}