feat: Allow setting environment variables from the URL #371
This commit is contained in:
parent
e77b7ac506
commit
6ec049059b
5 changed files with 177 additions and 12 deletions
73
README.md
73
README.md
|
@ -295,6 +295,79 @@ If key authentication fails, check:
|
|||
|
||||
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
|
||||
|
||||
WebSSH2 provides two main routes:
|
||||
|
|
|
@ -14,6 +14,7 @@ const { createNamespacedDebug } = require("./logger")
|
|||
const { createAuthMiddleware } = require("./middleware")
|
||||
const { ConfigError, handleError } = require("./errors")
|
||||
const { HTTP } = require("./constants")
|
||||
const { parseEnvVars } = require("./utils")
|
||||
|
||||
const debug = createNamespacedDebug("routes")
|
||||
|
||||
|
@ -43,6 +44,11 @@ module.exports = function(config) {
|
|||
*/
|
||||
router.get("/host/", auth, (req, res) => {
|
||||
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 {
|
||||
if (!config.ssh.host) {
|
||||
|
@ -76,8 +82,13 @@ module.exports = function(config) {
|
|||
})
|
||||
|
||||
// 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`)
|
||||
const envVars = parseEnvVars(req.query.env)
|
||||
if (envVars) {
|
||||
req.session.envVars = envVars
|
||||
debug("routes: Parsed environment variables: %O", envVars)
|
||||
}
|
||||
|
||||
try {
|
||||
const host = getValidatedHost(req.params.host)
|
||||
|
|
|
@ -206,14 +206,20 @@ class WebSSH2Socket extends EventEmitter {
|
|||
* Creates a new SSH shell session.
|
||||
*/
|
||||
createShell() {
|
||||
// Get envVars from socket session if they exist
|
||||
const envVars = this.socket.handshake.session.envVars || null
|
||||
|
||||
this.ssh
|
||||
.shell({
|
||||
term: this.sessionState.term,
|
||||
cols: this.sessionState.cols,
|
||||
rows: this.sessionState.rows
|
||||
})
|
||||
.then(stream => {
|
||||
stream.on("data", data => {
|
||||
.shell(
|
||||
{
|
||||
term: this.sessionState.term,
|
||||
cols: this.sessionState.cols,
|
||||
rows: this.sessionState.rows
|
||||
},
|
||||
envVars
|
||||
)
|
||||
.then((stream) => {
|
||||
stream.on("data", (data) => {
|
||||
this.socket.emit("data", data.toString("utf-8"))
|
||||
})
|
||||
// stream.stderr.on("data", data => debug(`STDERR: ${data}`)) // needed for shell.exec
|
||||
|
|
32
app/ssh.js
32
app/ssh.js
|
@ -190,12 +190,17 @@ class SSHConnection extends EventEmitter {
|
|||
|
||||
/**
|
||||
* Opens an interactive shell session over the SSH connection.
|
||||
* @param {Object} [options] - Optional parameters for the shell.
|
||||
* @returns {Promise<Object>} - A promise that resolves with the SSH shell stream.
|
||||
* @param {Object} options - Options for the shell
|
||||
* @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) => {
|
||||
this.conn.shell(options, (err, stream) => {
|
||||
this.conn.shell(shellOptions, (err, stream) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
|
@ -230,6 +235,25 @@ class SSHConnection extends EventEmitter {
|
|||
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
|
||||
|
|
51
app/utils.js
51
app/utils.js
|
@ -198,13 +198,64 @@ function maskSensitiveData(obj, options) {
|
|||
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 = {
|
||||
deepMerge,
|
||||
getValidatedHost,
|
||||
getValidatedPort,
|
||||
isValidCredentials,
|
||||
isValidEnvKey,
|
||||
isValidEnvValue,
|
||||
maskSensitiveData,
|
||||
modifyHtml,
|
||||
parseEnvVars,
|
||||
validateConfig,
|
||||
validateSshTerm
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue