Skip to content

Commit

Permalink
feat: Allow setting environment variables from the URL #371
Browse files Browse the repository at this point in the history
  • Loading branch information
billchurch committed Nov 30, 2024
1 parent e77b7ac commit 6ec0490
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 12 deletions.
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 12 additions & 1 deletion app/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
20 changes: 13 additions & 7 deletions app/socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 28 additions & 4 deletions app/ssh.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 changes: 51 additions & 0 deletions app/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit 6ec0490

Please sign in to comment.