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