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