1 /*
2 * Copyright (C) 2009-2010, Google Inc.
3 * and other copyright owners as documented in the project's IP log.
4 *
5 * This program and the accompanying materials are made available
6 * under the terms of the Eclipse Distribution License v1.0 which
7 * accompanies this distribution, is reproduced below, and is
8 * available at http://www.eclipse.org/org/documents/edl-v10.php
9 *
10 * All rights reserved.
11 *
12 * Redistribution and use in source and binary forms, with or
13 * without modification, are permitted provided that the following
14 * conditions are met:
15 *
16 * - Redistributions of source code must retain the above copyright
17 * notice, this list of conditions and the following disclaimer.
18 *
19 * - Redistributions in binary form must reproduce the above
20 * copyright notice, this list of conditions and the following
21 * disclaimer in the documentation and/or other materials provided
22 * with the distribution.
23 *
24 * - Neither the name of the Eclipse Foundation, Inc. nor the
25 * names of its contributors may be used to endorse or promote
26 * products derived from this software without specific prior
27 * written permission.
28 *
29 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42 */
43
44 package org.eclipse.jgit.http.server;
45
46 import static org.eclipse.jgit.util.HttpSupport.ENCODING_GZIP;
47 import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT_ENCODING;
48 import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_ENCODING;
49 import static org.eclipse.jgit.util.HttpSupport.HDR_ETAG;
50 import static org.eclipse.jgit.util.HttpSupport.TEXT_PLAIN;
51
52 import java.io.ByteArrayOutputStream;
53 import java.io.IOException;
54 import java.io.InputStream;
55 import java.io.OutputStream;
56 import java.security.MessageDigest;
57 import java.text.MessageFormat;
58 import java.util.zip.GZIPInputStream;
59 import java.util.zip.GZIPOutputStream;
60
61 import javax.servlet.ServletRequest;
62 import javax.servlet.http.HttpServletRequest;
63 import javax.servlet.http.HttpServletResponse;
64
65 import org.eclipse.jgit.internal.storage.dfs.DfsRepository;
66 import org.eclipse.jgit.lib.Constants;
67 import org.eclipse.jgit.lib.ObjectId;
68 import org.eclipse.jgit.lib.Repository;
69
70 /** Common utility functions for servlets. */
71 public final class ServletUtils {
72 /** Request attribute which stores the {@link Repository} instance. */
73 public static final String ATTRIBUTE_REPOSITORY = "org.eclipse.jgit.Repository";
74
75 /** Request attribute storing either UploadPack or ReceivePack. */
76 public static final String ATTRIBUTE_HANDLER = "org.eclipse.jgit.transport.UploadPackOrReceivePack";
77
78 /**
79 * Get the selected repository from the request.
80 *
81 * @param req
82 * the current request.
83 * @return the repository; never null.
84 * @throws IllegalStateException
85 * the repository was not set by the filter, the servlet is
86 * being invoked incorrectly and the programmer should ensure
87 * the filter runs before the servlet.
88 * @see #ATTRIBUTE_REPOSITORY
89 */
90 public static Repository getRepository(final ServletRequest req) {
91 Repository db = (Repository) req.getAttribute(ATTRIBUTE_REPOSITORY);
92 if (db == null)
93 throw new IllegalStateException(HttpServerText.get().expectedRepositoryAttribute);
94 return db;
95 }
96
97 /**
98 * Open the request input stream, automatically inflating if necessary.
99 * <p>
100 * This method automatically inflates the input stream if the request
101 * {@code Content-Encoding} header was set to {@code gzip} or the legacy
102 * {@code x-gzip}.
103 *
104 * @param req
105 * the incoming request whose input stream needs to be opened.
106 * @return an input stream to read the raw, uncompressed request body.
107 * @throws IOException
108 * if an input or output exception occurred.
109 */
110 public static InputStream getInputStream(final HttpServletRequest req)
111 throws IOException {
112 InputStream in = req.getInputStream();
113 final String enc = req.getHeader(HDR_CONTENT_ENCODING);
114 if (ENCODING_GZIP.equals(enc) || "x-gzip".equals(enc)) //$NON-NLS-1$
115 in = new GZIPInputStream(in);
116 else if (enc != null)
117 throw new IOException(MessageFormat.format(HttpServerText.get().encodingNotSupportedByThisLibrary
118 , HDR_CONTENT_ENCODING, enc));
119 return in;
120 }
121
122 /**
123 * Consume the entire request body, if one was supplied.
124 *
125 * @param req
126 * the request whose body must be consumed.
127 */
128 public static void consumeRequestBody(HttpServletRequest req) {
129 if (0 < req.getContentLength() || isChunked(req)) {
130 try {
131 consumeRequestBody(req.getInputStream());
132 } catch (IOException e) {
133 // Ignore any errors obtaining the input stream.
134 }
135 }
136 }
137
138 static boolean isChunked(HttpServletRequest req) {
139 return "chunked".equals(req.getHeader("Transfer-Encoding"));
140 }
141
142 /**
143 * Consume the rest of the input stream and discard it.
144 *
145 * @param in
146 * the stream to discard, closed if not null.
147 */
148 public static void consumeRequestBody(InputStream in) {
149 if (in == null)
150 return;
151 try {
152 while (0 < in.skip(2048) || 0 <= in.read()) {
153 // Discard until EOF.
154 }
155 } catch (IOException err) {
156 // Discard IOException during read or skip.
157 } finally {
158 try {
159 in.close();
160 } catch (IOException err) {
161 // Discard IOException during close of input stream.
162 }
163 }
164 }
165
166 /**
167 * Send a plain text response to a {@code GET} or {@code HEAD} HTTP request.
168 * <p>
169 * The text response is encoded in the Git character encoding, UTF-8.
170 * <p>
171 * If the user agent supports a compressed transfer encoding and the content
172 * is large enough, the content may be compressed before sending.
173 * <p>
174 * The {@code ETag} and {@code Content-Length} headers are automatically set
175 * by this method. {@code Content-Encoding} is conditionally set if the user
176 * agent supports a compressed transfer. Callers are responsible for setting
177 * any cache control headers.
178 *
179 * @param content
180 * to return to the user agent as this entity's body.
181 * @param req
182 * the incoming request.
183 * @param rsp
184 * the outgoing response.
185 * @throws IOException
186 * the servlet API rejected sending the body.
187 */
188 public static void sendPlainText(final String content,
189 final HttpServletRequest req, final HttpServletResponse rsp)
190 throws IOException {
191 final byte[] raw = content.getBytes(Constants.CHARACTER_ENCODING);
192 rsp.setContentType(TEXT_PLAIN);
193 rsp.setCharacterEncoding(Constants.CHARACTER_ENCODING);
194 send(raw, req, rsp);
195 }
196
197 /**
198 * Send a response to a {@code GET} or {@code HEAD} HTTP request.
199 * <p>
200 * If the user agent supports a compressed transfer encoding and the content
201 * is large enough, the content may be compressed before sending.
202 * <p>
203 * The {@code ETag} and {@code Content-Length} headers are automatically set
204 * by this method. {@code Content-Encoding} is conditionally set if the user
205 * agent supports a compressed transfer. Callers are responsible for setting
206 * {@code Content-Type} and any cache control headers.
207 *
208 * @param content
209 * to return to the user agent as this entity's body.
210 * @param req
211 * the incoming request.
212 * @param rsp
213 * the outgoing response.
214 * @throws IOException
215 * the servlet API rejected sending the body.
216 */
217 public static void send(byte[] content, final HttpServletRequest req,
218 final HttpServletResponse rsp) throws IOException {
219 content = sendInit(content, req, rsp);
220 final OutputStream out = rsp.getOutputStream();
221 try {
222 out.write(content);
223 out.flush();
224 } finally {
225 out.close();
226 }
227 }
228
229 private static byte[] sendInit(byte[] content,
230 final HttpServletRequest req, final HttpServletResponse rsp)
231 throws IOException {
232 rsp.setHeader(HDR_ETAG, etag(content));
233 if (256 < content.length && acceptsGzipEncoding(req)) {
234 content = compress(content);
235 rsp.setHeader(HDR_CONTENT_ENCODING, ENCODING_GZIP);
236 }
237 rsp.setContentLength(content.length);
238 return content;
239 }
240
241 static boolean acceptsGzipEncoding(final HttpServletRequest req) {
242 return acceptsGzipEncoding(req.getHeader(HDR_ACCEPT_ENCODING));
243 }
244
245 static boolean acceptsGzipEncoding(String accepts) {
246 if (accepts == null)
247 return false;
248
249 int b = 0;
250 while (b < accepts.length()) {
251 int comma = accepts.indexOf(',', b);
252 int e = 0 <= comma ? comma : accepts.length();
253 String term = accepts.substring(b, e).trim();
254 if (term.equals(ENCODING_GZIP))
255 return true;
256 b = e + 1;
257 }
258 return false;
259 }
260
261 private static byte[] compress(final byte[] raw) throws IOException {
262 final int maxLen = raw.length + 32;
263 final ByteArrayOutputStream out = new ByteArrayOutputStream(maxLen);
264 final GZIPOutputStream gz = new GZIPOutputStream(out);
265 gz.write(raw);
266 gz.finish();
267 gz.flush();
268 return out.toByteArray();
269 }
270
271 private static String etag(final byte[] content) {
272 final MessageDigest md = Constants.newMessageDigest();
273 md.update(content);
274 return ObjectId.fromRaw(md.digest()).getName();
275 }
276
277 static String identify(Repository git) {
278 if (git instanceof DfsRepository) {
279 return ((DfsRepository) git).getDescription().getRepositoryName();
280 } else if (git.getDirectory() != null) {
281 return git.getDirectory().getPath();
282 }
283 return "unknown";
284 }
285
286 private ServletUtils() {
287 // static utility class only
288 }
289 }