feat: support uploading of ssh-rsa private key from client for authentication #381
This commit is contained in:
parent
c48fadba08
commit
2f4083ff4f
3 changed files with 32 additions and 45 deletions
|
@ -25,6 +25,8 @@ class WebSSH2Socket extends EventEmitter {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
username: null,
|
username: null,
|
||||||
password: null,
|
password: null,
|
||||||
|
privatekey: null,
|
||||||
|
keyPassword: null,
|
||||||
host: null,
|
host: null,
|
||||||
port: null,
|
port: null,
|
||||||
term: null,
|
term: null,
|
||||||
|
@ -110,9 +112,16 @@ class WebSSH2Socket extends EventEmitter {
|
||||||
debug(`handleAuthenticate: ${this.socket.id}, %O`, maskSensitiveData(creds))
|
debug(`handleAuthenticate: ${this.socket.id}, %O`, maskSensitiveData(creds))
|
||||||
|
|
||||||
if (isValidCredentials(creds)) {
|
if (isValidCredentials(creds)) {
|
||||||
|
// Set term if provided, otherwise use config default
|
||||||
this.sessionState.term = validateSshTerm(creds.term)
|
this.sessionState.term = validateSshTerm(creds.term)
|
||||||
? creds.term
|
? creds.term
|
||||||
: this.config.ssh.term
|
: this.config.ssh.term
|
||||||
|
|
||||||
|
// Map the client's privateKey field to our internal privatekey field if present
|
||||||
|
if (creds.privateKey) {
|
||||||
|
creds.privatekey = creds.privateKey
|
||||||
|
}
|
||||||
|
|
||||||
this.initializeConnection(creds)
|
this.initializeConnection(creds)
|
||||||
} else {
|
} else {
|
||||||
debug(`handleAuthenticate: ${this.socket.id}, CREDENTIALS INVALID`)
|
debug(`handleAuthenticate: ${this.socket.id}, CREDENTIALS INVALID`)
|
||||||
|
@ -129,9 +138,8 @@ class WebSSH2Socket extends EventEmitter {
|
||||||
maskSensitiveData(creds)
|
maskSensitiveData(creds)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Add private key from config if available
|
// Add private key from config if available and not provided in creds
|
||||||
if (this.config.user.privatekey && !creds.privatekey) {
|
if (this.config.user.privatekey && !creds.privatekey) {
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
creds.privatekey = this.config.user.privatekey
|
creds.privatekey = this.config.user.privatekey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,6 +151,7 @@ class WebSSH2Socket extends EventEmitter {
|
||||||
username: creds.username,
|
username: creds.username,
|
||||||
password: creds.password,
|
password: creds.password,
|
||||||
privatekey: creds.privatekey,
|
privatekey: creds.privatekey,
|
||||||
|
keyPassword: creds.keyPassword,
|
||||||
host: creds.host,
|
host: creds.host,
|
||||||
port: creds.port
|
port: creds.port
|
||||||
})
|
})
|
||||||
|
@ -170,21 +179,16 @@ class WebSSH2Socket extends EventEmitter {
|
||||||
debug(
|
debug(
|
||||||
`initializeConnection: SSH CONNECTION ERROR: ${this.socket.id}, Host: ${creds.host}, Error: ${err.message}`
|
`initializeConnection: SSH CONNECTION ERROR: ${this.socket.id}, Host: ${creds.host}, Error: ${err.message}`
|
||||||
)
|
)
|
||||||
handleError(new SSHConnectionError(`${err.message}`))
|
const errorMessage =
|
||||||
this.socket.emit("ssherror", `${err.message}`)
|
err instanceof SSHConnectionError
|
||||||
|
? err.message
|
||||||
|
: "SSH connection failed"
|
||||||
|
this.socket.emit("authentication", {
|
||||||
|
action: "auth_result",
|
||||||
|
success: false,
|
||||||
|
message: errorMessage
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set up password prompt handler
|
|
||||||
this.ssh.on("password-prompt", (data) => {
|
|
||||||
this.socket.emit("authentication", {
|
|
||||||
action: "password_prompt",
|
|
||||||
message: `Key authentication failed. Please enter password for ${data.username}@${data.host}`
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.socket.on("password_response", (password) => {
|
|
||||||
this.ssh.emit("password-response", password)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
29
app/ssh.js
29
app/ssh.js
|
@ -30,24 +30,19 @@ class SSHConnection extends EventEmitter {
|
||||||
* @returns {boolean} - Whether the key appears to be valid
|
* @returns {boolean} - Whether the key appears to be valid
|
||||||
*/
|
*/
|
||||||
validatePrivateKey(key) {
|
validatePrivateKey(key) {
|
||||||
const keyStart = "-----BEGIN RSA PRIVATE KEY-----"
|
const keyPattern =
|
||||||
const keyEnd = "-----END RSA PRIVATE KEY-----"
|
/^-----BEGIN (?:RSA )?PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END (?:RSA )?PRIVATE KEY-----\r?\n?$/
|
||||||
return (
|
return keyPattern.test(key)
|
||||||
typeof key === "string" &&
|
|
||||||
key.includes(keyStart) &&
|
|
||||||
key.includes(keyEnd) &&
|
|
||||||
key.trim().length > keyStart.length + keyEnd.length
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connects to the SSH server using the provided credentials.
|
* Attempts to connect using the provided credentials
|
||||||
* @param {Object} creds - The credentials object containing host, port, username, and optional password.
|
* @param {Object} creds - The credentials object
|
||||||
* @returns {Promise<SSH>} - A promise that resolves with the SSH connection instance.
|
* @returns {Promise<Object>} - A promise that resolves with the SSH connection
|
||||||
*/
|
*/
|
||||||
connect(creds) {
|
connect(creds) {
|
||||||
this.creds = creds
|
|
||||||
debug("connect: %O", maskSensitiveData(creds))
|
debug("connect: %O", maskSensitiveData(creds))
|
||||||
|
this.creds = creds
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (this.conn) {
|
if (this.conn) {
|
||||||
this.conn.end()
|
this.conn.end()
|
||||||
|
@ -58,6 +53,7 @@ class SSHConnection extends EventEmitter {
|
||||||
|
|
||||||
// First try with key authentication if available
|
// First try with key authentication if available
|
||||||
const sshConfig = this.getSSHConfig(creds, true)
|
const sshConfig = this.getSSHConfig(creds, true)
|
||||||
|
debug("Initial connection config: %O", maskSensitiveData(sshConfig))
|
||||||
|
|
||||||
this.setupConnectionHandlers(resolve, reject)
|
this.setupConnectionHandlers(resolve, reject)
|
||||||
|
|
||||||
|
@ -80,14 +76,6 @@ class SSHConnection extends EventEmitter {
|
||||||
resolve(this.conn)
|
resolve(this.conn)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.conn.on("end", () => {
|
|
||||||
debug("connect: end")
|
|
||||||
})
|
|
||||||
|
|
||||||
this.conn.on("close", () => {
|
|
||||||
debug("connect: close")
|
|
||||||
})
|
|
||||||
|
|
||||||
this.conn.on("error", (err) => {
|
this.conn.on("error", (err) => {
|
||||||
debug(`connect: error: ${err.message}`)
|
debug(`connect: error: ${err.message}`)
|
||||||
|
|
||||||
|
@ -114,7 +102,6 @@ class SSHConnection extends EventEmitter {
|
||||||
this.setupConnectionHandlers(resolve, reject)
|
this.setupConnectionHandlers(resolve, reject)
|
||||||
this.conn.connect(passwordConfig)
|
this.conn.connect(passwordConfig)
|
||||||
} else {
|
} else {
|
||||||
// No password available, emit event to request password
|
|
||||||
debug("No password available, requesting password from client")
|
debug("No password available, requesting password from client")
|
||||||
this.emit("password-prompt", {
|
this.emit("password-prompt", {
|
||||||
host: this.creds.host,
|
host: this.creds.host,
|
||||||
|
|
12
app/utils.js
12
app/utils.js
|
@ -87,14 +87,9 @@ function getValidatedPort(portInput) {
|
||||||
* - port (number)
|
* - port (number)
|
||||||
* AND either:
|
* AND either:
|
||||||
* - password (string) OR
|
* - password (string) OR
|
||||||
* - privatekey (string)
|
* - privatekey/privateKey (string)
|
||||||
*
|
*
|
||||||
* @param {Object} creds - The credentials object.
|
* @param {Object} creds - The credentials object.
|
||||||
* @param {string} creds.username - The username.
|
|
||||||
* @param {string} [creds.password] - The password.
|
|
||||||
* @param {string} [creds.privatekey] - The private key.
|
|
||||||
* @param {string} creds.host - The host.
|
|
||||||
* @param {number} creds.port - The port.
|
|
||||||
* @returns {boolean} - Returns true if the credentials are valid, otherwise false.
|
* @returns {boolean} - Returns true if the credentials are valid, otherwise false.
|
||||||
*/
|
*/
|
||||||
function isValidCredentials(creds) {
|
function isValidCredentials(creds) {
|
||||||
|
@ -109,9 +104,10 @@ function isValidCredentials(creds) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must have either password or privatekey
|
// Must have either password or privatekey/privateKey
|
||||||
const hasPassword = typeof creds.password === "string"
|
const hasPassword = typeof creds.password === "string"
|
||||||
const hasPrivateKey = typeof creds.privatekey === "string"
|
const hasPrivateKey =
|
||||||
|
typeof creds.privatekey === "string" || typeof creds.privateKey === "string"
|
||||||
|
|
||||||
return hasPassword || hasPrivateKey
|
return hasPassword || hasPrivateKey
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue