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