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 }