com.spotify.helios.client.AuthenticatingHttpConnector Maven / Gradle / Ivy
/*
* Copyright (c) 2014 Spotify AB.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.spotify.helios.client;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.spotify.helios.client.HttpsHandlers.CertificateFileHttpsHandler;
import com.spotify.helios.client.HttpsHandlers.SshAgentHttpsHandler;
import com.spotify.helios.common.HeliosException;
import com.spotify.sshagentproxy.AgentProxy;
import com.spotify.sshagentproxy.Identity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
/**
* HttpConnector that wraps another connector to add the necessary hooks into HttpURLConnection for
* our SSH/TLS-based authentication.
*/
public class AuthenticatingHttpConnector implements HttpConnector {
private static final Logger log = LoggerFactory.getLogger(AuthenticatingHttpConnector.class);
private final String user;
private final Optional agentProxy;
private final Optional clientCertificatePath;
private final List identities;
private final EndpointIterator endpointIterator;
private final DefaultHttpConnector delegate;
public AuthenticatingHttpConnector(final String user,
final Optional agentProxyOpt,
final Optional clientCertificatePath,
final EndpointIterator endpointIterator,
final DefaultHttpConnector delegate) {
this(user, agentProxyOpt, clientCertificatePath, endpointIterator,
delegate, getSshIdentities(agentProxyOpt));
}
@VisibleForTesting
AuthenticatingHttpConnector(final String user,
final Optional agentProxyOpt,
final Optional clientCertificatePath,
final EndpointIterator endpointIterator,
final DefaultHttpConnector delegate,
final List identities) {
this.user = user;
this.agentProxy = agentProxyOpt;
this.clientCertificatePath = clientCertificatePath;
this.endpointIterator = endpointIterator;
this.delegate = delegate;
this.identities = identities;
}
@Override
public HttpURLConnection connect(final URI uri, final String method, final byte[] entity,
final Map> headers) throws HeliosException {
final Endpoint endpoint = endpointIterator.next();
// convert the URI whose hostname portion is a domain name into a URI where the host is an IP
// as we expect there to be several different IP addresses besides a common domain name
final URI ipUri;
try {
ipUri = toIpUri(endpoint, uri);
} catch (URISyntaxException e) {
throw new HeliosException(e);
}
try {
log.debug("connecting to {}", ipUri);
if (clientCertificatePath.isPresent()) {
// prioritize using the certificate file if set
return connectWithCertificateFile(ipUri, method, entity, headers);
} else if (agentProxy.isPresent() && !identities.isEmpty()) {
// ssh-agent based authentication
return connectWithIdentities(identities, ipUri, method, entity, headers);
} else {
// no authentication
return doConnect(ipUri, method, entity, headers);
}
} catch (ConnectException | SocketTimeoutException | UnknownHostException e) {
// UnknownHostException happens if we can't resolve hostname into IP address.
// UnknownHostException's getMessage method returns just the hostname which is a
// useless message, so log the exception class name to provide more info.
log.debug(e.toString());
throw new HeliosException("Unable to connect to master: " + ipUri, e);
} catch (IOException e) {
throw new HeliosException("Unexpected error connecting to " + ipUri, e);
}
}
private HttpURLConnection connectWithCertificateFile(final URI ipUri, final String method,
final byte[] entity,
final Map> headers)
throws HeliosException {
final ClientCertificatePath clientCertificatePath = this.clientCertificatePath.get();
log.debug("configuring CertificateFileHttpsHandler with {}", clientCertificatePath);
delegate.setExtraHttpsHandler(
new CertificateFileHttpsHandler(user, false, clientCertificatePath)
);
return doConnect(ipUri, method, entity, headers);
}
private HttpURLConnection connectWithIdentities(final List identities, final URI uri,
final String method, final byte[] entity,
final Map> headers)
throws IOException, HeliosException {
if (identities.isEmpty()) {
throw new IllegalArgumentException("identities cannot be empty");
}
final Queue queue = new LinkedList<>(identities);
HttpURLConnection connection = null;
while (!queue.isEmpty()) {
final Identity identity = queue.poll();
delegate.setExtraHttpsHandler(
new SshAgentHttpsHandler(user, false, agentProxy.get(), identity));
connection = doConnect(uri, method, entity, headers);
// check the status and retry the request if necessary
final int responseCode = connection.getResponseCode();
final boolean retryResponse =
responseCode == HTTP_FORBIDDEN || responseCode == HTTP_UNAUTHORIZED;
if (retryResponse && !queue.isEmpty()) {
// there was some sort of security error. if we have any more SSH identities to try,
// retry with the next available identity
log.debug("retrying with next SSH identity since {} failed",
identity == null ? "the previous one" : identity.getComment());
continue;
}
break;
}
return connection;
}
private HttpURLConnection doConnect(final URI uri, final String method, final byte[] entity,
final Map> headers)
throws HeliosException {
return delegate.connect(uri, method, entity, headers);
}
private URI toIpUri(Endpoint endpoint, URI uri) throws URISyntaxException {
final URI endpointUri = endpoint.getUri();
final String fullpath = endpointUri.getPath() + uri.getPath();
return new URI(
endpointUri.getScheme(),
endpointUri.getUserInfo(),
endpoint.getIp().getHostAddress(),
endpointUri.getPort(),
fullpath,
uri.getQuery(),
null);
}
private static List getSshIdentities(final Optional agentProxyOpt) {
// ssh identities (potentially) used in authentication
final ImmutableList.Builder listBuilder = ImmutableList.builder();
if (agentProxyOpt.isPresent()) {
try {
final List identities = agentProxyOpt.get().list();
for (final Identity identity : identities) {
if (identity.getPublicKey().getAlgorithm().equals("RSA")) {
// only RSA keys will work with our TLS implementation
listBuilder.add(identity);
}
}
} catch (Exception e) {
// We catch everything because right now the masters do not require authentication.
// So delay reporting errors to the user until the servers return 401 Unauthorized.
log.debug("Unable to get identities from ssh-agent. Note that this might not indicate"
+ " an actual problem unless your Helios cluster requires authentication"
+ " for all requests.", e);
}
}
return listBuilder.build();
}
@Override
public void close() throws IOException {
if (agentProxy.isPresent()) {
agentProxy.get().close();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy