client side smart HTTP
[jgit.git] / org.eclipse.jgit / src / org / eclipse / jgit / transport / TransportHttp.java
blobb9dfd1c0e7e7fdc9cefe42684c2782edf86b7d86
1 /*
2 * Copyright (C) 2009-2010, Google Inc.
3 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
4 * and other copyright owners as documented in the project's IP log.
6 * This program and the accompanying materials are made available
7 * under the terms of the Eclipse Distribution License v1.0 which
8 * accompanies this distribution, is reproduced below, and is
9 * available at http://www.eclipse.org/org/documents/edl-v10.php
11 * All rights reserved.
13 * Redistribution and use in source and binary forms, with or
14 * without modification, are permitted provided that the following
15 * conditions are met:
17 * - Redistributions of source code must retain the above copyright
18 * notice, this list of conditions and the following disclaimer.
20 * - Redistributions in binary form must reproduce the above
21 * copyright notice, this list of conditions and the following
22 * disclaimer in the documentation and/or other materials provided
23 * with the distribution.
25 * - Neither the name of the Eclipse Foundation, Inc. nor the
26 * names of its contributors may be used to endorse or promote
27 * products derived from this software without specific prior
28 * written permission.
30 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
31 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
32 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
33 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
34 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
35 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
36 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
37 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
38 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
39 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
40 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
41 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
42 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
45 package org.eclipse.jgit.transport;
47 import static org.eclipse.jgit.util.HttpSupport.ENCODING_GZIP;
48 import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT;
49 import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT_ENCODING;
50 import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_ENCODING;
51 import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_TYPE;
52 import static org.eclipse.jgit.util.HttpSupport.HDR_PRAGMA;
53 import static org.eclipse.jgit.util.HttpSupport.HDR_USER_AGENT;
54 import static org.eclipse.jgit.util.HttpSupport.METHOD_POST;
56 import java.io.BufferedReader;
57 import java.io.ByteArrayInputStream;
58 import java.io.FileNotFoundException;
59 import java.io.IOException;
60 import java.io.InputStream;
61 import java.io.InputStreamReader;
62 import java.io.OutputStream;
63 import java.net.HttpURLConnection;
64 import java.net.MalformedURLException;
65 import java.net.Proxy;
66 import java.net.ProxySelector;
67 import java.net.URL;
68 import java.util.ArrayList;
69 import java.util.Collection;
70 import java.util.Map;
71 import java.util.Set;
72 import java.util.TreeMap;
73 import java.util.zip.GZIPInputStream;
74 import java.util.zip.GZIPOutputStream;
76 import org.eclipse.jgit.errors.NoRemoteRepositoryException;
77 import org.eclipse.jgit.errors.NotSupportedException;
78 import org.eclipse.jgit.errors.PackProtocolException;
79 import org.eclipse.jgit.errors.TransportException;
80 import org.eclipse.jgit.lib.Config;
81 import org.eclipse.jgit.lib.Constants;
82 import org.eclipse.jgit.lib.ObjectId;
83 import org.eclipse.jgit.lib.ProgressMonitor;
84 import org.eclipse.jgit.lib.Ref;
85 import org.eclipse.jgit.lib.Repository;
86 import org.eclipse.jgit.lib.Config.SectionParser;
87 import org.eclipse.jgit.util.HttpSupport;
88 import org.eclipse.jgit.util.IO;
89 import org.eclipse.jgit.util.RawParseUtils;
90 import org.eclipse.jgit.util.TemporaryBuffer;
91 import org.eclipse.jgit.util.io.DisabledOutputStream;
92 import org.eclipse.jgit.util.io.UnionInputStream;
94 /**
95 * Transport over HTTP and FTP protocols.
96 * <p>
97 * If the transport is using HTTP and the remote HTTP service is Git-aware
98 * (speaks the "smart-http protocol") this client will automatically take
99 * advantage of the additional Git-specific HTTP extensions. If the remote
100 * service does not support these extensions, the client will degrade to direct
101 * file fetching.
102 * <p>
103 * If the remote (server side) repository does not have the specialized Git
104 * support, object files are retrieved directly through standard HTTP GET (or
105 * binary FTP GET) requests. This make it easy to serve a Git repository through
106 * a standard web host provider that does not offer specific support for Git.
108 * @see WalkFetchConnection
110 public class TransportHttp extends HttpTransport implements WalkTransport,
111 PackTransport {
112 private static final String SVC_UPLOAD_PACK = "git-upload-pack";
114 private static final String SVC_RECEIVE_PACK = "git-receive-pack";
116 private static final String userAgent = computeUserAgent();
118 static boolean canHandle(final URIish uri) {
119 if (!uri.isRemote())
120 return false;
121 final String s = uri.getScheme();
122 return "http".equals(s) || "https".equals(s) || "ftp".equals(s);
125 private static String computeUserAgent() {
126 String version;
127 final Package pkg = TransportHttp.class.getPackage();
128 if (pkg != null && pkg.getImplementationVersion() != null) {
129 version = pkg.getImplementationVersion();
130 } else {
131 version = "unknown"; //$NON-NLS-1$
133 return "JGit/" + version; //$NON-NLS-1$
136 private static final Config.SectionParser<HttpConfig> HTTP_KEY = new SectionParser<HttpConfig>() {
137 public HttpConfig parse(final Config cfg) {
138 return new HttpConfig(cfg);
142 private static class HttpConfig {
143 final int postBuffer;
145 HttpConfig(final Config rc) {
146 postBuffer = rc.getInt("http", "postbuffer", 1 * 1024 * 1024);
150 private final URL baseUrl;
152 private final URL objectsUrl;
154 private final HttpConfig http;
156 private final ProxySelector proxySelector;
158 TransportHttp(final Repository local, final URIish uri)
159 throws NotSupportedException {
160 super(local, uri);
161 try {
162 String uriString = uri.toString();
163 if (!uriString.endsWith("/"))
164 uriString += "/";
165 baseUrl = new URL(uriString);
166 objectsUrl = new URL(baseUrl, "objects/");
167 } catch (MalformedURLException e) {
168 throw new NotSupportedException("Invalid URL " + uri, e);
170 http = local.getConfig().get(HTTP_KEY);
171 proxySelector = ProxySelector.getDefault();
174 @Override
175 public FetchConnection openFetch() throws TransportException,
176 NotSupportedException {
177 final String service = SVC_UPLOAD_PACK;
178 try {
179 final HttpURLConnection c = connect(service);
180 final InputStream in = openInputStream(c);
181 try {
182 if (isSmartHttp(c, service)) {
183 readSmartHeaders(in, service);
184 return new SmartHttpFetchConnection(in);
186 } else {
187 // Assume this server doesn't support smart HTTP fetch
188 // and fall back on dumb object walking.
190 HttpObjectDB d = new HttpObjectDB(objectsUrl);
191 WalkFetchConnection wfc = new WalkFetchConnection(this, d);
192 BufferedReader br = new BufferedReader(
193 new InputStreamReader(in, Constants.CHARSET));
194 try {
195 wfc.available(d.readAdvertisedImpl(br));
196 } finally {
197 br.close();
199 return wfc;
201 } finally {
202 in.close();
204 } catch (NotSupportedException err) {
205 throw err;
206 } catch (TransportException err) {
207 throw err;
208 } catch (IOException err) {
209 throw new TransportException(uri, "error reading info/refs", err);
213 @Override
214 public PushConnection openPush() throws NotSupportedException,
215 TransportException {
216 final String service = SVC_RECEIVE_PACK;
217 try {
218 final HttpURLConnection c = connect(service);
219 final InputStream in = openInputStream(c);
220 try {
221 if (isSmartHttp(c, service)) {
222 readSmartHeaders(in, service);
223 return new SmartHttpPushConnection(in);
225 } else {
226 final String msg = "remote does not support smart HTTP push";
227 throw new NotSupportedException(msg);
229 } finally {
230 in.close();
232 } catch (NotSupportedException err) {
233 throw err;
234 } catch (TransportException err) {
235 throw err;
236 } catch (IOException err) {
237 throw new TransportException(uri, "error reading info/refs", err);
241 @Override
242 public void close() {
243 // No explicit connections are maintained.
246 private HttpURLConnection connect(final String service)
247 throws TransportException, NotSupportedException {
248 final URL u;
249 try {
250 final StringBuilder b = new StringBuilder();
251 b.append(baseUrl);
253 if (b.charAt(b.length() - 1) != '/')
254 b.append('/');
255 b.append(Constants.INFO_REFS);
257 b.append(b.indexOf("?") < 0 ? '?' : '&');
258 b.append("service=");
259 b.append(service);
261 u = new URL(b.toString());
262 } catch (MalformedURLException e) {
263 throw new NotSupportedException("Invalid URL " + uri, e);
266 try {
267 final HttpURLConnection conn = httpOpen(u);
268 String expType = "application/x-" + service + "-advertisement";
269 conn.setRequestProperty(HDR_ACCEPT, expType + ", */*");
270 final int status = HttpSupport.response(conn);
271 switch (status) {
272 case HttpURLConnection.HTTP_OK:
273 return conn;
275 case HttpURLConnection.HTTP_NOT_FOUND:
276 throw new NoRemoteRepositoryException(uri, u + " not found");
278 case HttpURLConnection.HTTP_FORBIDDEN:
279 throw new TransportException(uri, service + " not permitted");
281 default:
282 String err = status + " " + conn.getResponseMessage();
283 throw new TransportException(uri, err);
285 } catch (NotSupportedException e) {
286 throw e;
287 } catch (TransportException e) {
288 throw e;
289 } catch (IOException e) {
290 throw new TransportException(uri, "cannot open " + service, e);
294 final HttpURLConnection httpOpen(final URL u) throws IOException {
295 final Proxy proxy = HttpSupport.proxyFor(proxySelector, u);
296 HttpURLConnection conn = (HttpURLConnection) u.openConnection(proxy);
297 conn.setRequestProperty(HDR_ACCEPT_ENCODING, ENCODING_GZIP);
298 conn.setRequestProperty(HDR_PRAGMA, "no-cache");//$NON-NLS-1$
299 conn.setRequestProperty(HDR_USER_AGENT, userAgent);
300 return conn;
303 final InputStream openInputStream(HttpURLConnection conn)
304 throws IOException {
305 InputStream input = conn.getInputStream();
306 if (ENCODING_GZIP.equals(conn.getHeaderField(HDR_CONTENT_ENCODING)))
307 input = new GZIPInputStream(input);
308 return input;
311 IOException wrongContentType(String expType, String actType) {
312 final String why = "expected Content-Type " + expType
313 + "; received Content-Type " + actType;
314 return new TransportException(uri, why);
317 private boolean isSmartHttp(final HttpURLConnection c, final String service) {
318 final String expType = "application/x-" + service + "-advertisement";
319 final String actType = c.getContentType();
320 return expType.equals(actType);
323 private void readSmartHeaders(final InputStream in, final String service)
324 throws IOException {
325 // A smart reply will have a '#' after the first 4 bytes, but
326 // a dumb reply cannot contain a '#' until after byte 41. Do a
327 // quick check to make sure its a smart reply before we parse
328 // as a pkt-line stream.
330 final byte[] magic = new byte[5];
331 IO.readFully(in, magic, 0, magic.length);
332 if (magic[4] != '#') {
333 throw new TransportException(uri, "expected pkt-line with"
334 + " '# service=', got '" + RawParseUtils.decode(magic)
335 + "'");
338 final PacketLineIn pckIn = new PacketLineIn(new UnionInputStream(
339 new ByteArrayInputStream(magic), in));
340 final String exp = "# service=" + service;
341 final String act = pckIn.readString();
342 if (!exp.equals(act)) {
343 throw new TransportException(uri, "expected '" + exp + "', got '"
344 + act + "'");
347 while (pckIn.readString() != PacketLineIn.END) {
348 // for now, ignore the remaining header lines
352 class HttpObjectDB extends WalkRemoteObjectDatabase {
353 private final URL objectsUrl;
355 HttpObjectDB(final URL b) {
356 objectsUrl = b;
359 @Override
360 URIish getURI() {
361 return new URIish(objectsUrl);
364 @Override
365 Collection<WalkRemoteObjectDatabase> getAlternates() throws IOException {
366 try {
367 return readAlternates(INFO_HTTP_ALTERNATES);
368 } catch (FileNotFoundException err) {
369 // Fall through.
372 try {
373 return readAlternates(INFO_ALTERNATES);
374 } catch (FileNotFoundException err) {
375 // Fall through.
378 return null;
381 @Override
382 WalkRemoteObjectDatabase openAlternate(final String location)
383 throws IOException {
384 return new HttpObjectDB(new URL(objectsUrl, location));
387 @Override
388 Collection<String> getPackNames() throws IOException {
389 final Collection<String> packs = new ArrayList<String>();
390 try {
391 final BufferedReader br = openReader(INFO_PACKS);
392 try {
393 for (;;) {
394 final String s = br.readLine();
395 if (s == null || s.length() == 0)
396 break;
397 if (!s.startsWith("P pack-") || !s.endsWith(".pack"))
398 throw invalidAdvertisement(s);
399 packs.add(s.substring(2));
401 return packs;
402 } finally {
403 br.close();
405 } catch (FileNotFoundException err) {
406 return packs;
410 @Override
411 FileStream open(final String path) throws IOException {
412 final URL base = objectsUrl;
413 final URL u = new URL(base, path);
414 final HttpURLConnection c = httpOpen(u);
415 switch (HttpSupport.response(c)) {
416 case HttpURLConnection.HTTP_OK:
417 final InputStream in = openInputStream(c);
418 final int len = c.getContentLength();
419 return new FileStream(in, len);
420 case HttpURLConnection.HTTP_NOT_FOUND:
421 throw new FileNotFoundException(u.toString());
422 default:
423 throw new IOException(u.toString() + ": "
424 + HttpSupport.response(c) + " "
425 + c.getResponseMessage());
429 Map<String, Ref> readAdvertisedImpl(final BufferedReader br)
430 throws IOException, PackProtocolException {
431 final TreeMap<String, Ref> avail = new TreeMap<String, Ref>();
432 for (;;) {
433 String line = br.readLine();
434 if (line == null)
435 break;
437 final int tab = line.indexOf('\t');
438 if (tab < 0)
439 throw invalidAdvertisement(line);
441 String name;
442 final ObjectId id;
444 name = line.substring(tab + 1);
445 id = ObjectId.fromString(line.substring(0, tab));
446 if (name.endsWith("^{}")) {
447 name = name.substring(0, name.length() - 3);
448 final Ref prior = avail.get(name);
449 if (prior == null)
450 throw outOfOrderAdvertisement(name);
452 if (prior.getPeeledObjectId() != null)
453 throw duplicateAdvertisement(name + "^{}");
455 avail.put(name, new Ref(Ref.Storage.NETWORK, name, prior
456 .getObjectId(), id, true));
457 } else {
458 final Ref prior = avail.put(name, new Ref(
459 Ref.Storage.NETWORK, name, id));
460 if (prior != null)
461 throw duplicateAdvertisement(name);
464 return avail;
467 private PackProtocolException outOfOrderAdvertisement(final String n) {
468 return new PackProtocolException("advertisement of " + n
469 + "^{} came before " + n);
472 private PackProtocolException invalidAdvertisement(final String n) {
473 return new PackProtocolException("invalid advertisement of " + n);
476 private PackProtocolException duplicateAdvertisement(final String n) {
477 return new PackProtocolException("duplicate advertisements of " + n);
480 @Override
481 void close() {
482 // We do not maintain persistent connections.
486 class SmartHttpFetchConnection extends BasePackFetchConnection {
487 SmartHttpFetchConnection(final InputStream advertisement)
488 throws TransportException {
489 super(TransportHttp.this);
490 statelessRPC = true;
492 init(advertisement, DisabledOutputStream.INSTANCE);
493 outNeedsEnd = false;
494 try {
495 readAdvertisedRefs();
496 } catch (IOException err) {
497 close();
498 throw new TransportException(uri, "remote hung up", err);
502 @Override
503 protected void doFetch(final ProgressMonitor monitor,
504 final Collection<Ref> want, final Set<ObjectId> have)
505 throws TransportException {
506 final Service svc = new Service(SVC_UPLOAD_PACK);
507 init(svc.in, svc.out);
508 super.doFetch(monitor, want, have);
512 class SmartHttpPushConnection extends BasePackPushConnection {
513 SmartHttpPushConnection(final InputStream advertisement)
514 throws TransportException {
515 super(TransportHttp.this);
516 statelessRPC = true;
518 init(advertisement, DisabledOutputStream.INSTANCE);
519 outNeedsEnd = false;
520 try {
521 readAdvertisedRefs();
522 } catch (IOException err) {
523 close();
524 throw new TransportException(uri, "remote hung up", err);
528 protected void doPush(final ProgressMonitor monitor,
529 final Map<String, RemoteRefUpdate> refUpdates)
530 throws TransportException {
531 final Service svc = new Service(SVC_RECEIVE_PACK);
532 init(svc.in, svc.out);
533 super.doPush(monitor, refUpdates);
538 * State required to speak multiple HTTP requests with the remote.
539 * <p>
540 * A service wrapper provides a normal looking InputStream and OutputStream
541 * pair which are connected via HTTP to the named remote service. Writing to
542 * the OutputStream is buffered until either the buffer overflows, or
543 * reading from the InputStream occurs. If overflow occurs HTTP/1.1 and its
544 * chunked transfer encoding is used to stream the request data to the
545 * remote service. If the entire request fits in the memory buffer, the
546 * older HTTP/1.0 standard and a fixed content length is used instead.
547 * <p>
548 * It is an error to attempt to read without there being outstanding data
549 * ready for transmission on the OutputStream.
550 * <p>
551 * No state is preserved between write-read request pairs. The caller is
552 * responsible for replaying state vector information as part of the request
553 * data written to the OutputStream. Any session HTTP cookies may or may not
554 * be preserved between requests, it is left up to the JVM's implementation
555 * of the HTTP client.
557 class Service {
558 private final String serviceName;
560 private final String requestType;
562 private final String responseType;
564 private final UnionInputStream httpIn;
566 final HttpInputStream in;
568 final HttpOutputStream out;
570 HttpURLConnection conn;
572 Service(final String serviceName) {
573 this.serviceName = serviceName;
574 this.requestType = "application/x-" + serviceName + "-request";
575 this.responseType = "application/x-" + serviceName + "-result";
577 this.httpIn = new UnionInputStream();
578 this.in = new HttpInputStream(httpIn);
579 this.out = new HttpOutputStream();
582 void openStream() throws IOException {
583 conn = httpOpen(new URL(baseUrl, serviceName));
584 conn.setRequestMethod(METHOD_POST);
585 conn.setInstanceFollowRedirects(false);
586 conn.setDoOutput(true);
587 conn.setRequestProperty(HDR_CONTENT_TYPE, requestType);
588 conn.setRequestProperty(HDR_ACCEPT, responseType);
591 void execute() throws IOException {
592 out.close();
594 if (conn == null) {
595 // Output hasn't started yet, because everything fit into
596 // our request buffer. Send with a Content-Length header.
598 if (out.length() == 0) {
599 throw new TransportException(uri, "Starting read stage"
600 + " without written request data pending"
601 + " is not supported");
604 // Try to compress the content, but only if that is smaller.
605 TemporaryBuffer buf = new TemporaryBuffer.Heap(http.postBuffer);
606 try {
607 GZIPOutputStream gzip = new GZIPOutputStream(buf);
608 out.writeTo(gzip, null);
609 gzip.close();
610 if (out.length() < buf.length())
611 buf = out;
612 } catch (IOException err) {
613 // Most likely caused by overflowing the buffer, meaning
614 // its larger if it were compressed. Don't compress.
615 buf = out;
618 openStream();
619 if (buf != out)
620 conn.setRequestProperty(HDR_CONTENT_ENCODING, ENCODING_GZIP);
621 conn.setFixedLengthStreamingMode((int) buf.length());
622 final OutputStream httpOut = conn.getOutputStream();
623 try {
624 buf.writeTo(httpOut, null);
625 } finally {
626 httpOut.close();
630 out.reset();
632 final int status = HttpSupport.response(conn);
633 if (status != HttpURLConnection.HTTP_OK) {
634 throw new TransportException(uri, status + " "
635 + conn.getResponseMessage());
638 final String contentType = conn.getContentType();
639 if (!responseType.equals(contentType)) {
640 conn.getInputStream().close();
641 throw wrongContentType(responseType, contentType);
644 httpIn.add(openInputStream(conn));
645 conn = null;
648 class HttpOutputStream extends TemporaryBuffer {
649 HttpOutputStream() {
650 super(http.postBuffer);
653 @Override
654 protected OutputStream overflow() throws IOException {
655 openStream();
656 conn.setChunkedStreamingMode(0);
657 return conn.getOutputStream();
661 class HttpInputStream extends InputStream {
662 private final UnionInputStream src;
664 HttpInputStream(UnionInputStream httpIn) {
665 this.src = httpIn;
668 private InputStream self() throws IOException {
669 if (src.isEmpty()) {
670 // If we have no InputStreams available it means we must
671 // have written data previously to the service, but have
672 // not yet finished the HTTP request in order to get the
673 // response from the service. Ensure we get it now.
675 execute();
677 return src;
680 public int available() throws IOException {
681 return self().available();
684 public int read() throws IOException {
685 return self().read();
688 public int read(byte[] b, int off, int len) throws IOException {
689 return self().read(b, off, len);
692 public long skip(long n) throws IOException {
693 return self().skip(n);
696 public void close() throws IOException {
697 src.close();