Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Building a Browser-Based Remote Desktop Gateway with Spring Boot and Apache Guacamole

Tech May 8 4

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);
        }
    }
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.