package org.dcache.webdav;
import com.google.common.collect.ImmutableList;
import io.milton.http.Auth;
import io.milton.http.HttpManager;
import io.milton.servlet.ServletRequest;
import io.milton.servlet.ServletResponse;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.ContextHandler;
import javax.security.auth.Subject;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.AccessController;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import dmg.cells.nucleus.CDC;
import dmg.cells.nucleus.CellAddressCore;
import dmg.cells.nucleus.CellIdentityAware;
import org.dcache.auth.Subjects;
import org.dcache.util.Transfer;
import static com.google.common.base.Preconditions.checkArgument;
/**
* A Jetty handler that wraps a Milton HttpManager. Makes it possible
* to embed Milton in Jetty without using the Milton servlet.
*/
public class MiltonHandler
extends AbstractHandler
implements CellIdentityAware
{
private static final ImmutableList<String> ALLOWED_ORIGIN_PROTOCOL = ImmutableList.of("http", "https");
private HttpManager _httpManager;
private CellAddressCore _myAddress;
private List<String> _allowedClientOrigins;
public void setHttpManager(HttpManager httpManager)
{
_httpManager = httpManager;
}
public void setAllowedClientOrigins(String origins)
{
if (origins.isEmpty()) {
_allowedClientOrigins = Collections.emptyList();
} else {
List<String> originList = Arrays.stream(origins.split(","))
.map(String::trim)
.collect(Collectors.toList());
originList.forEach(MiltonHandler::checkOrigin);
_allowedClientOrigins = originList;
}
}
private static void checkOrigin(String s)
{
try {
URI uri = new URI(s);
checkArgument(ALLOWED_ORIGIN_PROTOCOL.contains(uri.toURL().getProtocol()), "Invalid URL: The URL: %s " +
"contain unsupported protocol. Use either http or https.", s);
checkArgument(!uri.getHost().isEmpty(), "Invalid URL: the host name is not provided in %s:", s);
checkArgument(uri.getUserInfo() == null, "The URL: %s is invalid. User information is not allowed " +
"to be part of the URL.", s);
checkArgument(uri.toURL().getPath().isEmpty(), "The URL: %s is invalid. Remove the \"path\" part of the " +
"URL.", s);
checkArgument(uri.toURL().getQuery() == null, "The URL: %s is invalid. Remove the query-path part of " +
"the URL.", s);
checkArgument(uri.toURL().getRef() == null, "URL: %s is invalid. Reason: no reference or fragment " +
"allowed in the URL.", s);
checkArgument(!uri.isOpaque(), "URL: %s is invalid. Check the scheme part of the URL", s);
} catch (MalformedURLException | URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
private void setCORSHeaders (HttpServletRequest request, HttpServletResponse response)
{
String clientOrigin = request.getHeader("origin");
if (Objects.equals(request.getMethod(), "OPTIONS")) {
response.setHeader("Access-Control-Allow-Methods", "PUT, DELETE");
response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Origin", clientOrigin);
if (_allowedClientOrigins.size() > 1) {
response.setHeader("Vary", "Origin");
}
} else {
response.setHeader("Access-Control-Allow-Origin", clientOrigin);
if (_allowedClientOrigins.size() > 1) {
response.setHeader("Vary", "Origin");
}
}
}
@Override
public void setCellAddress(CellAddressCore address)
{
_myAddress = address;
}
@Override
public void handle(String target, Request baseRequest,
HttpServletRequest request,HttpServletResponse response)
throws IOException, ServletException
{
try (CDC ignored = CDC.reset(_myAddress)) {
Transfer.initSession(false, false);
ServletContext context = ContextHandler.getCurrentContext();
String clientOrigin = request.getHeader("origin");
boolean isOriginAllow = _allowedClientOrigins.contains(clientOrigin);
if (isOriginAllow) {
setCORSHeaders(request, response);
}
switch (request.getMethod()) {
case "USERINFO":
response.sendError(501, "Not implemented");
break;
case "OPTIONS":
if (isOriginAllow) {
setCORSHeaders(request, response);
}
break;
default:
Subject subject = Subject.getSubject(AccessController.getContext());
ServletRequest req = new DcacheServletRequest(request, context);
ServletResponse resp = new DcacheServletResponse(response);
/* Although we don't rely on the authorization tag
* ourselves, Milton uses it to detect that the request
* was preauthenticated.
*/
req.setAuthorization(new Auth(Subjects.getUserName(subject), subject));
baseRequest.setHandled(true);
_httpManager.process(req, resp);
}
response.getOutputStream().flush();
response.flushBuffer();
}
}
/**
* Dcache specific subclass to workaround various Jetty/Milton problems.
*/
private class DcacheServletRequest extends ServletRequest {
public DcacheServletRequest(HttpServletRequest request,
ServletContext context) {
super(request, context);
}
@Override
public InputStream getInputStream() {
/* Jetty tells the client to continue uploading data as
* soon as the input stream is retrieved by the servlet.
* We want to redirect the request before that happens,
* hence we query the input stream lazily.
*/
return new InputStream() {
private InputStream inner;
private InputStream getRealInputStream() throws IOException
{
if (inner == null) {
inner = DcacheServletRequest.super.getInputStream();
}
return inner;
}
@Override
public int read() throws IOException
{
return getRealInputStream().read();
}
@Override
public int read(byte[] b) throws IOException
{
return getRealInputStream().read(b);
}
@Override
public int read(byte[] b, int off, int len)
throws IOException
{
return getRealInputStream().read(b, off, len);
}
@Override
public long skip(long n) throws IOException
{
return getRealInputStream().skip(n);
}
@Override
public int available() throws IOException
{
return getRealInputStream().available();
}
@Override
public void close() throws IOException
{
getRealInputStream().close();
}
@Override
public synchronized void mark(int readlimit)
{
throw new UnsupportedOperationException("Mark is unsupported");
}
@Override
public synchronized void reset() throws IOException
{
getRealInputStream().reset();
}
@Override
public boolean markSupported()
{
return false;
}
};
}
}
/**
* dCache specific subclass to workaround various Jetty/Milton problems.
*/
private class DcacheServletResponse extends ServletResponse
{
public DcacheServletResponse(HttpServletResponse r)
{
super(r);
}
@Override
public void setContentLengthHeader(Long length) {
/* If the length is unknown, Milton insists on
* setting an empty string value for the
* Content-Length header.
*
* Instead we want the Content-Length header
* to be skipped and rely on Jetty using
* chunked encoding.
*/
if (length != null) {
super.setContentLengthHeader(length);
}
}
}
}