Browse Source

Add web terminal (#3859)

* Add frontend for web-terminal

* Fix Terminal export

* Remove some messages

* Make terminal accessible to regular users

* Add message on terminal exit

* Use writeln for connection messages

* Order sys-config json and add BACKEND_PORT

* Fix json syntax

* Add variable to syshealth

* Add nodejs on install and maybe do a deb?

* Better uid and gid detection

* Whoops, removed something by accident

* Fix dependency and upgrade script

* Check on activation

* Create build directory

* And the other directory

* Make sure node is installed

* Don't run husky in CI

* Fix terminal ids

* Create folders and fix permissions

* Add separate service for web terminal

* Use the user shell

* Use systemd service instead of init.d

* Enable service postinst

* Use class instead of ID, move to xterm deps

* Restart service on upgrade

* Add some logging

* Keep clients in a Set just in case we need them

* Fix service starting, probably

* okay no var in path

* Better logs

* Again, a bit more accurate logs

* Add remote IP to nginx conf

* Add reasons for websockets closing

* Fix websockets error code

* Change service name for consistency

* Return to user list

* Add flag to installer and WEB_TERMINAL_PORT option

* Run service under the hestia-users group

* Add padding to terminal

* Add terminal configs

* Add some debug logs and reorder compile commands

* Add web terminal to service list

* Correctly show service status

* change service name in control panel, redirect settings

* Add command to change the web terminal port

* Render using WebGL or Canvas2D

* Open the terminal after loading the addon

* Build JS/CSS in deb

* Make sure service is disabled/enabled

* Check for node in compile script

* Don't show terminal button for `nologin` users

* Unset temp vars

* Make it work when impersonating

* Use 2 classes for consistency

---------

Co-authored-by: Jaap Marcus <[email protected]>
Jakob Bouchard 2 years ago
parent
commit
03c1d88762

+ 3 - 3
.drone.yml

@@ -25,7 +25,7 @@ steps:
       - git submodule update --init --recursive
   - name: Build Hestia package and install
     commands:
-      - npm ci
+      - npm ci --ignore-scripts
       - npm run build
       - ./src/hst_autocompile.sh --hestia --install '~localsrc'
   - name: Reset Web templates
@@ -83,7 +83,7 @@ steps:
       - git submodule update --init --recursive
   - name: Build Hestia package install
     commands:
-      - npm ci
+      - npm ci --ignore-scripts
       - npm run build
       - ./src/hst_autocompile.sh --hestia --install '~localsrc'
   - name: Reset Web templates
@@ -131,7 +131,7 @@ steps:
   - name: Build JS/CSS
     image: node:current-slim
     commands:
-      - npm ci
+      - npm ci --ignore-scripts
       - npm run build
   - name: Build
     image: debian:bullseye

+ 3 - 3
.github/workflows/lint.yml

@@ -46,7 +46,7 @@ jobs:
           node-version: 16
 
       - name: Install Node packages
-        run: npm ci
+        run: npm ci --ignore-scripts
 
       - name: Run Prettier
         run: npx prettier --check .
@@ -64,7 +64,7 @@ jobs:
           node-version: 16
 
       - name: Install Node packages
-        run: npm ci
+        run: npm ci --ignore-scripts
 
       - name: Run ESLint
         run: npx eslint .
@@ -82,7 +82,7 @@ jobs:
           node-version: 16
 
       - name: Install Node packages
-        run: npm ci
+        run: npm ci --ignore-scripts
 
       - name: Run Stylelint
         run: npx stylelint web/css/src/**/*.css

+ 12 - 12
bin/v-add-sys-sftp-jail

@@ -45,14 +45,14 @@ fi
 
 # Enabling jailed sftp
 if [ -z "$sftp_i" ]; then
-    echo " " >> $config
-    echo "# Hestia SFTP Chroot" >> $config
-    echo "Match User sftp_dummy99" >> $config
-    echo "    ChrootDirectory /srv/jail/%u" >> $config
-    echo "    X11Forwarding no" >> $config
-    echo "    AllowTCPForwarding no" >> $config
-    echo "    ForceCommand internal-sftp -d /home" >> $config
-    restart='yes'
+	echo " " >> $config
+	echo "# Hestia SFTP Chroot" >> $config
+	echo "Match User sftp_dummy99" >> $config
+	echo "    ChrootDirectory /srv/jail/%u" >> $config
+	echo "    X11Forwarding no" >> $config
+	echo "    AllowTCPForwarding no" >> $config
+	echo "    ForceCommand internal-sftp -d /home" >> $config
+	restart='yes'
 fi
 
 # Validating opensshd config
@@ -63,10 +63,10 @@ if [ "$restart" = 'yes' ]; then
 	if [ "$?" -ne 0 ]; then
 		mail_text="OpenSSH can not be restarted. Please check config:
             \n\n$(/usr/sbin/sshd -t)"
-        echo -e "$mail_text" |$SENDMAIL -s "$subj" $email
-    else
-        service sshd restart >/dev/null 2>&1
-    fi
+		echo -e "$mail_text" | $SENDMAIL -s "$subj" $email
+	else
+		service sshd restart > /dev/null 2>&1
+	fi
 fi
 
 # Checking users

+ 57 - 0
bin/v-add-sys-web-terminal

@@ -0,0 +1,57 @@
+#!/bin/bash
+# info: add system web terminal
+# options: NONE
+#
+# example: v-add-sys-web-terminal
+#
+# This function enables the web terminal.
+
+#----------------------------------------------------------#
+#                Variables & Functions                     #
+#----------------------------------------------------------#
+
+# Includes
+# shellcheck source=/etc/hestiacp/hestia.conf
+source /etc/hestiacp/hestia.conf
+# shellcheck source=/usr/local/hestia/func/main.sh
+source $HESTIA/func/main.sh
+# load config file
+source_conf "$HESTIA/conf/hestia.conf"
+
+#----------------------------------------------------------#
+#                    Verifications                         #
+#----------------------------------------------------------#
+
+if [ "$WEB_TERMINAL" = 'true' ]; then
+	exit
+fi
+
+# Perform verification if read-only mode is enabled
+check_hestia_demo_mode
+
+#----------------------------------------------------------#
+#                       Action                             #
+#----------------------------------------------------------#
+
+# Updating WEB_TERMINAL value
+$BIN/v-change-sys-config-value "WEB_TERMINAL" "true"
+
+# Check if nodejs and hestia-web-terminal are installed
+if [ ! -f "/usr/bin/node" ] || [ ! -f "$HESTIA/web-terminal/server.js" ]; then
+	apt-get -qq update
+	apt-get -qq install nodejs hestia-web-terminal -y
+else
+	# Starting web terminal websocket server
+	$BIN/v-start-service "hestia-web-terminal"
+	systemctl enable hestia-web-terminal
+fi
+
+#----------------------------------------------------------#
+#                       Hestia                             #
+#----------------------------------------------------------#
+
+# Logging
+$BIN/v-log-action "system" "Info" "Web Terminal" "Web terminal enabled."
+log_event "$OK" "$ARGUMENTS"
+
+exit

+ 89 - 0
bin/v-change-sys-web-terminal-port

@@ -0,0 +1,89 @@
+#!/bin/bash
+# info: change system web terminal backend port
+# options: PORT
+#
+# example: v-change-sys-web-terminal-port 5678
+#
+# This function for changing the system's web terminal backend port in NGINX configuration.
+
+#----------------------------------------------------------#
+#                Variables & Functions                     #
+#----------------------------------------------------------#
+
+# Argument definition
+PORT=$1
+NGINX_CONFIG="$HESTIA/nginx/conf/nginx.conf"
+
+# Includes
+# shellcheck source=/etc/hestiacp/hestia.conf
+source /etc/hestiacp/hestia.conf
+# shellcheck source=/usr/local/hestia/func/main.sh
+source $HESTIA/func/main.sh
+# load config file
+source_conf "$HESTIA/conf/hestia.conf"
+
+# Functions
+is_port_valid() {
+	# Check if PORT is numeric
+	if [[ ! "$PORT" =~ ^[0-9]+$ ]]; then
+		echo "Port should contains a numeric value only!"
+		log_event "$E_INVALID" "$ARGUMENTS"
+		exit "$E_INVALID"
+	fi
+
+	# Check if PORT is already used
+	BUSY_PORT=$(lsof -i:"$PORT")
+	if [ -n "$BUSY_PORT" ] && [ "$PORT" != "$BACKEND_PORT" ]; then
+		echo "Port is already used by Hestia, please set another one!"
+		log_event "$E_INUSE" "$ARGUMENTS"
+		exit "$E_INUSE"
+	fi
+}
+
+#----------------------------------------------------------#
+#                    Verifications                         #
+#----------------------------------------------------------#
+
+check_args '1' "$#" 'PORT'
+is_port_valid
+
+# Perform verification if read-only mode is enabled
+check_hestia_demo_mode
+
+#----------------------------------------------------------#
+#                       Action                             #
+#----------------------------------------------------------#
+
+# Get original port
+ORIGINAL_PORT=$(cat ${NGINX_CONFIG} | grep -m1 "proxy_pass http://localhost:" | sed 's/[^0-9]*//g')
+
+# Check if port is different to nginx.conf
+if [ "$ORIGINAL_PORT" = "$PORT" ]; then
+	# Nothing to do, exit
+	exit
+else
+	# Set new port in config via v-change-sys-config-value
+	$BIN/v-change-sys-config-value "WEB_TERMINAL_PORT" "$PORT"
+	# Replace port in config files.
+	sed -i "s/\(proxy_pass http:\/\/localhost:\)[0-9][0-9]*\([^0-9]*\;$\)/\1$PORT\2/" ${NGINX_CONFIG}
+
+	# Check if the web terminal backend is running
+	if [[ $(ps -eaf | grep -i hestia/web-terminal | sed '/^$/d' | wc -l) -gt 1 ]]; then
+		$BIN/v-restart-service hestia-web-terminal
+	fi
+
+	# Check if Hestia is running
+	if [[ $(ps -eaf | grep -i hestia | sed '/^$/d' | wc -l) -gt 1 ]]; then
+		$BIN/v-restart-service hestia
+	fi
+fi
+
+#----------------------------------------------------------#
+#                       Hestia                             #
+#----------------------------------------------------------#
+
+# Logging
+$BIN/v-log-action "system" "Warning" "System" "Hestia Control Panel web terminal port changed (New Value: $PORT, Old Value: $ORIGINAL_PORT)."
+log_event "$OK" "$ARGUMENTS"
+
+exit

+ 51 - 0
bin/v-delete-sys-web-terminal

@@ -0,0 +1,51 @@
+#!/bin/bash
+# info: delete web terminal
+# options: NONE
+#
+# example: v-delete-sys-web-terminal
+#
+# This function disables the web terminal.
+
+#----------------------------------------------------------#
+#                Variables & Functions                     #
+#----------------------------------------------------------#
+
+# Includes
+# shellcheck source=/etc/hestiacp/hestia.conf
+source /etc/hestiacp/hestia.conf
+# shellcheck source=/usr/local/hestia/func/main.sh
+source $HESTIA/func/main.sh
+# load config file
+source_conf "$HESTIA/conf/hestia.conf"
+
+#----------------------------------------------------------#
+#                    Verifications                         #
+#----------------------------------------------------------#
+
+if [ -z "$WEB_TERMINAL" ]; then
+	exit
+fi
+
+# Perform verification if read-only mode is enabled
+check_hestia_demo_mode
+
+#----------------------------------------------------------#
+#                       Action                             #
+#----------------------------------------------------------#
+
+# Updating WEB_TERMINAL value
+$BIN/v-change-sys-config-value "WEB_TERMINAL" "false"
+
+# Stopping web terminal websocket server
+$BIN/v-stop-service "hestia-web-terminal"
+systemctl disable hestia-web-terminal
+
+#----------------------------------------------------------#
+#                       Hestia                             #
+#----------------------------------------------------------#
+
+# Logging
+$BIN/v-log-action "system" "Warning" "Web Terminal" "Web terminal disabled."
+log_event "$OK" "$ARGUMENTS"
+
+exit

+ 85 - 82
bin/v-list-sys-config

@@ -27,92 +27,95 @@ source_conf "$HESTIA/conf/hestia.conf"
 json_list() {
 	echo '{
 		"config": {
-			"WEB_SYSTEM": "'$WEB_SYSTEM'",
-	    		"WEB_RGROUPS": "'$WEB_RGROUPS'",
-	    		"WEB_PORT": "'$WEB_PORT'",
-	    		"WEB_SSL": "'$WEB_SSL'",
-	    		"WEB_SSL_PORT": "'$WEB_SSL_PORT'",
-	    		"WEB_BACKEND": "'$WEB_BACKEND'",
-	    		"PROXY_SYSTEM": "'$PROXY_SYSTEM'",
-	    		"PROXY_PORT": "'$PROXY_PORT'",
-	    		"PROXY_SSL_PORT": "'$PROXY_SSL_PORT'",
-	    		"FTP_SYSTEM": "'$FTP_SYSTEM'",
-	    		"MAIL_SYSTEM": "'$MAIL_SYSTEM'",
-	    		"IMAP_SYSTEM": "'$IMAP_SYSTEM'",
-	    		"ANTIVIRUS_SYSTEM": "'$ANTIVIRUS_SYSTEM'",
-	    		"ANTISPAM_SYSTEM": "'$ANTISPAM_SYSTEM'",
-	    		"DB_SYSTEM": "'$DB_SYSTEM'",
-	    		"DNS_SYSTEM": "'$DNS_SYSTEM'",
-	    		"DNS_CLUSTER": "'$DNS_CLUSTER'",
+			"ANTISPAM_SYSTEM": "'$ANTISPAM_SYSTEM'",
+			"ANTIVIRUS_SYSTEM": "'$ANTIVIRUS_SYSTEM'",
+			"API": "'$API'",
+			"API_ALLOWED_IP": "'$API_ALLOWED_IP'",
+			"API_SYSTEM": "'$API_SYSTEM'",
+			"APP_NAME": "'$APP_NAME'",
+			"BACKEND_PORT": "'$BACKEND_PORT'",
+			"BACKUP": "'$BACKUP'",
+			"BACKUP_GZIP": "'$BACKUP_GZIP'",
+			"BACKUP_MODE": "'$BACKUP_MODE'",
+			"BACKUP_SYSTEM": "'$BACKUP_SYSTEM'",
+			"CRON_SYSTEM": "'$CRON_SYSTEM'",
+			"DB_PGA_ALIAS": "'$DB_PGA_ALIAS'",
+			"DB_PMA_ALIAS": "'$DB_PMA_ALIAS'",
+			"DB_SYSTEM": "'$DB_SYSTEM'",
+			"DEBUG_MODE": "'$DEBUG_MODE'",
+			"DEMO_MODE": "'$DEMO_MODE'",
+			"DISABLE_IP_CHECK": "'$DISABLE_IP_CHECK'",
+			"DISK_QUOTA": "'$DISK_QUOTA'",
+			"DNS_CLUSTER": "'$DNS_CLUSTER'",
 			"DNS_CLUSTER_SYSTEM": "'$DNS_CLUSTER_SYSTEM'",
-			"SUPPORT_DNSSEC": "'$SUPPORT_DNSSEC'",
-	    		"STATS_SYSTEM": "'$STATS_SYSTEM'",
-	    		"BACKUP_SYSTEM": "'$BACKUP_SYSTEM'",
-	    		"CRON_SYSTEM": "'$CRON_SYSTEM'",
-	    		"DISK_QUOTA": "'$DISK_QUOTA'",
-	    		"FIREWALL_SYSTEM": "'$FIREWALL_SYSTEM'",
-	    		"FIREWALL_EXTENSION": "'$FIREWALL_EXTENSION'",
-	    		"FILE_MANAGER": "'$FILE_MANAGER'",
-	    		"REPOSITORY": "'$REPOSITORY'",
-	    		"VERSION": "'$VERSION'",
-	    		"RELEASE_BRANCH": "'$RELEASE_BRANCH'",
-	    		"UPGRADE_SEND_EMAIL": "'$UPGRADE_SEND_EMAIL'",
-	    		"UPGRADE_SEND_EMAIL_LOG": "'$UPGRADE_SEND_EMAIL_LOG'",
-	    		"SMTP_RELAY": "'$SMTP_RELAY'",
-	    		"SMTP_RELAY_HOST": "'$SMTP_RELAY_HOST'",
-	    		"SMTP_RELAY_PORT": "'$SMTP_RELAY_PORT'",
-	    		"SMTP_RELAY_USER": "'$SMTP_RELAY_USER'",
-	    		"DEMO_MODE": "'$DEMO_MODE'",
-	    		"THEME": "'$THEME'",
-	    		"LANGUAGE": "'$LANGUAGE'",
-	    		"BACKUP_GZIP": "'$BACKUP_GZIP'",
-	    		"BACKUP": "'$BACKUP'",
-	    		"BACKUP_MODE": "'$BACKUP_MODE'",
-	    		"WEBMAIL_ALIAS": "'$WEBMAIL_ALIAS'",
-	    		"WEBMAIL_SYSTEM": "'$WEBMAIL_SYSTEM'",
-	    		"DB_PMA_ALIAS": "'$DB_PMA_ALIAS'",
-	    		"DB_PGA_ALIAS": "'$DB_PGA_ALIAS'",
-	    		"LOGIN_STYLE": "'$LOGIN_STYLE'",
-	    		"INACTIVE_SESSION_TIMEOUT": "'$INACTIVE_SESSION_TIMEOUT'",
-	    		"PHPMYADMIN_KEY": "'$PHPMYADMIN_KEY'",
-	    		"ENFORCE_SUBDOMAIN_OWNERSHIP": "'$ENFORCE_SUBDOMAIN_OWNERSHIP'",
-	    		"DEBUG_MODE": "'$DEBUG_MODE'",
-	    		"API": "'$API'",
-	    		"API_SYSTEM": "'$API_SYSTEM'",
-	    		"API_ALLOWED_IP": "'$API_ALLOWED_IP'",
-	    		"UPDATE_AVAILABLE": "'$UPDATE_AVAILABLE'",
-	    		"PLUGIN_APP_INSTALLER": "'$PLUGIN_APP_INSTALLER'",
-	    		"PLUGIN_FILE_MANAGER": "'$PLUGIN_FILE_MANAGER'",
-	    		"POLICY_SYSTEM_ENABLE_BACON": "'$POLICY_SYSTEM_ENABLE_BACON'",
-	    		"POLICY_SYSTEM_PROTECTED_ADMIN": "'$POLICY_SYSTEM_PROTECTED_ADMIN'",
-	    		"POLICY_SYSTEM_HIDE_ADMIN": "'$POLICY_SYSTEM_HIDE_ADMIN'",
-	    		"POLICY_SYSTEM_HIDE_SERVICES": "'$POLICY_SYSTEM_HIDE_SERVICES'",
-	    		"POLICY_SYSTEM_PASSWORD_RESET": "'$POLICY_SYSTEM_PASSWORD_RESET'",
-	    		"POLICY_USER_VIEW_SUSPENDED": "'$POLICY_USER_VIEW_SUSPENDED'",
+			"DNS_SYSTEM": "'$DNS_SYSTEM'",
+			"ENFORCE_SUBDOMAIN_OWNERSHIP": "'$ENFORCE_SUBDOMAIN_OWNERSHIP'",
+			"FILE_MANAGER": "'$FILE_MANAGER'",
+			"FIREWALL_EXTENSION": "'$FIREWALL_EXTENSION'",
+			"FIREWALL_SYSTEM": "'$FIREWALL_SYSTEM'",
+			"FROM_EMAIL": "'$FROM_EMAIL'",
+			"FROM_NAME": "'$FROM_NAME'",
+			"FTP_SYSTEM": "'$FTP_SYSTEM'",
+			"HIDE_DOCS": "'$HIDE_DOCS'",
+			"IMAP_SYSTEM": "'$IMAP_SYSTEM'",
+			"INACTIVE_SESSION_TIMEOUT": "'$INACTIVE_SESSION_TIMEOUT'",
+			"LANGUAGE": "'$LANGUAGE'",
+			"LOGIN_STYLE": "'$LOGIN_STYLE'",
+			"MAIL_SYSTEM": "'$MAIL_SYSTEM'",
+			"PHPMYADMIN_KEY": "'$PHPMYADMIN_KEY'",
+			"PLUGIN_APP_INSTALLER": "'$PLUGIN_APP_INSTALLER'",
+			"PLUGIN_FILE_MANAGER": "'$PLUGIN_FILE_MANAGER'",
 			"POLICY_BACKUP_SUSPENDED_USERS": "'$POLICY_BACKUP_SUSPENDED_USERS'",
-	    		"POLICY_USER_EDIT_DETAILS": "'$POLICY_USER_EDIT_DETAILS'",
-	    		"POLICY_USER_EDIT_WEB_TEMPLATES": "'$POLICY_USER_EDIT_WEB_TEMPLATES'",
-	    		"POLICY_USER_EDIT_DNS_TEMPLATES": "'$POLICY_USER_EDIT_DNS_TEMPLATES'",
-	    		"POLICY_USER_DELETE_LOGS": "'$POLICY_USER_DELETE_LOGS'",
-	    		"POLICY_USER_VIEW_LOGS": "'$POLICY_USER_VIEW_LOGS'",
-	    		"POLICY_USER_CHANGE_THEME": "'$POLICY_USER_CHANGE_THEME'",
-	    		"POLICY_CSRF_STRICTNESS": "'$POLICY_CSRF_STRICTNESS'",
-			"POLICY_SYNC_SKELETON": "'$POLICY_SYNC_SKELETON'",
+			"POLICY_CSRF_STRICTNESS": "'$POLICY_CSRF_STRICTNESS'",
 			"POLICY_SYNC_ERROR_DOCUMENTS": "'$POLICY_SYNC_ERROR_DOCUMENTS'",
-	    		"USE_SERVER_SMTP": "'$USE_SERVER_SMTP'",
-	    		"SERVER_SMTP_HOST": "'$SERVER_SMTP_HOST'",
-	    		"SERVER_SMTP_PORT": "'$SERVER_SMTP_PORT'",
-	    		"SERVER_SMTP_SECURITY": "'$SERVER_SMTP_SECURITY'",
-	    		"SERVER_SMTP_USER": "'$SERVER_SMTP_USER'",
-	    		"SERVER_SMTP_PASSWD": "'$SERVER_SMTP_PASSWD'",
-	    		"SERVER_SMTP_ADDR": "'$SERVER_SMTP_ADDR'",
-			"DISABLE_IP_CHECK": "'$DISABLE_IP_CHECK'",
-			"FROM_NAME": "'$FROM_NAME'",
-			"FROM_EMAIL": "'$FROM_EMAIL'",
-			"APP_NAME": "'$APP_NAME'",
-			"TITLE": "'$TITLE'",
+			"POLICY_SYNC_SKELETON": "'$POLICY_SYNC_SKELETON'",
+			"POLICY_SYSTEM_ENABLE_BACON": "'$POLICY_SYSTEM_ENABLE_BACON'",
+			"POLICY_SYSTEM_HIDE_ADMIN": "'$POLICY_SYSTEM_HIDE_ADMIN'",
+			"POLICY_SYSTEM_HIDE_SERVICES": "'$POLICY_SYSTEM_HIDE_SERVICES'",
+			"POLICY_SYSTEM_PASSWORD_RESET": "'$POLICY_SYSTEM_PASSWORD_RESET'",
+			"POLICY_SYSTEM_PROTECTED_ADMIN": "'$POLICY_SYSTEM_PROTECTED_ADMIN'",
+			"POLICY_USER_CHANGE_THEME": "'$POLICY_USER_CHANGE_THEME'",
+			"POLICY_USER_DELETE_LOGS": "'$POLICY_USER_DELETE_LOGS'",
+			"POLICY_USER_EDIT_DETAILS": "'$POLICY_USER_EDIT_DETAILS'",
+			"POLICY_USER_EDIT_DNS_TEMPLATES": "'$POLICY_USER_EDIT_DNS_TEMPLATES'",
+			"POLICY_USER_EDIT_WEB_TEMPLATES": "'$POLICY_USER_EDIT_WEB_TEMPLATES'",
+			"POLICY_USER_VIEW_LOGS": "'$POLICY_USER_VIEW_LOGS'",
+			"POLICY_USER_VIEW_SUSPENDED": "'$POLICY_USER_VIEW_SUSPENDED'",
+			"PROXY_PORT": "'$PROXY_PORT'",
+			"PROXY_SSL_PORT": "'$PROXY_SSL_PORT'",
+			"PROXY_SYSTEM": "'$PROXY_SYSTEM'",
+			"RELEASE_BRANCH": "'$RELEASE_BRANCH'",
+			"REPOSITORY": "'$REPOSITORY'",
+			"SERVER_SMTP_ADDR": "'$SERVER_SMTP_ADDR'",
+			"SERVER_SMTP_HOST": "'$SERVER_SMTP_HOST'",
+			"SERVER_SMTP_PASSWD": "'$SERVER_SMTP_PASSWD'",
+			"SERVER_SMTP_PORT": "'$SERVER_SMTP_PORT'",
+			"SERVER_SMTP_SECURITY": "'$SERVER_SMTP_SECURITY'",
+			"SERVER_SMTP_USER": "'$SERVER_SMTP_USER'",
+			"SMTP_RELAY": "'$SMTP_RELAY'",
+			"SMTP_RELAY_HOST": "'$SMTP_RELAY_HOST'",
+			"SMTP_RELAY_PORT": "'$SMTP_RELAY_PORT'",
+			"SMTP_RELAY_USER": "'$SMTP_RELAY_USER'",
+			"STATS_SYSTEM": "'$STATS_SYSTEM'",
 			"SUBJECT_EMAIL": "'$SUBJECT_EMAIL'",
-			"HIDE_DOCS": "'$HIDE_DOCS'"
+			"SUPPORT_DNSSEC": "'$SUPPORT_DNSSEC'",
+			"THEME": "'$THEME'",
+			"TITLE": "'$TITLE'",
+			"UPDATE_AVAILABLE": "'$UPDATE_AVAILABLE'",
+			"UPGRADE_SEND_EMAIL": "'$UPGRADE_SEND_EMAIL'",
+			"UPGRADE_SEND_EMAIL_LOG": "'$UPGRADE_SEND_EMAIL_LOG'",
+			"USE_SERVER_SMTP": "'$USE_SERVER_SMTP'",
+			"VERSION": "'$VERSION'",
+			"WEBMAIL_ALIAS": "'$WEBMAIL_ALIAS'",
+			"WEBMAIL_SYSTEM": "'$WEBMAIL_SYSTEM'",
+			"WEB_BACKEND": "'$WEB_BACKEND'",
+			"WEB_PORT": "'$WEB_PORT'",
+			"WEB_RGROUPS": "'$WEB_RGROUPS'",
+			"WEB_SSL": "'$WEB_SSL'",
+			"WEB_SSL_PORT": "'$WEB_SSL_PORT'",
+			"WEB_SYSTEM": "'$WEB_SYSTEM'",
+			"WEB_TERMINAL": "'$WEB_TERMINAL'",
+			"WEB_TERMINAL_PORT": "'$WEB_TERMINAL_PORT'"
 		}
 	}'
 }

