Mercurial > jhg
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 } |