blob: 951f3ceb245c6c8b64a4f797c21cf5560a43fafe [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.ignite.console.agent;
import java.io.File;
import java.io.IOException;
import java.net.Authenticator;
import java.net.ConnectException;
import java.net.PasswordAuthentication;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.CountDownLatch;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterException;
import io.socket.client.Ack;
import io.socket.client.IO;
import io.socket.client.Socket;
import io.socket.emitter.Emitter;
import okhttp3.OkHttpClient;
import org.apache.ignite.console.agent.handlers.ClusterListener;
import org.apache.ignite.console.agent.handlers.DatabaseListener;
import org.apache.ignite.console.agent.handlers.RestListener;
import org.apache.ignite.console.agent.rest.RestExecutor;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.X;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
import static io.socket.client.Socket.EVENT_CONNECT;
import static io.socket.client.Socket.EVENT_CONNECT_ERROR;
import static io.socket.client.Socket.EVENT_DISCONNECT;
import static io.socket.client.Socket.EVENT_ERROR;
import static org.apache.ignite.console.agent.AgentUtils.fromJSON;
import static org.apache.ignite.console.agent.AgentUtils.sslConnectionSpec;
import static org.apache.ignite.console.agent.AgentUtils.sslSocketFactory;
import static org.apache.ignite.console.agent.AgentUtils.toJSON;
import static org.apache.ignite.console.agent.AgentUtils.trustManager;
/**
* Ignite Web Agent launcher.
*/
public class AgentLauncher {
/** */
private static final Logger log = LoggerFactory.getLogger(AgentLauncher.class);
/** */
private static final String EVENT_SCHEMA_IMPORT_DRIVERS = "schemaImport:drivers";
/** */
private static final String EVENT_SCHEMA_IMPORT_SCHEMAS = "schemaImport:schemas";
/** */
private static final String EVENT_SCHEMA_IMPORT_METADATA = "schemaImport:metadata";
/** */
private static final String EVENT_NODE_VISOR_TASK = "node:visorTask";
/** */
private static final String EVENT_NODE_REST = "node:rest";
/** */
private static final String EVENT_RESET_TOKEN = "agent:reset:token";
/** */
private static final String EVENT_LOG_WARNING = "log:warn";
static {
// Optionally remove existing handlers attached to j.u.l root logger.
SLF4JBridgeHandler.removeHandlersForRootLogger();
// Add SLF4JBridgeHandler to j.u.l's root logger.
SLF4JBridgeHandler.install();
}
/**
* On error listener.
*/
private static final Emitter.Listener onError = args -> {
Throwable e = (Throwable)args[0];
ConnectException ce = X.cause(e, ConnectException.class);
if (ce != null) {
log.error("Failed to establish connection to server or missing proxy settings (connection refused).");
log.error("Documentation for proxy configuration can be found here: https://apacheignite-tools.readme.io/docs/getting-started#section-proxy-configuration");
}
else {
if (X.hasCause(e, SSLHandshakeException.class)) {
log.error("Failed to establish SSL connection to server, due to errors with SSL handshake:", e);
log.error("Add to environment variable JVM_OPTS parameter \"-Dtrust.all=true\" to skip certificate validation in case of using self-signed certificate.");
System.exit(1);
}
if (X.hasCause(e, UnknownHostException.class)) {
log.error("Failed to establish connection to server, due to errors with DNS or missing proxy settings.", e);
log.error("Documentation for proxy configuration can be found here: https://apacheignite-tools.readme.io/docs/getting-started#section-proxy-configuration");
System.exit(1);
}
if (X.hasCause(e, ProxyAuthException.class)) {
log.error("Failed to establish connection to server, due to proxy requires authentication.");
String userName = System.getProperty("https.proxyUsername", System.getProperty("http.proxyUsername"));
if (userName == null || userName.trim().isEmpty())
userName = readLine("Enter proxy user name: ");
else
System.out.println("Read username from system properties: " + userName);
char[] pwd = readPassword("Enter proxy password: ");
final PasswordAuthentication pwdAuth = new PasswordAuthentication(userName, pwd);
Authenticator.setDefault(new Authenticator() {
@Override protected PasswordAuthentication getPasswordAuthentication() {
return pwdAuth;
}
});
return;
}
IOException ignore = X.cause(e, IOException.class);
if (ignore != null && "404".equals(ignore.getMessage())) {
log.error("Failed to receive response from server (connection refused).");
return;
}
log.error("Connection error.", e);
}
};
/**
* On disconnect listener.
*/
private static final Emitter.Listener onDisconnect = args -> log.error("Connection closed: {}", args);
/**
* On token reset listener.
*/
private static final Emitter.Listener onLogWarning = args -> log.warn(String.valueOf(args[0]));
/**
* @param fmt Format string.
* @param args Arguments.
*/
private static String readLine(String fmt, Object... args) {
if (System.console() != null)
return System.console().readLine(fmt, args);
System.out.print(String.format(fmt, args));
return new Scanner(System.in).nextLine();
}
/**
* @param fmt Format string.
* @param args Arguments.
*/
private static char[] readPassword(String fmt, Object... args) {
if (System.console() != null)
return System.console().readPassword(fmt, args);
System.out.print(String.format(fmt, args));
return new Scanner(System.in).nextLine().toCharArray();
}
/**
* @param args Args.
*/
public static void main(String[] args) throws Exception {
log.info("Starting Apache Ignite Web Console Agent...");
final AgentConfiguration cfg = new AgentConfiguration();
JCommander jCommander = new JCommander(cfg);
String osName = System.getProperty("os.name").toLowerCase();
jCommander.setProgramName("ignite-web-agent." + (osName.contains("win") ? "bat" : "sh"));
try {
jCommander.parse(args);
}
catch (ParameterException pe) {
log.error("Failed to parse command line parameters: " + Arrays.toString(args), pe);
jCommander.usage();
return;
}
String prop = cfg.configPath();
AgentConfiguration propCfg = new AgentConfiguration();
try {
File f = AgentUtils.resolvePath(prop);
if (f == null)
log.warn("Failed to find agent property file: {}", prop);
else
propCfg.load(f.toURI().toURL());
}
catch (IOException e) {
if (!AgentConfiguration.DFLT_CFG_PATH.equals(prop))
log.warn("Failed to load agent property file: " + prop, e);
}
cfg.merge(propCfg);
if (cfg.help()) {
jCommander.usage();
return;
}
System.out.println();
System.out.println("Agent configuration:");
System.out.println(cfg);
System.out.println();
URI uri;
try {
uri = new URI(cfg.serverUri());
}
catch (URISyntaxException e) {
log.error("Failed to parse Ignite Web Console uri", e);
return;
}
if (cfg.tokens() == null) {
System.out.println("Security token is required to establish connection to the web console.");
System.out.println(String.format("It is available on the Profile page: https://%s/profile", uri.getHost()));
String tokens = String.valueOf(readPassword("Enter security tokens separated by comma: "));
cfg.tokens(new ArrayList<>(Arrays.asList(tokens.trim().split(","))));
}
// Create proxy authenticator using passed properties.
switch (uri.getScheme()) {
case "http":
case "https":
final String username = System.getProperty(uri.getScheme() + ".proxyUsername");
final char[] pwd = System.getProperty(uri.getScheme() + ".proxyPassword", "").toCharArray();
Authenticator.setDefault(new Authenticator() {
@Override protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, pwd);
}
});
break;
default:
// No-op.
}
List<String> nodeURIs = cfg.nodeURIs();
for (int i = nodeURIs.size() - 1; i >= 0; i--) {
String nodeURI = nodeURIs.get(i);
try {
new URI(nodeURI);
}
catch (URISyntaxException ignored) {
log.warn("Failed to parse Ignite node URI: {}.", nodeURI);
nodeURIs.remove(i);
}
}
if (nodeURIs.isEmpty()) {
log.error("Failed to find valid URIs for connect to Ignite node via REST. Please check agent settings");
return;
}
boolean serverTrustAll = Boolean.getBoolean("trust.all");
boolean hasServerTrustStore = cfg.serverTrustStore() != null;
if (serverTrustAll && hasServerTrustStore) {
log.warn("Options contains both '--server-trust-store' and '-Dtrust.all=true'. " +
"Option '-Dtrust.all=true' will be ignored on connect to Web server.");
serverTrustAll = false;
}
boolean nodeTrustAll = Boolean.getBoolean("trust.all");
boolean hasNodeTrustStore = cfg.nodeTrustStore() != null;
if (nodeTrustAll && hasNodeTrustStore) {
log.warn("Options contains both '--node-trust-store' and '-Dtrust.all=true'. " +
"Option '-Dtrust.all=true' will be ignored on connect to cluster.");
nodeTrustAll = false;
}
cfg.nodeURIs(nodeURIs);
IO.Options opts = new IO.Options();
opts.path = "/agents";
List<String> cipherSuites = cfg.cipherSuites();
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.proxyAuthenticator(new ProxyAuthenticator());
if (
serverTrustAll ||
hasServerTrustStore ||
cfg.serverKeyStore() != null
) {
X509TrustManager serverTrustMgr = trustManager(
serverTrustAll,
cfg.serverTrustStore(),
cfg.serverTrustStorePassword()
);
if (serverTrustAll)
builder.hostnameVerifier((hostname, session) -> true);
SSLSocketFactory sslSocketFactory = sslSocketFactory(
cfg.serverKeyStore(),
cfg.serverKeyStorePassword(),
serverTrustMgr,
cipherSuites
);
if (sslSocketFactory != null) {
if (serverTrustMgr != null)
builder.sslSocketFactory(sslSocketFactory, serverTrustMgr);
else
builder.sslSocketFactory(sslSocketFactory);
if (!F.isEmpty(cipherSuites))
builder.connectionSpecs(sslConnectionSpec(cipherSuites));
}
opts.secure = true;
}
OkHttpClient okHttpClient = builder.build();
opts.callFactory = okHttpClient;
opts.webSocketFactory = okHttpClient;
final Socket client = IO.socket(uri, opts);
try (
RestExecutor restExecutor = new RestExecutor(
nodeTrustAll,
cfg.nodeKeyStore(), cfg.nodeKeyStorePassword(),
cfg.nodeTrustStore(), cfg.nodeTrustStorePassword(),
cipherSuites);
ClusterListener clusterLsnr = new ClusterListener(cfg, client, restExecutor)
) {
Emitter.Listener onConnect = connectRes -> {
log.info("Connection established.");
JSONObject authMsg = new JSONObject();
try {
authMsg.put("tokens", toJSON(cfg.tokens()));
authMsg.put("disableDemo", cfg.disableDemo());
String clsName = AgentLauncher.class.getSimpleName() + ".class";
String clsPath = AgentLauncher.class.getResource(clsName).toString();
if (clsPath.startsWith("jar")) {
String manifestPath = clsPath.substring(0, clsPath.lastIndexOf('!') + 1) +
"/META-INF/MANIFEST.MF";
Manifest manifest = new Manifest(new URL(manifestPath).openStream());
Attributes attr = manifest.getMainAttributes();
authMsg.put("ver", attr.getValue("Implementation-Version"));
authMsg.put("bt", attr.getValue("Build-Time"));
}
client.emit("agent:auth", authMsg, (Ack) authRes -> {
if (authRes != null) {
if (authRes[0] instanceof String) {
log.error((String)authRes[0]);
System.exit(1);
}
if (authRes[0] == null && authRes[1] instanceof JSONArray) {
try {
List<String> activeTokens = fromJSON(authRes[1], List.class);
if (!F.isEmpty(activeTokens)) {
Collection<String> missedTokens = cfg.tokens();
cfg.tokens(activeTokens);
missedTokens.removeAll(activeTokens);
if (!F.isEmpty(missedTokens)) {
String tokens = F.concat(missedTokens, ", ");
log.warn("Failed to authenticate with token(s): {}. " +
"Please reload agent archive or check settings", tokens);
}
log.info("Authentication success.");
clusterLsnr.watch();
return;
}
}
catch (Exception e) {
log.error("Failed to authenticate agent. Please check agent\'s tokens", e);
System.exit(1);
}
}
}
log.error("Failed to authenticate agent. Please check agent\'s tokens");
System.exit(1);
});
}
catch (JSONException | IOException e) {
log.error("Failed to construct authentication message", e);
client.close();
}
};
DatabaseListener dbHnd = new DatabaseListener(cfg);
RestListener restHnd = new RestListener(cfg, restExecutor);
final CountDownLatch latch = new CountDownLatch(1);
log.info("Connecting to: {}", cfg.serverUri());
client
.on(EVENT_CONNECT, onConnect)
.on(EVENT_CONNECT_ERROR, onError)
.on(EVENT_ERROR, onError)
.on(EVENT_DISCONNECT, onDisconnect)
.on(EVENT_LOG_WARNING, onLogWarning)
.on(EVENT_RESET_TOKEN, res -> {
String tok = String.valueOf(res[0]);
log.warn("Security token has been reset: {}", tok);
cfg.tokens().remove(tok);
if (cfg.tokens().isEmpty()) {
client.off();
latch.countDown();
}
})
.on(EVENT_SCHEMA_IMPORT_DRIVERS, dbHnd.availableDriversListener())
.on(EVENT_SCHEMA_IMPORT_SCHEMAS, dbHnd.schemasListener())
.on(EVENT_SCHEMA_IMPORT_METADATA, dbHnd.metadataListener())
.on(EVENT_NODE_VISOR_TASK, restHnd)
.on(EVENT_NODE_REST, restHnd);
client.connect();
latch.await();
}
finally {
client.close();
}
}
}