+ 12 - 0
bin/v-list-sys-services

@@ -95,6 +95,11 @@ get_srv_state() {
 		pids=$(pgrep $name | tr '\n' '|')
 	fi
 
+	# Correctly handle hestia-web-terminal service
+	if [ "$name" == 'hestia-web-terminal' ] && [ "$(systemctl show $name.service | grep 'SubState=' | cut -f2 -d=)" == "running" ]; then
+		pids=$(systemctl show $name.service | grep '^MainPID=' | cut -f2 -d=)
+	fi
+
 	# Prevent from an SSH false positive when there is a TTY or SFTP connection but service is down
 	if [ "$name" == 'ssh' ] && [ "$(systemctl show sshd.service | grep 'SubState=' | cut -f2 -d=)" != "running" ]; then
 		pids=''
@@ -324,6 +329,13 @@ if [ -n "$FIREWALL_EXTENSION" ]; then
 	data="$data STATE='$state' CPU='$cpu' MEM='$mem' RTIME='$rtime'"
 fi
 
+# Checking WEB_TERMINAL
+if [ -n "$WEB_TERMINAL" ] && [ "$WEB_TERMINAL" != 'false' ]; then
+	get_srv_state hestia-web-terminal
+	data="$data\nNAME='web-terminal' SYSTEM='web terminal backend'"
+	data="$data STATE='$state' CPU='$cpu' MEM='$mem' RTIME='$rtime'"
+fi
+
 # Listing data
 case $format in
 	json) json_list ;;

