15e33a1deSShawn O. Pearce /* 2*5c5f7c6bSMatthias Sohn * Copyright (C) 2009-2010, Google Inc. and others 35e33a1deSShawn O. Pearce * 4*5c5f7c6bSMatthias Sohn * This program and the accompanying materials are made available under the 5*5c5f7c6bSMatthias Sohn * terms of the Eclipse Distribution License v. 1.0 which is available at 6*5c5f7c6bSMatthias Sohn * https://www.eclipse.org/org/documents/edl-v10.php. 75e33a1deSShawn O. Pearce * 8*5c5f7c6bSMatthias Sohn * SPDX-License-Identifier: BSD-3-Clause 95e33a1deSShawn O. Pearce */ 105e33a1deSShawn O. Pearce 115e33a1deSShawn O. Pearce package org.eclipse.jgit.http.server; 125e33a1deSShawn O. Pearce 13fbf6ce65SDavid Pursehouse import static java.nio.charset.StandardCharsets.UTF_8; 145e33a1deSShawn O. Pearce import static org.eclipse.jgit.util.HttpSupport.ENCODING_GZIP; 1588037184SZhen Chen import static org.eclipse.jgit.util.HttpSupport.ENCODING_X_GZIP; 165e33a1deSShawn O. Pearce import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT_ENCODING; 175e33a1deSShawn O. Pearce import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_ENCODING; 185e33a1deSShawn O. Pearce import static org.eclipse.jgit.util.HttpSupport.HDR_ETAG; 195e33a1deSShawn O. Pearce import static org.eclipse.jgit.util.HttpSupport.TEXT_PLAIN; 205e33a1deSShawn O. Pearce 215e33a1deSShawn O. Pearce import java.io.ByteArrayOutputStream; 225e33a1deSShawn O. Pearce import java.io.IOException; 235e33a1deSShawn O. Pearce import java.io.InputStream; 245e33a1deSShawn O. Pearce import java.io.OutputStream; 255e33a1deSShawn O. Pearce import java.security.MessageDigest; 26f3d8a8ecSSasa Zivkov import java.text.MessageFormat; 275e33a1deSShawn O. Pearce import java.util.zip.GZIPInputStream; 285e33a1deSShawn O. Pearce import java.util.zip.GZIPOutputStream; 295e33a1deSShawn O. Pearce 305e33a1deSShawn O. Pearce import javax.servlet.ServletRequest; 315e33a1deSShawn O. Pearce import javax.servlet.http.HttpServletRequest; 325e33a1deSShawn O. Pearce import javax.servlet.http.HttpServletResponse; 335e33a1deSShawn O. Pearce 345e33a1deSShawn O. Pearce import org.eclipse.jgit.lib.Constants; 355e33a1deSShawn O. Pearce import org.eclipse.jgit.lib.ObjectId; 365e33a1deSShawn O. Pearce import org.eclipse.jgit.lib.Repository; 375e33a1deSShawn O. Pearce 383b00041cSMatthias Sohn /** 393b00041cSMatthias Sohn * Common utility functions for servlets. 403b00041cSMatthias Sohn */ 415e33a1deSShawn O. Pearce public final class ServletUtils { 425e33a1deSShawn O. Pearce /** Request attribute which stores the {@link Repository} instance. */ 435e33a1deSShawn O. Pearce public static final String ATTRIBUTE_REPOSITORY = "org.eclipse.jgit.Repository"; 445e33a1deSShawn O. Pearce 45af3562f7SShawn O. Pearce /** Request attribute storing either UploadPack or ReceivePack. */ 46af3562f7SShawn O. Pearce public static final String ATTRIBUTE_HANDLER = "org.eclipse.jgit.transport.UploadPackOrReceivePack"; 47af3562f7SShawn O. Pearce 485e33a1deSShawn O. Pearce /** 495e33a1deSShawn O. Pearce * Get the selected repository from the request. 505e33a1deSShawn O. Pearce * 515e33a1deSShawn O. Pearce * @param req 525e33a1deSShawn O. Pearce * the current request. 535e33a1deSShawn O. Pearce * @return the repository; never null. 545e33a1deSShawn O. Pearce * @throws IllegalStateException 555e33a1deSShawn O. Pearce * the repository was not set by the filter, the servlet is 565e33a1deSShawn O. Pearce * being invoked incorrectly and the programmer should ensure 575e33a1deSShawn O. Pearce * the filter runs before the servlet. 585e33a1deSShawn O. Pearce * @see #ATTRIBUTE_REPOSITORY 595e33a1deSShawn O. Pearce */ getRepository(ServletRequest req)606d370d83SHan-Wen Nienhuys public static Repository getRepository(ServletRequest req) { 615e33a1deSShawn O. Pearce Repository db = (Repository) req.getAttribute(ATTRIBUTE_REPOSITORY); 625e33a1deSShawn O. Pearce if (db == null) 63f3d8a8ecSSasa Zivkov throw new IllegalStateException(HttpServerText.get().expectedRepositoryAttribute); 645e33a1deSShawn O. Pearce return db; 655e33a1deSShawn O. Pearce } 665e33a1deSShawn O. Pearce 675e33a1deSShawn O. Pearce /** 685e33a1deSShawn O. Pearce * Open the request input stream, automatically inflating if necessary. 695e33a1deSShawn O. Pearce * <p> 705e33a1deSShawn O. Pearce * This method automatically inflates the input stream if the request 715e33a1deSShawn O. Pearce * {@code Content-Encoding} header was set to {@code gzip} or the legacy 725e33a1deSShawn O. Pearce * {@code x-gzip}. 735e33a1deSShawn O. Pearce * 745e33a1deSShawn O. Pearce * @param req 755e33a1deSShawn O. Pearce * the incoming request whose input stream needs to be opened. 765e33a1deSShawn O. Pearce * @return an input stream to read the raw, uncompressed request body. 775e33a1deSShawn O. Pearce * @throws IOException 785e33a1deSShawn O. Pearce * if an input or output exception occurred. 795e33a1deSShawn O. Pearce */ getInputStream(HttpServletRequest req)806d370d83SHan-Wen Nienhuys public static InputStream getInputStream(HttpServletRequest req) 815e33a1deSShawn O. Pearce throws IOException { 825e33a1deSShawn O. Pearce InputStream in = req.getInputStream(); 835e33a1deSShawn O. Pearce final String enc = req.getHeader(HDR_CONTENT_ENCODING); 84e271e2f6SMatthias Sohn if (ENCODING_GZIP.equals(enc) || ENCODING_X_GZIP.equals(enc)) 855e33a1deSShawn O. Pearce in = new GZIPInputStream(in); 865e33a1deSShawn O. Pearce else if (enc != null) 87f3d8a8ecSSasa Zivkov throw new IOException(MessageFormat.format(HttpServerText.get().encodingNotSupportedByThisLibrary 88f3d8a8ecSSasa Zivkov , HDR_CONTENT_ENCODING, enc)); 895e33a1deSShawn O. Pearce return in; 905e33a1deSShawn O. Pearce } 915e33a1deSShawn O. Pearce 925e33a1deSShawn O. Pearce /** 93db00632dSShawn O. Pearce * Consume the entire request body, if one was supplied. 94db00632dSShawn O. Pearce * 95db00632dSShawn O. Pearce * @param req 96db00632dSShawn O. Pearce * the request whose body must be consumed. 97db00632dSShawn O. Pearce */ consumeRequestBody(HttpServletRequest req)98db00632dSShawn O. Pearce public static void consumeRequestBody(HttpServletRequest req) { 99db00632dSShawn O. Pearce if (0 < req.getContentLength() || isChunked(req)) { 100db00632dSShawn O. Pearce try { 101db00632dSShawn O. Pearce consumeRequestBody(req.getInputStream()); 102db00632dSShawn O. Pearce } catch (IOException e) { 103db00632dSShawn O. Pearce // Ignore any errors obtaining the input stream. 104db00632dSShawn O. Pearce } 105db00632dSShawn O. Pearce } 106db00632dSShawn O. Pearce } 107db00632dSShawn O. Pearce isChunked(HttpServletRequest req)1081c6c73c5SShawn O. Pearce static boolean isChunked(HttpServletRequest req) { 109db00632dSShawn O. Pearce return "chunked".equals(req.getHeader("Transfer-Encoding")); 110db00632dSShawn O. Pearce } 111db00632dSShawn O. Pearce 112db00632dSShawn O. Pearce /** 113db00632dSShawn O. Pearce * Consume the rest of the input stream and discard it. 114db00632dSShawn O. Pearce * 115db00632dSShawn O. Pearce * @param in 116db00632dSShawn O. Pearce * the stream to discard, closed if not null. 117db00632dSShawn O. Pearce */ consumeRequestBody(InputStream in)118db00632dSShawn O. Pearce public static void consumeRequestBody(InputStream in) { 119db00632dSShawn O. Pearce if (in == null) 120db00632dSShawn O. Pearce return; 121db00632dSShawn O. Pearce try { 122db00632dSShawn O. Pearce while (0 < in.skip(2048) || 0 <= in.read()) { 123db00632dSShawn O. Pearce // Discard until EOF. 124db00632dSShawn O. Pearce } 125db00632dSShawn O. Pearce } catch (IOException err) { 126db00632dSShawn O. Pearce // Discard IOException during read or skip. 127db00632dSShawn O. Pearce } finally { 128db00632dSShawn O. Pearce try { 129db00632dSShawn O. Pearce in.close(); 130db00632dSShawn O. Pearce } catch (IOException err) { 131db00632dSShawn O. Pearce // Discard IOException during close of input stream. 132db00632dSShawn O. Pearce } 133db00632dSShawn O. Pearce } 134db00632dSShawn O. Pearce } 135db00632dSShawn O. Pearce 136db00632dSShawn O. Pearce /** 1375e33a1deSShawn O. Pearce * Send a plain text response to a {@code GET} or {@code HEAD} HTTP request. 1385e33a1deSShawn O. Pearce * <p> 1395e33a1deSShawn O. Pearce * The text response is encoded in the Git character encoding, UTF-8. 1405e33a1deSShawn O. Pearce * <p> 1415e33a1deSShawn O. Pearce * If the user agent supports a compressed transfer encoding and the content 1425e33a1deSShawn O. Pearce * is large enough, the content may be compressed before sending. 1435e33a1deSShawn O. Pearce * <p> 1445e33a1deSShawn O. Pearce * The {@code ETag} and {@code Content-Length} headers are automatically set 1455e33a1deSShawn O. Pearce * by this method. {@code Content-Encoding} is conditionally set if the user 1465e33a1deSShawn O. Pearce * agent supports a compressed transfer. Callers are responsible for setting 1475e33a1deSShawn O. Pearce * any cache control headers. 1485e33a1deSShawn O. Pearce * 1495e33a1deSShawn O. Pearce * @param content 1505e33a1deSShawn O. Pearce * to return to the user agent as this entity's body. 1515e33a1deSShawn O. Pearce * @param req 1525e33a1deSShawn O. Pearce * the incoming request. 1535e33a1deSShawn O. Pearce * @param rsp 1545e33a1deSShawn O. Pearce * the outgoing response. 1555e33a1deSShawn O. Pearce * @throws IOException 1565e33a1deSShawn O. Pearce * the servlet API rejected sending the body. 1575e33a1deSShawn O. Pearce */ sendPlainText(final String content, final HttpServletRequest req, final HttpServletResponse rsp)1585e33a1deSShawn O. Pearce public static void sendPlainText(final String content, 1595e33a1deSShawn O. Pearce final HttpServletRequest req, final HttpServletResponse rsp) 1605e33a1deSShawn O. Pearce throws IOException { 161fbf6ce65SDavid Pursehouse final byte[] raw = content.getBytes(UTF_8); 1625e33a1deSShawn O. Pearce rsp.setContentType(TEXT_PLAIN); 163fbf6ce65SDavid Pursehouse rsp.setCharacterEncoding(UTF_8.name()); 1645e33a1deSShawn O. Pearce send(raw, req, rsp); 1655e33a1deSShawn O. Pearce } 1665e33a1deSShawn O. Pearce 1675e33a1deSShawn O. Pearce /** 1685e33a1deSShawn O. Pearce * Send a response to a {@code GET} or {@code HEAD} HTTP request. 1695e33a1deSShawn O. Pearce * <p> 1705e33a1deSShawn O. Pearce * If the user agent supports a compressed transfer encoding and the content 1715e33a1deSShawn O. Pearce * is large enough, the content may be compressed before sending. 1725e33a1deSShawn O. Pearce * <p> 1735e33a1deSShawn O. Pearce * The {@code ETag} and {@code Content-Length} headers are automatically set 1745e33a1deSShawn O. Pearce * by this method. {@code Content-Encoding} is conditionally set if the user 1755e33a1deSShawn O. Pearce * agent supports a compressed transfer. Callers are responsible for setting 1765e33a1deSShawn O. Pearce * {@code Content-Type} and any cache control headers. 1775e33a1deSShawn O. Pearce * 1785e33a1deSShawn O. Pearce * @param content 1795e33a1deSShawn O. Pearce * to return to the user agent as this entity's body. 1805e33a1deSShawn O. Pearce * @param req 1815e33a1deSShawn O. Pearce * the incoming request. 1825e33a1deSShawn O. Pearce * @param rsp 1835e33a1deSShawn O. Pearce * the outgoing response. 1845e33a1deSShawn O. Pearce * @throws IOException 1855e33a1deSShawn O. Pearce * the servlet API rejected sending the body. 1865e33a1deSShawn O. Pearce */ send(byte[] content, final HttpServletRequest req, final HttpServletResponse rsp)1875e33a1deSShawn O. Pearce public static void send(byte[] content, final HttpServletRequest req, 1885e33a1deSShawn O. Pearce final HttpServletResponse rsp) throws IOException { 1895e33a1deSShawn O. Pearce content = sendInit(content, req, rsp); 1905c70be00SDavid Pursehouse try (OutputStream out = rsp.getOutputStream()) { 1915e33a1deSShawn O. Pearce out.write(content); 1925e33a1deSShawn O. Pearce out.flush(); 1935e33a1deSShawn O. Pearce } 1945e33a1deSShawn O. Pearce } 1955e33a1deSShawn O. Pearce sendInit(byte[] content, final HttpServletRequest req, final HttpServletResponse rsp)1965e33a1deSShawn O. Pearce private static byte[] sendInit(byte[] content, 1975e33a1deSShawn O. Pearce final HttpServletRequest req, final HttpServletResponse rsp) 1985e33a1deSShawn O. Pearce throws IOException { 1995e33a1deSShawn O. Pearce rsp.setHeader(HDR_ETAG, etag(content)); 2005e33a1deSShawn O. Pearce if (256 < content.length && acceptsGzipEncoding(req)) { 2015e33a1deSShawn O. Pearce content = compress(content); 2025e33a1deSShawn O. Pearce rsp.setHeader(HDR_CONTENT_ENCODING, ENCODING_GZIP); 2035e33a1deSShawn O. Pearce } 2045e33a1deSShawn O. Pearce rsp.setContentLength(content.length); 2055e33a1deSShawn O. Pearce return content; 2065e33a1deSShawn O. Pearce } 2075e33a1deSShawn O. Pearce acceptsGzipEncoding(HttpServletRequest req)208f3ec7cf3SHan-Wen Nienhuys static boolean acceptsGzipEncoding(HttpServletRequest req) { 20918e822a7SShawn O. Pearce return acceptsGzipEncoding(req.getHeader(HDR_ACCEPT_ENCODING)); 21018e822a7SShawn O. Pearce } 21118e822a7SShawn O. Pearce acceptsGzipEncoding(String accepts)21218e822a7SShawn O. Pearce static boolean acceptsGzipEncoding(String accepts) { 21318e822a7SShawn O. Pearce if (accepts == null) 21418e822a7SShawn O. Pearce return false; 21518e822a7SShawn O. Pearce 21618e822a7SShawn O. Pearce int b = 0; 21718e822a7SShawn O. Pearce while (b < accepts.length()) { 21818e822a7SShawn O. Pearce int comma = accepts.indexOf(',', b); 21918e822a7SShawn O. Pearce int e = 0 <= comma ? comma : accepts.length(); 22018e822a7SShawn O. Pearce String term = accepts.substring(b, e).trim(); 22118e822a7SShawn O. Pearce if (term.equals(ENCODING_GZIP)) 22218e822a7SShawn O. Pearce return true; 22318e822a7SShawn O. Pearce b = e + 1; 22418e822a7SShawn O. Pearce } 22518e822a7SShawn O. Pearce return false; 2265e33a1deSShawn O. Pearce } 2275e33a1deSShawn O. Pearce compress(byte[] raw)2286d370d83SHan-Wen Nienhuys private static byte[] compress(byte[] raw) throws IOException { 2295e33a1deSShawn O. Pearce final int maxLen = raw.length + 32; 2305e33a1deSShawn O. Pearce final ByteArrayOutputStream out = new ByteArrayOutputStream(maxLen); 2315e33a1deSShawn O. Pearce final GZIPOutputStream gz = new GZIPOutputStream(out); 2325e33a1deSShawn O. Pearce gz.write(raw); 2335e33a1deSShawn O. Pearce gz.finish(); 2345e33a1deSShawn O. Pearce gz.flush(); 2355e33a1deSShawn O. Pearce return out.toByteArray(); 2365e33a1deSShawn O. Pearce } 2375e33a1deSShawn O. Pearce etag(byte[] content)2386d370d83SHan-Wen Nienhuys private static String etag(byte[] content) { 2395e33a1deSShawn O. Pearce final MessageDigest md = Constants.newMessageDigest(); 2405e33a1deSShawn O. Pearce md.update(content); 2415e33a1deSShawn O. Pearce return ObjectId.fromRaw(md.digest()).getName(); 2425e33a1deSShawn O. Pearce } 2435e33a1deSShawn O. Pearce identify(Repository git)244ae592cc6SShawn Pearce static String identify(Repository git) { 2458cd07cb8SDavid Ostrovsky String identifier = git.getIdentifier(); 2468cd07cb8SDavid Ostrovsky if (identifier == null) { 247ae592cc6SShawn Pearce return "unknown"; 248ae592cc6SShawn Pearce } 2498cd07cb8SDavid Ostrovsky return identifier; 2508cd07cb8SDavid Ostrovsky } 251ae592cc6SShawn Pearce ServletUtils()2525e33a1deSShawn O. Pearce private ServletUtils() { 2535e33a1deSShawn O. Pearce // static utility class only 2545e33a1deSShawn O. Pearce } 2555e33a1deSShawn O. Pearce } 256