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
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
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
;
68 import java
.util
.ArrayList
;
69 import java
.util
.Collection
;
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
;
95 * Transport over HTTP and FTP protocols.
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
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
,
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
) {
121 final String s
= uri
.getScheme();
122 return "http".equals(s
) || "https".equals(s
) || "ftp".equals(s
);
125 private static String
computeUserAgent() {
127 final Package pkg
= TransportHttp
.class.getPackage();
128 if (pkg
!= null && pkg
.getImplementationVersion() != null) {
129 version
= pkg
.getImplementationVersion();
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
{
162 String uriString
= uri
.toString();
163 if (!uriString
.endsWith("/"))
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();
175 public FetchConnection
openFetch() throws TransportException
,
176 NotSupportedException
{
177 final String service
= SVC_UPLOAD_PACK
;
179 final HttpURLConnection c
= connect(service
);
180 final InputStream in
= openInputStream(c
);
182 if (isSmartHttp(c
, service
)) {
183 readSmartHeaders(in
, service
);
184 return new SmartHttpFetchConnection(in
);
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
));
195 wfc
.available(d
.readAdvertisedImpl(br
));
204 } catch (NotSupportedException err
) {
206 } catch (TransportException err
) {
208 } catch (IOException err
) {
209 throw new TransportException(uri
, "error reading info/refs", err
);
214 public PushConnection
openPush() throws NotSupportedException
,
216 final String service
= SVC_RECEIVE_PACK
;
218 final HttpURLConnection c
= connect(service
);
219 final InputStream in
= openInputStream(c
);
221 if (isSmartHttp(c
, service
)) {
222 readSmartHeaders(in
, service
);
223 return new SmartHttpPushConnection(in
);
226 final String msg
= "remote does not support smart HTTP push";
227 throw new NotSupportedException(msg
);
232 } catch (NotSupportedException err
) {
234 } catch (TransportException err
) {
236 } catch (IOException err
) {
237 throw new TransportException(uri
, "error reading info/refs", err
);
242 public void close() {
243 // No explicit connections are maintained.
246 private HttpURLConnection
connect(final String service
)
247 throws TransportException
, NotSupportedException
{
250 final StringBuilder b
= new StringBuilder();
253 if (b
.charAt(b
.length() - 1) != '/')
255 b
.append(Constants
.INFO_REFS
);
257 b
.append(b
.indexOf("?") < 0 ?
'?' : '&');
258 b
.append("service=");
261 u
= new URL(b
.toString());
262 } catch (MalformedURLException e
) {
263 throw new NotSupportedException("Invalid URL " + uri
, e
);
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
);
272 case HttpURLConnection
.HTTP_OK
:
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");
282 String err
= status
+ " " + conn
.getResponseMessage();
283 throw new TransportException(uri
, err
);
285 } catch (NotSupportedException e
) {
287 } catch (TransportException 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
);
303 final InputStream
openInputStream(HttpURLConnection conn
)
305 InputStream input
= conn
.getInputStream();
306 if (ENCODING_GZIP
.equals(conn
.getHeaderField(HDR_CONTENT_ENCODING
)))
307 input
= new GZIPInputStream(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
)
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
)
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 '"
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
) {
361 return new URIish(objectsUrl
);
365 Collection
<WalkRemoteObjectDatabase
> getAlternates() throws IOException
{
367 return readAlternates(INFO_HTTP_ALTERNATES
);
368 } catch (FileNotFoundException err
) {
373 return readAlternates(INFO_ALTERNATES
);
374 } catch (FileNotFoundException err
) {
382 WalkRemoteObjectDatabase
openAlternate(final String location
)
384 return new HttpObjectDB(new URL(objectsUrl
, location
));
388 Collection
<String
> getPackNames() throws IOException
{
389 final Collection
<String
> packs
= new ArrayList
<String
>();
391 final BufferedReader br
= openReader(INFO_PACKS
);
394 final String s
= br
.readLine();
395 if (s
== null || s
.length() == 0)
397 if (!s
.startsWith("P pack-") || !s
.endsWith(".pack"))
398 throw invalidAdvertisement(s
);
399 packs
.add(s
.substring(2));
405 } catch (FileNotFoundException err
) {
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());
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
>();
433 String line
= br
.readLine();
437 final int tab
= line
.indexOf('\t');
439 throw invalidAdvertisement(line
);
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
);
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));
458 final Ref prior
= avail
.put(name
, new Ref(
459 Ref
.Storage
.NETWORK
, name
, id
));
461 throw duplicateAdvertisement(name
);
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
);
482 // We do not maintain persistent connections.
486 class SmartHttpFetchConnection
extends BasePackFetchConnection
{
487 SmartHttpFetchConnection(final InputStream advertisement
)
488 throws TransportException
{
489 super(TransportHttp
.this);
492 init(advertisement
, DisabledOutputStream
.INSTANCE
);
495 readAdvertisedRefs();
496 } catch (IOException err
) {
498 throw new TransportException(uri
, "remote hung up", err
);
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);
518 init(advertisement
, DisabledOutputStream
.INSTANCE
);
521 readAdvertisedRefs();
522 } catch (IOException err
) {
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.
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.
548 * It is an error to attempt to read without there being outstanding data
549 * ready for transmission on the OutputStream.
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.
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
{
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
);
607 GZIPOutputStream gzip
= new GZIPOutputStream(buf
);
608 out
.writeTo(gzip
, null);
610 if (out
.length() < buf
.length())
612 } catch (IOException err
) {
613 // Most likely caused by overflowing the buffer, meaning
614 // its larger if it were compressed. Don't compress.
620 conn
.setRequestProperty(HDR_CONTENT_ENCODING
, ENCODING_GZIP
);
621 conn
.setFixedLengthStreamingMode((int) buf
.length());
622 final OutputStream httpOut
= conn
.getOutputStream();
624 buf
.writeTo(httpOut
, null);
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
));
648 class HttpOutputStream
extends TemporaryBuffer
{
650 super(http
.postBuffer
);
654 protected OutputStream
overflow() throws IOException
{
656 conn
.setChunkedStreamingMode(0);
657 return conn
.getOutputStream();
661 class HttpInputStream
extends InputStream
{
662 private final UnionInputStream src
;
664 HttpInputStream(UnionInputStream httpIn
) {
668 private InputStream
self() throws IOException
{
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.
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
{