+ 1 - 4
bin/v-update-sys-hestia-git

@@ -20,8 +20,6 @@ source /etc/hestiacp/hestia.conf
 source $HESTIA/func/main.sh
 # load config file
 source_conf "$HESTIA/conf/hestia.conf"
-# define NodeJS version for download (required for building JS/CSS)
-nodejs_ver="20"
 
 # Perform verification if read-only mode is enabled
 check_hestia_demo_mode
@@ -30,7 +28,6 @@ check_hestia_demo_mode
 if [ -z $(which "node") ]; then
 	read -p "NodeJS not found. Install now to proceed? [Y/n] " answer
 	if [ "$answer" = 'y' ] || [ "$answer" = 'Y' ]; then
-		curl -fsSL "https://deb.nodesource.com/setup_$nodejs_ver.x" | bash - &&\
 		sudo apt-get install -y nodejs
 	else
 		exit 0
@@ -308,7 +305,7 @@ mkdir -p $BUILD_DIR_HESTIA/usr/local/hestia
 # Move needed directories
 cd $BUILD_DIR/hestiacp-$branch_dash
 
-npm install
+npm ci
 npm run build
 
 cp -rf bin func install web $BUILD_DIR_HESTIA/usr/local/hestia/

+ 7 - 1
build.js

@@ -9,7 +9,13 @@ import esbuild from 'esbuild';
 import * as lightningcss from 'lightningcss';
 
 // Packages to build but exclude from bundle
