From 41186ddf8eef2530b95fe90f03bd84ee841115d9 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Wed, 15 Apr 2020 15:40:55 +0200 Subject: First Commit --- .gitignore | 4 + pom.xml | 46 +++++ .../de/bwlehrpool/bwlp_guac/AvailableClient.java | 157 +++++++++++++++ .../bwlp_guac/BwlpAuthenticationProvider.java | 84 ++++++++ .../de/bwlehrpool/bwlp_guac/BwlpUserContext.java | 109 ++++++++++ .../de/bwlehrpool/bwlp_guac/ConnectionManager.java | 219 +++++++++++++++++++++ .../java/de/bwlehrpool/bwlp_guac/JsonClient.java | 17 ++ .../java/de/bwlehrpool/bwlp_guac/SlxConfig.java | 43 ++++ .../de/bwlehrpool/bwlp_guac/VncConnection.java | 141 +++++++++++++ .../de/bwlehrpool/bwlp_guac/WrappedConnection.java | 26 +++ src/main/resources/disclaimer.html | 6 + src/main/resources/guac-manifest.json | 7 + src/test/java/de/bwlehrpool/bwlp_guac/AppTest.java | 38 ++++ 13 files changed, 897 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/de/bwlehrpool/bwlp_guac/AvailableClient.java create mode 100644 src/main/java/de/bwlehrpool/bwlp_guac/BwlpAuthenticationProvider.java create mode 100644 src/main/java/de/bwlehrpool/bwlp_guac/BwlpUserContext.java create mode 100644 src/main/java/de/bwlehrpool/bwlp_guac/ConnectionManager.java create mode 100644 src/main/java/de/bwlehrpool/bwlp_guac/JsonClient.java create mode 100644 src/main/java/de/bwlehrpool/bwlp_guac/SlxConfig.java create mode 100644 src/main/java/de/bwlehrpool/bwlp_guac/VncConnection.java create mode 100644 src/main/java/de/bwlehrpool/bwlp_guac/WrappedConnection.java create mode 100644 src/main/resources/disclaimer.html create mode 100644 src/main/resources/guac-manifest.json create mode 100644 src/test/java/de/bwlehrpool/bwlp_guac/AppTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1898b99 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.classpath +/.project +/.settings +/target diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f4c361f --- /dev/null +++ b/pom.xml @@ -0,0 +1,46 @@ + + 4.0.0 + + de.bwlehrpool + bwlp-guac + 0.0.1-SNAPSHOT + jar + + bwlp-guac + http://maven.apache.org + + + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.1 + + 1.7 + 1.7 + + + + + + + + junit + junit + 3.8.1 + test + + + org.apache.guacamole + guacamole-ext + 1.1.0 + + + + diff --git a/src/main/java/de/bwlehrpool/bwlp_guac/AvailableClient.java b/src/main/java/de/bwlehrpool/bwlp_guac/AvailableClient.java new file mode 100644 index 0000000..7c4ca80 --- /dev/null +++ b/src/main/java/de/bwlehrpool/bwlp_guac/AvailableClient.java @@ -0,0 +1,157 @@ +package de.bwlehrpool.bwlp_guac; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.guacamole.protocol.GuacamoleConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.bwlehrpool.bwlp_guac.JsonClient.State; + +public class AvailableClient { + + 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 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; + update(source); + } + + /** + * 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.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; + this.connection = null; + } + } + this.lastConnectionCheck = 0; + this.password = source.password; + } + 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.connection = new WrappedConnection(this.clientip + "/" + CON_ID.incrementAndGet(), this); + 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)) + return this.connection; + return null; + } + + public synchronized void releaseConnection(String expectedOwner) { + if (isInUseBy(expectedOwner)) { + LOGGER.info("Prematurely releasing client " + this); + this.inUseBy = null; + this.connection = null; + } + } + + /** + * 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 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 < 5000) + return this.connectionOk; + for (;;) { + try (VncConnection vnc = new VncConnection(this.clientip, 5900)) { + LOGGER.debug("VNC Version for " + this.clientip + " is " + vnc.handshake()); + if (vnc.tryLogin(this.password)) { + this.lastConnectionCheck = now; + return this.connectionOk = true; + } + } catch (IOException e) { + LOGGER.info("Connection error VNC @ " + this, e); + if (retries-- > 0) { + try { + Thread.sleep(1000); + } catch (InterruptedException e1) { + Thread.currentThread().interrupt(); + break; + } + continue; + } + } + break; + } + this.lastConnectionCheck = now; + this.password = null; // Render invalid, so ConnectionManager::getForUser() doesn't turn into an infinite loop + return this.connectionOk = false; + } + } + +} diff --git a/src/main/java/de/bwlehrpool/bwlp_guac/BwlpAuthenticationProvider.java b/src/main/java/de/bwlehrpool/bwlp_guac/BwlpAuthenticationProvider.java new file mode 100644 index 0000000..4ec4f48 --- /dev/null +++ b/src/main/java/de/bwlehrpool/bwlp_guac/BwlpAuthenticationProvider.java @@ -0,0 +1,84 @@ +package de.bwlehrpool.bwlp_guac; + +import java.util.Collections; +import java.util.Map; +import java.util.WeakHashMap; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.AuthenticatedUser; +import org.apache.guacamole.net.auth.AuthenticationProvider; +import org.apache.guacamole.net.auth.Credentials; +import org.apache.guacamole.net.auth.UserContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BwlpAuthenticationProvider implements AuthenticationProvider { + + Logger LOGGER = LoggerFactory.getLogger(BwlpAuthenticationProvider.class); + + public String getIdentifier() { + return "de.bwlehrpool.bwgpul"; + } + + public Object getResource() throws GuacamoleException { + return null; + } + + public AuthenticatedUser authenticateUser(Credentials credentials) throws GuacamoleException { + // XXX We can somehow request additional fields to be shown during login by throwing an exception + // that declares additional ones; but when I tried, it removed the existing username and password + // field, so do we need to state them too? Seems wrong since we don't need them, we'd just want + // an additional field to pick the room/location we want to end up in and let the actual + // authentication plugin define the username/password fields. + return null; + } + + public AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser, Credentials credentials) + throws GuacamoleException { + return null; + } + + public UserContext getUserContext(AuthenticatedUser authenticatedUser) throws GuacamoleException { + LOGGER.warn("Ignoring getUserContext for " + authenticatedUser.toString()); + return null; + } + + public UserContext updateUserContext(UserContext context, AuthenticatedUser authenticatedUser, + Credentials credentials) throws GuacamoleException { + LOGGER.warn("Ignoring updateUserContext called with " + context.toString()); + return null; + } + + public UserContext decorate(UserContext context, AuthenticatedUser authenticatedUser, Credentials credentials) + throws GuacamoleException { + String username = authenticatedUser.getCredentials().getUsername(); + LOGGER.warn("decorate called for " + username); + BwlpUserContext user = oldMappings.get(username); + if (user != null) + return user; + LOGGER.warn("Doing the decoration"); + user = new BwlpUserContext(authenticatedUser, context); + oldMappings.put(username, user); + return user; + } + + private Map oldMappings = Collections + .synchronizedMap(new WeakHashMap()); + + public UserContext redecorate(UserContext decorated, UserContext context, AuthenticatedUser authenticatedUser, + Credentials credentials) throws GuacamoleException { + String username = authenticatedUser.getCredentials().getUsername(); + LOGGER.warn("REdecorate called for " + username); + BwlpUserContext user = oldMappings.get(username); + if (user != null && user.hasValidConnection()) + return user; + LOGGER.warn("Doing the REdecoration"); + user = new BwlpUserContext(authenticatedUser, context); + oldMappings.put(username, user); + return user; + } + + public void shutdown() { + } + +} diff --git a/src/main/java/de/bwlehrpool/bwlp_guac/BwlpUserContext.java b/src/main/java/de/bwlehrpool/bwlp_guac/BwlpUserContext.java new file mode 100644 index 0000000..7f0856c --- /dev/null +++ b/src/main/java/de/bwlehrpool/bwlp_guac/BwlpUserContext.java @@ -0,0 +1,109 @@ +package de.bwlehrpool.bwlp_guac; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.AbstractUserContext; +import org.apache.guacamole.net.auth.AuthenticatedUser; +import org.apache.guacamole.net.auth.AuthenticationProvider; +import org.apache.guacamole.net.auth.Connection; +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.User; +import org.apache.guacamole.net.auth.UserContext; +import org.apache.guacamole.net.auth.permission.ObjectPermissionSet; +import org.apache.guacamole.net.auth.simple.SimpleConnection; +import org.apache.guacamole.net.auth.simple.SimpleDirectory; +import org.apache.guacamole.net.auth.simple.SimpleObjectPermissionSet; +import org.apache.guacamole.net.auth.simple.SimpleUser; +import org.apache.guacamole.protocol.GuacamoleConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BwlpUserContext extends AbstractUserContext { + + private static final Logger LOGGER = LoggerFactory.getLogger(BwlpUserContext.class); + + private static final SimpleConnection FAKE = new SimpleConnection("FAKE", "FAKE", new GuacamoleConfiguration()); + + static { + FAKE.setParentIdentifier(DEFAULT_ROOT_CONNECTION_GROUP); + } + + private final AuthenticatedUser authUser; + private final UserContext originalContext; + + /** + * The Directory with access to all connections within the root group associated + * with this UserContext. + */ + private Directory connectionDirectory; + + public BwlpUserContext(AuthenticatedUser authenticatedUser, UserContext context) { + authUser = authenticatedUser; + originalContext = context; + // OK + addConn(); + } + + private void addConn() { + WrappedConnection connection = ConnectionManager.getForUser(authUser.getCredentials().getUsername()); + if (connection != null) { + connectionDirectory = new SimpleDirectory(connection); + } else { + connectionDirectory = new SimpleDirectory(); + } + } + + public User self() { + return new SimpleUser(authUser.getCredentials().getUsername()) { + + @Override + public ObjectPermissionSet getConnectionGroupPermissions() throws GuacamoleException { + return new SimpleObjectPermissionSet(getConnectionDirectory().getIdentifiers()); + } + + @Override + public ObjectPermissionSet getConnectionPermissions() throws GuacamoleException { + return new SimpleObjectPermissionSet(getConnectionGroupDirectory().getIdentifiers()); + } + + }; + } + + @Override + public Object getResource() throws GuacamoleException { + return null; + } + + public AuthenticationProvider getAuthenticationProvider() { + return originalContext.getAuthenticationProvider(); + } + + @Override + public Directory getConnectionDirectory() throws GuacamoleException { + return connectionDirectory; + } + + public boolean hasValidConnection() { + boolean ok = false; + try { + synchronized (this) { + for (String id : connectionDirectory.getIdentifiers()) { + Connection con = connectionDirectory.get(id); + if (con instanceof WrappedConnection) { + if (((WrappedConnection) con).checkConnection(3)) { + ok = true; + } + } else { + } + } + } + } catch (Exception e) { + LOGGER.warn("hasValidConnection", e); + } + return ok; + } + + public UserContext getOriginalContext() { + return originalContext; + } + +} diff --git a/src/main/java/de/bwlehrpool/bwlp_guac/ConnectionManager.java b/src/main/java/de/bwlehrpool/bwlp_guac/ConnectionManager.java new file mode 100644 index 0000000..0132e43 --- /dev/null +++ b/src/main/java/de/bwlehrpool/bwlp_guac/ConnectionManager.java @@ -0,0 +1,219 @@ +package de.bwlehrpool.bwlp_guac; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Iterator; +import java.util.LinkedHashMap; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.codehaus.jackson.map.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Broker for all clients available. + * Keeps track of which clients are online, offline, assigned to a user etc. + */ +public class ConnectionManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionManager.class); + + // TODO: Config + private static final String SOURCE_URL = SlxConfig.clientListUrl(); + + private static final LinkedHashMap clientPool = new LinkedHashMap(); + + /** + * Pass plain user name, get existing connection (if any), or a fresh one + * @param user (LDAP) user name + */ + public static WrappedConnection getForUser(String user) { + if (SOURCE_URL == null) { + LOGGER.warn("Don't have a source URL for client machines!"); + return null; + } + try { + updateList(); + // Find existing/free client + AvailableClient freeClient; + for (;;) { + freeClient = null; + synchronized (clientPool) { + for (AvailableClient ac : clientPool.values()) { + if (ac.isInUseBy(user)) { + freeClient = ac; + break; + } + } + if (freeClient == null) { + for (AvailableClient ac : clientPool.values()) { + if (ac.claim(user)) { + freeClient = ac; + break; + } + } + } + } + if (freeClient == null) + return null; // TODO: No more clients available -- how to handle? + // Found free or existing client, check if connection is (still) possible + if (freeClient.checkConnection(0)) { + LOGGER.info("Establishing mapping for user " + user + " to " + freeClient); + return freeClient.getConnection(user); + } + // Connection check failed - release and loop again + freeClient.releaseConnection(user); + } + } catch (Exception e) { + LOGGER.warn("KACKE AM DAMPFEN", e); + return null; + } + } + + private static long lastUpdate = 0; + + /** + * Fetch fresh client list from satellite server. + * Cache for 15 seconds. + */ + private static synchronized void updateList() { + long now = System.currentTimeMillis(); + if (now < lastUpdate) { + lastUpdate = now; + } + if (now - lastUpdate < 15000) + return; + // OK GO + lastUpdate = now; + ByteArrayOutputStream baos = new ByteArrayOutputStream(3000); + HttpURLConnection con; + try { + con = (HttpURLConnection) new URL(SOURCE_URL).openConnection(); + } catch (MalformedURLException e1) { + LOGGER.warn("Bad Connection Pool URL", e1); + return; + } catch (IOException e1) { + LOGGER.warn("Cannot connect to Connection Pool URL", e1); + return; + } + if (con instanceof HttpsURLConnection) { + ((HttpsURLConnection) con).setHostnameVerifier(ignorer); + ((HttpsURLConnection) con).setSSLSocketFactory(sockFac); + } + try (BufferedInputStream in = new BufferedInputStream(con.getInputStream())) { + byte dataBuffer[] = new byte[2048]; + int bytesRead; + while ((bytesRead = in.read(dataBuffer, 0, dataBuffer.length)) != -1) { + baos.write(dataBuffer, 0, bytesRead); + } + } catch (IOException e) { + LOGGER.warn("Error while reading reply of Connection Pool", e); + return; + } + populateList(baos.toByteArray()); + } + + private static void populateList(byte[] data) { + ObjectMapper mapper = new ObjectMapper(); + JsonClient[] list; + try { + list = mapper.readValue(data, JsonClient[].class); + } catch (Exception e) { + LOGGER.warn("Could not deserialize JSON from Connection Pool", e); + LOGGER.warn("Not updating local list"); + return; + } + synchronized (clientPool) { + for (JsonClient cnew : list) { + if (cnew.password == null || cnew.clientip == null) + continue; // Invalid + AvailableClient existing = clientPool.get(cnew.clientip); + if (existing == null) { + // New client + clientPool.put(cnew.clientip, new AvailableClient(cnew)); + LOGGER.info("New client " + cnew.clientip); + } else { + existing.update(cnew); + } + } + final long NOW = System.currentTimeMillis(); + for (Iterator it = clientPool.values().iterator(); it.hasNext();) { + AvailableClient c = it.next(); + if (c.isTimeout(NOW)) { + LOGGER.info("Removing client " + c + " from list"); + it.remove(); + } + + } + LOGGER.info("List updated. " + clientPool.size() + " clients."); + } + } + + /* + * Make SSL insecure + */ + + static { + SSLContext ctx = null; + try { + ctx = SSLContext.getInstance("TLSv1.2"); + } catch (NoSuchAlgorithmException e) { + LOGGER.warn("Could not get TLSv1.2 context, SSL will be secure :-(", e); + } + if (ctx != null) { + try { + ctx.init(null, new TrustManager[] { new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + } }, null); + } catch (KeyManagementException e) { + LOGGER.warn("Could not initialize TLSv1.2 SSL context", e); + ctx = null; + } + } + if (ctx == null) { + sockFac = null; + } else { + sockFac = ctx.getSocketFactory(); + } + } + + private static final SSLSocketFactory sockFac; + + private static final HostnameVerifier ignorer = new HostnameVerifier() { + + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }; + +} diff --git a/src/main/java/de/bwlehrpool/bwlp_guac/JsonClient.java b/src/main/java/de/bwlehrpool/bwlp_guac/JsonClient.java new file mode 100644 index 0000000..dee8fae --- /dev/null +++ b/src/main/java/de/bwlehrpool/bwlp_guac/JsonClient.java @@ -0,0 +1,17 @@ +package de.bwlehrpool.bwlp_guac; + +public class JsonClient { + + public String clientip; + + public String password; + + public State state; + + public boolean wol_in_progress; + + public static enum State { + OFFLINE, IDLE, OCCUPIED, STANDBY; + } + +} diff --git a/src/main/java/de/bwlehrpool/bwlp_guac/SlxConfig.java b/src/main/java/de/bwlehrpool/bwlp_guac/SlxConfig.java new file mode 100644 index 0000000..48c707f --- /dev/null +++ b/src/main/java/de/bwlehrpool/bwlp_guac/SlxConfig.java @@ -0,0 +1,43 @@ +package de.bwlehrpool.bwlp_guac; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.environment.Environment; +import org.apache.guacamole.environment.LocalEnvironment; +import org.apache.guacamole.properties.StringGuacamoleProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SlxConfig { + + private static final Logger LOGGER = LoggerFactory.getLogger(SlxConfig.class); + + private static final Environment ENVIRONMENT; + + private static final StringGuacamoleProperty CLIENTS_URL = new StringGuacamoleProperty() { + @Override + public String getName() { + return "slx-client-list-url"; + } + }; + + static { + Environment e; + try { + e = new LocalEnvironment(); + } catch (GuacamoleException ex) { + LOGGER.warn("Cannot create LocalEnvironment", ex); + e = null; + } + ENVIRONMENT = e; + } + + public static String clientListUrl() { + try { + return ENVIRONMENT.getProperty(CLIENTS_URL); + } catch (GuacamoleException e) { + LOGGER.warn("Cannot get client list url from properties", e); + return null; + } + } + +} diff --git a/src/main/java/de/bwlehrpool/bwlp_guac/VncConnection.java b/src/main/java/de/bwlehrpool/bwlp_guac/VncConnection.java new file mode 100644 index 0000000..8bd4bd0 --- /dev/null +++ b/src/main/java/de/bwlehrpool/bwlp_guac/VncConnection.java @@ -0,0 +1,141 @@ +package de.bwlehrpool.bwlp_guac; + +import java.io.Closeable; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class VncConnection implements Closeable { + + private static final Logger LOGGER = LoggerFactory.getLogger(VncConnection.class); + + private final Socket sock; + private final DataOutputStream out; + private final DataInputStream in; + + public VncConnection(String host, int port) throws IOException { + sock = new Socket(); + sock.connect(new InetSocketAddress(host, port), 1200); + sock.setSoTimeout(1000); + out = new DataOutputStream(sock.getOutputStream()); + in = new DataInputStream(sock.getInputStream()); + } + + public String handshake() throws IOException { + byte[] buffer = new byte[12]; + in.readFully(buffer); + out.write("RFB 003.008\n".getBytes()); + out.flush(); + return new String(buffer).substring(4, 11); + } + + public boolean tryLogin(String passwd) throws IOException { + if (passwd == null) + return false; // Paswordless not supported yet (although simpler..) + int numTypes = in.read(); + if (numTypes == 0) { + LOGGER.info("VNC Server @ " + sock.getRemoteSocketAddress() + " does not support any auth methods"); + printError(); + return false; + } + boolean ok = false; + for (int i = 0; i < numTypes; ++i) { + if (in.read() == 2) { + ok = true; + break; // Found "VNC Authentication" + } + } + if (!ok) { + LOGGER.info("VNC Server @ " + sock.getRemoteSocketAddress() + " does not support VNC Authentication"); + return false; + } + out.write(2); // Pick passwd auth + // Get challenge data + byte[] challenge = new byte[16]; + int ret = in.read(challenge); + if (ret != 16) { + LOGGER.info("Didn't receive challenge from VNC server @ " + sock.getRemoteSocketAddress()); + return false; + } + // pad pw to 8 bytes + byte[] pw_bytes = passwd.getBytes(); + pw_bytes = Arrays.copyOf(pw_bytes, 8); + // Encrypt + Cipher des; + try { + des = Cipher.getInstance("DES/ECB/NoPadding"); + des.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(reverseBits(pw_bytes), 0, pw_bytes.length, "DES")); + out.write(des.doFinal(challenge)); + } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException + | BadPaddingException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + // check reply + int securityReply = in.readInt(); + if (securityReply != 0) { + LOGGER.info("Security reply = " + securityReply + " for VNC server @ " + sock.getRemoteSocketAddress()); + return false; + } + return true; + } + + private void printError() throws IOException { + int len = in.readInt(); + byte[] msg = new byte[len]; + in.readFully(msg); + LOGGER.info(new String(msg, StandardCharsets.ISO_8859_1)); + } + + @Override + public void close() throws IOException { + try { + in.close(); + } catch (Exception e) { + } + try { + out.close(); + } catch (Exception e) { + } + try { + sock.close(); + } catch (Exception e) { + } + } + + /* + * + */ + + private byte[] reverseBits(byte[] b) { + byte[] result = new byte[b.length]; + for (int i = 0; i < b.length; i++) { + result[i] = reverseBits(b[i]); + } + return result; + } + + private byte reverseBits(byte input) { + byte result = 0x00; + for (int i = 0; i < 8; i++) { + result |= ((byte) ((input & (0x01 << i)) >>> i) << 7 - i); + } + return result; + } + +} diff --git a/src/main/java/de/bwlehrpool/bwlp_guac/WrappedConnection.java b/src/main/java/de/bwlehrpool/bwlp_guac/WrappedConnection.java new file mode 100644 index 0000000..616c20c --- /dev/null +++ b/src/main/java/de/bwlehrpool/bwlp_guac/WrappedConnection.java @@ -0,0 +1,26 @@ +package de.bwlehrpool.bwlp_guac; + +import org.apache.guacamole.net.auth.simple.SimpleConnection; +import org.apache.guacamole.protocol.GuacamoleConfiguration; + +public class WrappedConnection extends SimpleConnection { + + private static final String DEFAULT_ROOT_CONNECTION_GROUP = "ROOT"; + + private final AvailableClient ac; + + public WrappedConnection(String name, AvailableClient ac) { + super(name, name, makeConfig(ac)); + this.ac = ac; + setParentIdentifier(DEFAULT_ROOT_CONNECTION_GROUP); + } + + private static GuacamoleConfiguration makeConfig(AvailableClient ac) { + return ac.toGuacConfig(); + } + + public boolean checkConnection(int retries) { + return ac.checkConnection(retries); + } + +} diff --git a/src/main/resources/disclaimer.html b/src/main/resources/disclaimer.html new file mode 100644 index 0000000..be4cca3 --- /dev/null +++ b/src/main/resources/disclaimer.html @@ -0,0 +1,6 @@ + + +
+

