webssh2/app/config.js

378 lines
9.7 KiB
JavaScript

// server
// app/config.js
"use strict"
const path = require("path")
const fs = require("fs")
const readConfig = require("read-config-ng")
const Ajv = require("ajv")
const crypto = require("crypto")
/**
* @typedef {Object} Config
* @property {Object} listen - Listening configuration
* @property {string} listen.ip - IP address to listen on
* @property {number} listen.port - Port to listen on
* @property {Object} http - HTTP configuration
* @property {string[]} http.origins - Allowed origins
* @property {Object} user - User configuration
* @property {string|null} user.name - Username
* @property {string|null} user.password - Password
* @property {Object} ssh - SSH configuration
* @property {string|null} ssh.host - SSH host
* @property {number} ssh.port - SSH port
* @property {string} ssh.term - Terminal type
* @property {number} ssh.readyTimeout - Ready timeout
* @property {number} ssh.keepaliveInterval - Keepalive interval
* @property {number} ssh.keepaliveCountMax - Max keepalive count
* @property {Object} terminal - Terminal configuration
* @property {boolean} terminal.cursorBlink - Whether cursor blinks
* @property {number} terminal.scrollback - Scrollback limit
* @property {number} terminal.tabStopWidth - Tab stop width
* @property {string} terminal.bellStyle - Bell style
* @property {Object} header - Header configuration
* @property {string|null} header.text - Header text
* @property {string} header.background - Header background color
* @property {Object} options - Options configuration
* @property {boolean} options.challengeButton - Challenge button enabled
* @property {boolean} options.allowReauth - Allow reauthentication
* @property {Object} algorithms - Encryption algorithms
* @property {string[]} algorithms.kex - Key exchange algorithms
* @property {string[]} algorithms.cipher - Cipher algorithms
* @property {string[]} algorithms.hmac - HMAC algorithms
* @property {string[]} algorithms.compress - Compression algorithms
* @property {Object} serverlog - Server log configuration
* @property {boolean} serverlog.client - Client logging enabled
* @property {boolean} serverlog.server - Server logging enabled
* @property {boolean} accesslog - Access logging enabled
* @property {boolean} verify - Verification enabled
*/
/**
* Default configuration
* @type {Config}
*/
const defaultConfig = {
listen: {
ip: "0.0.0.0",
port: 2222
},
http: {
origins: ["*:*"]
},
user: {
name: null,
password: null
},
ssh: {
host: null,
port: 22,
term: "vt100",
readyTimeout: 20000,
keepaliveInterval: 120000,
keepaliveCountMax: 10
},
terminal: {
cursorBlink: true,
scrollback: 10000,
tabStopWidth: 8,
bellStyle: "sound"
},
header: {
text: null,
background: "green"
},
options: {
challengeButton: true,
allowReauth: false,
allowReplay: false
},
algorithms: {
kex: [
"ecdh-sha2-nistp256",
"ecdh-sha2-nistp384",
"ecdh-sha2-nistp521",
"diffie-hellman-group-exchange-sha256",
"diffie-hellman-group14-sha1"
],
cipher: [
"aes128-ctr",
"aes192-ctr",
"aes256-ctr",
"aes128-gcm",
"aes128-gcm@openssh.com",
"aes256-gcm",
"aes256-gcm@openssh.com",
"aes256-cbc"
],
hmac: ["hmac-sha2-256", "hmac-sha2-512", "hmac-sha1"],
compress: ["none", "zlib@openssh.com", "zlib"]
},
session: {
secret: generateSecureSecret(),
name: "webssh2.sid"
},
serverlog: {
client: false,
server: false
},
accesslog: false,
verify: false
}
/**
* Schema for validating the config
*/
const configSchema = {
type: "object",
properties: {
listen: {
type: "object",
properties: {
ip: { type: "string", format: "ipv4" },
port: { type: "integer", minimum: 1, maximum: 65535 }
},
required: ["ip", "port"]
},
http: {
type: "object",
properties: {
origins: {
type: "array",
items: { type: "string" }
}
},
required: ["origins"]
},
user: {
type: "object",
properties: {
name: { type: ["string", "null"] },
password: { type: ["string", "null"] }
},
required: ["name", "password"]
},
ssh: {
type: "object",
properties: {
host: { type: ["string", "null"] },
port: { type: "integer", minimum: 1, maximum: 65535 },
term: { type: "string" },
readyTimeout: { type: "integer" },
keepaliveInterval: { type: "integer" },
keepaliveCountMax: { type: "integer" }
},
required: [
"host",
"port",
"term",
"readyTimeout",
"keepaliveInterval",
"keepaliveCountMax"
]
},
terminal: {
type: "object",
properties: {
cursorBlink: { type: "boolean" },
scrollback: { type: "integer" },
tabStopWidth: { type: "integer" },
bellStyle: { type: "string" }
},
required: ["cursorBlink", "scrollback", "tabStopWidth", "bellStyle"]
},
header: {
type: "object",
properties: {
text: { type: ["string", "null"] },
background: { type: "string" }
},
required: ["text", "background"]
},
options: {
type: "object",
properties: {
challengeButton: { type: "boolean" },
allowReauth: { type: "boolean" },
allowReplay: { type: "boolean" }
},
required: ["challengeButton", "allowReauth", "allowReplay"]
},
algorithms: {
type: "object",
properties: {
kex: {
type: "array",
items: { type: "string" }
},
cipher: {
type: "array",
items: { type: "string" }
},
hmac: {
type: "array",
items: { type: "string" }
},
compress: {
type: "array",
items: { type: "string" }
}
},
required: ["kex", "cipher", "hmac", "compress"]
},
session: {
type: "object",
properties: {
secret: { type: "string" },
name: { type: "string" }
},
required: ["secret", "name"]
},
serverlog: {
type: "object",
properties: {
client: { type: "boolean" },
server: { type: "boolean" }
},
required: ["client", "server"]
},
accesslog: { type: "boolean" },
verify: { type: "boolean" }
},
required: [
"listen",
"http",
"user",
"ssh",
"terminal",
"header",
"options",
"algorithms",
"serverlog",
"accesslog",
"verify"
]
}
/**
* Gets the path to the config file
* @returns {string} The path to the config file
*/
function getConfigPath() {
const nodeRoot = path.dirname(require.main.filename)
return path.join(nodeRoot, "config.json")
}
/**
* Reads the config file
* @param {string} configPath - The path to the config file
* @returns {Config} The configuration object
*/
function readConfigFile(configPath) {
console.log("WebSSH2 service reading config from: " + configPath)
return readConfig(configPath)
}
/**
* Validates the configuration against the schema
* @param {Object} config - The configuration object to validate
* @returns {Object} The validated configuration object
* @throws {Error} If the configuration is invalid
*/
function validateConfig(config) {
const ajv = new Ajv()
const validate = ajv.compile(configSchema)
const valid = validate(config)
console.log("WebSSH2 service validating config")
if (!valid) {
throw new Error(
"Config validation error: " + ajv.errorsText(validate.errors)
)
}
return config
}
/**
* Logs an error message
* @param {string} message - The error message
* @param {Error} [error] - The error object
*/
function logError(message, error) {
console.error(message)
if (error) {
console.error("ERROR:\n\n " + error)
}
}
/**
* Loads and merges the configuration
* @returns {Config} The merged configuration
*/
function loadConfig() {
const configPath = getConfigPath()
try {
if (fs.existsSync(configPath)) {
const providedConfig = readConfigFile(configPath)
// Deep merge the provided config with the default config
const mergedConfig = deepMerge(
JSON.parse(JSON.stringify(defaultConfig)),
providedConfig
)
const validatedConfig = validateConfig(mergedConfig)
console.log("Merged and validated configuration")
return validatedConfig
} else {
logError(
"\n\nERROR: Missing config.json for webssh. Using default config: " +
JSON.stringify(defaultConfig) +
"\n\n See config.json.sample for details\n\n"
)
return defaultConfig
}
} catch (err) {
logError(
"\n\nERROR: Problem loading config.json for webssh. Using default config: " +
JSON.stringify(defaultConfig) +
"\n\n See config.json.sample for details\n\n",
err
)
return defaultConfig
}
}
/**
* Generates a secure random session secret
* @returns {string} A random 32-byte hex string
*/
function generateSecureSecret() {
return crypto.randomBytes(32).toString("hex")
}
/**
* Deep merges two objects
* @param {Object} target - The target object to merge into
* @param {Object} source - The source object to merge from
* @returns {Object} The merged object
*/
function deepMerge(target, source) {
for (const key in source) {
if (source.hasOwnProperty(key)) {
if (source[key] instanceof Object && !Array.isArray(source[key])) {
target[key] = deepMerge(target[key] || {}, source[key])
} else {
target[key] = source[key]
}
}
}
return target
}
/**
* The loaded configuration
* @type {Config}
*/
const config = loadConfig()
module.exports = config