-const externalPackages = ['chart.js/auto', 'alpinejs/dist/cdn.min.js'];
+const externalPackages = [
+	'chart.js/auto',
+	'alpinejs/dist/cdn.min.js',
+	'xterm',
+	'xterm-addon-webgl',
+	'xterm-addon-canvas',
+];
 
 // Build main bundle
 async function buildJS() {

+ 10 - 1
func/syshealth.sh

@@ -198,7 +198,7 @@ function syshealth_update_system_config_format() {
 	# SYSTEM CONFIGURATION
 	# Create array of known keys in configuration file
 	system="system"
-	known_keys="ANTISPAM_SYSTEM ANTIVIRUS_SYSTEM API_ALLOWED_IP API BACKEND_PORT BACKUP_GZIP BACKUP_MODE BACKUP_SYSTEM CRON_SYSTEM DB_PMA_ALIAS DB_SYSTEM DISK_QUOTA DNS_SYSTEM ENFORCE_SUBDOMAIN_OWNERSHIP FILE_MANAGER FIREWALL_EXTENSION FIREWALL_SYSTEM FTP_SYSTEM IMAP_SYSTEM INACTIVE_SESSION_TIMEOUT LANGUAGE LOGIN_STYLE MAIL_SYSTEM PROXY_PORT PROXY_SSL_PORT PROXY_SYSTEM RELEASE_BRANCH STATS_SYSTEM THEME UPDATE_HOSTNAME_SSL UPGRADE_SEND_EMAIL UPGRADE_SEND_EMAIL_LOG WEB_BACKEND WEBMAIL_ALIAS WEBMAIL_SYSTEM WEB_PORT WEB_RGROUPS WEB_SSL WEB_SSL_PORT WEB_SYSTEM VERSION DISABLE_IP_CHECK"
+	known_keys="ANTISPAM_SYSTEM ANTIVIRUS_SYSTEM API_ALLOWED_IP API BACKEND_PORT BACKUP_GZIP BACKUP_MODE BACKUP_SYSTEM CRON_SYSTEM DB_PMA_ALIAS DB_SYSTEM DISK_QUOTA DNS_SYSTEM ENFORCE_SUBDOMAIN_OWNERSHIP FILE_MANAGER FIREWALL_EXTENSION FIREWALL_SYSTEM FTP_SYSTEM IMAP_SYSTEM INACTIVE_SESSION_TIMEOUT LANGUAGE LOGIN_STYLE MAIL_SYSTEM PROXY_PORT PROXY_SSL_PORT PROXY_SYSTEM RELEASE_BRANCH STATS_SYSTEM THEME UPDATE_HOSTNAME_SSL UPGRADE_SEND_EMAIL UPGRADE_SEND_EMAIL_LOG WEB_BACKEND WEBMAIL_ALIAS WEBMAIL_SYSTEM WEB_PORT WEB_RGROUPS WEB_SSL WEB_SSL_PORT WEB_SYSTEM WEB_TERMINAL WEB_TERMINAL_PORT VERSION DISABLE_IP_CHECK"
 	write_kv_config_file
 	unset system
 	unset known_keys
@@ -375,6 +375,15 @@ function syshealth_repair_system_config() {
 		echo "[ ! ] Adding missing variable to hestia.conf: PLUGIN_APP_INSTALLER ('true')"
 		$BIN/v-change-sys-config-value "PLUGIN_APP_INSTALLER" "true"
 	fi
+	# Web Terminal
+	if [[ -z $(check_key_exists 'WEB_TERMINAL') ]]; then
+		echo "[ ! ] Adding missing variable to hestia.conf: WEB_TERMINAL ('false')"
+		$BIN/v-change-sys-config-value "WEB_TERMINAL" "false"
+	fi
+	if [[ -z $(check_key_exists 'WEB_TERMINAL_PORT') ]]; then
+		echo "[ ! ] Adding missing variable to hestia.conf: WEB_TERMINAL_PORT ('8085')"
+		$BIN/v-change-sys-config-value "WEB_TERMINAL_PORT" "8085"
+	fi
 	# Enable preview mode
 	if [[ -z $(check_key_exists 'POLICY_SYSTEM_ENABLE_BACON') ]]; then
 		echo "[ ! ] Adding missing variable to hestia.conf: POLICY_SYSTEM_ENABLE_BACON ('false')"

+ 6 - 0
func/upgrade.sh

@@ -856,6 +856,12 @@ upgrade_restart_services() {
 			fi
 			$BIN/v-restart-service "$FIREWALL_EXTENSION"
 		fi
+		if [ "$WEB_TERMINAL" = "true" ]; then
+			if [ "$DEBUG_MODE" = "true" ]; then
+				echo "      - hestia-web-terminal"
+			fi
+			$BIN/v-restart-service "hestia-web-terminal"
+		fi
 		# Restart SSH daemon service
 		if [ "$DEBUG_MODE" = "true" ]; then
 			echo "      - sshd"

+ 26 - 3
install/hst-install-debian.sh

@@ -40,9 +40,9 @@ mariadb_v="10.11"
 # Defining software pack for all distros
 software="acl apache2 apache2-suexec-custom apache2-suexec-pristine apache2-utils awstats bc bind9 bsdmainutils bsdutils
   clamav-daemon cron curl dnsutils dovecot-imapd dovecot-managesieved dovecot-pop3d dovecot-sieve e2fslibs e2fsprogs
-  exim4 exim4-daemon-heavy expect fail2ban flex ftp git hestia=${HESTIA_INSTALL_VER} hestia-nginx hestia-php idn2
-  imagemagick ipset jq libapache2-mod-fcgid libapache2-mod-php$fpm_v libapache2-mpm-itk libmail-dkim-perl lsb-release
-  lsof mariadb-client mariadb-common mariadb-server mc mysql-client mysql-common mysql-server net-tools nginx openssh-server
+  exim4 exim4-daemon-heavy expect fail2ban flex ftp git hestia=${HESTIA_INSTALL_VER} hestia-nginx hestia-php hestia-web-terminal
+  idn2 imagemagick ipset jq libapache2-mod-fcgid libapache2-mod-php$fpm_v libapache2-mpm-itk libmail-dkim-perl lsb-release
+  lsof mariadb-client mariadb-common mariadb-server mc mysql-client mysql-common mysql-server net-tools nginx nodejs openssh-server
   php$fpm_v php$fpm_v-apcu php$fpm_v-bz2 php$fpm_v-cgi php$fpm_v-cli php$fpm_v-common php$fpm_v-curl php$fpm_v-gd
   php$fpm_v-imagick php$fpm_v-imap php$fpm_v-intl php$fpm_v-ldap php$fpm_v-mbstring php$fpm_v-mysql php$fpm_v-opcache
   php$fpm_v-pgsql php$fpm_v-pspell php$fpm_v-readline php$fpm_v-xml php$fpm_v-zip postgresql postgresql-contrib
@@ -70,6 +70,7 @@ help() {
   -i, --iptables          Install Iptables      [yes|no]  default: yes
   -b, --fail2ban          Install Fail2ban      [yes|no]  default: yes
   -q, --quota             Filesystem Quota      [yes|no]  default: no
+  -W, --webterminal       Web terminal          [yes|no]  default: no
   -d, --api               Activate API          [yes|no]  default: yes
   -r, --port              Change Backend Port             default: 8083
   -l, --lang              Default language                default: en
@@ -217,6 +218,7 @@ for arg; do
 		--fail2ban) args="${args}-b " ;;
 		--multiphp) args="${args}-o " ;;
 		--quota) args="${args}-q " ;;
+		--webterminal) args="${args}-W " ;;
 		--port) args="${args}-r " ;;
 		--lang) args="${args}-l " ;;
 		--interactive) args="${args}-y " ;;
@@ -255,6 +257,7 @@ while getopts "a:w:v:j:k:m:M:g:d:x:z:Z:c:t:i:b:r:o:q:l:y:s:e:p:D:fh" Option; do
 		i) iptables=$OPTARG ;;    # Iptables
 		b) fail2ban=$OPTARG ;;    # Fail2ban
 		q) quota=$OPTARG ;;       # FS Quota
+		W) webterminal=$OPTARG ;; # Web Terminal
 		r) port=$OPTARG ;;        # Backend Port
 		l) lang=$OPTARG ;;        # Language
 		d) api=$OPTARG ;;         # Activate API
@@ -296,6 +299,7 @@ fi
 set_default_value 'iptables' 'yes'
 set_default_value 'fail2ban' 'yes'
 set_default_value 'quota' 'no'
+set_default_value 'webterminal' 'no'
 set_default_value 'interactive' 'yes'
 set_default_value 'api' 'yes'
 set_default_port '8083'
@@ -758,6 +762,12 @@ echo "[ * ] Hestia Control Panel"
 echo "deb [arch=$ARCH signed-by=/usr/share/keyrings/hestia-keyring.gpg] https://$RHOST/ $codename main" > $apt/hestia.list
 gpg --no-default-keyring --keyring /usr/share/keyrings/hestia-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys A189E93654F0B0E5 > /dev/null 2>&1
 
+# Installing NodeJS 20.x repo
+echo "[ * ] NodeJS 20.x"
+echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x $codename main" > $apt/nodesource.list
+echo "deb-src [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x $codename main" >> $apt/nodesource.list
+curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | gpg --dearmor | tee /usr/share/keyrings/nodesource.gpg > /dev/null 2>&1
+
 # Installing PostgreSQL repo
 if [ "$postgresql" = 'yes' ]; then
 	echo "[ * ] PostgreSQL"
@@ -950,6 +960,10 @@ if [ "$iptables" = 'no' ]; then
 	software=$(echo "$software" | sed -e "s/ipset//")
 	software=$(echo "$software" | sed -e "s/fail2ban//")
 fi
+if [ "$webterminal" = 'no' ]; then
+	software=$(echo "$software" | sed -e "s/nodejs//")
+	software=$(echo "$software" | sed -e "s/hestia-web-terminal//")
+fi
 if [ "$phpfpm" = 'yes' ]; then
 	software=$(echo "$software" | sed -e "s/php$fpm_v-cgi//")
 	software=$(echo "$software" | sed -e "s/libapache2-mpm-itk//")
@@ -959,6 +973,7 @@ fi
 if [ -d "$withdebs" ]; then
 	software=$(echo "$software" | sed -e "s/hestia-nginx//")
 	software=$(echo "$software" | sed -e "s/hestia-php//")
+	software=$(echo "$software" | sed -e "s/hestia-web-terminal//")
 	software=$(echo "$software" | sed -e "s/hestia=${HESTIA_INSTALL_VER}//")
 fi
 
@@ -1231,6 +1246,14 @@ else
 	write_config_value "DISK_QUOTA" "no"
 fi
 
