blob: 1336d6e939e23a772502d2a20d92419309ff1de5 [file] [log] [blame]
/*
* Copyright (C) 2009-2010, Google Inc.
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.http.server;
import static org.eclipse.jgit.util.HttpSupport.ENCODING_GZIP;
import static org.eclipse.jgit.util.HttpSupport.ENCODING_X_GZIP;
import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT_ENCODING;
import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_ENCODING;
import static org.eclipse.jgit.util.HttpSupport.HDR_ETAG;
import static org.eclipse.jgit.util.HttpSupport.TEXT_PLAIN;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.text.MessageFormat;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jgit.internal.storage.dfs.DfsRepository;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
/** Common utility functions for servlets. */
public final class ServletUtils {
/** Request attribute which stores the {@link Repository} instance. */
public static final String ATTRIBUTE_REPOSITORY = "org.eclipse.jgit.Repository";
/** Request attribute storing either UploadPack or ReceivePack. */
public static final String ATTRIBUTE_HANDLER = "org.eclipse.jgit.transport.UploadPackOrReceivePack";
/**
* Get the selected repository from the request.
*
* @param req
* the current request.
* @return the repository; never null.
* @throws IllegalStateException
* the repository was not set by the filter, the servlet is
* being invoked incorrectly and the programmer should ensure
* the filter runs before the servlet.
* @see #ATTRIBUTE_REPOSITORY
*/
public static Repository getRepository(final ServletRequest req) {
Repository db = (Repository) req.getAttribute(ATTRIBUTE_REPOSITORY);
if (db == null)
throw new IllegalStateException(HttpServerText.get().expectedRepositoryAttribute);
return db;
}
/**
* Open the request input stream, automatically inflating if necessary.
* <p>
* This method automatically inflates the input stream if the request
* {@code Content-Encoding} header was set to {@code gzip} or the legacy
* {@code x-gzip}.
*
* @param req
* the incoming request whose input stream needs to be opened.
* @return an input stream to read the raw, uncompressed request body.
* @throws IOException
* if an input or output exception occurred.
*/
public static InputStream getInputStream(final HttpServletRequest req)
throws IOException {
InputStream in = req.getInputStream();
final String enc = req.getHeader(HDR_CONTENT_ENCODING);
if (ENCODING_GZIP.equals(enc) || ENCODING_X_GZIP.equals(enc)) //$NON-NLS-1$
in = new GZIPInputStream(in);
else if (enc != null)
throw new IOException(MessageFormat.format(HttpServerText.get().encodingNotSupportedByThisLibrary
, HDR_CONTENT_ENCODING, enc));
return in;
}
/**
* Consume the entire request body, if one was supplied.
*
* @param req
* the request whose body must be consumed.
*/
public static void consumeRequestBody(HttpServletRequest req) {
if (0 < req.getContentLength() || isChunked(req)) {
try {
consumeRequestBody(req.getInputStream());
} catch (IOException e) {
// Ignore any errors obtaining the input stream.
}
}
}
static boolean isChunked(HttpServletRequest req) {
return "chunked".equals(req.getHeader("Transfer-Encoding"));
}
/**
* Consume the rest of the input stream and discard it.
*
* @param in
* the stream to discard, closed if not null.
*/
public static void consumeRequestBody(InputStream in) {
if (in == null)
return;
try {
while (0 < in.skip(2048) || 0 <= in.read()) {
// Discard until EOF.
}
} catch (IOException err) {
// Discard IOException during read or skip.
} finally {
try {
in.close();
} catch (IOException err) {
// Discard IOException during close of input stream.
}
}
}
/**
* Send a plain text response to a {@code GET} or {@code HEAD} HTTP request.
* <p>
* The text response is encoded in the Git character encoding, UTF-8.
* <p>
* If the user agent supports a compressed transfer encoding and the content
* is large enough, the content may be compressed before sending.
* <p>
* The {@code ETag} and {@code Content-Length} headers are automatically set
* by this method. {@code Content-Encoding} is conditionally set if the user
* agent supports a compressed transfer. Callers are responsible for setting
* any cache control headers.
*
* @param content
* to return to the user agent as this entity's body.
* @param req
* the incoming request.
* @param rsp
* the outgoing response.
* @throws IOException
* the servlet API rejected sending the body.
*/
public static void sendPlainText(final String content,
final HttpServletRequest req, final HttpServletResponse rsp)
throws IOException {
final byte[] raw = content.getBytes(Constants.CHARACTER_ENCODING);
rsp.setContentType(TEXT_PLAIN);
rsp.setCharacterEncoding(Constants.CHARACTER_ENCODING);
send(raw, req, rsp);
}
/**
* Send a response to a {@code GET} or {@code HEAD} HTTP request.
* <p>
* If the user agent supports a compressed transfer encoding and the content
* is large enough, the content may be compressed before sending.
* <p>
* The {@code ETag} and {@code Content-Length} headers are automatically set
* by this method. {@code Content-Encoding} is conditionally set if the user
* agent supports a compressed transfer. Callers are responsible for setting
* {@code Content-Type} and any cache control headers.
*
* @param content
* to return to the user agent as this entity's body.
* @param req
* the incoming request.
* @param rsp
* the outgoing response.
* @throws IOException
* the servlet API rejected sending the body.
*/
public static void send(byte[] content, final HttpServletRequest req,
final HttpServletResponse rsp) throws IOException {
content = sendInit(content, req, rsp);
final OutputStream out = rsp.getOutputStream();
try {
out.write(content);
out.flush();
} finally {
out.close();
}
}
private static byte[] sendInit(byte[] content,
final HttpServletRequest req, final HttpServletResponse rsp)
throws IOException {
rsp.setHeader(HDR_ETAG, etag(content));
if (256 < content.length && acceptsGzipEncoding(req)) {
content = compress(content);
rsp.setHeader(HDR_CONTENT_ENCODING, ENCODING_GZIP);
}
rsp.setContentLength(content.length);
return content;
}
static boolean acceptsGzipEncoding(final HttpServletRequest req) {
return acceptsGzipEncoding(req.getHeader(HDR_ACCEPT_ENCODING));
}
static boolean acceptsGzipEncoding(String accepts) {
if (accepts == null)
return false;
int b = 0;
while (b < accepts.length()) {
int comma = accepts.indexOf(',', b);
int e = 0 <= comma ? comma : accepts.length();
String term = accepts.substring(b, e).trim();
if (term.equals(ENCODING_GZIP))
return true;
b = e + 1;
}
return false;
}
private static byte[] compress(final byte[] raw) throws IOException {
final int maxLen = raw.length + 32;
final ByteArrayOutputStream out = new ByteArrayOutputStream(maxLen);
final GZIPOutputStream gz = new GZIPOutputStream(out);
gz.write(raw);
gz.finish();
gz.flush();
return out.toByteArray();
}
private static String etag(final byte[] content) {
final MessageDigest md = Constants.newMessageDigest();
md.update(content);
return ObjectId.fromRaw(md.digest()).getName();
}
static String identify(Repository git) {
if (git instanceof DfsRepository) {
return ((DfsRepository) git).getDescription().getRepositoryName();
} else if (git.getDirectory() != null) {
return git.getDirectory().getPath();
}
return "unknown";
}
private ServletUtils() {
// static utility class only
}
}