package de.bwlehrpool.bwlp_guac;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.protocol.GuacamoleConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.bwlehrpool.bwlp_guac.JsonClient.State;
public class AvailableClient implements Cloneable {
private static final Logger LOGGER = LoggerFactory.getLogger(AvailableClient.class);
private static final AtomicLong CON_ID = new AtomicLong();
private final String clientip;
private String password;
private int locationid;
private State state;
private String inUseBy;
private WrappedConnection connection;
/**
* TS when to delete this entry, or 0 = never
*/
private long deadline;
/**
* TA when to release this client if it's claimed, but state is not occupied
*/
private long idleClaimTimeout;
private long lastConnectionCheck;
private boolean connectionOk;
private final Object connCheckLock = new Object();
public AvailableClient(JsonClient source) {
this.clientip = source.clientip;
update(source);
}
private AvailableClient(String clientip) {
this.clientip = clientip;
}
/**
* Update this client's state, resetting "in use" if appropriate. Ie, the
* password changed, which would mean the remote VNC server was restarted, so it
* can't possibly be in use by the old user anymore.
*/
public synchronized void update(JsonClient source) {
if (this.inUseBy != null && source.state != State.OCCUPIED && !TunnelListener.hasTunnel(this.inUseBy)
&& System.currentTimeMillis() > this.idleClaimTimeout) {
LOGGER.info("Client " + this + " is available again (free client blocked by a user)");
this.inUseBy = null;
if (this.connection != null) {
this.connection.invalidate();
}
}
if (this.password == null || !this.password.equals(source.password)) {
if (source.state != State.OCCUPIED && this.inUseBy != null) {
LOGGER.info("Client " + this + " is available again");
this.inUseBy = null;
if (this.connection != null) {
this.connection.invalidate();
}
}
this.lastConnectionCheck = 0;
this.password = source.password;
}
// Update state if different, but only change to idle if inUseBy is null
if (this.inUseBy == null || source.state != State.IDLE) {
this.state = source.state;
}
this.locationid = source.locationid;
this.deadline = 0;
}
/**
* Try to reserve this client for the given user.
*/
public synchronized boolean claim(String user) {
if (this.inUseBy != null || this.password == null || this.state != State.IDLE || user == null)
return false;
this.inUseBy = user;
this.state = State.OCCUPIED;
this.idleClaimTimeout = System.currentTimeMillis() + 60000; // Give connect and login some time
return true;
}
public synchronized boolean isInUseBy(String user) {
return (this.inUseBy != null && this.inUseBy.equals(user));
}
public synchronized WrappedConnection getConnection(String expectedOwner) {
if (isInUseBy(expectedOwner)) {
if (this.connection == null || !this.connection.isValid())
this.connection = new WrappedConnection(this.clientip + "/" + CON_ID.incrementAndGet(), this);
return this.connection;
}
return null;
}
public synchronized void releaseConnection(String expectedOwner) {
if (isInUseBy(expectedOwner)) {
if (this.connection != null) {
this.connection.invalidate();
}
LOGGER.info("Prematurely releasing client " + this);
this.inUseBy = null;
} else {
LOGGER.info("Could not release client " + this + ". Already in use by " + this.inUseBy);
}
}
/**
* If this connection is not returned by the sat server anymore, we keep it
* around as long as there is someone using it, with some safety buffer.
*/
public boolean isTimeout(long NOW) {
if (deadline == 0) {
deadline = NOW + 120000; // Was recently updated from remote, set 2min timeout
return false;
}
long remaining = deadline - NOW;
if (remaining < 120000) {
synchronized (this) {
if (this.inUseBy != null && TunnelListener.hasTunnel(this.inUseBy)) {
deadline = NOW + 600000; // Another 10 minutes since the tunnel is active
LOGGER.info("Extending deadline of vanished client, because it's in use: " + this);
}
}
}
return NOW > deadline;
}
@Override
public String toString() {
return clientip + "/" + state + "/" + inUseBy;
}
public State getState() {
return state;
}
public int getLocationid() {
return locationid;
}
public GuacamoleConfiguration toGuacConfig() {
GuacamoleConfiguration cfg = new GuacamoleConfiguration();
cfg.setProtocol("vnc");
cfg.setParameter("hostname", this.clientip);
cfg.setParameter("port", Integer.toString(5900)); // TODO
cfg.setParameter("password", password);
return cfg;
}
/**
* Check if the given VNC credentials actually work, so we avoid assigning a
* bogus entry to a user.
*/
public boolean checkConnection(int retries) {
long now = System.currentTimeMillis();
synchronized (connCheckLock) {
if (now < this.lastConnectionCheck) {
this.lastConnectionCheck = 0;
}
if (now - this.lastConnectionCheck < 750)
return this.connectionOk;
for (;;) {
String version = null;
try (VncConnection vnc = new VncConnection(this.clientip, 5900)) {
version = vnc.handshake();
if (version == null) {
LOGGER.info("Host " + this.clientip + " doesn't speak RFB protocol");
break;
}
LOGGER.debug("VNC Version for " + this.clientip + " is " + version);
if (vnc.tryLogin(this.password)) {
LOGGER.info("Connection to " + this + " is OK");
this.lastConnectionCheck = System.currentTimeMillis();
return this.connectionOk = true;
}
} catch (IOException e) {
LOGGER.info("Connection error VNC (" + version + ") @ " + this);
if (retries-- > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
Thread.currentThread().interrupt();
break;
}
continue;
}
}
break;
}
this.lastConnectionCheck = System.currentTimeMillis();
this.password = null; // Render invalid, so ConnectionManager::getForUser() doesn't turn into an infinite loop
return this.connectionOk = false;
}
}
public void remoteLogin(Credentials credentials, String resolution) {
String username = null, password = null;
if (credentials != null) {
username = credentials.getUsername();
password = credentials.getPassword();
}
if (username == null) {
username = "";
} else {
LOGGER.info("Logging in user " + username + " on client " + this);
}
if (password == null) {
password = "";
}
try {
Socket socket = new Socket(); // TODO Port?
socket.connect(new InetSocketAddress(this.clientip, 7551), 1100);
socket.setSoTimeout(1000);
OutputStream output = new BufferedOutputStream(socket.getOutputStream());
int version = 1;
output.write(version >> 8);
output.write(version & 0xFF);
String data = username + "\n" + password + "\n" + resolution;
byte[] enc = Base64.getEncoder().encode(data.getBytes(StandardCharsets.UTF_8));
output.write(enc);
output.flush();
socket.close();
} catch (IOException e) {
LOGGER.warn("Login failed. User: " + username + " Client: " + this, e);
}
}
@Override
public AvailableClient clone() {
AvailableClient c = new AvailableClient(this.clientip);
c.state = this.state;
c.inUseBy = this.inUseBy;
c.password = this.password;
return c;
}
}