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 SmartOutputStream(final HttpServletRequest req, final HttpServletResponse rsp, boolean compressStream)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 overflow()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 close()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 writeResponse(TemporaryBuffer out)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 } 107