View Javadoc
1   /*
2    * Copyright (C) 2010, Google Inc. and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  
11  package org.eclipse.jgit.http.server;
12  
13  import static org.eclipse.jgit.http.server.ServletUtils.acceptsGzipEncoding;
14  import static org.eclipse.jgit.util.HttpSupport.ENCODING_GZIP;
15  import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_ENCODING;
16  
17  import java.io.IOException;
18  import java.io.OutputStream;
19  import java.util.zip.GZIPOutputStream;
20  
21  import javax.servlet.http.HttpServletRequest;
22  import javax.servlet.http.HttpServletResponse;
23  
24  import org.eclipse.jgit.util.TemporaryBuffer;
25  
26  /**
27   * Buffers a response, trying to gzip it if the user agent supports that.
28   * <p>
29   * If the response overflows the buffer, gzip is skipped and the response is
30   * streamed to the client as its produced, most likely using HTTP/1.1 chunked
31   * encoding. This is useful for servlets that produce mixed-mode content, where
32   * smaller payloads are primarily pure text that compresses well, while much
33   * larger payloads are heavily compressed binary data. {@link UploadPackServlet}
34   * is one such servlet.
35   */
36  class SmartOutputStream extends TemporaryBuffer {
37  	private static final int LIMIT = 32 * 1024;
38  
39  	private final HttpServletRequest req;
40  	private final HttpServletResponse rsp;
41  	private boolean compressStream;
42  	private boolean startedOutput;
43  
44  	SmartOutputStream(final HttpServletRequest req,
45  			final HttpServletResponse rsp,
46  			boolean compressStream) {
47  		super(LIMIT);
48  		this.req = req;
49  		this.rsp = rsp;
50  		this.compressStream = compressStream;
51  	}
52  
53  	/** {@inheritDoc} */
54  	@Override
55  	protected OutputStream overflow() throws IOException {
56  		startedOutput = true;
57  
58  		OutputStream out = rsp.getOutputStream();
59  		if (compressStream && acceptsGzipEncoding(req)) {
60  			rsp.setHeader(HDR_CONTENT_ENCODING, ENCODING_GZIP);
61  			out = new GZIPOutputStream(out);
62  		}
63  		return out;
64  	}
65  
66  	/** {@inheritDoc} */
67  	@Override
68  	public void close() throws IOException {
69  		super.close();
70  
71  		if (!startedOutput) {
72  			// If output hasn't started yet, the entire thing fit into our
73  			// buffer. Try to use a proper Content-Length header, and also
74  			// deflate the response with gzip if it will be smaller.
75  			if (256 < this.length() && acceptsGzipEncoding(req)) {
76  				TemporaryBuffer gzbuf = new TemporaryBuffer.Heap(LIMIT);
77  				try {
78  					try (GZIPOutputStream gzip = new GZIPOutputStream(gzbuf)) {
79  						this.writeTo(gzip, null);
80  					}
81  					if (gzbuf.length() < this.length()) {
82  						rsp.setHeader(HDR_CONTENT_ENCODING, ENCODING_GZIP);
83  						writeResponse(gzbuf);
84  						return;
85  					}
86  				} catch (IOException err) {
87  					// Most likely caused by overflowing the buffer, meaning
88  					// its larger if it were compressed. Discard compressed
89  					// copy and use the original.
90  				}
91  			}
92  			writeResponse(this);
93  		}
94  	}
95  
96  	private void writeResponse(TemporaryBuffer out) throws IOException {
97  		// The Content-Length cannot overflow when cast to an int, our
98  		// hardcoded LIMIT constant above assures us we wouldn't store
99  		// more than 2 GiB of content in memory.
100 		rsp.setContentLength((int) out.length());
101 		try (OutputStream os = rsp.getOutputStream()) {
102 			out.writeTo(os, null);
103 			os.flush();
104 		}
105 	}
106 }