xref: /JGit/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ServletUtils.java (revision 5c5f7c6b146b24f2bd4afae1902df85ad6e57ea3)
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 	 */
getRepository(ServletRequest req)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 	 */
getInputStream(HttpServletRequest req)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 	 */
consumeRequestBody(HttpServletRequest req)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 
isChunked(HttpServletRequest req)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 	 */
consumeRequestBody(InputStream in)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 	 */
sendPlainText(final String content, final HttpServletRequest req, final HttpServletResponse rsp)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 	 */
send(byte[] content, final HttpServletRequest req, final HttpServletResponse rsp)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 
sendInit(byte[] content, final HttpServletRequest req, final HttpServletResponse rsp)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 
acceptsGzipEncoding(HttpServletRequest req)208 	static boolean acceptsGzipEncoding(HttpServletRequest req) {
209 		return acceptsGzipEncoding(req.getHeader(HDR_ACCEPT_ENCODING));
210 	}
211 
acceptsGzipEncoding(String accepts)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 
compress(byte[] raw)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 
etag(byte[] content)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 
identify(Repository git)244 	static String identify(Repository git) {
245 		String identifier = git.getIdentifier();
246 		if (identifier == null) {
247 			return "unknown";
248 		}
249 		return identifier;
250 	}
251 
ServletUtils()252 	private ServletUtils() {
253 		// static utility class only
254 	}
255 }
256