changeset 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 f1f095e42555
children 1499139a600a
files src/org/tmatesoft/hg/internal/remote/Connector.java src/org/tmatesoft/hg/internal/remote/HttpConnector.java src/org/tmatesoft/hg/internal/remote/SshConnector.java src/org/tmatesoft/hg/repo/HgRemoteRepository.java
diffstat 4 files changed, 619 insertions(+), 374 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/remote/Connector.java	Sat Jul 27 18:34:14 2013 +0200
@@ -0,0 +1,66 @@
+/*
+ * 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 java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.Collection;
+import java.util.List;
+
+import org.tmatesoft.hg.core.HgRemoteConnectionException;
+import org.tmatesoft.hg.core.Nodeid;
+import org.tmatesoft.hg.core.SessionContext;
+import org.tmatesoft.hg.repo.HgRuntimeException;
+import org.tmatesoft.hg.repo.HgRemoteRepository.Range;
+
+/**
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public interface Connector {
+	static final String CMD_HELLO = "hello"; // TODO enum
+	static final String CMD_CAPABILITIES = "capabilities";
+	static final String CMD_HEADS = "heads";
+	static final String CMD_BETWEEN = "between";
+	static final String CMD_BRANCHES = "branches";
+	static final String CMD_CHANGEGROUP = "changegroup";
+	static final String CMD_UNBUNDLE = "unbundle";
+	static final String CMD_PUSHKEY = "pushkey";
+	static final String CMD_LISTKEYS = "listkeys";
+	static final String NS_BOOKMARKS = "bookmarks";
+	static final String NS_PHASES = "phases";
+	
+	void init(URL url, SessionContext sessionContext, Object globalConfig) throws HgRuntimeException;
+	String getServerLocation();
+	//
+	void connect() throws HgRemoteConnectionException, HgRuntimeException;
+	void disconnect() throws HgRemoteConnectionException, HgRuntimeException;
+	void sessionBegin() throws HgRemoteConnectionException, HgRuntimeException;
+	void sessionEnd() throws HgRemoteConnectionException, HgRuntimeException;
+	// 
+	String getCapabilities() throws HgRemoteConnectionException, HgRuntimeException;
+
+	InputStream heads() throws HgRemoteConnectionException, HgRuntimeException;
+	InputStream between(Collection<Range> ranges) throws HgRemoteConnectionException, HgRuntimeException;
+	InputStream branches(List<Nodeid> nodes) throws HgRemoteConnectionException, HgRuntimeException;
+	InputStream changegroup(List<Nodeid> roots) throws HgRemoteConnectionException, HgRuntimeException;
+	OutputStream unbundle(long outputLen, List<Nodeid> remoteHeads) throws HgRemoteConnectionException, HgRuntimeException;
+	InputStream pushkey(String opName, String namespace, String key, String oldValue, String newValue) throws HgRemoteConnectionException, HgRuntimeException;
+	InputStream listkeys(String namespace, String actionName) throws HgRemoteConnectionException, HgRuntimeException;
+}
--- /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);
+		}
+	}
+}
--- a/src/org/tmatesoft/hg/internal/remote/SshConnector.java	Thu Jul 25 22:12:14 2013 +0200
+++ b/src/org/tmatesoft/hg/internal/remote/SshConnector.java	Sat Jul 27 18:34:14 2013 +0200
@@ -21,24 +21,20 @@
 import java.io.EOFException;
 import java.io.File;
 import java.io.FilterInputStream;
+import java.io.FilterOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.net.URL;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 
 import org.tmatesoft.hg.core.HgRemoteConnectionException;
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.core.SessionContext;
-import org.tmatesoft.hg.internal.Internals;
-import org.tmatesoft.hg.repo.HgBundle;
 import org.tmatesoft.hg.repo.HgRemoteRepository.Range;
 import org.tmatesoft.hg.repo.HgRuntimeException;
 import org.tmatesoft.hg.util.LogFacility.Severity;
@@ -54,7 +50,7 @@
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
  */