+# Web terminal
+if [ "$webterminal" = 'yes' ]; then
+	write_config_value "WEB_TERMINAL" "true"
+else
+	write_config_value "WEB_TERMINAL" "false"
+fi
+write_config_value "WEB_TERMINAL_PORT" "8085"
+
 # Backups
 write_config_value "BACKUP_SYSTEM" "local"
 write_config_value "BACKUP_GZIP" "4"

+ 25 - 3
install/hst-install-ubuntu.sh

@@ -40,9 +40,9 @@ mariadb_v="10.11"
 # Defining software pack for all distros
 software="acl apache2 apache2.2-common apache2-suexec-custom apache2-utils apparmor-utils awstats bc bind9 bsdmainutils bsdutils
   clamav-daemon cron curl dnsutils dovecot-imapd dovecot-managesieved dovecot-pop3d dovecot-sieve e2fslibs e2fsprogs
-  exim4 exim4-daemon-heavy expect fail2ban flex ftp git hestia=${HESTIA_INSTALL_VER} hestia-nginx hestia-php idn2
-  imagemagick ipset jq libapache2-mod-fcgid libapache2-mod-php$fpm_v libapache2-mod-rpaf libonig5 libzip4 lsb-release
-  lsof mariadb-client mariadb-common mariadb-server mc mysql-client mysql-common mysql-server nginx openssh-server
+  exim4 exim4-daemon-heavy expect fail2ban flex ftp git hestia=${HESTIA_INSTALL_VER} hestia-nginx hestia-php hestia-web-terminal
+  idn2 imagemagick ipset jq libapache2-mod-fcgid libapache2-mod-php$fpm_v libapache2-mod-rpaf libonig5 libzip4 lsb-release
+  lsof mariadb-client mariadb-common mariadb-server mc mysql-client mysql-common mysql-server nginx nodejs openssh-server
   php$fpm_v php$fpm_v-apcu php$fpm_v-bz2 php$fpm_v-cgi php$fpm_v-cli php$fpm_v-common php$fpm_v-curl php$fpm_v-gd
   php$fpm_v-imagick php$fpm_v-imap php$fpm_v-intl php$fpm_v-ldap php$fpm_v-mbstring php$fpm_v-mysql php$fpm_v-opcache
   php$fpm_v-pgsql php$fpm_v-pspell php$fpm_v-readline php$fpm_v-xml php$fpm_v-zip postgresql postgresql-contrib
@@ -70,6 +70,7 @@ help() {
   -i, --iptables          Install Iptables      [yes|no]  default: yes
   -b, --fail2ban          Install Fail2ban      [yes|no]  default: yes
   -q, --quota             Filesystem Quota      [yes|no]  default: no
+  -W, --webterminal       Web Terminal          [yes|no]  default: no
   -d, --api               Activate API          [yes|no]  default: yes
   -r, --port              Change Backend Port             default: 8083
   -l, --lang              Default language                default: en
@@ -217,6 +218,7 @@ for arg; do
 		--fail2ban) args="${args}-b " ;;
 		--multiphp) args="${args}-o " ;;
 		--quota) args="${args}-q " ;;
+		--webterminal) args="${args}-W " ;;
 		--port) args="${args}-r " ;;
 		--lang) args="${args}-l " ;;
 		--interactive) args="${args}-y " ;;
@@ -255,6 +257,7 @@ while getopts "a:w:v:j:k:m:M:g:d:x:z:Z:c:t:i:b:r:o:q:l:y:s:e:p:D:fh" Option; do
 		i) iptables=$OPTARG ;;    # Iptables
 		b) fail2ban=$OPTARG ;;    # Fail2ban
 		q) quota=$OPTARG ;;       # FS Quota
+		W) webterminal=$OPTARG ;; # Web Terminal
 		r) port=$OPTARG ;;        # Backend Port
 		l) lang=$OPTARG ;;        # Language
 		d) api=$OPTARG ;;         # Activate API
@@ -296,6 +299,7 @@ fi
 set_default_value 'iptables' 'yes'
 set_default_value 'fail2ban' 'yes'
 set_default_value 'quota' 'no'
+set_default_value 'webterminal' 'no'
 set_default_value 'interactive' 'yes'
 set_default_value 'api' 'yes'
 set_default_port '8083'
@@ -721,6 +725,12 @@ echo "[ * ] Hestia Control Panel"
 echo "deb [arch=$ARCH signed-by=/usr/share/keyrings/hestia-keyring.gpg] https://$RHOST/ $codename main" > $apt/hestia.list
 gpg --no-default-keyring --keyring /usr/share/keyrings/hestia-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys A189E93654F0B0E5 > /dev/null 2>&1
 
+# Installing NodeJS 20.x repo
+echo "[ * ] NodeJS 20.x"
+echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x $codename main" > $apt/nodesource.list
+echo "deb-src [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x $codename main" >> $apt/nodesource.list
+curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | gpg --dearmor | tee /usr/share/keyrings/nodesource.gpg > /dev/null 2>&1
+
 # Installing PostgreSQL repo
 if [ "$postgresql" = 'yes' ]; then
 	echo "[ * ] PostgreSQL"
@@ -914,6 +924,10 @@ if [ "$iptables" = 'no' ]; then
 	software=$(echo "$software" | sed -e "s/ipset//")
 	software=$(echo "$software" | sed -e "s/fail2ban//")
 fi
+if [ "$webterminal" = 'no' ]; then
+	software=$(echo "$software" | sed -e "s/nodejs//")
+	software=$(echo "$software" | sed -e "s/hestia-web-terminal//")
+fi
 if [ "$phpfpm" = 'yes' ]; then
 	software=$(echo "$software" | sed -e "s/php$fpm_v-cgi//")
 	software=$(echo "$software" | sed -e "s/libapache2-mod-ruid2//")
@@ -1243,6 +1257,14 @@ else
 	write_config_value "DISK_QUOTA" "no"
 fi
 
+# Web terminal
+if [ "$webterminal" = 'yes' ]; then
+	write_config_value "WEB_TERMINAL" "true"
+else
+	write_config_value "WEB_TERMINAL" "false"
+fi
+write_config_value "WEB_TERMINAL_PORT" "8085"
+
 # Backups
 write_config_value "BACKUP_SYSTEM" "local"
 write_config_value "BACKUP_GZIP" "4"

+ 11 - 0
install/upgrade/versions/1.9.0.sh

@@ -26,3 +26,14 @@ upgrade_config_set_value 'UPGRADE_UPDATE_FILEMANAGER_CONFIG' 'false'
 # update config sftp jail
 $BIN/v-delete-sys-sftp-jail
 $BIN/v-add-sys-sftp-jail
+
+codename="$(lsb_release -s -c)"
+apt=/etc/apt/sources.list.d
+
+# Installing NodeJS 20.x repo
+if [ ! -f $apt/nodesource.list ] && [ ! -z $(which "node") ]; then
+	echo "[ * ] Adding NodeJS 20.x repo"
+	echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x $codename main" > $apt/nodesource.list
+	echo "deb-src [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x $codename main" >> $apt/nodesource.list
+	curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | gpg --dearmor | tee /usr/share/keyrings/nodesource.gpg > /dev/null 2>&1
+fi

+ 25 - 1
package-lock.json

@@ -15,7 +15,10 @@
 				"chart.js": "4.3.2",
 				"check-password-strength": "2.0.7",
 				"nanoid": "4.0.2",
-				"normalize.css": "8.0.1"
+				"normalize.css": "8.0.1",
+				"xterm": "5.2.1",
+				"xterm-addon-canvas": "0.4.0",
+				"xterm-addon-webgl": "0.15.0"
 			},
 			"devDependencies": {
 				"@prettier/plugin-php": "0.19.6",
@@ -6709,6 +6712,27 @@
 				"url": "https://github.com/sponsors/isaacs"
 			}
 		},
+		"node_modules/xterm": {
+			"version": "5.2.1",
+			"resolved": "https://registry.npmjs.org/xterm/-/xterm-5.2.1.tgz",
+			"integrity": "sha512-cs5Y1fFevgcdoh2hJROMVIWwoBHD80P1fIP79gopLHJIE4kTzzblanoivxTiQ4+92YM9IxS36H1q0MxIJXQBcA=="
+		},
+		"node_modules/xterm-addon-canvas": {
+			"version": "0.4.0",
+			"resolved": "https://registry.npmjs.org/xterm-addon-canvas/-/xterm-addon-canvas-0.4.0.tgz",
+			"integrity": "sha512-iTC8CdjX9+hGX7jiEuiDMXzHsY/FKJdVnbjep5xjRXNu7RKOk15xuecIkJ7HZORqMVPpr4DGS3jyd9XUoBuxqw==",
+			"peerDependencies": {
+				"xterm": "^5.0.0"
+			}
+		},
+		"node_modules/xterm-addon-webgl": {
+			"version": "0.15.0",
+			"resolved": "https://registry.npmjs.org/xterm-addon-webgl/-/xterm-addon-webgl-0.15.0.tgz",
+			"integrity": "sha512-ZLcqogMFHr4g/YRhcCh3xE8tTklnyut/M+O/XhVsFBRB/YCvYhPdLQ5/AQk54V0wjWAQpa8CF3W8DVR9OqyMCg==",
+			"peerDependencies": {
+				"xterm": "^5.0.0"
+			}
+		},
 		"node_modules/yallist": {
 			"version": "4.0.0",
 			"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",

+ 4 - 1
package.json

@@ -23,7 +23,10 @@
 		"chart.js": "4.3.2",
 		"check-password-strength": "2.0.7",
 		"nanoid": "4.0.2",
-		"normalize.css": "8.0.1"
+		"normalize.css": "8.0.1",
+		"xterm": "5.2.1",
+		"xterm-addon-canvas": "0.4.0",
+		"xterm-addon-webgl": "0.15.0"
 	},
 	"devDependencies": {
 		"@prettier/plugin-php": "0.19.6",

+ 1 - 1
src/deb/nginx/control

@@ -1,7 +1,7 @@
 Source: hestia-nginx
 Package: hestia-nginx
 Priority: optional
-Version: 1.25.1-2
+Version: 1.25.1-3
 Section: admin
 Maintainer: HestiaCP <[email protected]>
 Homepage: https://www.hestiacp.com

+ 8 - 0
src/deb/nginx/nginx.conf

@@ -166,6 +166,14 @@ http {
 			}
 		}
 
