/* 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.xrootd.door;
import com.google.common.net.InetAddresses;
import io.netty.channel.ChannelHandlerContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ClosedChannelException;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import diskCacheV111.util.CacheException;
import diskCacheV111.util.FileExistsCacheException;
import diskCacheV111.util.FileIsNewCacheException;
import diskCacheV111.util.FileNotFoundCacheException;
import diskCacheV111.util.FsPath;
import diskCacheV111.util.NotFileCacheException;
import diskCacheV111.util.PermissionDeniedCacheException;
import diskCacheV111.util.TimeoutCacheException;
import dmg.cells.nucleus.CellPath;
import org.dcache.auth.LoginReply;
import org.dcache.auth.attributes.Restriction;
import org.dcache.auth.attributes.Restrictions;
import org.dcache.cells.AbstractMessageCallback;
import org.dcache.namespace.FileAttribute;
import org.dcache.util.Checksum;
import org.dcache.util.Checksums;
import org.dcache.util.list.DirectoryEntry;
import org.dcache.vehicles.PnfsListDirectoryMessage;
import org.dcache.xrootd.core.XrootdException;
import org.dcache.xrootd.protocol.messages.DirListRequest;
import org.dcache.xrootd.protocol.messages.DirListResponse;
import org.dcache.xrootd.protocol.messages.MkDirRequest;
import org.dcache.xrootd.protocol.messages.MvRequest;
import org.dcache.xrootd.protocol.messages.OpenRequest;
import org.dcache.xrootd.protocol.messages.PrepareRequest;
import org.dcache.xrootd.protocol.messages.QueryRequest;
import org.dcache.xrootd.protocol.messages.QueryResponse;
import org.dcache.xrootd.protocol.messages.RedirectResponse;
import org.dcache.xrootd.protocol.messages.RmDirRequest;
import org.dcache.xrootd.protocol.messages.RmRequest;
import org.dcache.xrootd.protocol.messages.StatRequest;
import org.dcache.xrootd.protocol.messages.StatResponse;
import org.dcache.xrootd.protocol.messages.StatxRequest;
import org.dcache.xrootd.protocol.messages.StatxResponse;
import org.dcache.xrootd.protocol.messages.XrootdResponse;
import org.dcache.xrootd.util.OpaqueStringParser;
import org.dcache.xrootd.util.ParseException;
import static org.dcache.xrootd.protocol.XrootdProtocol.*;
/**
* Channel handler which redirects all open requests to a pool.
*/
public class XrootdRedirectHandler extends ConcurrentXrootdRequestHandler
{
private static final Logger _log =
LoggerFactory.getLogger(XrootdRedirectHandler.class);
private final XrootdDoor _door;
private final FsPath _rootPath;
private Restriction _authz = Restrictions.denyAll();
private final Map<String,String> _appIoQueues;
/**
* Custom entries for kXR_Qconfig requests.
*/
private final Map<String,String> _queryConfig;
public XrootdRedirectHandler(XrootdDoor door, FsPath rootPath, ExecutorService executor,
Map<String, String> queryConfig,
Map<String,String> appIoQueues)
{
super(executor);
_door = door;
_rootPath = rootPath;
_queryConfig = queryConfig;
_appIoQueues = appIoQueues;
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object event) throws Exception
{
if (event instanceof LoginEvent) {
loggedIn((LoginEvent) event);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable t)
{
if (t instanceof ClosedChannelException) {
_log.info("Connection closed");
} else if (t instanceof RuntimeException || t instanceof Error) {
Thread me = Thread.currentThread();
me.getUncaughtExceptionHandler().uncaughtException(me, t);
} else if (!isHealthCheck() || !(t instanceof IOException)){
_log.warn(t.toString());
}
}
/**
* The open, if successful, will always result in a redirect
* response to the proper pool, hence no subsequent requests like
* sync, read, write or close are expected at the door.
*/
@Override
protected XrootdResponse<OpenRequest> doOnOpen(ChannelHandlerContext ctx, OpenRequest req)
throws XrootdException
{
/* We ought to process this asynchronously to not block the calling thread during
* staging or queuing. We should also switch to an asynchronous reply model if
* the request is nearline or is queued on a pool. The naive approach to always
* use an asynchronous reply model doesn't work because the xrootd 3.x client
* introduces an artificial 1 second delay when processing such a response.
*/
InetSocketAddress localAddress = getDestinationAddress();
InetSocketAddress remoteAddress = getSourceAddress();
FilePerm neededPerm = req.getRequiredPermission();
_log.info("Opening {} for {}", req.getPath(), neededPerm.xmlText());
if (_log.isDebugEnabled()) {
logDebugOnOpen(req);
}
String ioQueue = appSpecificQueue(req);
Long size = null;
try {
Map<String,String> opaque = OpaqueStringParser.getOpaqueMap(req.getOpaque());
try {
String value = opaque.get("oss.asize");
if (value != null) {
size = Long.valueOf(value);
}
} catch (NumberFormatException exception) {
_log.warn("Ignoring malformed oss.asize: {}", exception.getMessage());
}
} catch (ParseException e) {
_log.warn("Ignoring malformed open opaque {}: {}", req.getOpaque(),
e.getMessage());
}
UUID uuid = UUID.randomUUID();
String opaque = OpaqueStringParser.buildOpaqueString(UUID_PREFIX, uuid.toString());
/* Interact with core dCache to open the requested file.
*/
try {
XrootdTransfer transfer;
if (neededPerm == FilePerm.WRITE) {
boolean createDir = req.isMkPath();
boolean overwrite = req.isDelete();
transfer =
_door.write(remoteAddress, createFullPath(req.getPath()), ioQueue,
uuid, createDir, overwrite, size, localAddress,
req.getSubject(), _authz);
} else {
transfer =
_door.read(remoteAddress, createFullPath(req.getPath()), ioQueue,
uuid, localAddress, req.getSubject(), _authz);
}
// ok, open was successful
InetSocketAddress address = transfer.getRedirect();
_log.info("Redirecting to {}", address);
/* xrootd developers say that IPv6 addresses must always be URI quoted.
* The spec doesn't require this, but clients depend on it.
*/
return new RedirectResponse<>(
req, InetAddresses.toUriString(address.getAddress()), address.getPort(), opaque, "");
} catch (FileNotFoundCacheException e) {
return withError(req, kXR_NotFound, "No such file");
} catch (FileExistsCacheException e) {
return withError(req, kXR_Unsupported, "File already exists");
} catch (TimeoutCacheException e) {
return withError(req, kXR_ServerError, "Internal timeout");
} catch (PermissionDeniedCacheException e) {
return withError(req, kXR_NotAuthorized, e.getMessage());
} catch (FileIsNewCacheException e) {
return withError(req, kXR_FileLocked, "File is locked by upload");
} catch (NotFileCacheException e) {
return withError(req, kXR_NotFile, "Not a file");
} catch (CacheException e) {
return withError(req, kXR_ServerError,
String.format("Failed to open file (%s [%d])", e.getMessage(), e.getRc()));
} catch (InterruptedException e) {
/* Interrupt may be caused by cell shutdown or client
* disconnect. If the client disconnected, then the error
* message will never reach the client, so saying that the
* server shut down is okay.
*/
return withError(req, kXR_ServerError, "Server shutdown");
}
}
private String appSpecificQueue(OpenRequest req)
{
String ioqueue = null;
String token = req.getSession().getToken();
try {
Map<String,String> attr = OpaqueStringParser.getOpaqueMap(token);
ioqueue = _appIoQueues.get(attr.get("xrd.appname"));
} catch (ParseException e) {
_log.debug("Ignoring malformed login token {}: {}", token, e.getMessage());
}
return ioqueue;
}
@Override
protected XrootdResponse<StatRequest> doOnStat(ChannelHandlerContext ctx, StatRequest req)
throws XrootdException
{
String path = req.getPath();
try {
InetSocketAddress client = getSourceAddress();
return new StatResponse(req, _door.getFileStatus(createFullPath(path), req.getSubject(), _authz,
client.getAddress().getHostAddress()));
} catch (FileNotFoundCacheException e) {
throw new XrootdException(kXR_NotFound, "No such file");
} catch (TimeoutCacheException e) {
throw new XrootdException(kXR_ServerError, "Internal timeout");
} catch (PermissionDeniedCacheException e) {
throw new XrootdException(kXR_NotAuthorized, e.getMessage());
} catch (CacheException e) {
throw new XrootdException(kXR_ServerError,
String.format("Failed to open file (%s [%d])",
e.getMessage(), e.getRc()));
}
}
@Override
protected XrootdResponse<StatxRequest> doOnStatx(ChannelHandlerContext ctx, StatxRequest req)
throws XrootdException
{
if (req.getPaths().length == 0) {
throw new XrootdException(kXR_ArgMissing, "no paths specified");
}
try {
FsPath[] paths = new FsPath[req.getPaths().length];
for (int i = 0; i < paths.length; i++) {
paths[i] = createFullPath(req.getPaths()[i]);
}
return new StatxResponse(req, _door.getMultipleFileStatuses(paths, req.getSubject(), _authz));
} catch (TimeoutCacheException e) {
throw new XrootdException(kXR_ServerError, "Internal timeout");
} catch (PermissionDeniedCacheException e) {
throw new XrootdException(kXR_NotAuthorized, e.getMessage());
} catch (CacheException e) {
throw new XrootdException(kXR_ServerError,
String.format("Failed to open file (%s [%d])",
e.getMessage(), e.getRc()));
}
}
@Override
protected XrootdResponse<RmRequest> doOnRm(ChannelHandlerContext ctx, RmRequest req)
throws XrootdException
{
if (req.getPath().isEmpty()) {
throw new XrootdException(kXR_ArgMissing, "no path specified");
}
_log.info("Trying to delete {}", req.getPath());
try {
_door.deleteFile(createFullPath(req.getPath()), req.getSubject(), _authz);
return withOk(req);
} catch (TimeoutCacheException e) {
throw new XrootdException(kXR_ServerError, "Internal timeout");
} catch (PermissionDeniedCacheException e) {
throw new XrootdException(kXR_NotAuthorized, e.getMessage());
} catch (FileNotFoundCacheException e) {
throw new XrootdException(kXR_NotFound, "No such file");
} catch (CacheException e) {
throw new XrootdException(kXR_ServerError,
String.format("Failed to delete file (%s [%d])",
e.getMessage(), e.getRc()));
}
}
@Override
protected XrootdResponse<RmDirRequest> doOnRmDir(ChannelHandlerContext ctx, RmDirRequest req)
throws XrootdException
{
if (req.getPath().isEmpty()) {
throw new XrootdException(kXR_ArgMissing, "no path specified");
}
_log.info("Trying to delete directory {}", req.getPath());
try {
_door.deleteDirectory(createFullPath(req.getPath()), req.getSubject(), _authz);
return withOk(req);
} catch (TimeoutCacheException e) {
throw new XrootdException(kXR_ServerError, "Internal timeout");
} catch (PermissionDeniedCacheException e) {
throw new XrootdException(kXR_NotAuthorized, e.getMessage());
} catch (FileNotFoundCacheException e) {
throw new XrootdException(kXR_NotFound, e.getMessage());
} catch (CacheException e) {
throw new XrootdException(kXR_ServerError,
String.format("Failed to delete directory " +
"(%s [%d]).",
e.getMessage(), e.getRc()));
}
}
@Override
protected XrootdResponse<MkDirRequest> doOnMkDir(ChannelHandlerContext ctx, MkDirRequest req)
throws XrootdException
{
if (req.getPath().isEmpty()) {
throw new XrootdException(kXR_ArgMissing, "no path specified");
}
_log.info("Trying to create directory {}", req.getPath());
try {
_door.createDirectory(createFullPath(req.getPath()),
req.shouldMkPath(),
req.getSubject(),
_authz);
return withOk(req);
} catch (TimeoutCacheException e) {
throw new XrootdException(kXR_ServerError, "Internal timeout");
} catch (PermissionDeniedCacheException e) {
throw new XrootdException(kXR_NotAuthorized, e.getMessage());
} catch (FileNotFoundCacheException | FileExistsCacheException e) {
throw new XrootdException(kXR_FSError, e.getMessage());
} catch (CacheException e) {
throw new XrootdException(kXR_ServerError,
String.format("Failed to create directory " +
"(%s [%d]).",
e.getMessage(), e.getRc()));
}
}
@Override
protected XrootdResponse<MvRequest> doOnMv(ChannelHandlerContext ctx, MvRequest req)
throws XrootdException
{
String sourcePath = req.getSourcePath();
if (sourcePath.isEmpty()) {
throw new XrootdException(kXR_ArgMissing, "no source path specified");
}
String targetPath = req.getTargetPath();
if (targetPath.isEmpty()) {
throw new XrootdException(kXR_ArgMissing, "no target path specified");
}
_log.info("Trying to rename {} to {}", req.getSourcePath(), req.getTargetPath());
try {
_door.moveFile(
createFullPath(req.getSourcePath()),
createFullPath(req.getTargetPath()),
req.getSubject(),
_authz);
return withOk(req);
} catch (TimeoutCacheException e) {
throw new XrootdException(kXR_ServerError, "Internal timeout");
} catch (PermissionDeniedCacheException e) {
throw new XrootdException(kXR_NotAuthorized, e.getMessage());
} catch (FileNotFoundCacheException e) {
throw new XrootdException(kXR_NotFound,
String.format("Source file does not exist (%s) ",
e.getMessage()));
} catch (FileExistsCacheException e) {
throw new XrootdException(kXR_FSError,
String.format("Will not overwrite existing file " +
"(%s).", e.getMessage()));
} catch (CacheException e) {
throw new XrootdException(kXR_ServerError,
String.format("Failed to move file " +
"(%s [%d]).",
e.getMessage(), e.getRc()));
}
}
@Override
protected XrootdResponse<QueryRequest> doOnQuery(ChannelHandlerContext ctx, QueryRequest msg) throws XrootdException
{
switch (msg.getReqcode()) {
case kXR_Qconfig:
StringBuilder s = new StringBuilder();
for (String name: msg.getArgs().split(" ")) {
switch (name) {
case "bind_max":
s.append(0);
break;
case "csname":
s.append("1:ADLER32,2:MD5");
break;
default:
s.append(_queryConfig.getOrDefault(name, name));
break;
}
s.append('\n');
}
return new QueryResponse(msg, s.toString());
case kXR_Qcksum:
try {
Set<Checksum> checksums = _door.getChecksums(createFullPath(msg.getArgs()),
msg.getSubject(),
_authz);
if (!checksums.isEmpty()) {
Checksum checksum = Checksums.preferrredOrder().min(checksums);
return new QueryResponse(msg, checksum.getType().getName() + " " + checksum.getValue());
}
} catch (FileNotFoundCacheException e) {
throw new XrootdException(kXR_NotFound, e.getMessage());
} catch (PermissionDeniedCacheException e) {
throw new XrootdException(kXR_NotAuthorized, e.getMessage());
} catch (CacheException e) {
throw new XrootdException(kXR_ServerError, e.getMessage());
}
throw new XrootdException(kXR_Unsupported, "No checksum available for this file.");
default:
return unsupported(ctx, msg);
}
}
@Override
protected XrootdResponse<DirListRequest> doOnDirList(ChannelHandlerContext ctx, DirListRequest request)
throws XrootdException
{
try {
String listPath = request.getPath();
if (listPath.isEmpty()) {
throw new XrootdException(kXR_ArgMissing, "no source path specified");
}
_log.info("Listing directory {}", listPath);
FsPath fullListPath = createFullPath(listPath);
if (request.isDirectoryStat()) {
_door.listPath(fullListPath, request.getSubject(), _authz,
new StatListCallback(request, fullListPath, ctx),
_door.getRequiredAttributesForFileStatus());
} else {
_door.listPath(fullListPath, request.getSubject(), _authz,
new ListCallback(request, ctx),
EnumSet.noneOf(FileAttribute.class));
}
return null;
} catch (PermissionDeniedCacheException e) {
throw new XrootdException(kXR_NotAuthorized, e.getMessage());
}
}
@Override
protected XrootdResponse<PrepareRequest> doOnPrepare(ChannelHandlerContext ctx, PrepareRequest msg)
throws XrootdException
{
return withOk(msg);
}
private void logDebugOnOpen(OpenRequest req)
{
int options = req.getOptions();
String openFlags =
"options to apply for open path (raw=" + options +" ):";
if ((options & kXR_async) == kXR_async) {
openFlags += " kXR_async";
}
if ((options & kXR_compress) == kXR_compress) {
openFlags += " kXR_compress";
}
if ((options & kXR_delete) == kXR_delete) {
openFlags += " kXR_delete";
}
if ((options & kXR_force) == kXR_force) {
openFlags += " kXR_force";
}
if ((options & kXR_new) == kXR_new) {
openFlags += " kXR_new";
}
if ((options & kXR_open_read) == kXR_open_read) {
openFlags += " kXR_open_read";
}
if ((options & kXR_open_updt) == kXR_open_updt) {
openFlags += " kXR_open_updt";
}
if ((options & kXR_refresh) == kXR_refresh) {
openFlags += " kXR_refresh";
}
if ((options & kXR_mkpath) == kXR_mkpath) {
openFlags += " kXR_mkpath";
}
if ((options & kXR_open_apnd) == kXR_open_apnd) {
openFlags += " kXR_open_apnd";
}
if ((options & kXR_retstat) == kXR_retstat) {
openFlags += " kXR_retstat";
}
_log.debug("open flags: "+openFlags);
int mode = req.getUMask();
String s = "";
if ((mode & kXR_ur) == kXR_ur) {
s += "r";
} else {
s += "-";
}
if ((mode & kXR_uw) == kXR_uw) {
s += "w";
} else {
s += "-";
}
if ((mode & kXR_ux) == kXR_ux) {
s += "x";
} else {
s += "-";
}
s += " ";
if ((mode & kXR_gr) == kXR_gr) {
s += "r";
} else {
s += "-";
}
if ((mode & kXR_gw) == kXR_gw) {
s += "w";
} else {
s += "-";
}
if ((mode & kXR_gx) == kXR_gx) {
s += "x";
} else {
s += "-";
}
s += " ";
if ((mode & kXR_or) == kXR_or) {
s += "r";
} else {
s += "-";
}
if ((mode & kXR_ow) == kXR_ow) {
s += "w";
} else {
s += "-";
}
if ((mode & kXR_ox) == kXR_ox) {
s += "x";
} else {
s += "-";
}
_log.debug("mode to apply to open path: {}", s);
}
/**
* Callback responding to client depending on the list directory messages
* it receives from Pnfs via the door.
* @author tzangerl
*
*/
private class ListCallback
extends AbstractMessageCallback<PnfsListDirectoryMessage>
{
protected final DirListRequest _request;
protected final ChannelHandlerContext _context;
protected final DirListResponse.Builder _response;
public ListCallback(DirListRequest request, ChannelHandlerContext context)
{
_request = request;
_response = DirListResponse.builder(request);
_context = context;
}
/**
* Respond to client if message contains errors. Try to use
* meaningful status codes from the xrootd-protocol to map the errors
* from PnfsManager.
*
* @param rc The error code of the message
* @param error Object describing the actual error that occurred
*/
@Override
public void failure(int rc, Object error)
{
switch (rc) {
case CacheException.TIMEOUT:
respond(_context,
withError(_request,
kXR_ServerError,
"Timeout when trying to list directory: " +
error.toString()));
break;
case CacheException.PERMISSION_DENIED:
respond(_context,
withError(_request,
kXR_NotAuthorized,
"Permission to list that directory denied: " +
error.toString()));
break;
case CacheException.FILE_NOT_FOUND:
respond(_context,
withError(_request, kXR_NotFound, "Path not found"));
break;
default:
respond(_context,
withError(_request,
kXR_ServerError,
"Error when processing list response: " +
error.toString()));
break;
}
}
/**
* Reply to client if no route to PNFS manager was found.
*
*/
@Override
public void noroute(CellPath path)
{
respond(_context,
withError(_request,
kXR_ServerError,
"Could not contact PNFS Manager."));
}
/**
* In case of a listing success, inspect the message. If the message
* is the final listing message, reply with kXR_ok and the full
* directory listing. If the message is not the final message, reply
* with oksofar and the partial directory listing.
*
* @param message The PnfsListDirectoryMessage-reply as it was received
* from the PNFSManager.
*/
@Override
public void success(PnfsListDirectoryMessage message)
{
message.getEntries().stream().map(DirectoryEntry::getName).forEach(_response::add);
if (message.isFinal()) {
respond(_context, _response.buildFinal());
} else {
respond(_context, _response.buildPartial());
}
}
/**
* Respond to client in the case of a timeout.
*/
@Override
public void timeout(String error) {
respond(_context,
withError(_request,
kXR_ServerError,
"Timeout when trying to list directory!"));
}
}
private class StatListCallback extends ListCallback
{
private final String _client;
protected final FsPath _dirPath;
public StatListCallback(DirListRequest request, FsPath dirPath, ChannelHandlerContext context)
{
super(request, context);
_client = getSourceAddress().getAddress().getHostAddress();
_dirPath = dirPath;
}
@Override
public void success(PnfsListDirectoryMessage message)
{
message.getEntries().stream().forEach(
e -> _response.add(e.getName(), _door.getFileStatus(_request.getSubject(), _authz, _dirPath.child(e.getName()), _client, e.getFileAttributes())));
if (message.isFinal()) {
respond(_context, _response.buildFinal());
} else {
respond(_context, _response.buildPartial());
}
}
}
/**
* Execute login strategy to make an user authorization decision.
*/
private void loggedIn(LoginEvent event)
{
LoginReply reply = event.getLoginReply();
_authz = Restrictions.none();
if (reply != null) {
_authz = reply.getRestriction();
}
}
/**
* Forms a full PNFS path. The path is created by concatenating
* the root path and path. The root path is guaranteed to be a
* prefix of the path returned.
*/
private FsPath createFullPath(String path)
throws PermissionDeniedCacheException
{
return _rootPath.chroot(path);
}
}