-public class SshConnector {
+public class SshConnector implements Connector {
 	private SessionContext sessionCtx;
 	private URL url;
 	private Connection conn;
@@ -64,9 +60,12 @@
 	private StreamGobbler remoteErr, remoteOut;
 	private OutputStream remoteIn;
 	
-	public void connect(URL url, SessionContext sessionContext, Object globalConfig) throws HgRemoteConnectionException {
+	public void init(URL url, SessionContext sessionContext, Object globalConfig) throws HgRuntimeException {
 		sessionCtx = sessionContext;
 		this.url = url;
+	}
+	
+	public void connect() throws HgRemoteConnectionException, HgRuntimeException {
 		try {
 			conn = new Connection(url.getHost(), url.getPort() == -1 ? 22 : url.getPort());
 			conn.connect();
@@ -78,7 +77,7 @@
 			ConnectionInfo ci = conn.getConnectionInfo();
 			System.out.printf("%s %s %s %d %s %s %s\n", ci.clientToServerCryptoAlgorithm, ci.clientToServerMACAlgorithm, ci.keyExchangeAlgorithm, ci.keyExchangeCounter, ci.serverHostKeyAlgorithm, ci.serverToClientCryptoAlgorithm, ci.serverToClientMACAlgorithm);
 		} catch (IOException ex) {
-			throw new HgRemoteConnectionException("Failed to authenticate", ex).setServerInfo(getLocation());
+			throw new HgRemoteConnectionException("Failed to authenticate", ex).setServerInfo(getServerLocation());
 		}
 	}
 	
@@ -119,8 +118,41 @@
 		forceSessionClose();
 	}
 
-	public String getLocation() {
-		return "";
+	public String getServerLocation() {
+		return ""; // FIXME
+	}
+	
+	public String getCapabilities() throws HgRemoteConnectionException {
+		try {
+			consume(remoteOut);
+			consume(remoteErr);
+			remoteIn.write(CMD_HELLO.getBytes());
+			remoteIn.write('\n');
+			remoteIn.write(CMD_CAPABILITIES.getBytes()); // see http connector for details
+			remoteIn.write('\n');
+			remoteIn.write(CMD_HEADS.getBytes());
+			remoteIn.write('\n');
+			checkError();
+			int responseLen = readResponseLength();
+			checkError();
+			FilterStream s = new FilterStream(remoteOut, responseLen);
+			BufferedReader r = new BufferedReader(new InputStreamReader(s));
+			String line;
+			while ((line = r.readLine()) != null) {
+				if (line.startsWith(CMD_CAPABILITIES) && line.length() > (CMD_CAPABILITIES.length()+1)) {
+					line = line.substring(CMD_CAPABILITIES.length());
+					if (line.charAt(0) == ':') {
+						return line.substring(CMD_CAPABILITIES.length() + 1);
+					}
+				}
+			}
+			r.close();
+			consume(remoteOut);
+			checkError();
+			return new String();
+		} catch (IOException ex) {
+			throw new HgRemoteConnectionException("Failed to initiate dialog with server", ex).setRemoteCommand(CMD_HELLO).setServerInfo(getServerLocation());
+		}
 	}
 
 	public InputStream heads() throws HgRemoteConnectionException {
@@ -148,10 +180,28 @@
 		return executeCommand("changegroup", Collections.singletonList(new Parameter("roots", l)));
 	}
 
-	public void unbundle(HgBundle bundle, List<Nodeid> remoteHeads) throws HgRemoteConnectionException, HgRuntimeException {
+	public OutputStream unbundle(long outputLen, List<Nodeid> remoteHeads) throws HgRemoteConnectionException, HgRuntimeException {
 		String l = join(remoteHeads, ' ');
-		Collections.singletonList(new Parameter("heads", l));
-		throw Internals.notImplemented();
+		try {
+			consume(remoteOut);
+			consume(remoteErr);
+			remoteIn.write(CMD_UNBUNDLE.getBytes());
+			remoteIn.write('\n');
+			writeParameters(Collections.singletonList(new Parameter("heads", l)));
+			checkError();
+			return new FilterOutputStream(remoteIn) {
+				@Override
+				public void close() throws IOException {
+					out.flush();
+					@SuppressWarnings("unused")
+					int responseLen = readResponseLength();
+					checkError();
+					// XXX perhaps, need to return responseLen to caller? 
+				}
+			};
+		} catch (IOException ex) {
+			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 {
@@ -167,70 +217,30 @@
 		return executeCommand("listkeys", Collections.singletonList(new Parameter("namespace", namespace)));
 	}
 	
-	
-	public Set<String> initCapabilities() throws HgRemoteConnectionException {
-		try {
-			final String CMD_CAPABILITIES = "capabilities";
-			final String CMD_HEADS = "heads";
-			final String CMD_HELLO = "hello";
-			consume(remoteOut);
-			consume(remoteErr);
-			remoteIn.write(CMD_HELLO.getBytes());
-			remoteIn.write('\n');
-			remoteIn.write(CMD_CAPABILITIES.getBytes()); // see http connector for
-			remoteIn.write('\n');
-			remoteIn.write(CMD_HEADS.getBytes());
-			remoteIn.write('\n');
-			checkError();
-			int responseLen = readResponseLength();
-			checkError();
-			FilterStream s = new FilterStream(remoteOut, responseLen);
-			BufferedReader r = new BufferedReader(new InputStreamReader(s));
-			String line;
-			while ((line = r.readLine()) != null) {
-				if (line.startsWith(CMD_CAPABILITIES) && line.length() > (CMD_CAPABILITIES.length()+1)) {
-					line = line.substring(CMD_CAPABILITIES.length());
-					if (line.charAt(0) == ':') {
-						String[] caps = line.substring(CMD_CAPABILITIES.length() + 1).split("\\s");
-						return new HashSet<String>(Arrays.asList(caps));
-					}
-				}
-			}
-			r.close();
-			consume(remoteOut);
-			checkError();
-			return Collections.emptySet();
-		} catch (IOException ex) {
-			throw new HgRemoteConnectionException("Failed to initiate dialog with server", ex).setRemoteCommand("hello").setServerInfo(getLocation());
-		} catch (HgRemoteConnectionException ex) {
-			ex.setRemoteCommand("hello").setServerInfo(getLocation());
-			throw ex;
-		}
-	}
-	
 	private InputStream executeCommand(String cmd, List<Parameter> parameters) throws HgRemoteConnectionException {
 		try {
 			consume(remoteOut);
 			consume(remoteErr);
 			remoteIn.write(cmd.getBytes());
 			remoteIn.write('\n');
-			for (Parameter p : parameters) {
-				remoteIn.write(p.name().getBytes());
-				remoteIn.write(' ');
-				remoteIn.write(String.valueOf(p.size()).getBytes());
-				remoteIn.write('\n');
-				remoteIn.write(p.data());
-				remoteIn.write('\n');
-			}
+			writeParameters(parameters);
 			checkError();
 			int responseLen = readResponseLength();
 			checkError();
 			return new FilterStream(remoteOut, responseLen);
 		} catch (IOException ex) {
-			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(cmd).setServerInfo(getLocation());
-		} catch (HgRemoteConnectionException ex) {
-			ex.setRemoteCommand(cmd).setServerInfo(getLocation());
-			throw ex;
+			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(cmd).setServerInfo(getServerLocation());
+		}
+	}
+	
+	private void writeParameters(List<Parameter> parameters) throws IOException {
+		for (Parameter p : parameters) {
+			remoteIn.write(p.name().getBytes());
+			remoteIn.write(' ');
+			remoteIn.write(String.valueOf(p.size()).getBytes());
+			remoteIn.write('\n');
+			remoteIn.write(p.data());
+			remoteIn.write('\n');
 		}
 	}
 
@@ -240,14 +250,14 @@
 		}
 	}
 
-	private void checkError() throws IOException, HgRemoteConnectionException {
+	private void checkError() throws IOException {
 		if (remoteErr.available() > 0) {
 			StringBuilder sb = new StringBuilder();
 			int c;
 			while ((c = remoteErr.read()) != -1) {
 				sb.append((char)c);
 			}
-			throw new HgRemoteConnectionException(sb.toString());
+			throw new IOException(sb.toString());
 		}
 	}
 	
--- a/src/org/tmatesoft/hg/repo/HgRemoteRepository.java	Thu Jul 25 22:12:14 2013 +0200
+++ b/src/org/tmatesoft/hg/repo/HgRemoteRepository.java	Sat Jul 27 18:34:14 2013 +0200
@@ -16,7 +16,7 @@
  */
 package org.tmatesoft.hg.repo;
 
-import static org.tmatesoft.hg.util.LogFacility.Severity.Info;
+import static org.tmatesoft.hg.internal.remote.Connector.*;
 import static org.tmatesoft.hg.util.Outcome.Kind.Failure;
 import static org.tmatesoft.hg.util.Outcome.Kind.Success;
 
@@ -31,12 +31,8 @@
 import java.io.StreamTokenizer;
 import java.net.ContentHandler;
 import java.net.ContentHandlerFactory;
-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.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -48,27 +44,23 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.prefs.BackingStoreException;
-import java.util.prefs.Preferences;
 import java.util.zip.InflaterInputStream;
 
-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.HgBadArgumentException;
 import org.tmatesoft.hg.core.HgIOException;
 import org.tmatesoft.hg.core.HgRemoteConnectionException;
 import org.tmatesoft.hg.core.HgRepositoryNotFoundException;
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.core.SessionContext;
+import org.tmatesoft.hg.internal.BundleSerializer;
 import org.tmatesoft.hg.internal.DataSerializer;
 import org.tmatesoft.hg.internal.DataSerializer.OutputStreamSerializer;
-import org.tmatesoft.hg.internal.BundleSerializer;
 import org.tmatesoft.hg.internal.EncodingHelper;
+import org.tmatesoft.hg.internal.FileUtils;
 import org.tmatesoft.hg.internal.Internals;
 import org.tmatesoft.hg.internal.PropertyMarshal;
+import org.tmatesoft.hg.internal.remote.Connector;
+import org.tmatesoft.hg.internal.remote.HttpConnector;
 import org.tmatesoft.hg.util.LogFacility.Severity;
 import org.tmatesoft.hg.util.Outcome;
 import org.tmatesoft.hg.util.Pair;
@@ -84,13 +76,11 @@
  */
 public class HgRemoteRepository implements SessionContext.Source {
 	
-	private final URL url;
-	private final SSLContext sslContext;
-	private final String authInfo;
 	private final boolean debug;
 	private HgLookup lookupHelper;
 	private final SessionContext sessionContext;
 	private Set<String> remoteCapabilities;
+	private Connector remote;
 	
 	static {
 		URLConnection.setContentHandlerFactory(new ContentHandlerFactory() {
@@ -123,50 +113,10 @@
 		if (url == null || ctx == null) {
 			throw new IllegalArgumentException();
 		}
-		this.url = url;
 		sessionContext = ctx;
 		debug = new PropertyMarshal(ctx).getBoolean("hg4j.remote.debug", false);
-		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 HgBadArgumentException("Can't initialize secure connection", ex);
-			}
-		} else {
-			sslContext = null;
-		}
-		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;
-		}
+		remote = new HttpConnector();
+		remote.init(url, ctx, null);
 	}
 	
 	public boolean isInvalid() throws HgRemoteConnectionException {
@@ -178,14 +128,7 @@
 	 * @return human-readable address of the server, without user credentials or any other security information
 	 */
 	public String getLocation() {
-		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());
-		}
+		return remote.getServerLocation();
 	}
 	
 	public SessionContext getSessionContext() {
@@ -193,15 +136,12 @@
 	}
 
 	public List<Nodeid> heads() throws HgRemoteConnectionException {
-		HttpURLConnection c = null;
+		if (isInvalid()) {
+			return Collections.emptyList();
+		}
 		try {
-			URL u = new URL(url, url.getPath() + "?cmd=heads");
-			c = setupConnection(u.openConnection());
-			c.connect();
-			if (debug) {
-				dumpResponseHeader(u, c);
-			}
-			InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII");
+			remote.sessionBegin();
+			InputStreamReader is = new InputStreamReader(remote.heads(), "US-ASCII");
 			StreamTokenizer st = new StreamTokenizer(is);
 			st.ordinaryChars('0', '9'); // wordChars performs |, hence need to 0 first
 			st.wordChars('0', '9');
@@ -211,14 +151,10 @@
 				parseResult.add(Nodeid.fromAscii(st.sval));
 			}
 			return parseResult;
-		} catch (MalformedURLException ex) {
-			throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("heads").setServerInfo(getLocation());
 		} catch (IOException ex) {
-			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("heads").setServerInfo(getLocation());
+			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_HEADS).setServerInfo(getLocation());
 		} finally {
-			if (c != null) {
-				c.disconnect();
-			}
+			remote.sessionEnd();
 		}
 	}
 	
@@ -234,44 +170,13 @@
 	 * @throws HgRemoteConnectionException 
 	 */
 	public Map<Range, List<Nodeid>> between(Collection<Range> ranges) throws HgRemoteConnectionException {
-		if (ranges.isEmpty()) {
+		if (ranges.isEmpty() || isInvalid()) {
 			return Collections.emptyMap();
 		}
-		// if fact, shall do other way round, this method shall send 
 		LinkedHashMap<Range, List<Nodeid>> rv = new LinkedHashMap<HgRemoteRepository.Range, List<Nodeid>>(ranges.size() * 4 / 3);
-		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);
-		}
-		HttpURLConnection c = null;
 		try {
-			boolean usePOST = ranges.size() > 3;
-			URL u = new URL(url, url.getPath() + "?cmd=between" + (usePOST ? "" : '&' + sb.toString()));
-			c = setupConnection(u.openConnection());
-			if (usePOST) {
-				c.setRequestMethod("POST");
-				c.setRequestProperty("Content-Length", String.valueOf(sb.length()/*nodeids are ASCII, bytes == characters */));
-				c.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
-				c.setDoOutput(true);
-				c.connect();
-				OutputStream os = c.getOutputStream();
-				os.write(sb.toString().getBytes());
-				os.flush();
-				os.close();
-			} else {
-				c.connect();
-			}
-			if (debug) {
-				System.out.printf("%d ranges, method:%s \n", ranges.size(), c.getRequestMethod());
-				dumpResponseHeader(u, c);
-			}
-			InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII");
+			remote.sessionBegin();
+			InputStreamReader is = new InputStreamReader(remote.between(ranges), "US-ASCII");
 			StreamTokenizer st = new StreamTokenizer(is);
 			st.ordinaryChars('0', '9');
 			st.wordChars('0', '9');
@@ -315,28 +220,20 @@
 			}
 			is.close();
 			return rv;
-		} catch (MalformedURLException ex) {
-			throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("between").setServerInfo(getLocation());
 		} catch (IOException ex) {
-			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("between").setServerInfo(getLocation());
+			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_BETWEEN).setServerInfo(getLocation());
 		} finally {
-			if (c != null) {
-				c.disconnect();
-			}
+			remote.sessionEnd();
 		}
 	}
 
 	public List<RemoteBranch> branches(List<Nodeid> nodes) throws HgRemoteConnectionException {
-		StringBuilder sb = appendNodeidListArgument("nodes", nodes, null);
-		HttpURLConnection c = null;
+		if (isInvalid()) {
+			return Collections.emptyList();
+		}
 		try {
-			URL u = new URL(url, url.getPath() + "?cmd=branches&" + sb.toString());
-			c = setupConnection(u.openConnection());
-			c.connect();
-			if (debug) {
-				dumpResponseHeader(u, c);
-			}
-			InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII");
+			remote.sessionBegin();
+			InputStreamReader is = new InputStreamReader(remote.branches(nodes), "US-ASCII");
 			StreamTokenizer st = new StreamTokenizer(is);
 			st.ordinaryChars('0', '9');
 			st.wordChars('0', '9');
@@ -354,14 +251,10 @@
 				rv.add(rb);
 			}
 			return rv;
-		} catch (MalformedURLException ex) {
-			throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("branches").setServerInfo(getLocation());
 		} catch (IOException ex) {
-			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("branches").setServerInfo(getLocation());
+			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_BRANCHES).setServerInfo(getLocation());
 		} finally {
-			if (c != null) {
-				c.disconnect();
-			}
+			remote.sessionEnd();
 		}
 	}
 
