Mercurial > hg4j
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 686:f1f095e42555 | 687:9859fcea475d |
|---|---|
| 1 /* | |
| 2 * Copyright (c) 2013 TMate Software Ltd | |
| 3 * | |
| 4 * This program is free software; you can redistribute it and/or modify | |
| 5 * it under the terms of the GNU General Public License as published by | |
| 6 * the Free Software Foundation; version 2 of the License. | |
| 7 * | |
| 8 * This program is distributed in the hope that it will be useful, | |
| 9 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 11 * GNU General Public License for more details. | |
| 12 * | |
| 13 * For information on how to redistribute this software under | |
| 14 * the terms of a license other than GNU General Public License | |
| 15 * contact TMate Software at support@hg4j.com | |
| 16 */ | |
| 17 package org.tmatesoft.hg.internal.remote; | |
| 18 | |
| 19 import static org.tmatesoft.hg.util.LogFacility.Severity.Info; | |
| 20 | |
| 21 import java.io.BufferedReader; | |
| 22 import java.io.FilterOutputStream; | |
| 23 import java.io.IOException; | |
| 24 import java.io.InputStream; | |
| 25 import java.io.InputStreamReader; | |
| 26 import java.io.OutputStream; | |
| 27 import java.net.HttpURLConnection; | |
| 28 import java.net.MalformedURLException; | |
| 29 import java.net.URL; | |
| 30 import java.net.URLConnection; | |
| 31 import java.security.cert.CertificateException; | |
| 32 import java.security.cert.X509Certificate; | |
| 33 import java.util.Collection; | |
| 34 import java.util.List; | |
| 35 import java.util.Map; | |
| 36 import java.util.prefs.BackingStoreException; | |
| 37 import java.util.prefs.Preferences; | |
| 38 | |
| 39 import javax.net.ssl.HttpsURLConnection; | |
| 40 import javax.net.ssl.SSLContext; | |
| 41 import javax.net.ssl.TrustManager; | |
| 42 import javax.net.ssl.X509TrustManager; | |
| 43 | |
| 44 import org.tmatesoft.hg.core.HgRemoteConnectionException; | |
| 45 import org.tmatesoft.hg.core.Nodeid; | |
| 46 import org.tmatesoft.hg.core.SessionContext; | |
| 47 import org.tmatesoft.hg.internal.PropertyMarshal; | |
| 48 import org.tmatesoft.hg.repo.HgRemoteRepository.Range; | |
| 49 import org.tmatesoft.hg.repo.HgRuntimeException; | |
| 50 | |
| 51 /** | |
| 52 * | |
| 53 * @author Artem Tikhomirov | |
| 54 * @author TMate Software Ltd. | |
| 55 */ | |
| 56 public class HttpConnector implements Connector { | |
| 57 private URL url; | |
| 58 private SSLContext sslContext; | |
| 59 private String authInfo; | |
| 60 private boolean debug; | |
| 61 private SessionContext sessionCtx; | |
| 62 // | |
| 63 private HttpURLConnection conn; | |
| 64 | |
| 65 public void init(URL url, SessionContext sessionContext, Object globalConfig) throws HgRuntimeException { | |
| 66 this.url = url; | |
| 67 sessionCtx = sessionContext; | |
| 68 debug = new PropertyMarshal(sessionCtx).getBoolean("hg4j.remote.debug", false); | |
| 69 if (url.getUserInfo() != null) { | |
| 70 String ai = null; | |
| 71 try { | |
| 72 // Hack to get Base64-encoded credentials | |
| 73 Preferences tempNode = Preferences.userRoot().node("xxx"); | |
| 74 tempNode.putByteArray("xxx", url.getUserInfo().getBytes()); | |
| 75 ai = tempNode.get("xxx", null); | |
| 76 tempNode.removeNode(); | |
| 77 } catch (BackingStoreException ex) { | |
| 78 sessionContext.getLog().dump(getClass(), Info, ex, null); | |
| 79 // IGNORE | |
| 80 } | |
| 81 authInfo = ai; | |
| 82 } else { | |
| 83 authInfo = null; | |
| 84 } | |
| 85 } | |
| 86 | |
| 87 public void connect() throws HgRemoteConnectionException, HgRuntimeException { | |
| 88 if ("https".equals(url.getProtocol())) { | |
| 89 try { | |
| 90 sslContext = SSLContext.getInstance("SSL"); | |
| 91 class TrustEveryone implements X509TrustManager { | |
| 92 public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { | |
| 93 if (debug) { | |
| 94 System.out.println("checkClientTrusted:" + authType); | |
| 95 } | |
| 96 } | |
| 97 public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { | |
| 98 if (debug) { | |
| 99 System.out.println("checkServerTrusted:" + authType); | |
| 100 } | |
| 101 } | |
| 102 public X509Certificate[] getAcceptedIssuers() { | |
| 103 return new X509Certificate[0]; | |
| 104 } | |
| 105 }; | |
| 106 sslContext.init(null, new TrustManager[] { new TrustEveryone() }, null); | |
| 107 } catch (Exception ex) { | |
| 108 throw new HgRemoteConnectionException("Can't initialize secure connection", ex); | |
| 109 } | |
| 110 } else { | |
| 111 sslContext = null; | |
| 112 } | |
| 113 } | |
| 114 | |
| 115 public void disconnect() throws HgRemoteConnectionException, HgRuntimeException { | |
| 116 // TODO Auto-generated method stub | |
| 117 | |
| 118 } | |
| 119 | |
| 120 public void sessionBegin() throws HgRemoteConnectionException, HgRuntimeException { | |
| 121 // TODO Auto-generated method stub | |
| 122 | |
| 123 } | |
| 124 | |
| 125 public void sessionEnd() throws HgRemoteConnectionException, HgRuntimeException { | |
| 126 if (conn != null) { | |
| 127 conn.disconnect(); | |
| 128 conn = null; | |
| 129 } | |
| 130 } | |
| 131 | |
| 132 public String getServerLocation() { | |
| 133 if (url.getUserInfo() == null) { | |
| 134 return url.toExternalForm(); | |
| 135 } | |
| 136 if (url.getPort() != -1) { | |
| 137 return String.format("%s://%s:%d%s", url.getProtocol(), url.getHost(), url.getPort(), url.getPath()); | |
| 138 } else { | |
| 139 return String.format("%s://%s%s", url.getProtocol(), url.getHost(), url.getPath()); | |
| 140 } | |
| 141 } | |
| 142 | |
| 143 public String getCapabilities() throws HgRemoteConnectionException { | |
| 144 // say hello to server, check response | |
| 145 try { | |
| 146 URL u = new URL(url, url.getPath() + "?cmd=hello"); | |
| 147 HttpURLConnection c = setupConnection(u.openConnection()); | |
| 148 c.connect(); | |
| 149 if (debug) { | |
| 150 dumpResponseHeader(u); | |
| 151 } | |
| 152 BufferedReader r = new BufferedReader(new InputStreamReader(c.getInputStream(), "US-ASCII")); | |
| 153 String line = r.readLine(); | |
| 154 c.disconnect(); | |
| 155 final String capsPrefix = CMD_CAPABILITIES + ':'; | |
| 156 if (line != null && line.startsWith(capsPrefix)) { | |
| 157 return line.substring(capsPrefix.length()).trim(); | |
| 158 } | |
| 159 // for whatever reason, some servers do not respond to hello command (e.g. svnkit) | |
| 160 // but respond to 'capabilities' instead. Try it. | |
| 161 // TODO [post-1.0] tests needed | |
| 162 u = new URL(url, url.getPath() + "?cmd=capabilities"); | |
| 163 c = setupConnection(u.openConnection()); | |
| 164 c.connect(); | |
| 165 if (debug) { | |
| 166 dumpResponseHeader(u); | |
| 167 } | |
| 168 r = new BufferedReader(new InputStreamReader(c.getInputStream(), "US-ASCII")); | |
| 169 line = r.readLine(); | |
| 170 c.disconnect(); | |
| 171 if (line != null && line.startsWith(capsPrefix)) { | |
| 172 return line.substring(capsPrefix.length()).trim(); | |
| 173 } | |
| 174 return new String(); | |
| 175 } catch (MalformedURLException ex) { | |
| 176 throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand(CMD_HELLO).setServerInfo(getServerLocation()); | |
| 177 } catch (IOException ex) { | |
| 178 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_HELLO).setServerInfo(getServerLocation()); | |
| 179 } | |
| 180 } | |
| 181 | |
| 182 public InputStream heads() throws HgRemoteConnectionException, HgRuntimeException { | |
| 183 try { | |
| 184 URL u = new URL(url, url.getPath() + "?cmd=heads"); | |
| 185 conn = setupConnection(u.openConnection()); | |
| 186 conn.connect(); | |
| 187 if (debug) { | |
| 188 dumpResponseHeader(u); | |
| 189 } | |
| 190 return conn.getInputStream(); | |
| 191 } catch (MalformedURLException ex) { | |
| 192 throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand(CMD_HEADS).setServerInfo(getServerLocation()); | |
| 193 } catch (IOException ex) { | |
| 194 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_HEADS).setServerInfo(getServerLocation()); | |
| 195 } | |
| 196 } | |
| 197 | |
| 198 public InputStream between(Collection<Range> ranges) throws HgRemoteConnectionException, HgRuntimeException { | |
| 199 StringBuilder sb = new StringBuilder(20 + ranges.size() * 82); | |
| 200 sb.append("pairs="); | |
| 201 for (Range r : ranges) { | |
| 202 r.append(sb); | |
| 203 sb.append('+'); | |
| 204 } | |
| 205 if (sb.charAt(sb.length() - 1) == '+') { | |
| 206 // strip last space | |
| 207 sb.setLength(sb.length() - 1); | |
| 208 } | |
| 209 try { | |
| 210 boolean usePOST = ranges.size() > 3; | |
| 211 URL u = new URL(url, url.getPath() + "?cmd=between" + (usePOST ? "" : '&' + sb.toString())); | |
| 212 conn = setupConnection(u.openConnection()); | |
| 213 if (usePOST) { | |
| 214 conn.setRequestMethod("POST"); | |
| 215 conn.setRequestProperty("Content-Length", String.valueOf(sb.length()/*nodeids are ASCII, bytes == characters */)); | |
| 216 conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); | |
| 217 conn.setDoOutput(true); | |
| 218 conn.connect(); | |
| 219 OutputStream os = conn.getOutputStream(); | |
| 220 os.write(sb.toString().getBytes()); | |
| 221 os.flush(); | |
| 222 os.close(); | |
| 223 } else { | |
| 224 conn.connect(); | |
| 225 } | |
| 226 if (debug) { | |
| 227 System.out.printf("%d ranges, method:%s \n", ranges.size(), conn.getRequestMethod()); | |
| 228 dumpResponseHeader(u); | |
| 229 } | |
| 230 return conn.getInputStream(); | |
| 231 } catch (MalformedURLException ex) { | |
| 232 throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand(CMD_BETWEEN).setServerInfo(getServerLocation()); | |
| 233 } catch (IOException ex) { | |
| 234 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_BETWEEN).setServerInfo(getServerLocation()); | |
| 235 } | |
| 236 } | |
| 237 | |
| 238 public InputStream branches(List<Nodeid> nodes) throws HgRemoteConnectionException, HgRuntimeException { | |
| 239 StringBuilder sb = appendNodeidListArgument("nodes", nodes, null); | |
| 240 try { | |
| 241 URL u = new URL(url, url.getPath() + "?cmd=branches&" + sb.toString()); | |
| 242 conn = setupConnection(u.openConnection()); | |
| 243 conn.connect(); | |
| 244 if (debug) { | |
| 245 dumpResponseHeader(u); | |
| 246 } | |
| 247 return conn.getInputStream(); | |
| 248 } catch (MalformedURLException ex) { | |
| 249 throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand(CMD_BRANCHES).setServerInfo(getServerLocation()); | |
| 250 } catch (IOException ex) { | |
| 251 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_BRANCHES).setServerInfo(getServerLocation()); | |
| 252 } | |
| 253 } | |
| 254 | |
| 255 public InputStream changegroup(List<Nodeid> roots) throws HgRemoteConnectionException, HgRuntimeException { | |
| 256 StringBuilder sb = appendNodeidListArgument("roots", roots, null); | |
| 257 try { | |
| 258 URL u = new URL(url, url.getPath() + "?cmd=changegroup&" + sb.toString()); | |
| 259 conn = setupConnection(u.openConnection()); | |
| 260 conn.connect(); | |
| 261 if (debug) { | |
| 262 dumpResponseHeader(u); | |
| 263 } | |
| 264 return conn.getInputStream(); | |
| 265 } catch (MalformedURLException ex) { // XXX in fact, this exception might be better to be re-thrown as RuntimeEx, | |
| 266 // as there's little user can do about this issue (URLs are constructed by our code) | |
| 267 throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("changegroup").setServerInfo(getServerLocation()); | |
| 268 } catch (IOException ex) { | |
| 269 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("changegroup").setServerInfo(getServerLocation()); | |
| 270 } | |
| 271 } | |
| 272 | |
| 273 // | |
| 274 // FIXME consider HttpURLConnection#setChunkedStreamingMode() as described at | |
| 275 // http://stackoverflow.com/questions/2793150/how-to-use-java-net-urlconnection-to-fire-and-handle-http-requests | |
| 276 public OutputStream unbundle(long outputLen, List<Nodeid> remoteHeads) throws HgRemoteConnectionException, HgRuntimeException { | |
| 277 StringBuilder sb = appendNodeidListArgument(CMD_HEADS, remoteHeads, null); | |
| 278 try { | |
| 279 final URL u = new URL(url, url.getPath() + "?cmd=unbundle&" + sb.toString()); | |
| 280 conn = setupConnection(u.openConnection()); | |
| 281 conn.setRequestMethod("POST"); | |
| 282 conn.setDoOutput(true); | |
| 283 conn.setRequestProperty("Content-Type", "application/mercurial-0.1"); | |
| 284 conn.setRequestProperty("Content-Length", String.valueOf(outputLen)); | |
| 285 conn.connect(); | |
| 286 return new FilterOutputStream(conn.getOutputStream()) { | |
| 287 public void close() throws IOException { | |
| 288 super.close(); | |
| 289 if (debug) { | |
| 290 dumpResponseHeader(u); | |
| 291 dumpResponse(); | |
| 292 } | |
| 293 try { | |
| 294 checkResponseOk("Push", CMD_UNBUNDLE); | |
| 295 } catch (HgRemoteConnectionException ex) { | |
| 296 IOException e = new IOException(ex.getMessage()); | |
| 297 // not e.initCause(ex); as HgRemoteConnectionException is just a message holder | |
| 298 e.setStackTrace(ex.getStackTrace()); | |
| 299 throw e; | |
| 300 } | |
| 301 } | |
| 302 }; | |
| 303 } catch (MalformedURLException ex) { | |
| 304 throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand(CMD_UNBUNDLE).setServerInfo(getServerLocation()); | |
| 305 } catch (IOException ex) { | |
| 306 // FIXME consume c.getErrorStream as http://docs.oracle.com/javase/6/docs/technotes/guides/net/http-keepalive.html suggests | |
| 307 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_UNBUNDLE).setServerInfo(getServerLocation()); | |
| 308 } | |
| 309 } | |
| 310 | |
| 311 public InputStream pushkey(String opName, String namespace, String key, String oldValue, String newValue) throws HgRemoteConnectionException, HgRuntimeException { | |
| 312 try { | |
| 313 final String p = String.format("%s?cmd=pushkey&namespace=%s&key=%s&old=%s&new=%s", url.getPath(), namespace, key, oldValue, newValue); | |
| 314 URL u = new URL(url, p); | |
| 315 conn = setupConnection(u.openConnection()); | |
| 316 conn.setRequestMethod("POST"); | |
| 317 conn.connect(); | |
| 318 if (debug) { | |
| 319 dumpResponseHeader(u); | |
| 320 } | |
| 321 checkResponseOk(opName, "pushkey"); | |
| 322 return conn.getInputStream(); | |
| 323 } catch (MalformedURLException ex) { | |
| 324 throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("pushkey").setServerInfo(getServerLocation()); | |
| 325 } catch (IOException ex) { | |
| 326 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("pushkey").setServerInfo(getServerLocation()); | |
| 327 } | |
| 328 } | |
| 329 | |
| 330 public InputStream listkeys(String namespace, String actionName) throws HgRemoteConnectionException, HgRuntimeException { | |
| 331 try { | |
| 332 URL u = new URL(url, url.getPath() + "?cmd=listkeys&namespace=" + namespace); | |
| 333 conn = setupConnection(u.openConnection()); | |
| 334 conn.connect(); | |
| 335 if (debug) { | |
| 336 dumpResponseHeader(u); | |
| 337 } | |
| 338 checkResponseOk(actionName, "listkeys"); | |
| 339 return conn.getInputStream(); | |
| 340 } catch (MalformedURLException ex) { | |
| 341 throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand(CMD_LISTKEYS).setServerInfo(getServerLocation()); | |
| 342 } catch (IOException ex) { | |
| 343 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_LISTKEYS).setServerInfo(getServerLocation()); | |
| 344 } | |
| 345 } | |
| 346 | |
| 347 private void checkResponseOk(String opName, String remoteCmd) throws HgRemoteConnectionException, IOException { | |
| 348 if (conn.getResponseCode() != 200) { | |
| 349 String m = conn.getResponseMessage() == null ? "unknown reason" : conn.getResponseMessage(); | |
| 350 String em = String.format("%s failed: %s (HTTP error:%d)", opName, m, conn.getResponseCode()); | |
| 351 throw new HgRemoteConnectionException(em).setRemoteCommand(remoteCmd).setServerInfo(getServerLocation()); | |
| 352 } | |
| 353 } | |
| 354 | |
| 355 private HttpURLConnection setupConnection(URLConnection urlConnection) { | |
| 356 urlConnection.setRequestProperty("User-Agent", "hg4j/1.0.0"); | |
| 357 urlConnection.addRequestProperty("Accept", "application/mercurial-0.1"); | |
| 358 if (authInfo != null) { | |
| 359 urlConnection.addRequestProperty("Authorization", "Basic " + authInfo); | |
| 360 } | |
| 361 if (sslContext != null) { | |
| 362 ((HttpsURLConnection) urlConnection).setSSLSocketFactory(sslContext.getSocketFactory()); | |
| 363 } | |
| 364 return (HttpURLConnection) urlConnection; | |
| 365 } | |
| 366 | |
| 367 private StringBuilder appendNodeidListArgument(String key, List<Nodeid> values, StringBuilder sb) { | |
| 368 if (sb == null) { | |
| 369 sb = new StringBuilder(20 + values.size() * 41); | |
| 370 } | |
| 371 sb.append(key); | |
| 372 sb.append('='); | |
| 373 for (Nodeid n : values) { | |
| 374 sb.append(n.toString()); | |
| 375 sb.append('+'); | |
| 376 } | |
| 377 if (sb.charAt(sb.length() - 1) == '+') { | |
| 378 // strip last space | |
| 379 sb.setLength(sb.length() - 1); | |
| 380 } | |
| 381 return sb; | |
| 382 } | |
| 383 | |
| 384 private void dumpResponseHeader(URL u) { | |
| 385 System.out.printf("Query (%d bytes):%s\n", u.getQuery().length(), u.getQuery()); | |
| 386 System.out.println("Response headers:"); | |
| 387 final Map<String, List<String>> headerFields = conn.getHeaderFields(); | |
| 388 for (String s : headerFields.keySet()) { | |
| 389 System.out.printf("%s: %s\n", s, conn.getHeaderField(s)); | |
| 390 } | |
| 391 } | |
| 392 | |
| 393 private void dumpResponse() throws IOException { | |
| 394 if (conn.getContentLength() > 0) { | |
| 395 final Object content = conn.getContent(); | |
| 396 System.out.println(content); | |
| 397 } | |
| 398 } | |
| 399 } |
