Skip to content

Commit

Permalink
xrootd/pool: Use login token to identify door endpoint
Browse files Browse the repository at this point in the history
Motivation:

On the pool, the xrootd mover provides only limited support for the
possible operations in the xroot protocol.  If a client makes a request
that the pool doesn't support then it will try to redirect the client
back to the door.  To do this, the mover needs to know the door's
endpoint: the hostname and port number.

The door endpoint is provided as part of the ProtocolInfo.  However, the
ProtocolInfo is only available to the xroot handler if the client has
already made a valid kXR_open request.  If a client connects to the door
and makes an unsupported request (without first opening a file) then the
xrootd transfer service does not know to which xroot door is should
redirect the client, so must fail the request.

Although the door only redirects the client to the pool when that client
wishes to open a file, the xrootd client sometimes caches this
information and issues subsequent (unsupported) requests directly to the
pool.  If the client does so on a separate TCP connection then the pool
cannot know from which door the client came, so cannot redirect the
client.

Modification:

The door now provides the client with a "login token" when redirecting
the client to the pool.  This token is a simple encoding of the door's
public endpoint.

As per the xroot protocol, the client is required to present this token
when first connecting to the endpoint, as part of the kXR_login
procedure.  This means the xrootd transfer handler (on the pool) will
know the door's public endpoint, so will be able to redirect the client
back to the door.

Note that, due to a bug in the xrootd software, the login token from the
door is ignored.  This bug will be fixed with the anticipated 5.5.0
release of xrootd:

    xrootd/xrootd#1533

Result:

dCache provides a more robust implemntation of xroot protocol.

Target: master
Requires-notes: no
Requires-book: no
Patch: https://rb.dcache.org/r/13491/
Acked-by: Albert Rossi
  • Loading branch information
paulmillar committed Jun 22, 2022
1 parent 22b1a8b commit 7d79a22
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 6 deletions.
5 changes: 5 additions & 0 deletions modules/dcache-xrootd/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,10 @@
<groupId>org.italiangrid</groupId>
<artifactId>voms-api-java</artifactId>
</dependency>
<dependency>
<groupId>com.github.npathai</groupId>
<artifactId>hamcrest-optional</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.io.Serializable;
import java.net.InetSocketAddress;
import java.util.EnumSet;
import java.util.Optional;
import java.util.UUID;
import org.dcache.auth.attributes.Restriction;

Expand Down Expand Up @@ -125,8 +126,8 @@ public UUID getUUID() {
return _uuid;
}

