feat: Allow setting environment variables from the URL #371

This commit is contained in:
Bill Church 2024-11-30 01:58:06 +00:00
parent e77b7ac506
commit 6ec049059b
No known key found for this signature in database
5 changed files with 177 additions and 12 deletions

View file

@ -295,6 +295,79 @@ If key authentication fails, check:
For additional support or troubleshooting, please open an issue on the GitHub repository. For additional support or troubleshooting, please open an issue on the GitHub repository.
### Environment Variables via URL
WebSSH2 supports passing environment variables through URL parameters, allowing you to customize the SSH session environment. This feature enables scenarios like automatically opening specific files or setting custom environment variables.
#### Server Configuration
Before using this feature, you must configure your SSH server to accept the environment variables you want to pass. Edit your `/etc/ssh/sshd_config` file to include the desired variables in the `AcceptEnv` directive:
```bash
# Allow client to pass locale environment variables and custom vars
AcceptEnv LANG LC_* VIM_FILE CUSTOM_ENV
```
Remember to restart your SSH server after making changes:
```bash
sudo systemctl restart sshd # For systemd-based systems
# or
sudo service sshd restart # For init.d-based systems
```
#### Usage
Pass environment variables using the `env` query parameter:
```bash
# Single environment variable
http://localhost:2222/ssh/host/example.com?env=VIM_FILE:config.txt
# Multiple environment variables
http://localhost:2222/ssh/host/example.com?env=VIM_FILE:config.txt,CUSTOM_ENV:test
```
#### Security Considerations
To maintain security, environment variables must meet these criteria:
- Variable names must:
- Start with a capital letter
- Contain only uppercase letters, numbers, and underscores
- Be listed in the SSH server's `AcceptEnv` directive
- Variable values cannot contain shell special characters (;, &, |, `, $)
Invalid environment variables will be silently ignored.
#### Example Usage
1. Configure your SSH server as shown above.
2. Create a URL with environment variables:
```
http://localhost:2222/ssh/host/example.com?env=VIM_FILE:settings.conf,CUSTOM_ENV:production
```
3. In your remote server's `.bashrc` or shell initialization file:
```bash
if [ ! -z "$VIM_FILE" ]; then
vim "$VIM_FILE"
fi
if [ ! -z "$CUSTOM_ENV" ]; then
echo "Running in $CUSTOM_ENV environment"
fi
```
#### Troubleshooting
If environment variables aren't being set:
1. Verify the variables are permitted in `/etc/ssh/sshd_config`
2. Check SSH server logs for any related errors
3. Ensure variable names and values meet the security requirements
4. Test with a simple variable first to isolate any issues
## Routes ## Routes
WebSSH2 provides two main routes: WebSSH2 provides two main routes:

View file

@ -14,6 +14,7 @@ const { createNamespacedDebug } = require("./logger")
const { createAuthMiddleware } = require("./middleware") const { createAuthMiddleware } = require("./middleware")
const { ConfigError, handleError } = require("./errors") const { ConfigError, handleError } = require("./errors")
const { HTTP } = require("./constants") const { HTTP } = require("./constants")
const { parseEnvVars } = require("./utils")
const debug = createNamespacedDebug("routes") const debug = createNamespacedDebug("routes")
@ -43,6 +44,11 @@ module.exports = function(config) {
*/ */
router.get("/host/", auth, (req, res) => { router.get("/host/", auth, (req, res) => {
debug(`router.get.host: /ssh/host/ route`) debug(`router.get.host: /ssh/host/ route`)
const envVars = parseEnvVars(req.query.env)
if (envVars) {
req.session.envVars = envVars
debug("routes: Parsed environment variables: %O", envVars)
}
try { try {
if (!config.ssh.host) { if (!config.ssh.host) {
@ -76,8 +82,13 @@ module.exports = function(config) {
}) })
// Scenario 2: Auth required, uses HTTP Basic Auth // Scenario 2: Auth required, uses HTTP Basic Auth
router.get("/host/:host", auth, (req, res) => { router.get("/host/:host?", auth, (req, res) => {
debug(`router.get.host: /ssh/host/${req.params.host} route`) debug(`router.get.host: /ssh/host/${req.params.host} route`)
const envVars = parseEnvVars(req.query.env)
if (envVars) {
req.session.envVars = envVars
debug("routes: Parsed environment variables: %O", envVars)
}
try { try {
const host = getValidatedHost(req.params.host) const host = getValidatedHost(req.params.host)

View file

@ -206,14 +206,20 @@ class WebSSH2Socket extends EventEmitter {
* Creates a new SSH shell session. * Creates a new SSH shell session.
*/ */
createShell() { createShell() {
// Get envVars from socket session if they exist
const envVars = this.socket.handshake.session.envVars || null
this.ssh this.ssh
.shell({ .shell(
term: this.sessionState.term, {
cols: this.sessionState.cols, term: this.sessionState.term,
rows: this.sessionState.rows cols: this.sessionState.cols,
}) rows: this.sessionState.rows
.then(stream => { },
stream.on("data", data => { envVars
)
.then((stream) => {
stream.on("data", (data) => {
this.socket.emit("data", data.toString("utf-8")) this.socket.emit("data", data.toString("utf-8"))
}) })
// stream.stderr.on("data", data => debug(`STDERR: ${data}`)) // needed for shell.exec // stream.stderr.on("data", data => debug(`STDERR: ${data}`)) // needed for shell.exec

View file

@ -190,12 +190,17 @@ class SSHConnection extends EventEmitter {
/** /**
* Opens an interactive shell session over the SSH connection. * Opens an interactive shell session over the SSH connection.
* @param {Object} [options] - Optional parameters for the shell. * @param {Object} options - Options for the shell
* @returns {Promise<Object>} - A promise that resolves with the SSH shell stream. * @param {Object} [envVars] - Environment variables to set
* @returns {Promise<Object>} - A promise that resolves with the SSH shell stream
*/ */
shell(options) { shell(options, envVars) {
const shellOptions = Object.assign({}, options, {
env: this.getEnvironment(envVars)
})
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.conn.shell(options, (err, stream) => { this.conn.shell(shellOptions, (err, stream) => {
if (err) { if (err) {
reject(err) reject(err)
} else { } else {
@ -230,6 +235,25 @@ class SSHConnection extends EventEmitter {
this.conn = null this.conn = null
} }
} }
/**
* Gets the environment variables for the SSH session
* @param {Object} envVars - Environment variables from URL
* @returns {Object} - Combined environment variables
*/
getEnvironment(envVars) {
const env = {
TERM: this.config.ssh.term
}
if (envVars) {
Object.keys(envVars).forEach((key) => {
env[key] = envVars[key]
})
}
return env
}
} }
module.exports = SSHConnection module.exports = SSHConnection

View file

@ -198,13 +198,64 @@ function maskSensitiveData(obj, options) {
return maskedObject return maskedObject
} }
/**
* Validates and sanitizes environment variable key names
* @param {string} key - The environment variable key to validate
* @returns {boolean} - Whether the key is valid
*/
function isValidEnvKey(key) {
// Only allow uppercase letters, numbers, and underscore
return /^[A-Z][A-Z0-9_]*$/.test(key)
}
/**
* Validates and sanitizes environment variable values
* @param {string} value - The environment variable value to validate
* @returns {boolean} - Whether the value is valid
*/
function isValidEnvValue(value) {
// Disallow special characters that could be used for command injection
return !/[;&|`$]/.test(value)
}
/**
* Parses and validates environment variables from URL query string
* @param {string} envString - The environment string from URL query
* @returns {Object|null} - Object containing validated env vars or null if invalid
*/
function parseEnvVars(envString) {
if (!envString) return null
const envVars = {}
const pairs = envString.split(",")
for (let i = 0; i < pairs.length; i += 1) {
const pair = pairs[i].split(":")
if (pair.length !== 2) continue
const key = pair[0].trim()
const value = pair[1].trim()
if (isValidEnvKey(key) && isValidEnvValue(value)) {
envVars[key] = value
} else {
debug(`parseEnvVars: Invalid env var pair: ${key}:${value}`)
}
}
return Object.keys(envVars).length > 0 ? envVars : null
}
module.exports = { module.exports = {
deepMerge, deepMerge,
getValidatedHost, getValidatedHost,
getValidatedPort, getValidatedPort,
isValidCredentials, isValidCredentials,
isValidEnvKey,
isValidEnvValue,
maskSensitiveData, maskSensitiveData,
modifyHtml, modifyHtml,
parseEnvVars,
validateConfig, validateConfig,
validateSshTerm validateSshTerm
} }