@@ -382,32 +275,23 @@
 	 * as one may expect according to http://mercurial.selenic.com/wiki/BundleFormat)
 	 */
 	public HgBundle getChanges(List<Nodeid> roots) throws HgRemoteConnectionException, HgRuntimeException {
+		if (isInvalid()) {
+			return null; // XXX valid retval???
+		}
 		List<Nodeid> _roots = roots.isEmpty() ? Collections.singletonList(Nodeid.NULL) : roots;
-		StringBuilder sb = appendNodeidListArgument("roots", _roots, null);
-		HttpURLConnection c = null;
 		try {
-			URL u = new URL(url, url.getPath() + "?cmd=changegroup&" + sb.toString());
-			c = setupConnection(u.openConnection());
-			c.connect();
+			remote.sessionBegin();
+			File tf = writeBundle(remote.changegroup(_roots), false, "HG10GZ" /*didn't see any other that zip*/);
 			if (debug) {
-				dumpResponseHeader(u, c);
-			}
-			File tf = writeBundle(c.getInputStream(), false, "HG10GZ" /*didn't see any other that zip*/);
-			if (debug) {
-				System.out.printf("Wrote bundle %s for roots %s\n", tf, sb);
+				System.out.printf("Wrote bundle %s for roots %s\n", tf, roots);
 			}
 			return getLookupHelper().loadBundle(tf);
-		} 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(getLocation());
 		} catch (IOException ex) {
-			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("changegroup").setServerInfo(getLocation());
+			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_CHANGEGROUP).setServerInfo(getLocation());
 		} catch (HgRepositoryNotFoundException ex) {
-			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("changegroup").setServerInfo(getLocation());
+			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_CHANGEGROUP).setServerInfo(getLocation());
 		} finally {
-			if (c != null) {
-				c.disconnect();
-			}
+			remote.sessionEnd();
 		}
 	}
 	