public InetSocketAddress getDoorAddress() {
return _doorAddress;
public Optional<InetSocketAddress> getDoorAddress() {
return Optional.ofNullable(_doorAddress);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* dCache - http://www.dcache.org/
*
* Copyright (C) 2022 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;

import com.google.common.base.Splitter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.Optional;

import static com.google.common.base.Preconditions.checkArgument;

/**
* Utility class to handle login token.
*/
public class LoginTokens
{
private static final Logger LOGGER = LoggerFactory.getLogger(LoginTokens.class);

private static final String JOIN_ELEMENT = "&";
private static final char KEY_VALUE_SEPARATOR = '=';
private static final String HOST_AND_PORT_KEY = "org.dcache.door";
private static final char HOST_PORT_SEPERATOR = ':';

private LoginTokens() {} // Prevent instantiation.

public static String encodeToken(InetSocketAddress addr) {
return HOST_AND_PORT_KEY + KEY_VALUE_SEPARATOR
+ addr.getHostString() + HOST_PORT_SEPERATOR + addr.getPort();
}

public static Optional<InetSocketAddress> decodeToken(String token) {
try {
/*checkArgument(token.startsWith(INITIAL_ELEMENT),
"Missing initial \"" + INITIAL_ELEMENT + "\"");*/

Map<String,String> data = Splitter.on(JOIN_ELEMENT)
.withKeyValueSeparator(KEY_VALUE_SEPARATOR)
.split(token);
String hostAndPort = data.get(HOST_AND_PORT_KEY);
checkArgument(hostAndPort != null, "Missing \"" + HOST_AND_PORT_KEY + "\" key");

int seperator = hostAndPort.indexOf(HOST_PORT_SEPERATOR);
checkArgument(seperator > -1, "Missing '" + HOST_PORT_SEPERATOR + "' in "
+ HOST_AND_PORT_KEY + " value");
checkArgument(seperator > 0, "'" + HOST_PORT_SEPERATOR
+ "' cannot be first character in " + HOST_AND_PORT_KEY);
checkArgument(seperator < hostAndPort.length()-1, "'" + HOST_PORT_SEPERATOR
+ "' cannot be last character in " + HOST_AND_PORT_KEY);

String host = hostAndPort.substring(0, seperator);
String port = hostAndPort.substring(seperator+1);

InetSocketAddress addr = new InetSocketAddress(InetAddress.getByName(host),
Integer.parseInt(port));
return Optional.of(addr);
} catch (UnknownHostException | IllegalArgumentException e) {
LOGGER.warn("Bad kXR_login token \"{}\": {}", token, e.getMessage()); // should be DEBUG
}

return Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* dCache - http://www.dcache.org/
*
* Copyright (C) 2014 Deutsches Elektronen-Synchrotron
* Copyright (C) 2014 - 2022 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
Expand Down Expand Up @@ -98,6 +98,7 @@
import org.dcache.util.Checksum;
import org.dcache.util.list.DirectoryEntry;
import org.dcache.vehicles.PnfsListDirectoryMessage;
import org.dcache.xrootd.LoginTokens;
import org.dcache.xrootd.core.XrootdException;
import org.dcache.xrootd.core.XrootdSession;
import org.dcache.xrootd.protocol.XrootdProtocol;
Expand Down Expand Up @@ -446,8 +447,10 @@ uuid, localAddress(), subject,

_log.info("Redirecting to {}, {}", host, address);

String token = LoginTokens.encodeToken(localAddress());

return new RedirectResponse<>(req, host, address.getPort(),
opaqueString, "");
opaqueString, token);
} catch (ParseException e) {
return withError(ctx, req, kXR_ArgInvalid, "Path arguments do not parse");
} catch (FileNotFoundCacheException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* dCache - http://www.dcache.org/
*
* Copyright (C) 2014 - 2020 Deutsches Elektronen-Synchrotron
* Copyright (C) 2014 - 2022 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
Expand Down Expand Up @@ -67,6 +67,7 @@
import org.dcache.vehicles.XrootdProtocolInfo;
import org.dcache.xrootd.AbstractXrootdRequestHandler;
import org.dcache.xrootd.CacheExceptionMapper;
import org.dcache.xrootd.LoginTokens;
import org.dcache.xrootd.core.XrootdException;
import org.dcache.xrootd.core.XrootdSessionIdentifier;
import org.dcache.xrootd.core.XrootdSigverDecoder;
Expand Down Expand Up @@ -285,6 +286,10 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) {
}
}

private void acceptDoorAddress(InetSocketAddress addr) {
_redirectingDoor = addr;
}

@Override
protected XrootdResponse<LoginRequest> doOnLogin(ChannelHandlerContext ctx, LoginRequest msg)
throws XrootdException {
Expand All @@ -297,6 +302,8 @@ protected XrootdResponse<LoginRequest> doOnLogin(ChannelHandlerContext ctx, Logi
*/
String sec;

LoginTokens.decodeToken(msg.getToken()).ifPresent(this::acceptDoorAddress);

/**
* If TLS is on, we don't need authentication.
*
Expand Down Expand Up @@ -445,7 +452,8 @@ protected XrootdResponse<OpenRequest> doOnOpen(ChannelHandlerContext ctx,

int fd = addDescriptor(descriptor);

_redirectingDoor = protocolInfo.getDoorAddress();
protocolInfo.getDoorAddress().ifPresent(this::acceptDoorAddress);

file = null;
_hasOpenedFiles = true;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/* dCache - http://www.dcache.org/
*
* Copyright (C) 2022 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;

import java.net.InetSocketAddress;
import java.util.Optional;
import java.util.OptionalInt;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.Test;

import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresentAnd;

public class LoginTokensTest {

/**
* A simple Hamcrest Matcher implementation that checks the value of some InetSocketAddress.
* The port number must be specified, using a fluent-style.
*/
private static class HasValue extends BaseMatcher<InetSocketAddress>
{
private final String expectedHost;
private OptionalInt expectedPort = OptionalInt.empty();

public HasValue(String host)
{
expectedHost = host;
}

public HasValue andPort(int port) {
expectedPort = OptionalInt.of(port);
return this;
}

@Override
public boolean matches(Object actual) {
if (!(actual instanceof InetSocketAddress)) {
return false;
}
InetSocketAddress addr = (InetSocketAddress)actual;
return addr.getHostString().equals(expectedHost)
&& addr.getPort() == expectedPort.getAsInt();
}

@Override
public void describeTo(Description description) {
description.appendValue(expectedHost + ":" + expectedPort.getAsInt());
}
}

private InetSocketAddress addr;

@Test
public void shouldEncodeHostAndPort() {
givenHostAndPort("localhost", 1094);

String token = LoginTokens.encodeToken(addr);

assertThat(token, is(equalTo("org.dcache.door=localhost:1094")));
}

@Test
public void shouldDecodeHostAndPort() {
Optional<InetSocketAddress> door = LoginTokens.decodeToken("org.dcache.door=localhost:1094");

assertThat(door, isPresentAnd(hasHost("localhost").andPort(1094)));
}

@Test
public void shouldIgnoreTokenWithMissingKey() {
Optional<InetSocketAddress> door = LoginTokens.decodeToken("?xrd.cc=de&xrd.tz=1&xrd.appname=xrdcp&xrd.info=&xrd.hostname=sprocket.desy.de&xrd.rn=v5.1.1");

assertThat(door, isEmpty());
}

@Test
public void shouldIgnoreTokenWithMissingHostname() {
Optional<InetSocketAddress> door = LoginTokens.decodeToken("org.dcache.door=:1094");

assertThat(door, isEmpty());
}

@Test
public void shouldIgnoreTokenWithMissingPort() {
Optional<InetSocketAddress> door = LoginTokens.decodeToken("org.dcache.door=localhost:");

assertThat(door, isEmpty());
}

@Test
public void shouldIgnoreTokenWithNoSeperator() {
Optional<InetSocketAddress> door = LoginTokens.decodeToken("org.dcache.door=localhost");

assertThat(door, isEmpty());
}

// Support methods

private void givenHostAndPort(String host, int port)
{
addr = InetSocketAddress.createUnresolved(host, port);
}

private static HasValue hasHost(String host)
{
return new HasValue(host);
}
}

0 comments on commit 7d79a22

Please sign in to comment.