Download patch file with /patch?zip or /patch?download

For ?zip compress the patch file text inside of a ZIP archive.
The inner file name is "commitsha1.diff". Modern UI shells on
Windows, Mac OS X and Linux make it easy to unpack the compressed
ZIP file to get access to the raw text.

For ?download a filename is suggested in the Content-Dispostion
response header, suggesting the browser to download the base64
encoded stream to the local drive as "commitsha1.diff.base64".

Encoding the patch is necessary to prevent XSS attacks made against
the Gerrit site. The ZIP wrapping does not allow an attacker to
make a valid Java applet; the filename ending in ".diff" is not
a valid Java class file name. The base64 wrapping can only be
treated as plain text by a browser as it does not contain HTML
special characters.

Change-Id: Ia4c41e51c5f57607c45e2588629a88b47e1d9d09
This commit is contained in:
David Ostrovsky 2013-08-22 00:24:51 -07:00 committed by Shawn Pearce
parent 888161bb20
commit 973f38bc4a
4 changed files with 70 additions and 2 deletions

View File

@ -1533,6 +1533,15 @@ The formatted patch is returned as text encoded inside base64:
RnJvbSA3ZGFkY2MxNTNmZGVhMTdhYTg0ZmYzMmE2ZTI0NWRiYjY...
----
Adding query parameter `zip` (for example `/changes/.../patch?zip`)
returns the patch as a single file inside of a ZIP archive. Clients
can expand the ZIP to obtain the plain text patch, avoiding the
need for a base64 decoding step. This option implies `download`.
Query parameter `download` (e.g. `/changes/.../patch?download`)
will suggest the browser save the patch as `commitsha1.diff.base64`,
for later processing by command line tools.
[[get-mergeable]]
Get Mergeable
~~~~~~~~~~~~~

View File

@ -62,6 +62,7 @@ public abstract class BinaryResult implements Closeable {
private long contentLength = -1;
private boolean gzip = true;
private boolean base64 = false;
private String attachmentName;
/** @return the MIME type of the result, for HTTP clients. */
public String getContentType() {
@ -89,6 +90,17 @@ public abstract class BinaryResult implements Closeable {
return this;
}
/** Get the attachment file name; null if not set. */
public String getAttachmentName() {
return attachmentName;
}
/** Set the attachment file name and return {@code this}. */
public BinaryResult setAttachmentName(String attachmentName) {
this.attachmentName = attachmentName;
return this;
}
/** @return length in bytes of the result; -1 if not known. */
public long getContentLength() {
return contentLength;

View File

@ -695,6 +695,11 @@ public class RestApiServlet extends HttpServlet {
BinaryResult bin) throws IOException {
final BinaryResult appResult = bin;
try {
if (bin.getAttachmentName() != null) {
res.setHeader(
"Content-Disposition",
"attachment; filename=\"" + bin.getAttachmentName() + "\"");
}
if (bin.isBase64()) {
bin = stackBase64(res, bin);
}

View File

@ -25,21 +25,31 @@ import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.kohsuke.args4j.Option;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class GetPatch implements RestReadView<RevisionResource> {
private final GitRepositoryManager repoManager;
@Option(name = "--zip")
private boolean zip;
@Option(name = "--download")
private boolean download;
@Inject
GetPatch(GitRepositoryManager repoManager) {
this.repoManager = repoManager;
@ -71,6 +81,20 @@ public class GetPatch implements RestReadView<RevisionResource> {
BinaryResult bin = new BinaryResult() {
@Override
public void writeTo(OutputStream out) throws IOException {
if (zip) {
ZipOutputStream zos = new ZipOutputStream(out);
ZipEntry e = new ZipEntry(fileName(rw, commit));
e.setTime(commit.getCommitTime() * 1000L);
zos.putNextEntry(e);
format(zos);
zos.closeEntry();
zos.finish();
} else {
format(out);
}
}
private void format(OutputStream out) throws IOException {
out.write(formatEmailHeader(commit).getBytes(UTF_8));
DiffFormatter fmt = new DiffFormatter(out);
fmt.setRepository(repo);
@ -83,8 +107,20 @@ public class GetPatch implements RestReadView<RevisionResource> {
rw.release();
repo.close();
}
}.setContentType("application/mbox")
.base64();
};
if (zip) {
bin.disableGzip()
.setContentType("application/zip")
.setAttachmentName(fileName(rw, commit) + ".zip");
} else {
bin.base64()
.setContentType("application/mbox")
.setAttachmentName(download
? fileName(rw, commit) + ".base64"
: null);
}
close = false;
return bin;
} finally {
@ -132,4 +168,10 @@ public class GetPatch implements RestReadView<RevisionResource> {
df.setCalendar(Calendar.getInstance(author.getTimeZone(), Locale.US));
return df.format(author.getWhen());
}
private static String fileName(RevWalk rw, RevCommit commit)
throws IOException {
AbbreviatedObjectId id = rw.getObjectReader().abbreviate(commit, 8);
return id.name() + ".diff";
}
}