Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
package com.seeq.link.agent;
import java.io.EOFException;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.URI;
import java.nio.ByteBuffer;
import java.security.KeyStore;
import java.time.Duration;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpProxy;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.websocket.api.CloseStatus;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.UpgradeException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.common.OpCode;
import com.seeq.link.agent.interfaces.LoginAuthManager;
import com.seeq.link.agent.interfaces.SeeqWsConnection;
import com.seeq.link.agent.interfaces.SeeqWsConnectionPool;
import com.seeq.link.agent.interfaces.TooManyPingsLostException;
import com.seeq.link.agent.interfaces.WsPingManager;
import com.seeq.link.sdk.BaseConnection;
import com.seeq.link.sdk.interfaces.CertificateService;
import com.seeq.link.sdk.utilities.Event;
import com.seeq.link.sdk.utilities.EventListener;
import com.seeq.utilities.SeeqNames;
import com.seeq.utilities.exception.OperationCanceledException;
import lombok.extern.slf4j.Slf4j;
/**
* Handles a WebSocket-based connection to the Seeq Application Server.
*/
@WebSocket
@Slf4j
public class DefaultSeeqWsConnection extends BaseConnection implements SeeqWsConnection {
private static final ByteBuffer EMPTY_PAYLOAD = ByteBuffer.wrap(new byte[0]);
private final String linkURL;
private final int poolMemberIndex;
private final LoginAuthManager loginAuthManager;
/**
* Disconnect whenever the agent key file is modified so that we log out and the reconnect logic reads in the new
* key and connects with the new credentials.
*/
private final EventListener onAgentKeyChanged = this.disconnectOnChange("Agent key");
/**
* Disconnect whenever the Pre-provisioned one-time password is modified in the vault so that we continue the
* provisioning logic.
*/
private final EventListener onPreProvisionedOTPChanged =
this.disconnectOnChange("Pre-provisioned one-time password");
/**
* Disconnect whenever the provisioned password is modified, another agent finished the provisioning, we can login.
*/
private final EventListener onProvisionedPasswordChanged = this.disconnectOnChange("Provisioned password");
private final CertificateService certificateService;
private final boolean isRemoteAgent;
private final String agentIdentification;
private final SeeqWsConnectionPool pool;
private final boolean connectedOnDegradedState;
private final WsPingManager pingManager;
private Session session;
private final Event messageReceivedEvent = new Event<>();
private final ResourceBundle agentStrings = ResourceBundle.getBundle("AgentStringsBundle");
private HttpClient httpClient;
private WebSocketClient webSocketClient;
private final ExecutorService incomingMessageThreadPool;
private final ExecutorService outgoingMessageThreadPool;
public static final int INCOMING_MESSAGE_THREAD_POOL_SIZE = 10;
private Event.ListenerRegistration messageReceivedListener;
@Override
public Event getMessageReceivedEvent() {
return this.messageReceivedEvent;
}
@Override
public void enableKeepAlive(Duration newInterval, Duration newTimeout) {
this.pingManager.enableKeepAlive(newInterval, newTimeout);
}
@Slf4j
public static class UnhandledExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
LOG.error("Uncaught exception in thread: {}", t.getName(), e);
}
}
public DefaultSeeqWsConnection(SeeqWsConnectionPool pool, int poolMemberIndex, String linkURL,
LoginAuthManager loginAuthManager, CertificateService certificateService, boolean isRemoteAgent,
String agentIdentification, ExecutorService incomingMessageThreadPool,
ExecutorService outgoingMessageThreadPool, boolean connectedOnDegradedState,
WsPingManager pingManager) {
this.pool = pool;
this.poolMemberIndex = poolMemberIndex;
this.linkURL = linkURL;
this.loginAuthManager = loginAuthManager;
this.loginAuthManager.getAgentKeyModifiedEvent().add(this.onAgentKeyChanged);
this.loginAuthManager.getPreProvisionedOneTimePasswordModifiedEvent().add(this.onPreProvisionedOTPChanged);
this.loginAuthManager.getProvisionedPasswordModifiedEvent().add(this.onProvisionedPasswordChanged);
this.certificateService = certificateService;
this.isRemoteAgent = isRemoteAgent;
this.agentIdentification = agentIdentification;
this.pingManager = pingManager;
this.incomingMessageThreadPool = incomingMessageThreadPool;
this.outgoingMessageThreadPool = outgoingMessageThreadPool;
this.connectedOnDegradedState = connectedOnDegradedState;
if (this.isRemoteAgent) {
// Tell Java to use the system-wide proxy settings in case someone setup a proxy in Internet Explorer
System.setProperty("java.net.useSystemProxies", "true");
}
}
@Override
public void initialize() {
LOG.info("'{}' being initialized", this.getConnectionId());
// We set short reconnect delays here because we want to reconnect as quickly as possible
this.setMinReconnectDelay(Duration.ofSeconds(1));
this.setMaxReconnectDelay(Duration.ofSeconds(1));
// Any received message on this WebSocket will be passed to the pool
this.messageReceivedListener = this.getMessageReceivedEvent().add(this.pool::onWebsocketMessageReceived);
LOG.info("'{}' initialized", this.getConnectionId());
}
@Override
public void destroy() {
LOG.info("'{}' being destroyed", this.getConnectionId());
this.messageReceivedListener.remove();
this.messageReceivedListener = null;
this.disable();
this.loginAuthManager.getAgentKeyModifiedEvent().remove(this.onAgentKeyChanged);
this.loginAuthManager.getPreProvisionedOneTimePasswordModifiedEvent().remove(this.onPreProvisionedOTPChanged);
this.loginAuthManager.getProvisionedPasswordModifiedEvent().remove(this.onProvisionedPasswordChanged);
try {
this.shutdownWebSocketClient();
this.shutdownHttpClient();
} catch (Exception e) {
LOG.error("Error shutting down the clients", e);
}
LOG.info("'{}' destroyed", this.getConnectionId());
}
private boolean sendPing(byte[] message) {
if (this.getState() != ConnectionState.CONNECTED) {
return false;
}
LOG.trace("Sending ping using {}", this.getConnectionId());
// Last-chance check for request cancellation; beyond this point, we can't guarantee that a message won't be
// sent.
OperationCanceledException.checkForCancellation();
ByteBuffer messageBuffer = ByteBuffer.wrap(message);
try {
// sendPing is sending an asyncFrame and it doesn't return a Future, so we can't use the same pattern as
// in sendMessage.
this.session.getRemote().sendPing(messageBuffer);
} catch (IOException e) {
LOG.error("Error sending ping to Seeq Server:", e);
return false;
}
return true;
}
@Override
public boolean sendMessage(byte[] message) {
if (this.getState() != ConnectionState.CONNECTED) {
return false;
}
LOG.debug("Sending message using {}", this.getConnectionId());
// Last-chance check for request cancellation; beyond this point, we can't guarantee that a message won't be
// sent (although it may not be, since the Future.get() calls also check for thread interruption).
OperationCanceledException.checkForCancellation();
ByteBuffer messageBuffer = ByteBuffer.wrap(message);
Callable> messageSendTask = () -> this.session.getRemote().sendBytesByFuture(messageBuffer);
Future> dispatchFuture = this.outgoingMessageThreadPool.submit(messageSendTask);
try {
// We have a 120-second "sanity" timeout; no message should take this long to transfer.
// CRAB-14864 (Dave H): We wait for the send to complete on the incoming message thread, *not* the outgoing
// message thread. This is to fulfill the contract of SeeqWsConnection.sendMessage(), which is synchronous.
dispatchFuture
.get() // <--- dispatch future
.get(120, TimeUnit.SECONDS); // <--- sendBytesByFuture future
return true;
} catch (InterruptedException ex) {
LOG.debug("Agent request thread {} interrupted during sendMessage(); most likely cause is request " +
"cancellation", Thread.currentThread().getName());
return false;
} catch (ExecutionException ex) {
LOG.error("Error sending message to Seeq Server:", ex);
return false;
} catch (TimeoutException ex) {
LOG.error("Timeout sending message to Seeq Server.");
return false;
}
}
/** Handles the act of connecting to the Seeq Application Server. */
@Override
protected void connect() {
this.setState(ConnectionState.CONNECTING,
String.format(this.agentStrings.getString("ConnectingWithDefaultCredentials"), this.linkURL));
String authorizationToken = this.loginAuthManager.getAuthToken();
if (authorizationToken == null) {
LOG.info("Waiting for authentication token, can't connect...");
this.setState(ConnectionState.DISCONNECTED, this.agentStrings.getString("Disconnected"));
return;
}
try {
this.ensureStarted();
ClientUpgradeRequest upgradeRequest = new ClientUpgradeRequest();
upgradeRequest.setHeader(SeeqNames.API.Headers.Auth, authorizationToken);
// The identityPath will get stripped off and replaced if we're connecting through Nginx. This is for
// local agent connections that don't go through Nginx.
upgradeRequest.setHeader(SeeqNames.API.Headers.IdentityPath, this.loginAuthManager.getIdentityPath());
upgradeRequest.setHeader(SeeqNames.API.Headers.PoolId, this.agentIdentification);
if (this.connectedOnDegradedState) {
upgradeRequest.setHeader(SeeqNames.API.Headers.PoolConnectedOnDegradedState, "true");
}
this.webSocketClient.connect(this, new URI(this.linkURL), upgradeRequest).get();
this.pingManager.start(this.getConnectionId(), this::sendPing);
} catch (Exception e) {
// When the pool destroys a connection it is disabled and its connection monitor thread is interrupted
if (this.getState() == ConnectionState.DISABLED) {
if (e instanceof OperationCanceledException) {
throw (OperationCanceledException) e;
}
if (e instanceof InterruptedException) {
throw new OperationCanceledException("Thread interrupted", e);
}
}
this.handleConnectException(e.getCause() != null ? e.getCause() : e);
this.setState(ConnectionState.DISCONNECTED, this.agentStrings.getString("Disconnected"));
}
}
private void ensureStarted() throws Exception {
if (this.webSocketClient != null) {
return;
}
// Context: While the proxy configuration of the JVM (which is inherited from the OS, including Windows, in
// which the JVM is running) is automatically applied for normal HTTP connections, web sockets do not get the
// same treatment. This is a problem, as a websocket should get routed the same way as HTTP traffic almost
// always, especially in corporate environments that tend to proxy every internet connection.
//
// Search for proxies first by using the ws:// protocol and then the http:// protocol. XOM didn't
// have the ws:// protocol set in their proxy configuration, and Dustin isn't sure if anyone does. If
// you're reading this then you probably are fixing a bug in this area and know more than me. This
// was modified for CRAB-17622 to check both protocols for proxy configuration.
Optional maybeProxy = Stream.of(this.linkURL, this.linkURL.replaceFirst("ws", "http"))
.flatMap(url -> ProxySelector.getDefault().select(URI.create(url)).stream()
.peek(proxy -> LOG.debug("Java's ProxySelector for the agent-link websocket " +
"connection returned Proxy of type '{}' at address '{}' to use for the " +
"destination url of '{}'.", proxy.type(), proxy.address(), url)))
.filter(p -> p.address() instanceof InetSocketAddress)
.map(p -> (InetSocketAddress) p.address())
.map(a -> new HttpProxy(a.getHostName(), a.getPort()))
// Collect to an intermediate list so the peek above prints for all elements
.collect(Collectors.toList())
.stream()
.findFirst();
SslContextFactory sslContextFactory = new SslContextFactory.Client();
KeyStore keyStore = this.certificateService.getKeyStore();
sslContextFactory.setTrustStore(keyStore);
this.httpClient = new HttpClient(sslContextFactory);
if (this.isRemoteAgent && maybeProxy.isPresent()) {
LOG.info("Using '{}' as the proxy for the agent-link websocket connection", maybeProxy.get());
this.httpClient.getProxyConfiguration().getProxies().add(maybeProxy.get());
} else if (this.isRemoteAgent) {
LOG.info("No suitable proxy found for the agent-link websocket connection. Connecting directly.");
} else {
LOG.debug("This is a local agent. Ignoring any HTTP proxies and connecting the agent-link websocket " +
"directly.");
}
this.httpClient.start();
this.webSocketClient = new WebSocketClient(this.httpClient);
// The websocket may be idle 5 min before closing. It is the default value but we want it set explicitly to
// be clear and to not be affected by jetty migrations.
this.webSocketClient.setMaxIdleTimeout(300000);
// Keep this is sync with C# DotNetWebSocketConnection#connect
// It must be bigger than the maximum number of websockets per pool (currently 10) * heartbeat period
this.webSocketClient.setAsyncWriteTimeout(120000);
// Ensure that we can receive messages up to 50MB in size
this.webSocketClient.getPolicy().setMaxBinaryMessageSize(50 * 1024 * 1024);
try {
this.webSocketClient.start();
} catch (Exception e) {
this.shutdownWebSocketClient();
this.shutdownHttpClient();
LOG.error("Error starting WebSocketClient", e);
throw e;
}
}
private void shutdownWebSocketClient() {
try {
this.webSocketClient.setStopTimeout(3_000);
this.webSocketClient.stop();
} catch (Exception e) {
LOG.debug("Failed to gracefully stop the webSocketClient", e);
} finally {
this.webSocketClient.destroy();
}
}
private void shutdownHttpClient() {
try {
this.httpClient.setStopTimeout(3_000);
this.httpClient.stop();
} catch (Exception e) {
LOG.debug("Failed to gracefully stop the httpClient", e);
} finally {
this.httpClient.destroy();
}
}
@Override
protected void monitor() {
if (this.getState() == ConnectionState.CONNECTED) {
try {
this.pingManager.checkStalePings();
} catch (TooManyPingsLostException e) {
LOG.error("Disconnecting the WebSocket. Reason: {}", e.getMessage());
this.disconnect();
}
}
}
/**
* Disconnect from Seeq Application Server and ensure socket has been closed.
*/
@Override
protected void disconnect() {
this.pingManager.stop();
Session theSession;
synchronized (this) {
theSession = this.session;
this.session = null;
}
if (theSession == null) {
return;
}
try {
if (theSession.isOpen()) {
theSession.close(new CloseStatus(1000, "Socket closed by webSocketClient"));
}
} catch (Exception e) {
LOG.debug("Failed to close the session", e);
}
this.setState(ConnectionState.DISCONNECTED, this.agentStrings.getString("Disconnected"));
}
@OnWebSocketConnect
public void onSocketOpen(Session session) {
synchronized (this) {
this.session = session;
}
this.setState(ConnectionState.CONNECTED,
String.format(this.agentStrings.getString("Connected"), this.getConnectionId()));
}
@OnWebSocketClose
public void onSocketClose(int statusCode, String reason) {
LOG.info("SeeqConnection.OnSocketClose() received (code: " + statusCode + ", reason: " + reason + ")");
this.disconnect();
}
@OnWebSocketMessage
public void onSocketMessage(byte[] buffer, int offset, int length) {
final byte[] message = Arrays.copyOfRange(buffer, offset, length);
// All message handlers are placed onto a worker thread so that the network does not block.
this.incomingMessageThreadPool.submit(() -> this.onMessageReceivedCallback(message));
}
@OnWebSocketFrame
public void onWebSocketFrame(Frame frame) {
if (OpCode.PONG == frame.getOpCode()) {
ByteBuffer payload = frame.getPayload() != null ? frame.getPayload() : EMPTY_PAYLOAD;
// copy of the ByteBuffer to preserve the data for later use without the risk of it being modified
byte[] messageArray = copyByteBuffer(payload).array();
if (this.pingManager.hasPongFormat(messageArray)) {
this.pingManager.handlePong(messageArray);
}
}
}
private static ByteBuffer copyByteBuffer(ByteBuffer src) {
ByteBuffer dest = ByteBuffer.allocate(src.remaining());
dest.put(src);
dest.flip();
return dest;
}
private void onMessageReceivedCallback(byte[] message) {
this.messageReceivedEvent.dispatch(this, new MessageReceivedEventArgs(this, message));
}
@OnWebSocketError
public void onSocketError(Session session, Throwable cause) {
this.handleConnectException(cause);
}
private void handleConnectException(Throwable cause) {
boolean bootingUp = false;
if (cause.getClass().equals(ConnectException.class)) {
ConnectException connectException = (ConnectException) cause;
if (connectException.getMessage().contains("no further information")) {
bootingUp = true;
}
} else if (cause.getClass().equals(UpgradeException.class)) {
UpgradeException upgradeException = (UpgradeException) cause;
if (upgradeException.getMessage().contains("got <404>") ||
upgradeException.getMessage().contains("0 null") ||
upgradeException.getMessage().contains("400 Unknown Version") ||
upgradeException.getMessage().contains("404 Not Found")) {
bootingUp = true;
}
} else if (cause.getClass().equals(EOFException.class)) {
EOFException eofException = (EOFException) cause;
if (eofException.getMessage().contains("Reading WebSocket Upgrade response")) {
bootingUp = true;
}
}
if (bootingUp) {
LOG.info("WebSocket connection refused, server may be booting up, unresponsive, inaccessible or down. " +
"Retrying shortly...");
} else {
LOG.error("Socket error", cause);
}
}
/**
* The connection String for the Seeq Application Server. This is the same as the URL property.
*/
@Override
public String getConnectionId() {
return this.linkURL + " [#" + this.poolMemberIndex + "]";
}
@Override
protected void setState(ConnectionState newState, String message) {
super.setState(newState, message);
this.pool.onWebSocketChangeState(newState);
}
@Override
public boolean equals(Object o) {
// We override equals and hashCode because pool.destroyExcessConnections will remove connections
// equals to a certain value
if (this == o) {return true;}
if (o == null || this.getClass() != o.getClass()) {return false;}
DefaultSeeqWsConnection that = (DefaultSeeqWsConnection) o;
return this.poolMemberIndex == that.poolMemberIndex &&
Objects.equals(this.linkURL, that.linkURL);
}
@Override
public int hashCode() {
return Objects.hash(this.linkURL, this.poolMemberIndex);
}
@Override
public String toString() {
return "#" + this.poolMemberIndex + " " + this.getState();
}
private EventListener disconnectOnChange(String label) {
return (source, message) -> {
if (DefaultSeeqWsConnection.this.getState() != ConnectionState.DISABLED &&
DefaultSeeqWsConnection.this.loginAuthManager.shouldRestartDueToSecretsChange()) {
LOG.info(label + " changed, disconnecting and then reconnecting");
DefaultSeeqWsConnection.this.disconnect();
}
};
}
}