@@ -418,41 +302,33 @@
 			// or get from remote server???
 			throw Internals.notImplemented();
 		}
-		StringBuilder sb = appendNodeidListArgument("heads", remoteHeads, null);
-		
-		HttpURLConnection c = null;
+		if (isInvalid()) {
+			return;
+		}
 		DataSerializer.DataSource bundleData = BundleSerializer.newInstance(sessionContext, bundle);
+		OutputStream os = null;
 		try {
-			URL u = new URL(url, url.getPath() + "?cmd=unbundle&" + sb.toString());
-			c = setupConnection(u.openConnection());
-			c.setRequestMethod("POST");
-			c.setRequestProperty("Content-Length", String.valueOf(bundleData.serializeLength()));
-			c.setRequestProperty("Content-Type", "application/mercurial-0.1");
-			c.setDoOutput(true);
-			c.connect();
-			OutputStream os = c.getOutputStream();
+			remote.sessionBegin();
+			os = remote.unbundle(bundleData.serializeLength(), remoteHeads);
 			bundleData.serialize(new OutputStreamSerializer(os));
 			os.flush();
 			os.close();
-			if (debug) {
-				dumpResponseHeader(u, c);
-				dumpResponse(c);
-			}
-			checkResponseOk(c, "Push", "unbundle");
-		} catch (MalformedURLException ex) {
-			throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("unbundle").setServerInfo(getLocation());
+			os = null;
 		} catch (IOException ex) {
 			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("unbundle").setServerInfo(getLocation());
 		} catch (HgIOException ex) {
 			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("unbundle").setServerInfo(getLocation());
 		} finally {
-			if (c != null) {
-				c.disconnect();
-			}
+			new FileUtils(sessionContext.getLog(), this).closeQuietly(os);
+			remote.sessionEnd();
 		}
 	}
 
 	public Bookmarks getBookmarks() throws HgRemoteConnectionException, HgRuntimeException {
+		initCapabilities();
+		if (!remoteCapabilities.contains(CMD_PUSHKEY)) { // (sic!) listkeys is available when pushkey in caps
+			return new Bookmarks(Collections.<Pair<String, Nodeid>>emptyList());
+		}
 		final String actionName = "Get remote bookmarks";
 		final List<Pair<String, String>> values = listkeys("bookmarks", actionName);
 		ArrayList<Pair<String, Nodeid>> rv = new ArrayList<Pair<String, Nodeid>>();
@@ -470,10 +346,10 @@
 
 	public Outcome updateBookmark(String name, Nodeid oldRev, Nodeid newRev) throws HgRemoteConnectionException, HgRuntimeException {
 		initCapabilities();
-		if (!remoteCapabilities.contains("pushkey")) {
+		if (!remoteCapabilities.contains(CMD_PUSHKEY)) {
 			return new Outcome(Failure, "Server doesn't support pushkey protocol");
 		}
-		if (pushkey("Update remote bookmark", "bookmarks", name, oldRev.toString(), newRev.toString())) {
+		if (pushkey("Update remote bookmark", NS_BOOKMARKS, name, oldRev.toString(), newRev.toString())) {
 			return new Outcome(Success, String.format("Bookmark %s updated to %s", name, newRev.shortNotation()));
 		}
 		return new Outcome(Failure, String.format("Bookmark update (%s: %s -> %s) failed", name, oldRev.shortNotation(), newRev.shortNotation()));
@@ -481,11 +357,11 @@
 	
 	public Phases getPhases() throws HgRemoteConnectionException, HgRuntimeException {
 		initCapabilities();
-		if (!remoteCapabilities.contains("pushkey")) {
+		if (!remoteCapabilities.contains(CMD_PUSHKEY)) {
 			// old server defaults to publishing
 			return new Phases(true, Collections.<Nodeid>emptyList());
 		}
-		final List<Pair<String, String>> values = listkeys("phases", "Get remote phases");
+		final List<Pair<String, String>> values = listkeys(NS_PHASES, "Get remote phases");
 		boolean publishing = false;
 		ArrayList<Nodeid> draftRoots = new ArrayList<Nodeid>();
 		for (Pair<String, String> l : values) {
@@ -507,10 +383,10 @@
 	
 	public Outcome updatePhase(HgPhase from, HgPhase to, Nodeid n) throws HgRemoteConnectionException, HgRuntimeException {
 		initCapabilities();
-		if (!remoteCapabilities.contains("pushkey")) {
+		if (!remoteCapabilities.contains(CMD_PUSHKEY)) {
 			return new Outcome(Failure, "Server doesn't support pushkey protocol");
 		}
-		if (pushkey("Update remote phases", "phases", n.toString(), String.valueOf(from.mercurialOrdinal()), String.valueOf(to.mercurialOrdinal()))) {
+		if (pushkey("Update remote phases", NS_PHASES, n.toString(), String.valueOf(from.mercurialOrdinal()), String.valueOf(to.mercurialOrdinal()))) {
 			return new Outcome(Success, String.format("Phase of %s updated to %s", n.shortNotation(), to.name()));
 		}
 		return new Outcome(Failure, String.format("Phase update (%s: %s -> %s) failed", n.shortNotation(), from.name(), to.name()));
@@ -523,47 +399,17 @@
 	
 	
 	private void initCapabilities() throws HgRemoteConnectionException {
-		if (remoteCapabilities == null) {
-			remoteCapabilities = new HashSet<String>();
-			// 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, c);
-				}
-				BufferedReader r = new BufferedReader(new InputStreamReader(c.getInputStream(), "US-ASCII"));
-				String line = r.readLine();
-				c.disconnect();
-				final String capsPrefix = "capabilities:";
-				if (line == null || !line.startsWith(capsPrefix)) {
-					// 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, c);
-					}
-					r = new BufferedReader(new InputStreamReader(c.getInputStream(), "US-ASCII"));
-					line = r.readLine();
-					c.disconnect();
-					if (line == null || line.trim().length() == 0) {
-						return;
-					}
-				} else {
-					line = line.substring(capsPrefix.length()).trim();
-				}
-				String[] caps = line.split("\\s");
-				remoteCapabilities.addAll(Arrays.asList(caps));
-				c.disconnect();
-			} catch (MalformedURLException ex) {
-				throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("hello").setServerInfo(getLocation());
-			} catch (IOException ex) {
-				throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("hello").setServerInfo(getLocation());
-			}
+		if (remoteCapabilities != null) {
+			return;
+		}
+		remote.connect();
+		try {
+			remote.sessionBegin();
+			String capsLine = remote.getCapabilities();
+			String[] caps = capsLine.split("\\s");
+			remoteCapabilities = new HashSet<String>(Arrays.asList(caps));
+		} finally {
+			remote.sessionEnd();
 		}
 	}
 
@@ -575,18 +421,12 @@
 	}
 
 	private List<Pair<String,String>> listkeys(String namespace, String actionName) throws HgRemoteConnectionException, HgRuntimeException {
-		HttpURLConnection c = null;
 		try {
-			URL u = new URL(url, url.getPath() + "?cmd=listkeys&namespace=" + namespace);
-			c = setupConnection(u.openConnection());
-			c.connect();
-			if (debug) {
-				dumpResponseHeader(u, c);
-			}
-			checkResponseOk(c, actionName, "listkeys");
+			remote.sessionBegin();
 			ArrayList<Pair<String, String>> rv = new ArrayList<Pair<String, String>>();
+			InputStream response = remote.listkeys(namespace, actionName);
 			// output of listkeys is encoded with UTF-8
-			BufferedReader r = new BufferedReader(new InputStreamReader(c.getInputStream(), EncodingHelper.getUTF8()));
+			BufferedReader r = new BufferedReader(new InputStreamReader(response, EncodingHelper.getUTF8()));
 			String l;
 			while ((l = r.readLine()) != null) {
 				int sep = l.indexOf('\t');
@@ -598,94 +438,24 @@
 			}
 			r.close();
 			return rv;
-		} catch (MalformedURLException ex) {
-			throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("listkeys").setServerInfo(getLocation());
 		} catch (IOException ex) {
-			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("listkeys").setServerInfo(getLocation());
+			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_LISTKEYS).setServerInfo(getLocation());
 		} finally {
-			if (c != null) {
-				c.disconnect();
-			}
+			remote.sessionEnd();
 		}
 	}
 	
 	private boolean pushkey(String opName, String namespace, String key, String oldValue, String newValue) throws HgRemoteConnectionException, HgRuntimeException {
-		HttpURLConnection c = null;
 		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);
-			c = setupConnection(u.openConnection());
-			c.setRequestMethod("POST");
-			c.connect();
-			if (debug) {
-				dumpResponseHeader(u, c);
-			}
-			checkResponseOk(c, opName, "pushkey");
-			final InputStream is = c.getInputStream();
+			remote.sessionBegin();
+			final InputStream is = remote.pushkey(opName, namespace, key, oldValue, newValue);
 			int rv = is.read();
 			is.close();
 			return rv == '1';
-		} catch (MalformedURLException ex) {
-			throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("pushkey").setServerInfo(getLocation());
 		} catch (IOException ex) {
-			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("pushkey").setServerInfo(getLocation());
+			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_PUSHKEY).setServerInfo(getLocation());
 		} finally {
-			if (c != null) {
-				c.disconnect();
-			}
-		}
-	}
-	
-	private void checkResponseOk(HttpURLConnection c, String opName, String remoteCmd) throws HgRemoteConnectionException, IOException {
-		if (c.getResponseCode() != 200) {
-			String m = c.getResponseMessage() == null ? "unknown reason" : c.getResponseMessage();
-			String em = String.format("%s failed: %s (HTTP error:%d)", opName, m, c.getResponseCode());
-			throw new HgRemoteConnectionException(em).setRemoteCommand(remoteCmd).setServerInfo(getLocation());
-		}
-	}
-
-	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, HttpURLConnection c) {
-		System.out.printf("Query (%d bytes):%s\n", u.getQuery().length(), u.getQuery());
-		System.out.println("Response headers:");
-		final Map<String, List<String>> headerFields = c.getHeaderFields();
-		for (String s : headerFields.keySet()) {
-			System.out.printf("%s: %s\n", s, c.getHeaderField(s));
-		}
-	}
-	
-	private void dumpResponse(HttpURLConnection c) throws IOException {
-		if (c.getContentLength() > 0) {
-			final Object content = c.getContent();
-			System.out.println(content);
+			remote.sessionEnd();
 		}
 	}