summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Rettberg2020-04-15 15:40:55 +0200
committerSimon Rettberg2020-04-15 15:40:55 +0200
commit41186ddf8eef2530b95fe90f03bd84ee841115d9 (patch)
treed131446372d68738d6d22993f64ca52ea90732f6
downloadbwlp-guacamole-ext-41186ddf8eef2530b95fe90f03bd84ee841115d9.tar.gz
bwlp-guacamole-ext-41186ddf8eef2530b95fe90f03bd84ee841115d9.tar.xz
bwlp-guacamole-ext-41186ddf8eef2530b95fe90f03bd84ee841115d9.zip
First Commit
-rw-r--r--.gitignore4
-rw-r--r--pom.xml46
-rw-r--r--src/main/java/de/bwlehrpool/bwlp_guac/AvailableClient.java157
-rw-r--r--src/main/java/de/bwlehrpool/bwlp_guac/BwlpAuthenticationProvider.java84
-rw-r--r--src/main/java/de/bwlehrpool/bwlp_guac/BwlpUserContext.java109
-rw-r--r--src/main/java/de/bwlehrpool/bwlp_guac/ConnectionManager.java219
-rw-r--r--src/main/java/de/bwlehrpool/bwlp_guac/JsonClient.java17
-rw-r--r--src/main/java/de/bwlehrpool/bwlp_guac/SlxConfig.java43
-rw-r--r--src/main/java/de/bwlehrpool/bwlp_guac/VncConnection.java141
-rw-r--r--src/main/java/de/bwlehrpool/bwlp_guac/WrappedConnection.java26
-rw-r--r--src/main/resources/disclaimer.html6
-rw-r--r--src/main/resources/guac-manifest.json7
-rw-r--r--src/test/java/de/bwlehrpool/bwlp_guac/AppTest.java38
13 files changed, 897 insertions, 0 deletions
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 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>de.bwlehrpool</groupId>
+ <artifactId>bwlp-guac</artifactId>
+ <version>0.0.1-SNAPSHOT</version>
+ <packaging>jar</packaging>
+
+ <name>bwlp-guac</name>
+ <url>http://maven.apache.org</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>3.6.1</version>
+ <configuration>
+ <source>1.7</source>
+ <target>1.7</target>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>3.8.1</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.guacamole</groupId>
+ <artifactId>guacamole-ext</artifactId>
+ <version>1.1.0</version>
+ </dependency>
+
+ </dependencies>
+</project>
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<String, BwlpUserContext> oldMappings = Collections
+ .synchronizedMap(new WeakHashMap<String, BwlpUserContext>());
+
+ 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<Connection> 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>(connection);
+ } else {
+ connectionDirectory = new SimpleDirectory<Connection>();
+ }
+ }
+
+ 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<Connection> 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<String, AvailableClient> clientPool = new LinkedHashMap<String, AvailableClient>();
+
+ /**
+ * 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<AvailableClient> 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 @@
+<meta name="after" content=".login-ui .login-dialog">
+
+<div class="welcome">
+ <h2>bwLehrpool</h2>
+</div>
+
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 );
+ }
+}