package de.bwlehrpool.bwlp_guac; import java.io.*; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.ArrayList; 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(); public ArrayList groupList = new ArrayList(); private final String clientip; private String password; private int locationid; private State state; private String inUseBy; private WrappedConnection connection; private long deadline; private long lastConnectionCheck; private boolean connectionOk; private final Object connCheckLock = new Object(); public AvailableClient(JsonClient source) { this.clientip = source.clientip; this.locationid = source.locationid; 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)) { LOGGER.info("Free client blocked by a disconnected user detected."); LOGGER.info("Client " + this + " is available again"); this.inUseBy = null; if (this.connection != null) this.connection.invalidate(); } if (this.password == null || !this.password.equals(source.password)) { if (source.state != State.OCCUPIED) { if (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; } if (this.inUseBy == null || source.state != State.IDLE) this.state = source.state; 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; 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 for another 5 minutes just in case. */ public boolean isTimeout(long NOW) { if (deadline == 0) { deadline = NOW + 300000; return false; } return deadline < NOW; } @Override public String toString() { return clientip + "/" + state + "/" + inUseBy; } public State getState() { return state; } public int getLocationid() { return locationid; } public void markAsMissing() { this.state = State.OFFLINE; } 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 = credentials.getUsername(); try { LOGGER.info("Logging in user " + username + " on client " + this); Socket socket = new Socket(this.clientip, 7551); // TODO Port? OutputStream output = socket.getOutputStream(); int version = 1; output.write(version & 0xFF); output.write(version >> 8); String data = username + "\n" + credentials.getPassword() + "\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; } }