| /* |
| * 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(); |
| } |
| } |
| } |