+		location /_shell/ {
+			proxy_pass http://localhost:8085;
+			proxy_http_version 1.1;
+			proxy_set_header Upgrade $http_upgrade;
+			proxy_set_header Connection "Upgrade";
+			proxy_set_header X-Real-IP $remote_addr;
+		}
+
 		location ~ \.php$ {
 			include                  fastcgi_params;
 			fastcgi_param            HTTP_EARLY_DATA $rfc_early_data if_not_empty;

+ 9 - 0
src/deb/web-terminal/.eslintrc.cjs

@@ -0,0 +1,9 @@
+module.exports = {
+	env: {
+		browser: false,
+		node: true,
+	},
+	rules: {
+		'no-console': 'off',
+	},
+};

+ 14 - 0
src/deb/web-terminal/control

@@ -0,0 +1,14 @@
+Source: hestia-web-terminal
+Package: hestia-web-terminal
+Priority: optional
+Version: 1.0.0
+Section: admin
+Maintainer: HestiaCP <[email protected]>
+Homepage: https://www.hestiacp.com
+Architecture: amd64
+Depends: hestia, nodejs (>= 18.0.0)
+Description: hestia web terminal
+ hestia is an open source hosting control panel.
+ hestia has a clean and focused interface without the clutter.
+ hestia has the latest of very innovative technologies.
+ hestia is a fork from VestaCP, special thanks to vestacp.com and Serghey Rodin

+ 30 - 0
src/deb/web-terminal/copyright

@@ -0,0 +1,30 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: hestia
+Source: https://www.hestiacp.com
+
+Files: *
+Copyright: 2018-2023, Hestia Control Panel <[email protected]>
+License: GPL-3.0+
+Remarks: Hestia is a fork from VestaCP, special thanks to vestacp.com and Serghey Rodin
+
+License: GPL-3.0+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ .
+ This package is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU General Public License for more details.
+ .
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ .
+ On Debian systems, the complete text of the GNU General
+ Public License version 3 can be found in /usr/share/common-licenses/GPL-3.
+
+# Please also look if there are files or directories which have a
+# different copyright/license attached and list them here.
+# Please avoid to pick license terms that are more restrictive than the
+# packaged work, as it may make Debian's contributions unacceptable upstream.

+ 16 - 0
src/deb/web-terminal/hestia-web-terminal.service

@@ -0,0 +1,16 @@
+[Unit]
+Description=HestiaCP Web Terminal
+Documentation=https://hestiacp.com/docs/
+After=network.target
+
+[Service]
+User=root
+Group=hestia-users
+Environment=NODE_ENV=production
+Environment=HESTIA=/usr/local/hestia
+ExecStart=/usr/local/hestia/web-terminal/server.js
+ExecStop=/bin/kill -s TERM $MAINPID
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target

+ 69 - 0
src/deb/web-terminal/package-lock.json

@@ -0,0 +1,69 @@
+{
+	"name": "@hestiacp/web-terminal-ws",
+	"version": "1.0.0",
+	"lockfileVersion": 3,
+	"requires": true,
+	"packages": {
+		"": {
+			"name": "@hestiacp/web-terminal-ws",
+			"version": "1.0.0",
+			"dependencies": {
+				"node-pty": "1.0.0",
+				"ws": "8.13.0"
+			},
+			"devDependencies": {
+				"@types/node": "20.4.5",
+				"@types/ws": "8.5.5"
+			}
+		},
+		"node_modules/@types/node": {
+			"version": "20.4.5",
+			"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.5.tgz",
+			"integrity": "sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==",
+			"dev": true
+		},
+		"node_modules/@types/ws": {
+			"version": "8.5.5",
+			"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
+			"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==",
+			"dev": true,
+			"dependencies": {
+				"@types/node": "*"
+			}
+		},
+		"node_modules/nan": {
+			"version": "2.17.0",
+			"resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz",
+			"integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ=="
+		},
+		"node_modules/node-pty": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz",
+			"integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==",
+			"hasInstallScript": true,
+			"dependencies": {
+				"nan": "^2.17.0"
+			}
+		},
+		"node_modules/ws": {
+			"version": "8.13.0",
+			"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
+			"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
+			"engines": {
+				"node": ">=10.0.0"
+			},
+			"peerDependencies": {
+				"bufferutil": "^4.0.1",
+				"utf-8-validate": ">=5.0.2"
+			},
+			"peerDependenciesMeta": {
+				"bufferutil": {
+					"optional": true
+				},
+				"utf-8-validate": {
+					"optional": true
+				}
+			}
+		}
+	}
+}

+ 17 - 0
src/deb/web-terminal/package.json

@@ -0,0 +1,17 @@
+{
+	"name": "@hestiacp/web-terminal-ws",
+	"private": true,
+	"version": "1.0.0",
+	"type": "module",
+	"scripts": {
+		"start": "node server.js"
+	},
+	"dependencies": {
+		"node-pty": "1.0.0",
+		"ws": "8.13.0"
+	},
+	"devDependencies": {
+		"@types/ws": "8.5.5",
+		"@types/node": "20.4.5"
+	}
+}

+ 35 - 0
src/deb/web-terminal/postinst

@@ -0,0 +1,35 @@
+#!/bin/bash
+
+set -e
+
+if [ "$1" != "configure" ]; then
+	exit 0
+fi
+
+# Run triggers below only on updates
+if [ ! -e "/usr/local/hestia/data/users/admin" ]; then
+	exit
+fi
+
+###############################################################
+#                Initialize functions/variables               #
+###############################################################
+
+if [ -z "$HESTIA" ]; then
+	export HESTIA='/usr/local/hestia'
+	PATH=$PATH:/usr/local/hestia/bin
+	export PATH
+fi
+
+# Load upgrade functions and variables
+source /usr/local/hestia/func/main.sh
+source /usr/local/hestia/func/upgrade.sh
+source /usr/local/hestia/conf/hestia.conf
+source /usr/local/hestia/install/upgrade/upgrade.conf
+
+# Restart hestia-web-terminal service
+if [ -f "/etc/systemd/system/hestia-web-terminal.service" ]; then
+	systemctl daemon-reload > /dev/null 2>&1
+	systemctl enable hestia-web-terminal > /dev/null 2>&1
+	systemctl restart hestia-web-terminal > /dev/null 2>&1
+fi

+ 115 - 0
src/deb/web-terminal/server.js

@@ -0,0 +1,115 @@
+#!/usr/bin/env node
+
+import { execSync } from 'node:child_process';
+import { readFileSync } from 'node:fs';
+import { spawn } from 'node-pty';
+import { WebSocketServer } from 'ws';
+
+const hostname = execSync('hostname', { silent: true }).toString().trim();
+const systemIPs = JSON.parse(
+	execSync(`${process.env.HESTIA}/bin/v-list-sys-ips json`, { silent: true }).toString()
+);
+const { config } = JSON.parse(
+	execSync(`${process.env.HESTIA}/bin/v-list-sys-config json`, { silent: true }).toString()
+);
+
+const wss = new WebSocketServer({
+	port: parseInt(config.WEB_TERMINAL_PORT, 10),
+	verifyClient: async (info, cb) => {
+		if (!info.req.headers.cookie.includes('PHPSESSID')) {
+			cb(false, 401, 'Unauthorized');
+			return;
+		}
+
+		const origin = info.origin || info.req.headers.origin;
+		let matches = origin === `https://${hostname}:${config.BACKEND_PORT}`;
+
+		if (!matches) {
+			for (const ip of Object.keys(systemIPs)) {
+				if (origin === `https://${ip}:${config.BACKEND_PORT}`) {
+					matches = true;
+					break;
+				}
+			}
+		}
+
+		if (matches) {
+			cb(true);
+			return;
+		}
+		cb(false, 403, 'Forbidden');
+	},
+});
+
+wss.on('connection', (ws, req) => {
+	wss.clients.add(ws);
+
+	const remoteIP = req.headers['x-real-ip'] || req.socket.remoteAddress;
+
+	// Check if session is valid
+	const sessionID = req.headers.cookie.split('=')[1];
+	console.log(`New connection from ${remoteIP} (${sessionID})`);
+
+	const file = readFileSync(`${process.env.HESTIA}/data/sessions/sess_${sessionID}`);
+	if (!file) {
+		console.error(`Invalid session ID ${sessionID}, refusing connection`);
+		ws.close(1000, 'Your session has expired.');
+		return;
+	}
+	const session = file.toString();
+
+	// Get username
+	const login = session.split('user|s:')[1].split('"')[1];
+	const impersonating = session.split('look|s:')[1].split('"')[1];
+	const username = impersonating.length > 0 ? impersonating : login;
+
+	// Get user info
+	const passwd = readFileSync('/etc/passwd').toString();
+	const userline = passwd.split('\n').find((line) => line.startsWith(`${username}:`));
+	if (!userline) {
+		console.error(`User ${username} not found, refusing connection`);
+		ws.close(1000, 'You are not allowed to access this server.');
+		return;
+	}
+	const [, , uid, gid, , homedir, shell] = userline.split(':');
+
+	if (shell.endsWith('nologin')) {
+		console.error(`User ${username} has no shell, refusing connection`);
+		ws.close(1000, 'You have no shell access.');
+		return;
+	}
+
+	// Spawn shell as logged in user
+	const pty = spawn(shell, [], {
+		name: 'xterm-color',
+		uid: parseInt(uid, 10),
+		gid: parseInt(gid, 10),
+		cwd: homedir,
+		env: {
+			SHELL: shell,
+			TERM: 'xterm-color',
+			USER: username,
+			HOME: homedir,
+			PWD: homedir,
+			HESTIA: process.env.HESTIA,
+		},
+	});
+	console.log(`New pty (${pty.pid}): ${shell} as ${username} (${uid}:${gid}) in ${homedir}`);
+
+	// Send/receive data from websocket/pty
+	pty.on('data', (data) => ws.send(data));
+	ws.on('message', (data) => pty.write(data));
+
+	// Ensure pty is killed when websocket is closed and vice versa
+	pty.on('exit', () => {
+		console.log(`Ended pty (${pty.pid})`);
+		if (ws.OPEN) {
+			ws.close();
+		}
+	});
+	ws.on('close', () => {
+		console.log(`Ended connection from ${remoteIP} (${sessionID})`);
+		pty.kill();
+		wss.clients.delete(ws);
+	});
+});

