385 lines
10 KiB
JavaScript
385 lines
10 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.autoLog - Auto log enabled
|
|
* @property {boolean} options.allowReauth - Allow reauthentication
|
|
* @property {boolean} options.allowReconnect - Allow reconnection
|
|
* @property {boolean} options.allowReplay - Allow replay
|
|
* @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,
|
|
autoLog: false,
|
|
allowReauth: false,
|
|
allowReconnect: 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" },
|
|
autoLog: { type: "boolean" },
|
|
allowReauth: { type: "boolean" },
|
|
allowReconnect: { 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
|