#970 initial support of mercurials httppostargs protocol

This commit is contained in:
Sebastian Sdorra
2018-03-30 11:20:22 +02:00
parent a34acd8ed4
commit b43e406b76
7 changed files with 279 additions and 21 deletions

View File

@@ -38,16 +38,17 @@ package sonia.scm.web;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.RepositoryProvider;
import sonia.scm.web.filter.ProviderPermissionFilter;
//~--- JDK imports ------------------------------------------------------------
import java.util.Set;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Set;
/**
* Permission filter for mercurial repositories.
@@ -60,6 +61,8 @@ public class HgPermissionFilter extends ProviderPermissionFilter
private static final Set<String> READ_METHODS = ImmutableSet.of("GET", "HEAD", "OPTIONS", "TRACE");
private final HgRepositoryHandler repositoryHandler;
/**
* Constructs a new instance.
*
@@ -67,17 +70,36 @@ public class HgPermissionFilter extends ProviderPermissionFilter
* @param repositoryProvider repository provider
*/
@Inject
public HgPermissionFilter(ScmConfiguration configuration,
RepositoryProvider repositoryProvider)
public HgPermissionFilter(ScmConfiguration configuration, RepositoryProvider repositoryProvider, HgRepositoryHandler repositoryHandler)
{
super(configuration, repositoryProvider);
this.repositoryHandler = repositoryHandler;
}
//~--- get methods ----------------------------------------------------------
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
HgServletRequest hgRequest = new HgServletRequest(request);
super.doFilter(hgRequest, response, chain);
// TODO closing stream in case of fire?
}
@Override
protected boolean isWriteRequest(HttpServletRequest request)
{
if (repositoryHandler.getConfig().isEnableHttpPostArgs()) {
return isHttpPostArgsWriteRequest(request);
}
return isDefaultWriteRequest(request);
}
private boolean isHttpPostArgsWriteRequest(HttpServletRequest request) {
return WireProtocol.isWriteRequest(request);
}
private boolean isDefaultWriteRequest(HttpServletRequest request) {
if (READ_METHODS.contains(request.getMethod())) {
return WireProtocol.isWriteRequest(request);
}

View File

@@ -0,0 +1,55 @@
package sonia.scm.web;
import com.google.common.base.Preconditions;
import javax.servlet.ServletInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
/**
* HgServletInputStream is a wrapper around the original {@link ServletInputStream} and provides some extra
* functionality to support the mercurial client.
*/
public class HgServletInputStream extends ServletInputStream {
private final ServletInputStream original;
private ByteArrayInputStream captured;
HgServletInputStream(ServletInputStream original) {
this.original = original;
}
/**
* Reads the given amount of bytes from the stream and captures them, if the {@link #read()} methods is called the
* captured bytes are returned before the rest of the stream.
*
* @param size amount of bytes to read
*
* @return byte array
*
* @throws IOException if the method is called twice
*/
public byte[] readAndCapture(int size) throws IOException {
Preconditions.checkState(captured == null, "readAndCapture can only be called once per request");
// TODO should we enforce a limit? to prevent OOM?
byte[] bytes = new byte[size];
original.read(bytes);
captured = new ByteArrayInputStream(bytes);
return bytes;
}
@Override
public int read() throws IOException {
if (captured != null && captured.available() > 0) {
return captured.read();
}
return original.read();
}
@Override
public void close() throws IOException {
original.close();
}
}

View File

@@ -0,0 +1,31 @@
package sonia.scm.web;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
/**
* {@link HttpServletRequestWrapper} which adds some functionality in order to support the mercurial client.
*/
public final class HgServletRequest extends HttpServletRequestWrapper {
private HgServletInputStream hgServletInputStream;
/**
* Constructs a request object wrapping the given request.
*
* @param request
* @throws IllegalArgumentException if the request is null
*/
public HgServletRequest(HttpServletRequest request) {
super(request);
}
@Override
public HgServletInputStream getInputStream() throws IOException {
if (hgServletInputStream == null) {
hgServletInputStream = new HgServletInputStream(super.getInputStream());
}
return hgServletInputStream;
}
}

View File

@@ -33,14 +33,17 @@
package sonia.scm.web;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.util.HttpUtil;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.*;
/**
@@ -73,7 +76,10 @@ public final class WireProtocol {
* - no command was specified with the request (is required for the hgweb ui)
* - the command in the query string was found in the list of read request
* - if query string contains the batch command, then all commands specified in X-HgArg headers must be
* in the list of read request
* in the list of read requests
* - in case of enabled HttpPostArgs protocol and query string container the batch command, the header X-HgArgs-Post
* is read and the commands which are specified in the body from 0 to the value of X-HgArgs-Post must be in the list
* of read requests
*
* @param request http request
*
@@ -94,16 +100,40 @@ public final class WireProtocol {
@VisibleForTesting
static List<String> commandsOf(HttpServletRequest request) {
List<String> listOfCmds = Lists.newArrayList();
String cmd = getCommandFromQueryString(request);
if (cmd != null) {
listOfCmds.add(cmd);
if (isBatchCommand(cmd)) {
parseHgArgHeaders(request, listOfCmds);
handleHttpPostArgs(request, listOfCmds);
}
}
return Collections.unmodifiableList(listOfCmds);
}
private static void handleHttpPostArgs(HttpServletRequest request, List<String> listOfCmds) {
int hgArgsPostSize = request.getIntHeader("X-HgArgs-Post");
if (hgArgsPostSize > 0) {
if (request instanceof HgServletRequest) {
HgServletRequest hgRequest = (HgServletRequest) request;
try {
byte[] bytes = hgRequest.getInputStream().readAndCapture(hgArgsPostSize);
String hgArgs = new String(bytes, Charsets.US_ASCII);
String decoded = decodeValue(hgArgs);
parseHgCommandHeader(listOfCmds, decoded);
} catch (IOException ex) {
throw Throwables.propagate(ex);
}
} else {
throw new IllegalArgumentException("could not process the httppostargs protocol without HgServletRequest");
}
}
}
private static void parseHgArgHeaders(HttpServletRequest request, List<String> listOfCmds) {
Enumeration headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
@@ -115,9 +145,13 @@ public final class WireProtocol {
private static void parseHgArgHeader(HttpServletRequest request, List<String> listOfCmds, String header) {
if (isHgArgHeader(header)) {
String value = getHeaderDecoded(request, header);
if (isHgArgCommandHeader(value)) {
parseHgCommandHeader(listOfCmds, value);
}
parseHgArgValue(listOfCmds, value);
}
}
private static void parseHgArgValue(List<String> listOfCmds, String value) {
if (isHgArgCommandHeader(value)) {
parseHgCommandHeader(listOfCmds, value);
}
}
@@ -143,7 +177,11 @@ public final class WireProtocol {
}
private static String getHeaderDecoded(HttpServletRequest request, String header) {
return HttpUtil.decode(Strings.nullToEmpty(request.getHeader(header)));
return decodeValue(request.getHeader(header));
}
private static String decodeValue(String value) {
return HttpUtil.decode(Strings.nullToEmpty(value));
}
private static boolean isHgArgHeader(String header) {