+ 93 - 6
src/hst_autocompile.sh

@@ -85,12 +85,13 @@ get_branch_file() {
 
 usage() {
 	echo "Usage:"
-	echo "    $0 (--all|--hestia|--nginx|--php) [options] [branch] [Y]"
+	echo "    $0 (--all|--hestia|--nginx|--php|--web-terminal) [options] [branch] [Y]"
 	echo ""
 	echo "    --all           Build all hestia packages."
 	echo "    --hestia        Build only the Control Panel package."
 	echo "    --nginx         Build only the backend nginx engine package."
 	echo "    --php           Build only the backend php engine package"
+	echo "    --web-terminal  Build only the backend web terminal websocket package"
 	echo "  Options:"
 	echo "    --install       Install generated packages"
 	echo "    --keepbuild     Don't delete downloaded source and build folders"
@@ -136,6 +137,7 @@ for i in $*; do
 		--all)
 			NGINX_B='true'
 			PHP_B='true'
+			WEB_TERMINAL_B='true'
 			HESTIA_B='true'
 			;;
 		--nginx)
@@ -144,6 +146,9 @@ for i in $*; do
 		--php)
 			PHP_B='true'
 			;;
+		--web-terminal)
+			WEB_TERMINAL_B='true'
+			;;
 		--hestia)
 			HESTIA_B='true'
 			;;
@@ -206,10 +211,12 @@ if [ -f "$SRC_DIR/src/deb/hestia/control" ] && [ "$use_src_folder" == 'true' ];
 	BUILD_VER=$(cat $SRC_DIR/src/deb/hestia/control | grep "Version:" | cut -d' ' -f2)
 	NGINX_V=$(cat $SRC_DIR/src/deb/nginx/control | grep "Version:" | cut -d' ' -f2)
 	PHP_V=$(cat $SRC_DIR/src/deb/php/control | grep "Version:" | cut -d' ' -f2)
+	WEB_TERMINAL_V=$(cat $SRC_DIR/src/deb/web-terminal/control | grep "Version:" | cut -d' ' -f2)
 else
 	BUILD_VER=$(curl -s https://raw.githubusercontent.com/$REPO/$branch/src/deb/hestia/control | grep "Version:" | cut -d' ' -f2)
 	NGINX_V=$(curl -s https://raw.githubusercontent.com/$REPO/$branch/src/deb/nginx/control | grep "Version:" | cut -d' ' -f2)
 	PHP_V=$(curl -s https://raw.githubusercontent.com/$REPO/$branch/src/deb/php/control | grep "Version:" | cut -d' ' -f2)
+	WEB_TERMINAL_V=$(curl -s https://raw.githubusercontent.com/$REPO/$branch/src/deb/web-terminal/control | grep "Version:" | cut -d' ' -f2)
 fi
 
 if [ -z "$BUILD_VER" ]; then
@@ -217,7 +224,7 @@ if [ -z "$BUILD_VER" ]; then
 	exit 1
 fi
 
-echo "Build version $BUILD_VER, with Nginx version $NGINX_V and PHP version $PHP_V"
+echo "Build version $BUILD_VER, with Nginx version $NGINX_V, PHP version $PHP_V and Web Terminal version $WEB_TERMINAL_V"
 
 if [ -e "/etc/redhat-release" ]; then
 	HESTIA_V="${BUILD_VER}"
@@ -263,7 +270,15 @@ if [ "$dontinstalldeps" != 'true' ]; then
 		fi
 	else
 		# Set package dependencies for compiling
-		SOFTWARE='wget tar git curl build-essential libxml2-dev libz-dev libzip-dev libgmp-dev libcurl4-gnutls-dev unzip openssl libssl-dev pkg-config libsqlite3-dev libonig-dev rpm lsb-release'
+		SOFTWARE='wget tar git curl build-essential libxml2-dev libz-dev libzip-dev libgmp-dev libcurl4-gnutls-dev unzip openssl nodejs libssl-dev pkg-config libsqlite3-dev libonig-dev rpm lsb-release'
+
+		# Installing NodeJS 20.x repo
+		if [ ! -f $apt/nodesource.list ] && [ ! -z $(which "node") ]; then
+			echo "Adding NodeJS 20.x repo..."
+			echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x $codename main" > $apt/nodesource.list
+			echo "deb-src [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x $codename main" >> $apt/nodesource.list
+			curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | gpg --dearmor | tee /usr/share/keyrings/nodesource.gpg > /dev/null 2>&1
+		fi
 
 		echo "Updating system APT repositories..."
 		apt-get -qq update > /dev/null 2>&1
@@ -295,6 +310,7 @@ if [ "$HESTIA_DEBUG" ]; then
 	echo "Hestia version   : $BUILD_VER"
 	echo "Nginx version    : $NGINX_V"
 	echo "PHP version      : $PHP_V"
+	echo "Web Term version : $WEB_TERMINAL_V"
 	echo "Architecture     : $BUILD_ARCH"
 	echo "Debug mode       : $HESTIA_DEBUG"
 	echo "Source directory : $SRC_DIR"
@@ -330,7 +346,7 @@ branch_dash=$(echo "$branch" | sed 's/\//-/g')
 if [ "$NGINX_B" = true ]; then
 	echo "Building hestia-nginx package..."
 	if [ "$CROSS" = "true" ]; then
-		echo "Cross compile not supported for hestia-nginx or hestia-php"
+		echo "Cross compile not supported for hestia-nginx, hestia-php or hestia-web-terminal"
 		exit 1
 	fi
 
@@ -476,7 +492,7 @@ fi
 
 if [ "$PHP_B" = true ]; then
 	if [ "$CROSS" = "true" ]; then
-		echo "Cross compile not supported for hestia-nginx or hestia-php"
+		echo "Cross compile not supported for hestia-nginx, hestia-php or hestia-web-terminal"
 		exit 1
 	fi
 
@@ -608,6 +624,75 @@ if [ "$PHP_B" = true ]; then
 	fi
 fi
 
+#################################################################################
+#
+# Building hestia-web-terminal
+#
+#################################################################################
+
+if [ "$WEB_TERMINAL_B" = true ]; then
+	if [ "$CROSS" = "true" ]; then
+		echo "Cross compile not supported for hestia-nginx, hestia-php or hestia-web-terminal"
+		exit 1
+	fi
+
+	echo "Building hestia-web-terminal package..."
+
+	if [ "$BUILD_DEB" = true ]; then
+		BUILD_DIR_HESTIA_TERMINAL=$BUILD_DIR/hestia-web-terminal_$WEB_TERMINAL_V
+
+		# Check if target directory exist
+		if [ -d $BUILD_DIR_HESTIA_TERMINAL ]; then
+			rm -r $BUILD_DIR_HESTIA_TERMINAL
+		fi
+
+		# Create directory
+		mkdir -p $BUILD_DIR_HESTIA_TERMINAL
+		chown -R root:root $BUILD_DIR_HESTIA_TERMINAL
+
+		# Get Debian package files
+		[ "$HESTIA_DEBUG" ] && echo DEBUG: mkdir -p $BUILD_DIR_HESTIA_TERMINAL/DEBIAN
+		mkdir -p $BUILD_DIR_HESTIA_TERMINAL/DEBIAN
+		get_branch_file 'src/deb/web-terminal/control' "$BUILD_DIR_HESTIA_TERMINAL/DEBIAN/control"
+		if [ "$BUILD_ARCH" != "amd64" ]; then
+			sed -i "s/amd64/${BUILD_ARCH}/g" "$BUILD_DIR_HESTIA_TERMINAL/DEBIAN/control"
+		fi
+
+		get_branch_file 'src/deb/web-terminal/copyright' "$BUILD_DIR_HESTIA_TERMINAL/DEBIAN/copyright"
+		get_branch_file 'src/deb/web-terminal/postinst' "$BUILD_DIR_HESTIA_TERMINAL/DEBIAN/postinst"
+		chmod +x $BUILD_DIR_HESTIA_TERMINAL/DEBIAN/postinst
+
+		# Get server files
+		[ "$HESTIA_DEBUG" ] && echo DEBUG: mkdir -p "${BUILD_DIR_HESTIA_TERMINAL}/usr/local/hestia/web-terminal"
+		mkdir -p "${BUILD_DIR_HESTIA_TERMINAL}/usr/local/hestia/web-terminal"
+		get_branch_file 'src/deb/web-terminal/package.json' "${BUILD_DIR_HESTIA_TERMINAL}/usr/local/hestia/web-terminal/package.json"
+		get_branch_file 'src/deb/web-terminal/package-lock.json' "${BUILD_DIR_HESTIA_TERMINAL}/usr/local/hestia/web-terminal/package-lock.json"
+		get_branch_file 'src/deb/web-terminal/server.js' "${BUILD_DIR_HESTIA_TERMINAL}/usr/local/hestia/web-terminal/server.js"
+		chmod +x "${BUILD_DIR_HESTIA_TERMINAL}/usr/local/hestia/web-terminal/server.js"
+
+		cd $BUILD_DIR_HESTIA_TERMINAL/usr/local/hestia/web-terminal
+		npm ci --omit=dev
+
+		# Systemd service
+		[ "$HESTIA_DEBUG" ] && echo DEBUG: mkdir -p $BUILD_DIR_HESTIA_TERMINAL/etc/systemd/system
+		mkdir -p $BUILD_DIR_HESTIA_TERMINAL/etc/systemd/system
+		get_branch_file 'src/deb/web-terminal/hestia-web-terminal.service' "$BUILD_DIR_HESTIA_TERMINAL/etc/systemd/system/hestia-web-terminal.service"
+
+		# Build the package
+		echo Building Web Terminal DEB
+		[ "$HESTIA_DEBUG" ] && echo DEBUG: dpkg-deb -Zxz --build $BUILD_DIR_HESTIA_TERMINAL $DEB_DIR
+		dpkg-deb -Zxz --build $BUILD_DIR_HESTIA_TERMINAL $DEB_DIR
+
+		# clear up the source folder
+		if [ "$KEEPBUILD" != 'true' ]; then
+			rm -r $BUILD_DIR_HESTIA_TERMINAL
+			if [ "$use_src_folder" == 'true' ] && [ -d $BUILD_DIR/hestiacp-$branch_dash ]; then
+				rm -r $BUILD_DIR/hestiacp-$branch_dash
+			fi
+		fi
+	fi
+fi
+
 #################################################################################
 #
 # Building hestia
@@ -651,8 +736,10 @@ if [ "$HESTIA_B" = true ]; then
 
 			mkdir -p $BUILD_DIR_HESTIA/usr/local/hestia
 
-			# Move needed directories
+			# Build web and move needed directories
 			cd $BUILD_DIR/hestiacp-$branch_dash
+			npm ci
+			npm run build
 			cp -rf bin func install web $BUILD_DIR_HESTIA/usr/local/hestia/
 
 			# Set permissions

+ 8 - 0
web/css/src/themes/default.css

@@ -1,3 +1,4 @@
+@import url("node:xterm/css/xterm.css");
 @import url("node:normalize.css/normalize.css");
 @import url("node:@fortawesome/fontawesome-free/css/fontawesome");
 @import url("node:@fortawesome/fontawesome-free/css/solid");
@@ -1330,6 +1331,13 @@
 	}
 }
 
+/* Web Terminal
+   ========================================================================== */
+.web-terminal {
+	padding: 10px;
+	background-color: #000;
+}
+
 /* Forms
    ========================================================================== */
 

+ 23 - 0
web/edit/server/index.php

@@ -513,6 +513,29 @@ if (!empty($_POST["save"])) {
 			}
 		}
 	}
