Building a Browser-Based Remote Desktop Gateway with Spring Boot and Apache Guacamole
Apache Guacamole Architecture
Apache Guacamole functions as a clientless remote desktop gateway supporting standard protocols including VNC, RDP, and SSH. The architecture consists of three primary components working in concert.
Protocol Layer
The web application layer remains completely ignorant of specific remote desktop protocols. Rather than implementing VNC or RDP directly, it understands only the Guacamole protocol—a specialized format for remote display rendering and event transmission. This design enables protocol agnosticism: adding support for new remote desktop protocols requires implementing a translation layer between that protocol and the Guacamole protocol. The guacd daemon handles this translation.
guacd Daemon
The guacd component serves as Guacamole's core engine. It dynamically loads protocol support modules (called "client plugins") based on instructions received from the web application, connecting to remote desktops accordingly. This daemon implements sufficient Guacamole protocol logic to determine which protocol support to load and what parameters to pass. Once loaded, client plugins operate independently and maintain full control over their communication with the web application.
Web Application Layer
Users interact exclusively with the web application component. This layer provides the user interface and authentication mechanisms while delegating actual remote desktop handling to guacd.
Docker Installatoin
The fastest deployment method uses Docker. Guacamole comprises two services: the web application and the guacd proxy daemon.
Pull Required Images
docker pull guacamole/guacamole
docker pull guacamole/guacd
docker pull mysql/mysql-server:5.7
Initialize MySQL Database
docker run --rm guacamole/guacamole /opt/guacamole/bin/initdb.sh --mysql > initdb.sql
docker run --name mysql --restart=always -e MYSQL_ROOT_PASSWORD=secure_password -d mysql/mysql-server:5.7
docker cp initdb.sql mysql:/initdb.sql
docker exec -it mysql mysql -uroot -psecure_password
Execute the following SQL commands:
CREATE DATABASE guacamole_db;
CREATE USER 'guac_user'@'%' IDENTIFIED BY 'guac_password';
GRANT SELECT,INSERT,UPDATE,DELETE ON guacamole_db.* TO 'guac_user'@'%';
FLUSH PRIVILEGES;
exit
docker exec -it mysql bash -c "mysql -uroot -psecure_password -Dguacamole_db < /initdb.sql"
Launch Services
docker run --name guacd --restart=always -d guacamole/guacd
docker run --name guacamole --restart=always \
--link guacd:guacd \
--link mysql:mysql \
-e MYSQL_DATABASE='guacamole_db' \
-e MYSQL_USER='guac_user' \
-e MYSQL_PASSWORD='guac_password' \
-p 8080:8080 \
guacamole/guacamole
Verify services are running with docker ps. Access the management interface at http://server_ip:8080/guacamole. Default credentials are guacadmin for both username and password.
Spring Boot Integration
Maven Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-common</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-common-js</artifactId>
<version>1.1.0</version>
<type>zip</type>
<scope>runtime</scope>
</dependency>
WebSocket Tunnel Endpoint
@ServerEndpoint(value = "/guacamole-ws", subprotocols = "guacamole")
@Component
public class RemoteDesktopTunnel extends GuacamoleWebSocketTunnelEndpoint {
@Override
protected GuacamoleTunnel establishTunnel(
Session httpSession,
EndpointConfig endpointConfig) throws GuacamoleException {
String proxyHost = "192.168.1.100";
int proxyPort = 4822;
GuacamoleConfiguration config = new GuacamoleConfiguration();
config.setProtocol("rdp");
config.setParameter("hostname", "192.168.1.200");
config.setParameter("port", "3389");
config.setParameter("username", "admin");
config.setParameter("password", "remote_password");
config.setParameter("ignore-cert", "true");
GuacamoleSocket socket = new ConfiguredGuacamoleSocket(
new InetGuacamoleSocket(proxyHost, proxyPort),
config
);
return new SimpleGuacamoleTunnel(socket);
}
}
Client-Side Implementation
Include the Guacamole JavaScript library in your HTML and initialize the client:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="guacamole.css"/>
<title>Remote Desktop Client</title>
</head>
<body>
<div id="remote-display"></div>
<script type="text/javascript" src="guacamole-common-js/all.min.js"></script>
<script type="text/javascript">
var displayElement = document.getElementById("remote-display");
var guacSession = new Guacamole.Client(
new Guacamole.WebSocketTunnel("ws://localhost:9632/api/guacamole-ws")
);
displayElement.appendChild(guacSession.getDisplay().getElement());
guacSession.onerror = function(error) {
console.error("Connection error:", error);
};
guacSession.connect();
window.addEventListener("beforeunload", function() {
guacSession.disconnect();
});
var mouseHandler = new Guacamole.Mouse(guacSession.getDisplay().getElement());
mouseHandler.onmousedown =
mouseHandler.onmouseup =
mouseHandler.onmousemove = function(state) {
guacSession.sendMouseState(state);
};
var keyboardHandler = new Guacamole.Keyboard(document);
keyboardHandler.onkeydown = function(keysym) {
guacSession.sendKeyEvent(1, keysym);
};
keyboardHandler.onkeyup = function(keysym) {
guacSession.sendKeyEvent(0, keysym);
};
</script>
</body>
</html>
For HTTP tunnel connectivity instead of WebSocket:
var guacSession = new Guacamole.Client(
new Guacamole.HTTPTunnel("tunnel")
);
Passing Parameters Through WebSocket
Append query parameters to the connection URL:
var guacSession = new Guacamole.Client(
new Guacamole.WebSocketTunnel("ws://192.168.1.50:9632/api/guacamole-ws?target=192.168.1.200&")
);
Retrieve parameters on the server side:
String targetHost = session.getRequestParameterMap().get("target").get(0);
Configuring Display Resolution
Specify optimal screen dimensions when establishing the connection:
@Override
protected GuacamoleTunnel establishTunnel(
Session httpSession,
EndpointConfig endpointConfig) throws GuacamoleException {
GuacamoleConfiguration config = new GuacamoleConfiguration();
config.setProtocol("rdp");
config.setParameter("hostname", remoteHost);
config.setParameter("port", "3389");
config.setParameter("username", username);
config.setParameter("password", password);
GuacamoleClientInformation clientInfo = new GuacamoleClientInformation();
clientInfo.setOptimalScreenWidth(1920);
clientInfo.setOptimalScreenHeight(1080);
GuacamoleSocket socket = new ConfiguredGuacamoleSocket(
new InetGuacamoleSocket(proxyHost, proxyPort),
config,
clientInfo
);
return new SimpleGuacamoleTunnel(socket);
}
Session Recording
Enable session recording by configuring recording parameters:
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
String recordingPath = "/var/recordings";
config.setParameter("recording-path", recordingPath);
config.setParameter("create-recording-path", "true");
config.setParameter("recording-name", timestamp + ".guac");
Decode recorded sessions using the guacenc utility:
guacenc session_recording_20240101120000.guac
Convert to other formats using ffmpeg if needed.
Terminating Connections
Close WebSocket sessions programmatically:
private void closeSession(Session wsSession) {
if (wsSession.isOpen()) {
try {
CloseReason reason = new CloseReason(
CloseReason.CloseCodes.NORMAL_CLOSURE,
"Session terminated"
);
wsSession.close(reason);
} catch (IOException e) {
logger.error("Error closing session", e);
}
}
}