View Javadoc
1   /*
2    * Copyright (C) 2009-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 java.nio.charset.StandardCharsets.UTF_8;
14  import static org.eclipse.jgit.util.HttpSupport.ENCODING_GZIP;
15  import static org.eclipse.jgit.util.HttpSupport.ENCODING_X_GZIP;
16  import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT_ENCODING;
17  import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_ENCODING;
18  import static org.eclipse.jgit.util.HttpSupport.HDR_ETAG;
19  import static org.eclipse.jgit.util.HttpSupport.TEXT_PLAIN;
20  
21  import java.io.ByteArrayOutputStream;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.security.MessageDigest;
26  import java.text.MessageFormat;
27  import java.util.zip.GZIPInputStream;
28  import java.util.zip.GZIPOutputStream;
29  
30  import javax.servlet.ServletRequest;
31  import javax.servlet.http.HttpServletRequest;
32  import javax.servlet.http.HttpServletResponse;
33  
34  import org.eclipse.jgit.lib.Constants;
35  import org.eclipse.jgit.lib.ObjectId;
36  import org.eclipse.jgit.lib.Repository;
37  
38  /**
39   * Common utility functions for servlets.
40   */
41  public final class ServletUtils {
42  	/** Request attribute which stores the {@link Repository} instance. */
43  	public static final String ATTRIBUTE_REPOSITORY = "org.eclipse.jgit.Repository";
44  
45  	/** Request attribute storing either UploadPack or ReceivePack. */
46  	public static final String ATTRIBUTE_HANDLER = "org.eclipse.jgit.transport.UploadPackOrReceivePack";
47  
48  	/**
49  	 * Get the selected repository from the request.
50  	 *
51  	 * @param req
52  	 *            the current request.
53  	 * @return the repository; never null.
54  	 * @throws IllegalStateException
55  	 *             the repository was not set by the filter, the servlet is
56  	 *             being invoked incorrectly and the programmer should ensure
57  	 *             the filter runs before the servlet.
58  	 * @see #ATTRIBUTE_REPOSITORY
59  	 */
60  	public static Repository getRepository(ServletRequest req) {
61  		Repository db = (Repository) req.getAttribute(ATTRIBUTE_REPOSITORY);
62  		if (db == null)
63  			throw new IllegalStateException(HttpServerText.get().expectedRepositoryAttribute);
64  		return db;
65  	}
66  
67  	/**
68  	 * Open the request input stream, automatically inflating if necessary.
69  	 * <p>
70  	 * This method automatically inflates the input stream if the request
71  	 * {@code Content-Encoding} header was set to {@code gzip} or the legacy
72  	 * {@code x-gzip}.
73  	 *
74  	 * @param req
75  	 *            the incoming request whose input stream needs to be opened.
76  	 * @return an input stream to read the raw, uncompressed request body.
77  	 * @throws IOException
78  	 *             if an input or output exception occurred.
79  	 */
80  	public static InputStream getInputStream(HttpServletRequest req)
81  			throws IOException {
82  		InputStream in = req.getInputStream();
83  		final String enc = req.getHeader(HDR_CONTENT_ENCODING);
84  		if (ENCODING_GZIP.equals(enc) || ENCODING_X_GZIP.equals(enc))
85  			in = new GZIPInputStream(in);
86  		else if (enc != null)
87  			throw new IOException(MessageFormat.format(HttpServerText.get().encodingNotSupportedByThisLibrary
88  					, HDR_CONTENT_ENCODING, enc));
89  		return in;
90  	}
91  
92  	/**
93  	 * Consume the entire request body, if one was supplied.
94  	 *
95  	 * @param req
96  	 *            the request whose body must be consumed.
97  	 */
98  	public static void consumeRequestBody(HttpServletRequest req) {
99  		if (0 < req.getContentLength() || isChunked(req)) {
100 			try {
101 				consumeRequestBody(req.getInputStream());
102 			} catch (IOException e) {
103 				// Ignore any errors obtaining the input stream.
104 			}
105 		}
106 	}
107 
108 	static boolean isChunked(HttpServletRequest req) {
109 		return "chunked".equals(req.getHeader("Transfer-Encoding"));
110 	}
111 
112 	/**
113 	 * Consume the rest of the input stream and discard it.
114 	 *
115 	 * @param in
116 	 *            the stream to discard, closed if not null.
117 	 */
118 	public static void consumeRequestBody(InputStream in) {
119 		if (in == null)
120 			return;
121 		try {
122 			while (0 < in.skip(2048) || 0 <= in.read()) {
123 				// Discard until EOF.
124 			}
125 		} catch (IOException err) {
126 			// Discard IOException during read or skip.
127 		} finally {
128 			try {
129 				in.close();
130 			} catch (IOException err) {
131 				// Discard IOException during close of input stream.
132 			}
133 		}
134 	}
135 
136 	/**
137 	 * Send a plain text response to a {@code GET} or {@code HEAD} HTTP request.
138 	 * <p>
139 	 * The text response is encoded in the Git character encoding, UTF-8.
140 	 * <p>
141 	 * If the user agent supports a compressed transfer encoding and the content
142 	 * is large enough, the content may be compressed before sending.
143 	 * <p>
144 	 * The {@code ETag} and {@code Content-Length} headers are automatically set
145 	 * by this method. {@code Content-Encoding} is conditionally set if the user
146 	 * agent supports a compressed transfer. Callers are responsible for setting
147 	 * any cache control headers.
148 	 *
149 	 * @param content
150 	 *            to return to the user agent as this entity's body.
151 	 * @param req
152 	 *            the incoming request.
153 	 * @param rsp
154 	 *            the outgoing response.
155 	 * @throws IOException
156 	 *             the servlet API rejected sending the body.
157 	 */
158 	public static void sendPlainText(final String content,
159 			final HttpServletRequest req, final HttpServletResponse rsp)
160 			throws IOException {
161 		final byte[] raw = content.getBytes(UTF_8);
162 		rsp.setContentType(TEXT_PLAIN);
163 		rsp.setCharacterEncoding(UTF_8.name());
164 		send(raw, req, rsp);
165 	}
166 
167 	/**
168 	 * Send a response to a {@code GET} or {@code HEAD} HTTP request.
169 	 * <p>
170 	 * If the user agent supports a compressed transfer encoding and the content
171 	 * is large enough, the content may be compressed before sending.
172 	 * <p>
173 	 * The {@code ETag} and {@code Content-Length} headers are automatically set
174 	 * by this method. {@code Content-Encoding} is conditionally set if the user
175 	 * agent supports a compressed transfer. Callers are responsible for setting
176 	 * {@code Content-Type} and any cache control headers.
177 	 *
178 	 * @param content
179 	 *            to return to the user agent as this entity's body.
180 	 * @param req
181 	 *            the incoming request.
182 	 * @param rsp
183 	 *            the outgoing response.
184 	 * @throws IOException
185 	 *             the servlet API rejected sending the body.
186 	 */
187 	public static void send(byte[] content, final HttpServletRequest req,
188 			final HttpServletResponse rsp) throws IOException {
189 		content = sendInit(content, req, rsp);
190 		try (OutputStream out = rsp.getOutputStream()) {
191 			out.write(content);
192 			out.flush();
193 		}
194 	}
195 
196 	private static byte[] sendInit(byte[] content,
197 			final HttpServletRequest req, final HttpServletResponse rsp)
198 			throws IOException {
199 		rsp.setHeader(HDR_ETAG, etag(content));
200 		if (256 < content.length && acceptsGzipEncoding(req)) {
201 			content = compress(content);
202 			rsp.setHeader(HDR_CONTENT_ENCODING, ENCODING_GZIP);
203 		}
204 		rsp.setContentLength(content.length);
205 		return content;
206 	}
207 
208 	static boolean acceptsGzipEncoding(HttpServletRequest req) {
209 		return acceptsGzipEncoding(req.getHeader(HDR_ACCEPT_ENCODING));
210 	}
211 
212 	static boolean acceptsGzipEncoding(String accepts) {
213 		if (accepts == null)
214 			return false;
215 
216 		int b = 0;
217 		while (b < accepts.length()) {
218 			int comma = accepts.indexOf(',', b);
219 			int e = 0 <= comma ? comma : accepts.length();
220 			String term = accepts.substring(b, e).trim();
221 			if (term.equals(ENCODING_GZIP))
222 				return true;
223 			b = e + 1;
224 		}
225 		return false;
226 	}
227 
228 	private static byte[] compress(byte[] raw) throws IOException {
229 		final int maxLen = raw.length + 32;
230 		final ByteArrayOutputStream out = new ByteArrayOutputStream(maxLen);
231 		final GZIPOutputStream gz = new GZIPOutputStream(out);
232 		gz.write(raw);
233 		gz.finish();
234 		gz.flush();
235 		return out.toByteArray();
236 	}
237 
238 	private static String etag(byte[] content) {
239 		final MessageDigest md = Constants.newMessageDigest();
240 		md.update(content);
241 		return ObjectId.fromRaw(md.digest()).getName();
242 	}
243 
244 	static String identify(Repository git) {
245 		String identifier = git.getIdentifier();
246 		if (identifier == null) {
247 			return "unknown";
248 		}
249 		return identifier;
250 	}
251 
252 	private ServletUtils() {
253 		// static utility class only
254 	}
255 }