+	// Set Web Terminal support
+	if (empty($_SESSION["error_msg"])) {
+		if (
+			!empty($_POST["v_web_terminal"]) &&
+			$_SESSION["WEB_TERMINAL"] != $_POST["v_web_terminal"]
+		) {
+			if ($_POST["v_web_terminal"] == "true") {
+				exec(HESTIA_CMD . "v-add-sys-web-terminal", $output, $return_var);
+				check_return_code($return_var, $output);
+				unset($output);
+				if (empty($_SESSION["error_msg"])) {
+					$_SESSION["WEB_TERMINAL"] = "true";
+				}
+			} else {
+				exec(HESTIA_CMD . "v-delete-sys-web-terminal", $output, $return_var);
+				check_return_code($return_var, $output);
+				unset($output);
+				if (empty($_SESSION["error_msg"])) {
+					$_SESSION["WEB_TERMINAL"] = "false";
+				}
+			}
+		}
+	}
 	// Set phpMyAdmin SSO key
 	if (empty($_SESSION["error_msg"])) {
 		if (!empty($_POST["v_phpmyadmin_key"])) {

+ 9 - 0
web/edit/server/web-terminal/index.php

@@ -0,0 +1,9 @@
+<?php
+$TAB = "SERVER";
+
+// Main include
+include $_SERVER["DOCUMENT_ROOT"] . "/inc/main.php";
+
+// Check user
+header("Location: /list/server");
+exit();

+ 10 - 1
web/inc/main.php

@@ -94,12 +94,21 @@ if (!isset($_SESSION["user"]) && !defined("NO_AUTH_REQUIRED")) {
 	exit();
 }
 
-// Generate CSRF Token
+// Generate CSRF Token and set user shell variable
 if (isset($_SESSION["user"])) {
 	if (!isset($_SESSION["token"])) {
 		$token = bin2hex(random_bytes(16));
 		$_SESSION["token"] = $token;
 	}
+	$username = $_SESSION["user"];
+	if (isset($_SESSION["look"])) {
+		$username = $_SESSION["look"];
+	}
+	exec(HESTIA_CMD . "v-list-user " . quoteshellarg($username) . " json", $output, $return_var);
+	$data = json_decode(implode("", $output), true);
+	unset($output, $return_var);
+	$_SESSION["login_shell"] = $data[$username]["SHELL"];
+	unset($data, $username);
 }
 
 if ($_SESSION["RELEASE_BRANCH"] == "release" && $_SESSION["DEBUG_MODE"] == "false") {

+ 2 - 0
web/js/src/index.js

@@ -25,6 +25,7 @@ import handleTabPanels from './tabPanels';
 import handleToggleAdvanced from './toggleAdvanced';
 import handleUnlimitedInput from './unlimitedInput';
 import initRrdCharts from './rrdCharts';
+import initWebTerminal from './webTerminal';
 
 initListeners();
 focusFirstInput();
@@ -49,6 +50,7 @@ function initListeners() {
 	handleTabPanels();
 	handleToggleAdvanced();
 	initRrdCharts();
+	initWebTerminal();
 }
 
 document.addEventListener('alpine:init', () => {

+ 60 - 0
web/js/src/webTerminal.js

@@ -0,0 +1,60 @@
+export default async function initWebTerminal() {
+	const container = document.querySelector('.js-web-terminal');
+	if (!container) {
+		return;
+	}
+
+	const Terminal = await loadXterm();
+	const terminal = new Terminal();
+	let Addon = null;
+	if (typeof WebGL2RenderingContext !== 'undefined') {
+		Addon = await loadWebGLAddon();
+	} else {
+		Addon = await loadCanvasAddon();
+	}
+	terminal.loadAddon(new Addon());
+	terminal.open(container);
+
+	const socket = new WebSocket(`wss://${window.location.host}/_shell/`);
+	socket.addEventListener('open', (_) => {
+		terminal.onData((data) => socket.send(data));
+		socket.addEventListener('message', (evt) => terminal.write(evt.data));
+	});
+	socket.addEventListener('error', (_) => {
+		terminal.reset();
+		terminal.writeln('Connection error.');
+	});
+	socket.addEventListener('close', (evt) => {
+		if (evt.wasClean) {
+			terminal.reset();
+			terminal.writeln(evt.reason ?? 'Connection closed.');
+		}
+	});
+}
+
+/** @returns {Promise<typeof import("xterm").Terminal>} */
+async function loadXterm() {
+	// NOTE: String expression used to prevent ESBuild from resolving
+	// the import on build (xterm is a separate bundle)
+	const xtermBundlePath = '/js/dist/xterm.min.js';
+	const xtermModule = await import(`${xtermBundlePath}`);
+	return xtermModule.default.Terminal;
+}
+
+/** @returns {Promise<typeof import("xterm-addon-webgl").WebglAddon>} */
+async function loadWebGLAddon() {
+	// NOTE: String expression used to prevent ESBuild from resolving
+	// the import on build (xterm-addon-webgl is a separate bundle)
+	const xtermBundlePath = '/js/dist/xterm-addon-webgl.min.js';
+	const xtermModule = await import(`${xtermBundlePath}`);
+	return xtermModule.default.WebglAddon;
+}
+
+/** @returns {Promise<typeof import("xterm-addon-canvas").CanvasAddon>} */
+async function loadCanvasAddon() {
+	// NOTE: String expression used to prevent ESBuild from resolving
+	// the import on build (xterm-addon-canvas is a separate bundle)
+	const xtermBundlePath = '/js/dist/xterm-addon-canvas.min.js';
+	const xtermModule = await import(`${xtermBundlePath}`);
+	return xtermModule.default.CanvasAddon;
+}

+ 14 - 0
web/list/terminal/index.php

@@ -0,0 +1,14 @@
+<?php
+
+$TAB = "TERMINAL";
+
+// Main include
+include $_SERVER["DOCUMENT_ROOT"] . "/inc/main.php";
+
+if ($_SESSION["login_shell"] == "nologin") {
+	header("Location: /list/user/");
+	exit();
+}
+
+// Render page
+render_page($user, $TAB, "list_terminal");

+ 14 - 0
web/templates/includes/panel.php

@@ -166,6 +166,20 @@
 								<?php } ?>
 							<?php } ?>
 
+							<!-- Web Terminal -->
+							<?php if (isset($_SESSION["WEB_TERMINAL"]) && !empty($_SESSION["WEB_TERMINAL"]) && $_SESSION["WEB_TERMINAL"] == "true") { ?>
+								<?php if ($_SESSION["userContext"] === "admin" &&  $_SESSION["look"] === "admin" && $_SESSION["POLICY_SYSTEM_PROTECTED_ADMIN"] == "yes") { ?>
+									<!-- Hide web terminal when impersonating admin -->
+								<?php } else if ($_SESSION["login_shell"] != "nologin") { ?>
+									<li class="top-bar-menu-item">
+										<a title="<?= _("Web terminal") ?>" class="top-bar-menu-link <?php if ($TAB == 'TERMINAL') echo 'active' ?>" href="/list/terminal/">
+											<i class="fas fa-terminal"></i>
+											<span class="top-bar-menu-link-label u-hide-desktop"><?= _("Web terminal") ?></span>
+										</a>
+									</li>
+								<?php } ?>
+							<?php } ?>
+
 							<!-- Server Settings -->
 							<?php if (($_SESSION["userContext"] === "admin" && $_SESSION["POLICY_SYSTEM_HIDE_SERVICES"] !== "yes") || $_SESSION["user"] === "admin") { ?>
 								<?php if ($_SESSION["userContext"] === "admin" && $_SESSION["look"] !== '') { ?>

+ 13 - 0
web/templates/pages/edit_server.php

@@ -1391,6 +1391,19 @@
 							</option>
 						</select>
 					</div>
+					<div class="u-mb10">
+						<label for="v_web_terminal" class="form-label">
+							<?= _("Web Terminal") ?>
+						</label>
+						<select class="form-select" name="v_web_terminal" id="v_web_terminal">
+							<option value="false">
+								<?= _("No") ?>
+							</option>
+							<option value="true" <?= $_SESSION["WEB_TERMINAL"] == "true" ? "selected" : "" ?>>
+								<?= _("Yes") ?>
+							</option>
+						</select>
+					</div>
 					<div class="u-mb10">
 						<label for="v_quota" class="form-label">
 							<?= _("File System Disk Quota") ?>

+ 20 - 0
web/templates/pages/list_terminal.php

@@ -0,0 +1,20 @@
+<!-- Begin toolbar -->
+<div class="toolbar">
+	<div class="toolbar-inner">
+		<div class="toolbar-buttons">
+			<a class="button button-secondary button-back js-button-back" href="/list/user/">
+				<i class="fas fa-arrow-left icon-blue"></i><?= _("Back") ?>
+			</a>
+		</div>
+	</div>
+</div>
+<!-- End toolbar -->
+
+<div class="container">
+	<div class="form-container form-container-wide">
+		<div class="js-web-terminal web-terminal"></div>
+	</div>
+</div>
+
+<footer class="app-footer">
+</footer>