package org.koshinuke.jgit.server;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectDatabase;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.storage.file.ObjectDirectory;
import org.eclipse.jgit.storage.file.PackFile;
import org.eclipse.jgit.transport.ReceivePack;
import org.eclipse.jgit.transport.UploadPack;
import org.koshinuke.conf.Configuration;
import org.koshinuke.jersey.auth.BasicAuth;
import org.koshinuke.model.KoshinukePrincipal;
import org.koshinuke.util.GitUtil;
import com.google.common.base.Function;
import com.sun.jersey.api.core.HttpContext;
import com.sun.jersey.api.core.HttpResponseContext;
import com.sun.jersey.spi.resource.Singleton;
/**
* @author taichi
* @see <a
* href="http://schacon.github.com/git/git-http-backend.html">git-http-backend(1)</a>
* @see <a
* href="https://github.com/gitster/git/blob/master/http-backend.c">http-backend.c</a>
*/
@BasicAuth
@Singleton
@Path("/{project: ([\\w\\-\\+\\.]|%[0-9a-fA-F]{2})+}/{repository: ([\\w\\-\\+\\.]|%[0-9a-fA-F]{2})+}.git/")
public class GitHttpdService {
public static final String UPLOAD_PACK = "git-upload-pack";
public static final String RECEIVE_PACK = "git-receive-pack";
static final String SUB_TYPE_UPD = "x-" + UPLOAD_PACK;
static final String SUB_TYPE_RCV = "x-" + RECEIVE_PACK;
static final String CT_UPD = "application/" + SUB_TYPE_UPD;
static final String CT_RCV = "application/" + SUB_TYPE_RCV;
Map<String, InfoRefsAction> actions = new HashMap<>(2);
{
this.actions.put(UPLOAD_PACK, new InfoRefsAction() {
@Override
public Response execute(KoshinukePrincipal principal,
String project, String repository) {
return GitHttpdService.this.uploadPackInfo(project, repository);
}
});
this.actions.put(RECEIVE_PACK, new InfoRefsAction() {
@Override
public Response execute(KoshinukePrincipal principal,
String project, String repository) {
return GitHttpdService.this.receivePackInfo(principal, project,
repository);
}
});
}
protected final Configuration config;
public GitHttpdService(@Context Configuration config) {
this.config = config;
}
@POST
@Path(UPLOAD_PACK)
@Consumes(CT_UPD + "-request")
public Response uploadPack(final @Context HttpContext context,
@PathParam("project") String project,
@PathParam("repository") String repository) {
return GitUtil.handleLocal(this.config.getRepositoryRootDir(), project,
repository, new Function<Repository, Response>() {
@Override
public Response apply(Repository input) {
if (GitHttpdService.this.isEnabledUploadPack(input)) {
UploadPack pack = GitHttpdService.this
.makeUploadPack(input);
try {
HttpResponseContext response = context
.getResponse();
response.setResponse(noCache(null).type(
CT_UPD + "-result").build());
OutputStream out = response.getOutputStream();
InputStream in = context.getRequest()
.getEntity(InputStream.class);
pack.upload(in, out, null);
out.flush();
} catch (IOException e) {
throw new WebApplicationException(e);
} finally {
// PackFileのキャッシュを消す。
// UploadPackのどこかにメモリリークがある様だ。
// 関係ないキャッシュも消えるが、 メモリリークでFDを消費しきるよりは遥かにマシ。
GitUtil.clearCache();
}
return null;
}
return Response.status(Status.FORBIDDEN).build();
}
});
}
@POST
@Path(RECEIVE_PACK)
@Consumes(CT_RCV + "-request")
public Response receivePack(final @Context HttpContext context,
final @Context KoshinukePrincipal principal,
@PathParam("project") String project,
@PathParam("repository") String repository) {
return GitUtil.handleLocal(this.config.getRepositoryRootDir(), project,
repository, new Function<Repository, Response>() {
@Override
public Response apply(Repository input) {
if (GitHttpdService.this.isEnabledReceivePack(input)) {
ReceivePack pack = GitHttpdService.this
.makeReceivePack(principal, input);
try {
pack.setBiDirectionalPipe(false);
HttpResponseContext response = context
.getResponse();
response.setResponse(noCache(null).type(
CT_RCV + "-result").build());
OutputStream out = response.getOutputStream();
InputStream in = context.getRequest()
.getEntity(InputStream.class);
pack.receive(in, out, null);
out.flush();
} catch (IOException e) {
throw new WebApplicationException(e);
}
return null;
}
return Response.status(Status.FORBIDDEN).build();
}
});
}
@GET
@Path(Constants.INFO_REFS)
public Response infoRefs(@Context KoshinukePrincipal principal,
@PathParam("project") String project,
@PathParam("repository") String repository,
@QueryParam("service") String service) throws IOException {
InfoRefsAction action = this.actions.get(service);
if (action != null) {
return action.execute(principal, project, repository);
}
return this.buildResponse(project, repository,
new Function<Repository, Response>() {
@Override
public Response apply(Repository input) {
return noCache(new EachRefPack(input)).type(
MediaType.TEXT_PLAIN).build();
}
});
}
public static final MediaType UPLOAD_PACK_INFO = new MediaType(
"application", SUB_TYPE_UPD + "-advertisement");
public static final MediaType RECEIVE_PACK_INFO = new MediaType(
"application", SUB_TYPE_RCV + "-advertisement");
protected Response uploadPackInfo(String project, String repository) {
return GitUtil.handleLocal(this.config.getRepositoryRootDir(), project,
repository, new Function<Repository, Response>() {
@Override
public Response apply(Repository input) {
if (GitHttpdService.this.isEnabledUploadPack(input)) {
return noCache(
GitHttpdService.this.makeUploadPack(input))
.type(UPLOAD_PACK_INFO).build();
}
return Response.status(Status.FORBIDDEN).build();
}
});
}
protected Response receivePackInfo(final KoshinukePrincipal p,
String project, String repository) {
return GitUtil.handleLocal(this.config.getRepositoryRootDir(), project,
repository, new Function<Repository, Response>() {
@Override
public Response apply(Repository input) {
if (GitHttpdService.this.isEnabledReceivePack(input)) {
ReceivePack rp = GitHttpdService.this
.makeReceivePack(p, input);
return noCache(rp).type(RECEIVE_PACK_INFO).build();
}
return Response.status(Status.FORBIDDEN).build();
}
});
}
@GET
@Path("{file: HEAD|objects/info/(http\\-)?alternates}")
@Produces("text/plain; charset=utf-8")
public Response infoFile(@PathParam("project") String project,
@PathParam("repository") String repository,
@PathParam("file") String file) {
return this.buildFileResponse(project, repository, file,
new Function<File, Response>() {
@Override
public Response apply(File input) {
return noCache(input).build();
}
});
}
public static ResponseBuilder noCache(Object entity) {
return Response
.ok()
.entity(entity)
.expires(new Date(0))
.header("Pragma", "no-cache")
.cacheControl(
CacheControl
.valueOf("no-cache, max-age=0, must-revalidate"));
}
@GET
@Path("objects/info/packs")
@Produces("text/plain; charset=utf-8")
public Response infoPacks(@PathParam("project") String project,
@PathParam("repository") String repository) {
return this.buildResponse(project, repository,
new Function<Repository, Response>() {
@Override
public Response apply(Repository input) {
StringBuilder stb = new StringBuilder();
ObjectDatabase odb = input.getObjectDatabase();
if (odb instanceof ObjectDirectory) {
ObjectDirectory dir = (ObjectDirectory) odb;
for (PackFile pack : dir.getPacks()) {
stb.append("P ");
stb.append(pack.getPackFile().getName());
stb.append('\n');
}
}
stb.append('\n');
return noCache(stb.toString()).build();
}
});
}
@GET
@Path("objects/{path: [0-9a-f]{2}/[0-9a-f]{38}}")
@Produces("application/x-git-loose-object")
public Response objectsLoose(@PathParam("project") String project,
@PathParam("repository") String repository,
@PathParam("path") String path) {
return this.buildObjectsResponse(project, repository, path);
}
static final String packPath = "objects/{path: pack/pack-[0-9a-f]{40}}\\.";
static final String CT_PACK = "application/x-git-packed-objects";
@GET
@Path(packPath + "pack}")
@Produces(CT_PACK)
public Response objectsPack(@PathParam("project") String project,
@PathParam("repository") String repository,
@PathParam("path") String path) {
return this.buildObjectsResponse(project, repository, path);
}
@GET
@Path(packPath + "idx}")
@Produces(CT_PACK + "-toc")
public Response objectsIdx(@PathParam("project") String project,
@PathParam("repository") String repository,
@PathParam("path") String path) {
return this.buildObjectsResponse(project, repository, path);
}
protected Response buildObjectsResponse(String project, String repository,
final String path) {
return this.buildFileResponse(project, repository, path,
new Function<File, Response>() {
@Override
public Response apply(File input) {
Calendar c = Calendar.getInstance();
Date now = c.getTime();
c.add(Calendar.YEAR, 1);
return Response
.ok(input)
.header(HttpHeaders.DATE, now)
.expires(c.getTime())
.cacheControl(
CacheControl
.valueOf("public, max-age=31536000"))
.build();
}
});
}
protected Response buildFileResponse(String project, String repository,
final String path, final Function<File, Response> handler) {
return this.buildResponse(project, repository,
new Function<Repository, Response>() {
@Override
public Response apply(Repository input) {
File odir = ((ObjectDirectory) input
.getObjectDatabase()).getDirectory();
File f = new File(odir, path);
if (f.exists() == false) {
return Response.status(Status.NOT_FOUND).build();
}
return handler.apply(f);
}
});
}
protected Response buildResponse(String project, String repository,
final Function<Repository, Response> handler) {
return GitUtil.handleLocal(this.config.getRepositoryRootDir(), project,
repository, new Function<Repository, Response>() {
@Override
public Response apply(Repository input) {
if (GitHttpdService.this.isEnabledGetanyfile(input) == false) {
return Response.status(Status.FORBIDDEN).build();
}
return handler.apply(input);
}
});
}
protected boolean isEnabledGetanyfile(Repository repository) {
return repository.getConfig().getBoolean("http", "getanyfile", true);
}
protected boolean isEnabledUploadPack(Repository repository) {
return repository.getConfig().getBoolean("http", "uploadpack", true);
}
protected boolean isEnabledReceivePack(Repository repository) {
return repository.getConfig().getBoolean("http", "receivepack", false);
}
protected UploadPack makeUploadPack(Repository repository) {
UploadPack pack = new UploadPack(repository);
pack.setBiDirectionalPipe(false);
return pack;
}
protected ReceivePack makeReceivePack(KoshinukePrincipal p,
Repository repository) {
ReceivePack rp = new ReceivePack(repository);
rp.setRefLogIdent(new PersonIdent(p.getName(), p.getMail()));
return rp;
}
}