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.
|
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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
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.
|
* 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
|
||||||
|
|
51
app/utils.js
51
app/utils.js
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue