View Javadoc
1   /*
2    * Copyright (C) 2011, 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.http.server.ServletUtils.ATTRIBUTE_HANDLER;
47  import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDE_BAND;
48  import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDE_BAND_64K;
49  import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_SIDE_BAND_64K;
50  import static org.eclipse.jgit.transport.SideBandOutputStream.CH_ERROR;
51  import static org.eclipse.jgit.transport.SideBandOutputStream.SMALL_BUF;
52  import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
53  import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
54  import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
55  
56  import java.io.ByteArrayOutputStream;
57  import java.io.IOException;
58  import java.io.OutputStream;
59  import java.util.Arrays;
60  import java.util.Collections;
61  import java.util.List;
62  
63  import javax.servlet.http.HttpServletRequest;
64  import javax.servlet.http.HttpServletResponse;
65  
66  import org.eclipse.jgit.lib.Constants;
67  import org.eclipse.jgit.transport.PacketLineIn;
68  import org.eclipse.jgit.transport.PacketLineOut;
69  import org.eclipse.jgit.transport.ReceivePack;
70  import org.eclipse.jgit.transport.RequestNotYetReadException;
71  import org.eclipse.jgit.transport.SideBandOutputStream;
72  import org.eclipse.jgit.transport.UploadPack;
73  
74  /**
75   * Utility functions for handling the Git-over-HTTP protocol.
76   */
77  public class GitSmartHttpTools {
78  	private static final String INFO_REFS = Constants.INFO_REFS;
79  
80  	/** Name of the git-upload-pack service. */
81  	public static final String UPLOAD_PACK = "git-upload-pack";
82  
83  	/** Name of the git-receive-pack service. */
84  	public static final String RECEIVE_PACK = "git-receive-pack";
85  
86  	/** Content type supplied by the client to the /git-upload-pack handler. */
87  	public static final String UPLOAD_PACK_REQUEST_TYPE =
88  			"application/x-git-upload-pack-request";
89  
90  	/** Content type returned from the /git-upload-pack handler. */
91  	public static final String UPLOAD_PACK_RESULT_TYPE =
92  			"application/x-git-upload-pack-result";
93  
94  	/** Content type supplied by the client to the /git-receive-pack handler. */
95  	public static final String RECEIVE_PACK_REQUEST_TYPE =
96  			"application/x-git-receive-pack-request";
97  
98  	/** Content type returned from the /git-receive-pack handler. */
99  	public static final String RECEIVE_PACK_RESULT_TYPE =
100 			"application/x-git-receive-pack-result";
101 
102 	/** Git service names accepted by the /info/refs?service= handler. */
103 	public static final List<String> VALID_SERVICES =
104 			Collections.unmodifiableList(Arrays.asList(new String[] {
105 					UPLOAD_PACK, RECEIVE_PACK }));
106 
107 	private static final String INFO_REFS_PATH = "/" + INFO_REFS;
108 	private static final String UPLOAD_PACK_PATH = "/" + UPLOAD_PACK;
109 	private static final String RECEIVE_PACK_PATH = "/" + RECEIVE_PACK;
110 
111 	private static final List<String> SERVICE_SUFFIXES =
112 			Collections.unmodifiableList(Arrays.asList(new String[] {
113 					INFO_REFS_PATH, UPLOAD_PACK_PATH, RECEIVE_PACK_PATH }));
114 
115 	/**
116 	 * Check a request for Git-over-HTTP indicators.
117 	 *
118 	 * @param req
119 	 *            the current HTTP request that may have been made by Git.
120 	 * @return true if the request is likely made by a Git client program.
121 	 */
122 	public static boolean isGitClient(HttpServletRequest req) {
123 		return isInfoRefs(req) || isUploadPack(req) || isReceivePack(req);
124 	}
125 
126 	/**
127 	 * Send an error to the Git client or browser.
128 	 * <p>
129 	 * Server implementors may use this method to send customized error messages
130 	 * to a Git protocol client using an HTTP 200 OK response with the error
131 	 * embedded in the payload. If the request was not issued by a Git client,
132 	 * an HTTP response code is returned instead.
133 	 *
134 	 * @param req
135 	 *            current request.
136 	 * @param res
137 	 *            current response.
138 	 * @param httpStatus
139 	 *            HTTP status code to set if the client is not a Git client.
140 	 * @throws IOException
141 	 *             the response cannot be sent.
142 	 */
143 	public static void sendError(HttpServletRequest req,
144 			HttpServletResponse res, int httpStatus) throws IOException {
145 		sendError(req, res, httpStatus, null);
146 	}
147 
148 	/**
149 	 * Send an error to the Git client or browser.
150 	 * <p>
151 	 * Server implementors may use this method to send customized error messages
152 	 * to a Git protocol client using an HTTP 200 OK response with the error
153 	 * embedded in the payload. If the request was not issued by a Git client,
154 	 * an HTTP response code is returned instead.
155 	 * <p>
156 	 * This method may only be called before handing off the request to
157 	 * {@link UploadPack#upload(java.io.InputStream, OutputStream, OutputStream)}
158 	 * or
159 	 * {@link ReceivePack#receive(java.io.InputStream, OutputStream, OutputStream)}.
160 	 *
161 	 * @param req
162 	 *            current request.
163 	 * @param res
164 	 *            current response.
165 	 * @param httpStatus
166 	 *            HTTP status code to set if the client is not a Git client.
167 	 * @param textForGit
168 	 *            plain text message to display on the user's console. This is
169 	 *            shown only if the client is likely to be a Git client. If null
170 	 *            or the empty string a default text is chosen based on the HTTP
171 	 *            response code.
172 	 * @throws IOException
173 	 *             the response cannot be sent.
174 	 */
175 	public static void sendError(HttpServletRequest req,
176 			HttpServletResponse res, int httpStatus, String textForGit)
177 			throws IOException {
178 		if (textForGit == null || textForGit.length() == 0) {
179 			switch (httpStatus) {
180 			case SC_FORBIDDEN:
181 				textForGit = HttpServerText.get().repositoryAccessForbidden;
182 				break;
183 			case SC_NOT_FOUND:
184 				textForGit = HttpServerText.get().repositoryNotFound;
185 				break;
186 			case SC_INTERNAL_SERVER_ERROR:
187 				textForGit = HttpServerText.get().internalServerError;
188 				break;
189 			default:
190 				textForGit = "HTTP " + httpStatus;
191 				break;
192 			}
193 		}
194 
195 		if (isInfoRefs(req)) {
196 			sendInfoRefsError(req, res, textForGit);
197 		} else if (isUploadPack(req)) {
198 			sendUploadPackError(req, res, textForGit);
199 		} else if (isReceivePack(req)) {
200 			sendReceivePackError(req, res, textForGit);
201 		} else {
202 			if (httpStatus < 400)
203 				ServletUtils.consumeRequestBody(req);
204 			res.sendError(httpStatus);
205 		}
206 	}
207 
208 	private static void sendInfoRefsError(HttpServletRequest req,
209 			HttpServletResponse res, String textForGit) throws IOException {
210 		ByteArrayOutputStream buf = new ByteArrayOutputStream(128);
211 		PacketLineOut pck = new PacketLineOut(buf);
212 		String svc = req.getParameter("service");
213 		pck.writeString("# service=" + svc + "\n");
214 		pck.end();
215 		pck.writeString("ERR " + textForGit);
216 		send(req, res, infoRefsResultType(svc), buf.toByteArray());
217 	}
218 
219 	private static void sendUploadPackError(HttpServletRequest req,
220 			HttpServletResponse res, String textForGit) throws IOException {
221 		ByteArrayOutputStream buf = new ByteArrayOutputStream(128);
222 		PacketLineOut pckOut = new PacketLineOut(buf);
223 
224 		boolean sideband;
225 		UploadPack up = (UploadPack) req.getAttribute(ATTRIBUTE_HANDLER);
226 		if (up != null) {
227 			try {
228 				sideband = up.isSideBand();
229 			} catch (RequestNotYetReadException e) {
230 				sideband = isUploadPackSideBand(req);
231 			}
232 		} else
233 			sideband = isUploadPackSideBand(req);
234 
235 		if (sideband)
236 			writeSideBand(buf, textForGit);
237 		else
238 			writePacket(pckOut, textForGit);
239 		send(req, res, UPLOAD_PACK_RESULT_TYPE, buf.toByteArray());
240 	}
241 
242 	private static boolean isUploadPackSideBand(HttpServletRequest req) {
243 		try {
244 			// The client may be in a state where they have sent the sideband
245 			// capability and are expecting a response in the sideband, but we might
246 			// not have an UploadPack, or it might not have read any of the request.
247 			// So, cheat and read the first line.
248 			String line = new PacketLineIn(req.getInputStream()).readString();
249 			UploadPack.FirstLine parsed = new UploadPack.FirstLine(line);
250 			return (parsed.getOptions().contains(OPTION_SIDE_BAND)
251 					|| parsed.getOptions().contains(OPTION_SIDE_BAND_64K));
252 		} catch (IOException e) {
253 			// Probably the connection is closed and a subsequent write will fail, but
254 			// try it just in case.
255 			return false;
256 		}
257 	}
258 
259 	private static void sendReceivePackError(HttpServletRequest req,
260 			HttpServletResponse res, String textForGit) throws IOException {
261 		ByteArrayOutputStream buf = new ByteArrayOutputStream(128);
262 		PacketLineOut pckOut = new PacketLineOut(buf);
263 
264 		boolean sideband;
265 		ReceivePack rp = (ReceivePack) req.getAttribute(ATTRIBUTE_HANDLER);
266 		if (rp != null) {
267 			try {
268 				sideband = rp.isSideBand();
269 			} catch (RequestNotYetReadException e) {
270 				sideband = isReceivePackSideBand(req);
271 			}
272 		} else
273 			sideband = isReceivePackSideBand(req);
274 
275 		if (sideband)
276 			writeSideBand(buf, textForGit);
277 		else
278 			writePacket(pckOut, textForGit);
279 		send(req, res, RECEIVE_PACK_RESULT_TYPE, buf.toByteArray());
280 	}
281 
282 	private static boolean isReceivePackSideBand(HttpServletRequest req) {
283 		try {
284 			// The client may be in a state where they have sent the sideband
285 			// capability and are expecting a response in the sideband, but we might
286 			// not have a ReceivePack, or it might not have read any of the request.
287 			// So, cheat and read the first line.
288 			String line = new PacketLineIn(req.getInputStream()).readString();
289 			ReceivePack.FirstLine parsed = new ReceivePack.FirstLine(line);
290 			return parsed.getCapabilities().contains(CAPABILITY_SIDE_BAND_64K);
291 		} catch (IOException e) {
292 			// Probably the connection is closed and a subsequent write will fail, but
293 			// try it just in case.
294 			return false;
295 		}
296 	}
297 
298 	private static void writeSideBand(OutputStream out, String textForGit)
299 			throws IOException {
300 		@SuppressWarnings("resource" /* java 7 */)
301 		OutputStream msg = new SideBandOutputStream(CH_ERROR, SMALL_BUF, out);
302 		msg.write(Constants.encode("error: " + textForGit));
303 		msg.flush();
304 	}
305 
306 	private static void writePacket(PacketLineOut pckOut, String textForGit)
307 			throws IOException {
308 		pckOut.writeString("error: " + textForGit);
309 	}
310 
311 	private static void send(HttpServletRequest req, HttpServletResponse res,
312 			String type, byte[] buf) throws IOException {
313 		ServletUtils.consumeRequestBody(req);
314 		res.setStatus(HttpServletResponse.SC_OK);
315 		res.setContentType(type);
316 		res.setContentLength(buf.length);
317 		OutputStream os = res.getOutputStream();
318 		try {
319 			os.write(buf);
320 		} finally {
321 			os.close();
322 		}
323 	}
324 
325 	/**
326 	 * Get the response Content-Type a client expects for the request.
327 	 * <p>
328 	 * This method should only be invoked if
329 	 * {@link #isGitClient(HttpServletRequest)} is true.
330 	 *
331 	 * @param req
332 	 *            current request.
333 	 * @return the Content-Type the client expects.
334 	 * @throws IllegalArgumentException
335 	 *             the request is not a Git client request. See
336 	 *             {@link #isGitClient(HttpServletRequest)}.
337 	 */
338 	public static String getResponseContentType(HttpServletRequest req) {
339 		if (isInfoRefs(req))
340 			return infoRefsResultType(req.getParameter("service"));
341 		else if (isUploadPack(req))
342 			return UPLOAD_PACK_RESULT_TYPE;
343 		else if (isReceivePack(req))
344 			return RECEIVE_PACK_RESULT_TYPE;
345 		else
346 			throw new IllegalArgumentException();
347 	}
348 
349 	static String infoRefsResultType(String svc) {
350 		return "application/x-" + svc + "-advertisement";
351 	}
352 
353 	/**
354 	 * Strip the Git service suffix from a request path.
355 	 *
356 	 * Generally the suffix is stripped by the {@code SuffixPipeline} handling
357 	 * the request, so this method is rarely needed.
358 	 *
359 	 * @param path
360 	 *            the path of the request.
361 	 * @return the path up to the last path component before the service suffix;
362 	 *         the path as-is if it contains no service suffix.
363 	 */
364 	public static String stripServiceSuffix(String path) {
365 		for (String suffix : SERVICE_SUFFIXES) {
366 			if (path.endsWith(suffix))
367 				return path.substring(0, path.length() - suffix.length());
368 		}
369 		return path;
370 	}
371 
372 	/**
373 	 * Check if the HTTP request was for the /info/refs?service= Git handler.
374 	 *
375 	 * @param req
376 	 *            current request.
377 	 * @return true if the request is for the /info/refs service.
378 	 */
379 	public static boolean isInfoRefs(HttpServletRequest req) {
380 		return req.getRequestURI().endsWith(INFO_REFS_PATH)
381 				&& VALID_SERVICES.contains(req.getParameter("service"));
382 	}
383 
384 	/**
385 	 * Check if the HTTP request path ends with the /git-upload-pack handler.
386 	 *
387 	 * @param pathOrUri
388 	 *            path or URI of the request.
389 	 * @return true if the request is for the /git-upload-pack handler.
390 	 */
391 	public static boolean isUploadPack(String pathOrUri) {
392 		return pathOrUri != null && pathOrUri.endsWith(UPLOAD_PACK_PATH);
393 	}
394 
395 	/**
396 	 * Check if the HTTP request was for the /git-upload-pack Git handler.
397 	 *
398 	 * @param req
399 	 *            current request.
400 	 * @return true if the request is for the /git-upload-pack handler.
401 	 */
402 	public static boolean isUploadPack(HttpServletRequest req) {
403 		return isUploadPack(req.getRequestURI())
404 				&& UPLOAD_PACK_REQUEST_TYPE.equals(req.getContentType());
405 	}
406 
407 	/**
408 	 * Check if the HTTP request was for the /git-receive-pack Git handler.
409 	 *
410 	 * @param req
411 	 *            current request.
412 	 * @return true if the request is for the /git-receive-pack handler.
413 	 */
414 	public static boolean isReceivePack(HttpServletRequest req) {
415 		String uri = req.getRequestURI();
416 		return uri != null && uri.endsWith(RECEIVE_PACK_PATH)
417 				&& RECEIVE_PACK_REQUEST_TYPE.equals(req.getContentType());
418 	}
419 
420 	private GitSmartHttpTools() {
421 	}
422 }