bwLehrpool

+
+ diff --git a/src/main/resources/guac-manifest.json b/src/main/resources/guac-manifest.json new file mode 100644 index 0000000..ef3c431 --- /dev/null +++ b/src/main/resources/guac-manifest.json @@ -0,0 +1,7 @@ +{ + "guacamoleVersion" : "*", + "name" : "bwLehrpool virtual pool", + "namespace" : "de.bwlehrpool", + "authProviders": ["de.bwlehrpool.bwlp_guac.BwlpAuthenticationProvider"], + "html" : [ "disclaimer.html" ] +} diff --git a/src/test/java/de/bwlehrpool/bwlp_guac/AppTest.java b/src/test/java/de/bwlehrpool/bwlp_guac/AppTest.java new file mode 100644 index 0000000..51d66ec --- /dev/null +++ b/src/test/java/de/bwlehrpool/bwlp_guac/AppTest.java @@ -0,0 +1,38 @@ +package de.bwlehrpool.bwlp_guac; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Unit test for simple App. + */ +public class AppTest + extends TestCase +{ + /** + * Create the test case + * + * @param testName name of the test case + */ + public AppTest( String testName ) + { + super( testName ); + } + + /** + * @return the suite of tests being tested + */ + public static Test suite() + { + return new TestSuite( AppTest.class ); + } + + /** + * Rigourous Test :-) + */ + public void testApp() + { + assertTrue( true ); + } +} -- cgit v1.2.3-55-g7522