/*******************************************************************************
* Copyright (c) 2016 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.v2;
import java.net.URL;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.cloudfoundry.client.CloudFoundryClient;
import org.cloudfoundry.reactor.ConnectionContext;
import org.cloudfoundry.reactor.DefaultConnectionContext;
import org.cloudfoundry.reactor.ProxyConfiguration;
import org.cloudfoundry.reactor.TokenProvider;
import org.cloudfoundry.reactor.client.ReactorCloudFoundryClient;
import org.cloudfoundry.reactor.doppler.ReactorDopplerClient;
import org.cloudfoundry.reactor.tokenprovider.OneTimePasscodeTokenProvider;
import org.cloudfoundry.reactor.tokenprovider.PasswordGrantTokenProvider;
import org.cloudfoundry.reactor.tokenprovider.RefreshTokenGrantTokenProvider;
import org.cloudfoundry.reactor.uaa.ReactorUaaClient;
import org.eclipse.core.net.proxy.IProxyData;
import org.eclipse.core.net.proxy.IProxyService;
import org.eclipse.core.runtime.Assert;
import org.springframework.ide.eclipse.boot.dash.BootDashActivator;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFCredentials;
import org.springframework.ide.eclipse.boot.util.Log;
import org.springframework.util.StringUtils;
/**
* TODO: Remove this class when the 'thread leak bug' in V2 client is fixed.
*
* At the moment each time {@link SpringCloudFoundryClient} is create a threadpool
* is created by the client and it is never cleaned up. The only way we have
* to mitigate this leak is to try and create as few clients as possible.
* <p>
* So we have a permanent cache of clients here that is reused.
* <p>
* When the bug is fixed then this should no longer be necessary and we can removed this cache
* and just create the client as needed.
*
* @author Kris De Volder
*/
public class CloudFoundryClientCache {
public class CFClientProvider {
final ConnectionContext connection;
final TokenProvider tokenProvider;
//Note the three client objects below are 'stateless' wrappers and it would be
// fine to recreate as needed instead of store them
final CloudFoundryClient client;
final ReactorUaaClient uaaClient;
final ReactorDopplerClient doppler;
private ProxyConfiguration getProxy(String host) {
try {
if (StringUtils.hasText(host)) {
URL url = new URL("https://"+host);
// In certain cases, the activator would have stopped and the plugin may
// no longer be available. Usually onl happens on shutdown.
BootDashActivator plugin = BootDashActivator.getDefault();
if (plugin != null) {
IProxyService proxyService = plugin.getProxyService();
if (proxyService != null) {
IProxyData[] selectedProxies = proxyService.select(url.toURI());
// No proxy configured or not found
if (selectedProxies == null || selectedProxies.length == 0) {
return null;
}
IProxyData data = selectedProxies[0];
int proxyPort = data.getPort();
String proxyHost = data.getHost();
String user = data.getUserId();
String password = data.getPassword();
if (proxyHost!=null) {
return ProxyConfiguration.builder()
.host(proxyHost)
.port(proxyPort==-1?Optional.empty():Optional.of(proxyPort))
.username(Optional.ofNullable(user))
.password(Optional.ofNullable(password))
.build();
// return proxyHost != null ? new HttpProxyConfiguration(proxyHost, proxyPort,
// data.isRequiresAuthentication(), user, password) : null;
}
}
}
}
} catch (Exception e) {
Log.log(e);
}
return null;
}
public CFClientProvider(Params params) {
long sslTimeout = Long.getLong("sts.bootdash.cf.client.ssl.handshake.timeout", 60); //TODO: make a preference for this?
Optional<Boolean> keepAlive = getBooleanSystemProp("http.keepAlive");
debug("cf client keepAlive = "+keepAlive);
connection = DefaultConnectionContext.builder()
.proxyConfiguration(Optional.ofNullable(getProxy(params.host)))
.apiHost(params.host)
.sslHandshakeTimeout(Duration.ofSeconds(sslTimeout))
.keepAlive(keepAlive)
.skipSslValidation(params.skipSsl)
.build();
tokenProvider = createTokenProvider(params);
client = ReactorCloudFoundryClient.builder()
.connectionContext(connection)
.tokenProvider(tokenProvider)
.build();
uaaClient = ReactorUaaClient.builder()
.connectionContext(connection)
.tokenProvider(tokenProvider)
.build();
doppler = ReactorDopplerClient.builder()
.connectionContext(connection)
.tokenProvider(tokenProvider)
.build();
}
private TokenProvider createTokenProvider(Params params) {
CFCredentials creds = params.credentials;
switch (creds.getType()) {
case PASSWORD:
return PasswordGrantTokenProvider.builder()
.username(params.username)
.password(creds.getSecret())
.build();
case REFRESH_TOKEN:
return RefreshTokenGrantTokenProvider.builder()
.token(creds.getSecret())
.build();
case TEMPORARY_CODE:
return OneTimePasscodeTokenProvider.builder()
.passcode(creds.getSecret())
.build();
default:
throw new IllegalStateException("BUG! Missing switch case?");
}
}
private Optional<Boolean> getBooleanSystemProp(String name) {
String str = System.getProperty(name);
if (str!=null) {
return Optional.of(Boolean.valueOf(str));
}
return Optional.empty();
}
}
private static final boolean DEBUG = true;
private static void debug(String string) {
if (DEBUG) {
System.out.println(string);
}
}
public static class Params {
public final String username;
public final CFCredentials credentials;
public final String host;
public final boolean skipSsl;
public Params(String username, CFCredentials credentials, String host, boolean skipSsl) {
super();
this.username = username;
this.credentials = credentials;
this.host = host;
this.skipSsl = skipSsl;
}
@Override
public String toString() {
return "Params [username=" + username + ", host=" + host + ", skipSsl=" + skipSsl
+ "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((host == null) ? 0 : host.hashCode());
result = prime * result + ((credentials == null) ? 0 : credentials.hashCode());
result = prime * result + (skipSsl ? 1231 : 1237);
result = prime * result + ((username == null) ? 0 : username.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Params other = (Params) obj;
if (host == null) {
if (other.host != null)
return false;
} else if (!host.equals(other.host))
return false;
if (credentials == null) {
if (other.credentials != null)
return false;
} else if (!credentials.equals(other.credentials))
return false;
if (skipSsl != other.skipSsl)
return false;
if (username == null) {
if (other.username != null)
return false;
} else if (!username.equals(other.username))
return false;
return true;
}
}
private Map<Params, CFClientProvider> cache = new HashMap<>();
private int clientCount = 0;
public synchronized CFClientProvider getOrCreate(String username, CFCredentials credentials, String host, boolean skipSsl) {
Params params = new Params(username, credentials, host, skipSsl);
CFClientProvider client = cache.get(params);
if (client==null) {
clientCount++;
debug("Creating client ["+clientCount+"]: "+params);
cache.put(params, client = create(params));
} else {
debug("Reusing client ["+clientCount+"]: "+params);
}
return client;
}
protected CFClientProvider create(Params params) {
return new CFClientProvider(params);
}
}