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 javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
47  import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
48  import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
49  import static org.eclipse.jgit.http.server.ServletUtils.ATTRIBUTE_HANDLER;
50  import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_SIDE_BAND_64K;
51  import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDE_BAND;
52  import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDE_BAND_64K;
53  import static org.eclipse.jgit.transport.SideBandOutputStream.CH_ERROR;
54  import static org.eclipse.jgit.transport.SideBandOutputStream.SMALL_BUF;
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.internal.transport.parser.FirstCommand;
67  import org.eclipse.jgit.internal.transport.parser.FirstWant;
68  import org.eclipse.jgit.lib.Constants;
69  import org.eclipse.jgit.transport.PacketLineIn;
70  import org.eclipse.jgit.transport.PacketLineOut;
71  import org.eclipse.jgit.transport.ReceivePack;
72  import org.eclipse.jgit.transport.RequestNotYetReadException;
73  import org.eclipse.jgit.transport.SideBandOutputStream;
74  import org.eclipse.jgit.transport.UploadPack;
75  
76  /**
77   * Utility functions for handling the Git-over-HTTP protocol.
78   */
79  public class GitSmartHttpTools {
80  	private static final String INFO_REFS = Constants.INFO_REFS;
81  
82  	/** Name of the git-upload-pack service. */
83  	public static final String UPLOAD_PACK = "git-upload-pack";
84  
85  	/** Name of the git-receive-pack service. */
86  	public static final String RECEIVE_PACK = "git-receive-pack";
87  
88  	/** Content type supplied by the client to the /git-upload-pack handler. */
89  	public static final String UPLOAD_PACK_REQUEST_TYPE =
90  			"application/x-git-upload-pack-request";
91  
92  	/** Content type returned from the /git-upload-pack handler. */
93  	public static final String UPLOAD_PACK_RESULT_TYPE =
94  			"application/x-git-upload-pack-result";
95  
96  	/** Content type supplied by the client to the /git-receive-pack handler. */
97  	public static final String RECEIVE_PACK_REQUEST_TYPE =
98  			"application/x-git-receive-pack-request";
99  
100 	/** Content type returned from the /git-receive-pack handler. */
101 	public static final String RECEIVE_PACK_RESULT_TYPE =
102 			"application/x-git-receive-pack-result";
103 
104 	/** Git service names accepted by the /info/refs?service= handler. */
105 	public static final List<String> VALID_SERVICES =
106 			Collections.unmodifiableList(Arrays.asList(new String[] {
107 					UPLOAD_PACK, RECEIVE_PACK }));
108 
109 	private static final String INFO_REFS_PATH = "/" + INFO_REFS;
110 	private static final String UPLOAD_PACK_PATH = "/" + UPLOAD_PACK;
111 	private static final String RECEIVE_PACK_PATH = "/" + RECEIVE_PACK;
112 
113 	private static final List<String> SERVICE_SUFFIXES =
114 			Collections.unmodifiableList(Arrays.asList(new String[] {
115 					INFO_REFS_PATH, UPLOAD_PACK_PATH, RECEIVE_PACK_PATH }));
116 
117 	/**
118 	 * Check a request for Git-over-HTTP indicators.
119 	 *
120 	 * @param req
121 	 *            the current HTTP request that may have been made by Git.
122 	 * @return true if the request is likely made by a Git client program.
123 	 */
124 	public static boolean isGitClient(HttpServletRequest req) {
125 		return isInfoRefs(req) || isUploadPack(req) || isReceivePack(req);
126 	}
127 
128 	/**
129 	 * Send an error to the Git client or browser.
130 	 * <p>
131 	 * Server implementors may use this method to send customized error messages
132 	 * to a Git protocol client using an HTTP 200 OK response with the error
133 	 * embedded in the payload. If the request was not issued by a Git client,
134 	 * an HTTP response code is returned instead.
135 	 *
136 	 * @param req
137 	 *            current request.
138 	 * @param res
139 	 *            current response.
140 	 * @param httpStatus
141 	 *            HTTP status code to set if the client is not a Git client.
142 	 * @throws IOException
143 	 *             the response cannot be sent.
144 	 */
145 	public static void sendError(HttpServletRequest req,
146 			HttpServletResponse res, int httpStatus) throws IOException {
147 		sendError(req, res, httpStatus, null);
148 	}
149 
150 	/**
151 	 * Send an error to the Git client or browser.
152 	 * <p>
153 	 * Server implementors may use this method to send customized error messages
154 	 * to a Git protocol client using an HTTP 200 OK response with the error
155 	 * embedded in the payload. If the request was not issued by a Git client,
156 	 * an HTTP response code is returned instead.
157 	 * <p>
158 	 * This method may only be called before handing off the request to
159 	 * {@link org.eclipse.jgit.transport.UploadPack#upload(java.io.InputStream, OutputStream, OutputStream)}
160 	 * or
161 	 * {@link org.eclipse.jgit.transport.ReceivePack#receive(java.io.InputStream, OutputStream, OutputStream)}.
162 	 *
163 	 * @param req
164 	 *            current request.
165 	 * @param res
166 	 *            current response.
167 	 * @param httpStatus
168 	 *            HTTP status code to set if the client is not a Git client.
169 	 * @param textForGit
170 	 *            plain text message to display on the user's console. This is
171 	 *            shown only if the client is likely to be a Git client. If null
172 	 *            or the empty string a default text is chosen based on the HTTP
173 	 *            response code.
174 	 * @throws IOException
175 	 *             the response cannot be sent.
176 	 */
177 	public static void sendError(HttpServletRequest req,
178 			HttpServletResponse res, int httpStatus, String textForGit)
179 			throws IOException {
180 		if (textForGit == null || textForGit.length() == 0) {
181 			switch (httpStatus) {
182 			case SC_FORBIDDEN:
183 				textForGit = HttpServerText.get().repositoryAccessForbidden;
184 				break;
185 			case SC_NOT_FOUND:
186 				textForGit = HttpServerText.get().repositoryNotFound;
187 				break;
188 			case SC_INTERNAL_SERVER_ERROR:
189 				textForGit = HttpServerText.get().internalServerError;
190 				break;
191 			default:
192 				textForGit = "HTTP " + httpStatus;
193 				break;
194 			}
195 		}
196 
197 		if (isInfoRefs(req)) {
198 			sendInfoRefsError(req, res, textForGit);
199 		} else if (isUploadPack(req)) {
200 			sendUploadPackError(req, res, textForGit);
201 		} else if (isReceivePack(req)) {
202 			sendReceivePackError(req, res, textForGit);
203 		} else {
204 			if (httpStatus < 400)
205 				ServletUtils.consumeRequestBody(req);
206 			res.sendError(httpStatus, textForGit);
207 		}
208 	}
209 
210 	private static void sendInfoRefsError(HttpServletRequest req,
211 			HttpServletResponse res, String textForGit) throws IOException {
212 		ByteArrayOutputStream buf = new ByteArrayOutputStream(128);
213 		PacketLineOut pck = new PacketLineOut(buf);
214 		String svc = req.getParameter("service");
215 		pck.writeString("# service=" + svc + "\n");
216 		pck.end();
217 		pck.writeString("ERR " + textForGit);
218 		send(req, res, infoRefsResultType(svc), buf.toByteArray());
219 	}
220 
221 	private static void sendUploadPackError(HttpServletRequest req,
222 			HttpServletResponse res, String textForGit) throws IOException {
223 		ByteArrayOutputStream buf = new ByteArrayOutputStream(128);
224 		PacketLineOut pckOut = new PacketLineOut(buf);
225 
226 		boolean sideband;
227 		UploadPack up = (UploadPack) req.getAttribute(ATTRIBUTE_HANDLER);
228 		if (up != null) {
229 			try {
230 				sideband = up.isSideBand();
231 			} catch (RequestNotYetReadException e) {
232 				sideband = isUploadPackSideBand(req);
233 			}
234 		} else
235 			sideband = isUploadPackSideBand(req);
236 
237 		if (sideband)
238 			writeSideBand(buf, textForGit);
239 		else
240 			writePacket(pckOut, textForGit);
241 		send(req, res, UPLOAD_PACK_RESULT_TYPE, buf.toByteArray());
242 	}
243 
244 	private static boolean isUploadPackSideBand(HttpServletRequest req) {
245 		try {
246 			// The client may be in a state where they have sent the sideband
247 			// capability and are expecting a response in the sideband, but we might
248 			// not have an UploadPack, or it might not have read any of the request.
249 			// So, cheat and read the first line.
250 			String line = new PacketLineIn(req.getInputStream()).readString();
251 			FirstWant parsed = FirstWant.fromLine(line);
252 			return (parsed.getCapabilities().contains(OPTION_SIDE_BAND)
253 					|| parsed.getCapabilities().contains(OPTION_SIDE_BAND_64K));
254 		} catch (IOException e) {
255 			// Probably the connection is closed and a subsequent write will fail, but
256 			// try it just in case.
257 			return false;
258 		}
259 	}
260 
261 	private static void sendReceivePackError(HttpServletRequest req,
262 			HttpServletResponse res, String textForGit) throws IOException {
263 		ByteArrayOutputStream buf = new ByteArrayOutputStream(128);
264 		PacketLineOut pckOut = new PacketLineOut(buf);
265 
266 		boolean sideband;
267 		ReceivePack rp = (ReceivePack) req.getAttribute(ATTRIBUTE_HANDLER);
268 		if (rp != null) {
269 			try {
270 				sideband = rp.isSideBand();
271 			} catch (RequestNotYetReadException e) {
272 				sideband = isReceivePackSideBand(req);
273 			}
274 		} else
275 			sideband = isReceivePackSideBand(req);
276 
277 		if (sideband)
278 			writeSideBand(buf, textForGit);
279 		else
280 			writePacket(pckOut, textForGit);
281 		send(req, res, RECEIVE_PACK_RESULT_TYPE, buf.toByteArray());
282 	}
283 
284 	private static boolean isReceivePackSideBand(HttpServletRequest req) {
285 		try {
286 			// The client may be in a state where they have sent the sideband
287 			// capability and are expecting a response in the sideband, but we might
288 			// not have a ReceivePack, or it might not have read any of the request.
289 			// So, cheat and read the first line.
290 			String line = new PacketLineIn(req.getInputStream()).readString();
291 			FirstCommand parsed = FirstCommand.fromLine(line);
292 			return parsed.getCapabilities().contains(CAPABILITY_SIDE_BAND_64K);
293 		} catch (IOException e) {
294 			// Probably the connection is closed and a subsequent write will fail, but
295 			// try it just in case.
296 			return false;
297 		}
298 	}
299 
300 	private static void writeSideBand(OutputStream out, String textForGit)
301 			throws IOException {
302 		try (OutputStream msg = new SideBandOutputStream(CH_ERROR, SMALL_BUF,
303 				out)) {
304 			msg.write(Constants.encode("error: " + textForGit));
305 			msg.flush();
306 		}
307 	}
308 
309 	private static void writePacket(PacketLineOut pckOut, String textForGit)
310 			throws IOException {
311 		pckOut.writeString("error: " + textForGit);
312 	}
313 
314 	private static void send(HttpServletRequest req, HttpServletResponse res,
315 			String type, byte[] buf) throws IOException {
316 		ServletUtils.consumeRequestBody(req);
317 		res.setStatus(HttpServletResponse.SC_OK);
318 		res.setContentType(type);
319 		res.setContentLength(buf.length);
320 		try (OutputStream os = res.getOutputStream()) {
321 			os.write(buf);
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 }