/* dCache - http://www.dcache.org/ * * Copyright (C) 2014 Deutsches Elektronen-Synchrotron * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.dcache.webdav.transfer; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.ListenableFuture; import eu.emi.security.authn.x509.X509Credential; import io.milton.http.Response; import io.milton.http.Response.Status; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.HttpConnection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Required; import javax.annotation.Nullable; import javax.security.auth.Subject; import java.io.OutputStream; import java.io.PrintWriter; import java.net.InetSocketAddress; import java.net.URI; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.Set; import diskCacheV111.services.TransferManagerHandler; import diskCacheV111.util.CacheException; import diskCacheV111.util.FsPath; import diskCacheV111.util.TimeoutCacheException; import diskCacheV111.vehicles.IoJobInfo; import diskCacheV111.vehicles.IpProtocolInfo; import diskCacheV111.vehicles.RemoteHttpDataTransferProtocolInfo; import diskCacheV111.vehicles.RemoteHttpsDataTransferProtocolInfo; import diskCacheV111.vehicles.transferManager.CancelTransferMessage; import diskCacheV111.vehicles.transferManager.RemoteGsiftpTransferProtocolInfo; import diskCacheV111.vehicles.transferManager.RemoteTransferManagerMessage; import diskCacheV111.vehicles.transferManager.TransferCompleteMessage; import diskCacheV111.vehicles.transferManager.TransferFailedMessage; import diskCacheV111.vehicles.transferManager.TransferStatusQueryMessage; import dmg.cells.nucleus.CellMessageReceiver; import dmg.cells.nucleus.NoRouteToCellException; import org.dcache.auth.OpenIdCredential; import org.dcache.auth.StaticOpenIdCredential; import org.dcache.auth.attributes.Restriction; import org.dcache.cells.CellStub; import org.dcache.webdav.transfer.CopyFilter.CredentialSource; import static com.google.common.base.Preconditions.checkArgument; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.dcache.util.ByteUnit.MiB; import static org.dcache.webdav.transfer.CopyFilter.CredentialSource.*; /** * This class provides the basis for interactions with the remotetransfer * service. It is used by the CopyFilter to manage a requested transfer and * to provide feedback on that transfer in the form of performance markers. * <p> * The performance markers are similar to those provided during an FTP * transfer. They have the form: * <code> * Perf Marker * Timestamp: 1360578938 * Stripe Index: 0 * Stripe Bytes Transferred: 49397760 * Total Stripe Count: 2 * End * </code> * * Once the transfer has completed successfully, {@code success: Created} is * reported. On failure {@code failure: <explanation>} is returned. * <p> * Although the third-party transfer protocol, described in CopyFilter is * documented as supporting 'https' URIs, this implementation supports * different transports for the third-party transfer. */ public class RemoteTransferHandler implements CellMessageReceiver { /** * The different directions that the data will travel. */ public enum Direction { /** Request to pull data from remote site. */ PULL("Source"), /** Request to push data to some remote site. */ PUSH("Destination"); private final String header; Direction(String header) { this.header = header; } public String getHeaderName() { return header; } } /** * The different transport schemes supported. */ public enum TransferType { GSIFTP("gsiftp", 2811, GRIDSITE, EnumSet.noneOf(CredentialSource.class)), HTTP( "http", 80, NONE, EnumSet.noneOf(CredentialSource.class)), HTTPS( "https", 443, GRIDSITE, EnumSet.of(OIDC)); private static final ImmutableMap<String,TransferType> BY_SCHEME = ImmutableMap.of("gsiftp", GSIFTP, "http", HTTP, "https", HTTPS); private final int _defaultPort; private final CredentialSource _defaultCredentialSource; private final EnumSet<CredentialSource> _supported; private final String _scheme; TransferType(String scheme, int port, CredentialSource defaultSource, EnumSet<CredentialSource> additionalSources) { _defaultPort = port; _defaultCredentialSource = defaultSource; _supported = EnumSet.copyOf(additionalSources); _supported.add(defaultSource); _scheme = scheme; } public int getDefaultPort() { return _defaultPort; } public CredentialSource getDefaultCredentialSource() { return _defaultCredentialSource; } public boolean isSupported(CredentialSource source) { return _supported.contains(source); } public String getScheme() { return _scheme; } public static TransferType fromScheme(String scheme) { return BY_SCHEME.get(scheme.toLowerCase()); } public static Set<String> validSchemes() { return BY_SCHEME.keySet(); } } private enum TransferFlag { REQUIRE_VERIFICATION } private static final Logger LOG = LoggerFactory.getLogger(RemoteTransferHandler.class); private static final long DUMMY_LONG = 0; private static final String REQUEST_HEADER_VERIFICATION = "RequireChecksumVerification"; private static final String REQUEST_HEADER_TRANSFER_HEADER_PREFIX = "TransferHeader"; private final HashMap<Long,RemoteTransfer> _transfers = new HashMap<>(); private boolean _defaultVerification; private long _performanceMarkerPeriod; private CellStub _transferManager; @Required public void setTransferManagerStub(CellStub stub) { _transferManager = stub; } @Required public void setPerformanceMarkerPeroid(long peroid) { _performanceMarkerPeriod = peroid; } public long getPerformanceMarkerPeroid() { return _performanceMarkerPeriod; } @Required public void setDefaultVerification(boolean verify) { _defaultVerification = verify; } public boolean isDefaultVerification() { return _defaultVerification; } public void acceptRequest(OutputStream out, Map<String,String> requestHeaders, Subject subject, Restriction restriction, FsPath path, URI remote, Object credential, Direction direction) throws ErrorResponseException, InterruptedException { checkArgument(credential instanceof X509Credential || credential instanceof OpenIdCredential, "Credential not supported for Third-Party Transfer"); EnumSet<TransferFlag> flags = EnumSet.noneOf(TransferFlag.class); flags = addVerificationFlag(flags, requestHeaders); ImmutableMap<String,String> transferHeaders = buildTransferHeaders(requestHeaders); RemoteTransfer transfer = new RemoteTransfer(out, subject, restriction, path, remote, credential, flags, transferHeaders, direction); long id; synchronized (_transfers) { id = transfer.start(); _transfers.put(id, transfer); } try { transfer.awaitCompletion(); } finally { synchronized (_transfers) { _transfers.remove(id); } } } private EnumSet<TransferFlag> addVerificationFlag(EnumSet<TransferFlag> existingFlags, Map<String,String> headers) throws ErrorResponseException { String header = headers.get(REQUEST_HEADER_VERIFICATION); boolean verification; if (header == null) { verification = _defaultVerification; } else { switch (header) { case "true": verification = true; break; case "false": verification = false; break; default: throw new ErrorResponseException(Status.SC_BAD_REQUEST, "HTTP request header '" + REQUEST_HEADER_VERIFICATION + "' " + "has unknown value \"" + header + "\": " + "valid values are true or false"); } } EnumSet<TransferFlag> result = EnumSet.copyOf(existingFlags); if (verification) { result.add(TransferFlag.REQUIRE_VERIFICATION); } return result; } private ImmutableMap<String,String> buildTransferHeaders(Map<String,String> requestHeaders) { ImmutableMap.Builder<String,String> builder = ImmutableMap.builder(); for (Map.Entry<String,String> header : requestHeaders.entrySet()) { String key = header.getKey(); if (key.startsWith(REQUEST_HEADER_TRANSFER_HEADER_PREFIX)) { builder.put(key.substring(REQUEST_HEADER_TRANSFER_HEADER_PREFIX.length()), header.getValue()); } } return builder.build(); } public void messageArrived(TransferCompleteMessage message) { synchronized (_transfers) { RemoteTransfer transfer = _transfers.get(message.getId()); if (transfer != null) { transfer.success(); } } } public void messageArrived(TransferFailedMessage message) { synchronized (_transfers) { RemoteTransfer transfer = _transfers.get(message.getId()); if (transfer != null) { transfer.failure(String.valueOf(message.getErrorObject())); } } } /** * Class that represents a client's request to transfer a file to some * remote server. * <p> * This class needs to be aware of the client closing its end of the TCP * connection while the transfer underway. In the protocol, this is used * to indicate that the transfer should be cancelled. Unfortunately, there * is no container-independent mechanism for discovering this, so * Jetty-specific code is needed. */ private class RemoteTransfer { private final TransferType _type; private final Subject _subject; private final Restriction _restriction; private final FsPath _path; private final URI _destination; @Nullable private final PrivateKey _privateKey; @Nullable private final X509Certificate[] _certificateChain; @Nullable private final OpenIdCredential _oidCredential; private final CredentialSource _source; private final PrintWriter _out; private final EnumSet<TransferFlag> _flags; private final ImmutableMap<String,String> _transferHeaders; private final Direction _direction; private String _problem; private long _id; private final EndPoint _endpoint = HttpConnection.getCurrentConnection().getEndPoint(); private boolean _finished; public RemoteTransfer(OutputStream out, Subject subject, Restriction restriction, FsPath path, URI destination, @Nullable Object credential, EnumSet<TransferFlag> flags, ImmutableMap<String,String> transferHeaders, Direction direction) throws ErrorResponseException { _subject = subject; _restriction = restriction; _path = path; _destination = destination; _type = TransferType.fromScheme(destination.getScheme()); if (credential instanceof X509Credential) { _privateKey = ((X509Credential)credential).getKey(); _certificateChain = ((X509Credential)credential).getCertificateChain(); _source = CredentialSource.GRIDSITE; _oidCredential = null; } else if (credential instanceof OpenIdCredential) { _privateKey = null; _certificateChain = null; _source = CredentialSource.OIDC; _oidCredential = (OpenIdCredential) credential; } else { _privateKey = null; _certificateChain = null; _source = null; _oidCredential = null; } _out = new PrintWriter(out); _flags = flags; _transferHeaders = transferHeaders; _direction = direction; } private long start() throws ErrorResponseException, InterruptedException { boolean isStore = _direction == Direction.PULL; RemoteTransferManagerMessage message = new RemoteTransferManagerMessage(_destination, _path, isStore, DUMMY_LONG, buildProtocolInfo()); message.setSubject(_subject); message.setRestriction(_restriction); try { _id = _transferManager.sendAndWait(message).getId(); return _id; } catch (NoRouteToCellException | TimeoutCacheException e) { LOG.error("Failed to send request to transfer manager: {}", e.getMessage()); throw new ErrorResponseException(Response.Status.SC_INTERNAL_SERVER_ERROR, "transfer service unavailable"); } catch (CacheException e) { LOG.error("Error from transfer manager: {}", e.getMessage()); throw new ErrorResponseException(Response.Status.SC_INTERNAL_SERVER_ERROR, "transfer not accepted: " + e.getMessage()); } } /** * Check that the client is still connected. To be effective, the * Connector should make use of NIO (e.g., SelectChannelConnector or * SslSelectChannelConnector) and this method should be called after * output has been written to the client. */ private void checkClientConnected() { if (!_endpoint.isOpen()) { CancelTransferMessage message = new CancelTransferMessage(_id, DUMMY_LONG); message.setExplanation("client went away"); try { _transferManager.sendAndWait(message); } catch (NoRouteToCellException | CacheException e) { LOG.error("Failed to cancel transfer id={}: {}", _id, e.toString()); // Our attempt to kill the transfer failed. We leave the // performance markers going as they will trigger further // attempts to kill the transfer. } catch (InterruptedException e) { // Do nothing: this dCache domain is shutting down. } } } private IpProtocolInfo buildProtocolInfo() throws ErrorResponseException { int buffer = MiB.toBytes(1); int port = _destination.getPort(); if (port == -1) { port = _type.getDefaultPort(); } InetSocketAddress address = new InetSocketAddress(_destination.getHost(), port); switch (_type) { case GSIFTP: return new RemoteGsiftpTransferProtocolInfo("RemoteGsiftpTransfer", 1, 1, address, _destination.toASCIIString(), null, null, buffer, MiB.toBytes(1), _privateKey, _certificateChain, null); case HTTP: return new RemoteHttpDataTransferProtocolInfo("RemoteHttpDataTransfer", 1, 1, address, _destination.toASCIIString(), _flags.contains(TransferFlag.REQUIRE_VERIFICATION), _transferHeaders); case HTTPS: if (_source == CredentialSource.OIDC) { return new RemoteHttpsDataTransferProtocolInfo("RemoteHttpsDataTransfer", 1, 1, address, _destination.toASCIIString(), _flags.contains(TransferFlag.REQUIRE_VERIFICATION), _transferHeaders, _oidCredential); } else { return new RemoteHttpsDataTransferProtocolInfo("RemoteHttpsDataTransfer", 1, 1, address, _destination.toASCIIString(), _flags.contains(TransferFlag.REQUIRE_VERIFICATION), _transferHeaders, _privateKey, _certificateChain); } } throw new RuntimeException("Unexpected TransferType: " + _type); } public synchronized void success() { _problem = null; _finished = true; notifyAll(); } public synchronized void failure(String explanation) { _problem = explanation; _finished = true; notifyAll(); } public synchronized void awaitCompletion() throws InterruptedException { do { generateMarker(); wait(_performanceMarkerPeriod); } while (!_finished); if (_problem == null) { _out.println("success: Created"); } else { _out.println("failure: " + _problem); } _out.flush(); } private void generateMarker() throws InterruptedException { TransferStatusQueryMessage message = new TransferStatusQueryMessage(_id); ListenableFuture<TransferStatusQueryMessage> future = _transferManager.send(message, _performanceMarkerPeriod/2); int state = TransferManagerHandler.UNKNOWN_ID; IoJobInfo info = null; try { TransferStatusQueryMessage reply = CellStub.getMessage(future); state = reply.getState(); info = reply.getMoverInfo(); } catch (NoRouteToCellException | CacheException e) { LOG.warn("Failed to fetch information for progress marker: {}", e.getMessage()); } sendMarker(state, info); checkClientConnected(); } /** * Print a performance marker on the reply channel that looks something * like: * * Perf Marker * Timestamp: 1360578938 * Stripe Index: 0 * Stripe Bytes Transferred: 49397760 * Total Stripe Count: 2 * End * */ public void sendMarker(int state, IoJobInfo info) { _out.println("Perf Marker"); _out.println(" Timestamp: " + MILLISECONDS.toSeconds(System.currentTimeMillis())); _out.println(" State: " + state); _out.println(" State description: " + TransferManagerHandler.describeState(state)); _out.println(" Stripe Index: 0"); if (info != null) { _out.println(" Stripe Start Time: " + MILLISECONDS.toSeconds(info.getStartTime())); _out.println(" Stripe Last Transferred: " + MILLISECONDS.toSeconds(info.getLastTransferred())); _out.println(" Stripe Transfer Time: " + MILLISECONDS.toSeconds(info.getTransferTime())); _out.println(" Stripe Bytes Transferred: " + info.getBytesTransferred()); _out.println(" Stripe Status: " + info.getStatus()); } _out.println(" Total Stripe Count: 1"); _out.println("End"); _out.flush(); } } }