parent
8608ae2f08
commit
d0d5461a67
@ -31,8 +31,6 @@ export interface Args extends VsArgs {
|
|||||||
readonly open?: boolean
|
readonly open?: boolean
|
||||||
readonly port?: number
|
readonly port?: number
|
||||||
readonly socket?: string
|
readonly socket?: string
|
||||||
readonly "ssh-host-key"?: string
|
|
||||||
readonly "disable-ssh"?: boolean
|
|
||||||
readonly version?: boolean
|
readonly version?: boolean
|
||||||
readonly force?: boolean
|
readonly force?: boolean
|
||||||
readonly "list-extensions"?: boolean
|
readonly "list-extensions"?: boolean
|
||||||
@ -99,9 +97,6 @@ const options: Options<Required<Args>> = {
|
|||||||
version: { type: "boolean", short: "v", description: "Display version information." },
|
version: { type: "boolean", short: "v", description: "Display version information." },
|
||||||
_: { type: "string[]" },
|
_: { type: "string[]" },
|
||||||
|
|
||||||
"disable-ssh": { type: "boolean", description: "Disable the SSH server." },
|
|
||||||
"ssh-host-key": { type: "string", path: true, description: "SSH server host key." },
|
|
||||||
|
|
||||||
"user-data-dir": { type: "string", path: true, description: "Path to the user data directory." },
|
"user-data-dir": { type: "string", path: true, description: "Path to the user data directory." },
|
||||||
"extensions-dir": { type: "string", path: true, description: "Path to the extensions directory." },
|
"extensions-dir": { type: "string", path: true, description: "Path to the extensions directory." },
|
||||||
"builtin-extensions-dir": { type: "string", path: true },
|
"builtin-extensions-dir": { type: "string", path: true },
|
||||||
|
@ -11,8 +11,7 @@ import { UpdateHttpProvider } from "./app/update"
|
|||||||
import { VscodeHttpProvider } from "./app/vscode"
|
import { VscodeHttpProvider } from "./app/vscode"
|
||||||
import { Args, optionDescriptions, parse } from "./cli"
|
import { Args, optionDescriptions, parse } from "./cli"
|
||||||
import { AuthType, HttpServer, HttpServerOptions } from "./http"
|
import { AuthType, HttpServer, HttpServerOptions } from "./http"
|
||||||
import { SshProvider } from "./ssh/server"
|
import { generateCertificate, generatePassword, hash, open } from "./util"
|
||||||
import { generateCertificate, generatePassword, generateSshHostKey, hash, open } from "./util"
|
|
||||||
import { ipcMain, wrap } from "./wrapper"
|
import { ipcMain, wrap } from "./wrapper"
|
||||||
|
|
||||||
process.on("uncaughtException", (error) => {
|
process.on("uncaughtException", (error) => {
|
||||||
@ -101,32 +100,6 @@ const main = async (args: Args): Promise<void> => {
|
|||||||
|
|
||||||
logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`)
|
logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`)
|
||||||
|
|
||||||
let sshHostKey = args["ssh-host-key"]
|
|
||||||
if (!args["disable-ssh"] && !sshHostKey) {
|
|
||||||
try {
|
|
||||||
sshHostKey = await generateSshHostKey()
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Unable to start SSH server", field("error", error.message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let sshPort: number | undefined
|
|
||||||
if (!args["disable-ssh"] && sshHostKey) {
|
|
||||||
const sshProvider = httpServer.registerHttpProvider("/ssh", SshProvider, sshHostKey)
|
|
||||||
try {
|
|
||||||
sshPort = await sshProvider.listen()
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(`SSH server: ${error.message}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof sshPort !== "undefined") {
|
|
||||||
logger.info(`SSH server listening on localhost:${sshPort}`)
|
|
||||||
logger.info(" - To disable use `--disable-ssh`")
|
|
||||||
} else {
|
|
||||||
logger.info("SSH server disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serverAddress && !options.socket && args.open) {
|
if (serverAddress && !options.socket && args.open) {
|
||||||
// The web socket doesn't seem to work if browsing with 0.0.0.0.
|
// The web socket doesn't seem to work if browsing with 0.0.0.0.
|
||||||
const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost")
|
const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost")
|
||||||
|
@ -1,110 +0,0 @@
|
|||||||
import * as http from "http"
|
|
||||||
import * as net from "net"
|
|
||||||
import * as ssh from "ssh2"
|
|
||||||
import * as ws from "ws"
|
|
||||||
import * as fs from "fs"
|
|
||||||
import { logger } from "@coder/logger"
|
|
||||||
import safeCompare from "safe-compare"
|
|
||||||
import { HttpProvider, HttpResponse, HttpProviderOptions, Route } from "../http"
|
|
||||||
import { HttpCode } from "../../common/http"
|
|
||||||
import { forwardSshPort, fillSshSession } from "./ssh"
|
|
||||||
import { hash } from "../util"
|
|
||||||
|
|
||||||
export class SshProvider extends HttpProvider {
|
|
||||||
private readonly wss = new ws.Server({ noServer: true })
|
|
||||||
private sshServer: ssh.Server
|
|
||||||
|
|
||||||
public constructor(options: HttpProviderOptions, hostKeyPath: string) {
|
|
||||||
super(options)
|
|
||||||
const hostKey = fs.readFileSync(hostKeyPath)
|
|
||||||
this.sshServer = new ssh.Server({ hostKeys: [hostKey] }, this.handleSsh)
|
|
||||||
|
|
||||||
this.sshServer.on("error", (err) => {
|
|
||||||
logger.trace(`SSH server error: ${err.stack}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public async listen(): Promise<number> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.sshServer.once("error", reject)
|
|
||||||
this.sshServer.listen(() => {
|
|
||||||
resolve(this.sshServer.address().port)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleRequest(): Promise<HttpResponse> {
|
|
||||||
// SSH has no HTTP endpoints
|
|
||||||
return { code: HttpCode.NotFound }
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleWebSocket(
|
|
||||||
_route: Route,
|
|
||||||
request: http.IncomingMessage,
|
|
||||||
socket: net.Socket,
|
|
||||||
head: Buffer,
|
|
||||||
): Promise<void> {
|
|
||||||
// Create a fake websocket to the sshServer
|
|
||||||
const sshSocket = net.connect(this.sshServer.address().port, "localhost")
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
||||||
// Send SSH data to WS as compressed binary
|
|
||||||
sshSocket.on("data", (data) => {
|
|
||||||
ws.send(data, {
|
|
||||||
binary: true,
|
|
||||||
compress: true,
|
|
||||||
fin: true,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Send WS data to SSH as buffer
|
|
||||||
ws.on("message", (msg) => {
|
|
||||||
// Buffer.from is cool with all types, but casting as string keeps typing simple
|
|
||||||
sshSocket.write(Buffer.from(msg as string))
|
|
||||||
})
|
|
||||||
|
|
||||||
ws.on("error", (err) => {
|
|
||||||
logger.error(`SSH websocket error: ${err.stack}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine how to handle incoming SSH connections.
|
|
||||||
*/
|
|
||||||
private handleSsh = (client: ssh.Connection, info: ssh.ClientInfo): void => {
|
|
||||||
logger.debug(`Incoming SSH connection from ${info.ip}`)
|
|
||||||
client.on("authentication", (ctx) => {
|
|
||||||
// Allow any auth to go through if we have no password
|
|
||||||
if (!this.options.password) {
|
|
||||||
return ctx.accept()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise require the same password as code-server
|
|
||||||
if (ctx.method === "password") {
|
|
||||||
if (
|
|
||||||
safeCompare(this.options.password, hash(ctx.password)) ||
|
|
||||||
safeCompare(this.options.password, ctx.password)
|
|
||||||
) {
|
|
||||||
return ctx.accept()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject, letting them know that password is the only method we allow
|
|
||||||
ctx.reject(["password"])
|
|
||||||
})
|
|
||||||
client.on("tcpip", forwardSshPort)
|
|
||||||
client.on("session", fillSshSession)
|
|
||||||
client.on("error", (err) => {
|
|
||||||
// Don't bother logging Keepalive errors, they probably just disconnected
|
|
||||||
if (err.message === "Keepalive timeout") {
|
|
||||||
return logger.debug("SSH client keepalive timeout")
|
|
||||||
}
|
|
||||||
logger.error(`SSH client error: ${err.stack}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,201 +0,0 @@
|
|||||||
/**
|
|
||||||
* Provides utilities for handling SSH connections
|
|
||||||
*/
|
|
||||||
import * as fs from "fs"
|
|
||||||
import * as path from "path"
|
|
||||||
import * as ssh from "ssh2"
|
|
||||||
import { FileEntry, SFTPStream } from "ssh2-streams"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fills out all the functionality of SFTP using fs.
|
|
||||||
*/
|
|
||||||
export function fillSftpStream(accept: () => SFTPStream): void {
|
|
||||||
const sftp = accept()
|
|
||||||
|
|
||||||
let oid = 0
|
|
||||||
const fds: { [key: number]: boolean } = {}
|
|
||||||
const ods: {
|
|
||||||
[key: number]: {
|
|
||||||
path: string
|
|
||||||
read: boolean
|
|
||||||
}
|
|
||||||
} = {}
|
|
||||||
|
|
||||||
const sftpStatus = (reqID: number, err?: NodeJS.ErrnoException | null): boolean => {
|
|
||||||
let code = ssh.SFTP_STATUS_CODE.OK
|
|
||||||
if (err) {
|
|
||||||
if (err.code === "EACCES") {
|
|
||||||
code = ssh.SFTP_STATUS_CODE.PERMISSION_DENIED
|
|
||||||
} else if (err.code === "ENOENT") {
|
|
||||||
code = ssh.SFTP_STATUS_CODE.NO_SUCH_FILE
|
|
||||||
} else {
|
|
||||||
code = ssh.SFTP_STATUS_CODE.FAILURE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sftp.status(reqID, code)
|
|
||||||
}
|
|
||||||
|
|
||||||
sftp.on("OPEN", (reqID, filename) => {
|
|
||||||
fs.open(filename, "w", (err, fd) => {
|
|
||||||
if (err) {
|
|
||||||
return sftpStatus(reqID, err)
|
|
||||||
}
|
|
||||||
fds[fd] = true
|
|
||||||
const buf = Buffer.alloc(4)
|
|
||||||
buf.writeUInt32BE(fd, 0)
|
|
||||||
return sftp.handle(reqID, buf)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("OPENDIR", (reqID, path) => {
|
|
||||||
const buf = Buffer.alloc(4)
|
|
||||||
const id = oid++
|
|
||||||
buf.writeUInt32BE(id, 0)
|
|
||||||
ods[id] = {
|
|
||||||
path,
|
|
||||||
read: false,
|
|
||||||
}
|
|
||||||
sftp.handle(reqID, buf)
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("READDIR", (reqID, handle) => {
|
|
||||||
const od = handle.readUInt32BE(0)
|
|
||||||
if (!ods[od]) {
|
|
||||||
return sftp.status(reqID, ssh.SFTP_STATUS_CODE.NO_SUCH_FILE)
|
|
||||||
}
|
|
||||||
if (ods[od].read) {
|
|
||||||
sftp.status(reqID, ssh.SFTP_STATUS_CODE.EOF)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return fs.readdir(ods[od].path, (err, files) => {
|
|
||||||
if (err) {
|
|
||||||
return sftpStatus(reqID, err)
|
|
||||||
}
|
|
||||||
return Promise.all(
|
|
||||||
files.map((f) => {
|
|
||||||
return new Promise<FileEntry>((resolve, reject) => {
|
|
||||||
const fullPath = path.join(ods[od].path, f)
|
|
||||||
fs.stat(fullPath, (err, stats) => {
|
|
||||||
if (err) {
|
|
||||||
return reject(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
filename: f,
|
|
||||||
longname: fullPath,
|
|
||||||
attrs: {
|
|
||||||
atime: stats.atimeMs,
|
|
||||||
gid: stats.gid,
|
|
||||||
mode: stats.mode,
|
|
||||||
size: stats.size,
|
|
||||||
mtime: stats.mtimeMs,
|
|
||||||
uid: stats.uid,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.then((files) => {
|
|
||||||
sftp.name(reqID, files)
|
|
||||||
ods[od].read = true
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
sftp.status(reqID, ssh.SFTP_STATUS_CODE.FAILURE)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("WRITE", (reqID, handle, offset, data) => {
|
|
||||||
const fd = handle.readUInt32BE(0)
|
|
||||||
if (!fds[fd]) {
|
|
||||||
return sftp.status(reqID, ssh.SFTP_STATUS_CODE.NO_SUCH_FILE)
|
|
||||||
}
|
|
||||||
return fs.write(fd, data, offset, (err) => sftpStatus(reqID, err))
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("CLOSE", (reqID, handle) => {
|
|
||||||
const fd = handle.readUInt32BE(0)
|
|
||||||
if (!fds[fd]) {
|
|
||||||
if (ods[fd]) {
|
|
||||||
delete ods[fd]
|
|
||||||
return sftp.status(reqID, ssh.SFTP_STATUS_CODE.OK)
|
|
||||||
}
|
|
||||||
return sftp.status(reqID, ssh.SFTP_STATUS_CODE.NO_SUCH_FILE)
|
|
||||||
}
|
|
||||||
return fs.close(fd, (err) => sftpStatus(reqID, err))
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("STAT", (reqID, path) => {
|
|
||||||
fs.stat(path, (err, stats) => {
|
|
||||||
if (err) {
|
|
||||||
return sftpStatus(reqID, err)
|
|
||||||
}
|
|
||||||
return sftp.attrs(reqID, {
|
|
||||||
atime: stats.atime.getTime(),
|
|
||||||
gid: stats.gid,
|
|
||||||
mode: stats.mode,
|
|
||||||
mtime: stats.mtime.getTime(),
|
|
||||||
size: stats.size,
|
|
||||||
uid: stats.uid,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("MKDIR", (reqID, path) => {
|
|
||||||
fs.mkdir(path, (err) => sftpStatus(reqID, err))
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("LSTAT", (reqID, path) => {
|
|
||||||
fs.lstat(path, (err, stats) => {
|
|
||||||
if (err) {
|
|
||||||
return sftpStatus(reqID, err)
|
|
||||||
}
|
|
||||||
return sftp.attrs(reqID, {
|
|
||||||
atime: stats.atimeMs,
|
|
||||||
gid: stats.gid,
|
|
||||||
mode: stats.mode,
|
|
||||||
mtime: stats.mtimeMs,
|
|
||||||
size: stats.size,
|
|
||||||
uid: stats.uid,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("REMOVE", (reqID, path) => {
|
|
||||||
fs.unlink(path, (err) => sftpStatus(reqID, err))
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("RMDIR", (reqID, path) => {
|
|
||||||
fs.rmdir(path, (err) => sftpStatus(reqID, err))
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("REALPATH", (reqID, path) => {
|
|
||||||
fs.realpath(path, (pathErr, resolved) => {
|
|
||||||
if (pathErr) {
|
|
||||||
return sftpStatus(reqID, pathErr)
|
|
||||||
}
|
|
||||||
fs.stat(path, (statErr, stat) => {
|
|
||||||
if (statErr) {
|
|
||||||
return sftpStatus(reqID, statErr)
|
|
||||||
}
|
|
||||||
sftp.name(reqID, [
|
|
||||||
{
|
|
||||||
filename: resolved,
|
|
||||||
longname: resolved,
|
|
||||||
attrs: {
|
|
||||||
mode: stat.mode,
|
|
||||||
uid: stat.uid,
|
|
||||||
gid: stat.gid,
|
|
||||||
size: stat.size,
|
|
||||||
atime: stat.atime.getTime(),
|
|
||||||
mtime: stat.mtime.getTime(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
return
|
|
||||||
})
|
|
||||||
return
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,122 +0,0 @@
|
|||||||
/**
|
|
||||||
* Provides utilities for handling SSH connections
|
|
||||||
*/
|
|
||||||
import * as net from "net"
|
|
||||||
import * as cp from "child_process"
|
|
||||||
import * as ssh from "ssh2"
|
|
||||||
import * as nodePty from "node-pty"
|
|
||||||
import { fillSftpStream } from "./sftp"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fills out all of the functionality of SSH using node equivalents.
|
|
||||||
*/
|
|
||||||
export function fillSshSession(accept: () => ssh.Session): void {
|
|
||||||
let pty: nodePty.IPty | undefined
|
|
||||||
let activeProcess: cp.ChildProcess
|
|
||||||
let ptyInfo: ssh.PseudoTtyInfo | undefined
|
|
||||||
const env: { [key: string]: string } = {}
|
|
||||||
|
|
||||||
const session = accept()
|
|
||||||
|
|
||||||
// Run a command, stream back the data
|
|
||||||
const cmd = (command: string, channel: ssh.ServerChannel): void => {
|
|
||||||
if (ptyInfo) {
|
|
||||||
// Remove undefined and project env vars
|
|
||||||
// keysToRemove taken from sanitizeProcessEnvironment
|
|
||||||
const keysToRemove = [/^ELECTRON_.+$/, /^GOOGLE_API_KEY$/, /^VSCODE_.+$/, /^SNAP(|_.*)$/]
|
|
||||||
const env = Object.keys(process.env).reduce((prev, k) => {
|
|
||||||
if (process.env[k] === undefined) {
|
|
||||||
return prev
|
|
||||||
}
|
|
||||||
const val = process.env[k] as string
|
|
||||||
if (keysToRemove.find((rx) => val.search(rx))) {
|
|
||||||
return prev
|
|
||||||
}
|
|
||||||
prev[k] = val
|
|
||||||
return prev
|
|
||||||
}, {} as { [key: string]: string })
|
|
||||||
|
|
||||||
pty = nodePty.spawn(command, [], {
|
|
||||||
cols: ptyInfo.cols,
|
|
||||||
rows: ptyInfo.rows,
|
|
||||||
env,
|
|
||||||
})
|
|
||||||
pty.onData((d) => channel.write(d))
|
|
||||||
pty.on("exit", (exitCode) => {
|
|
||||||
channel.exit(exitCode)
|
|
||||||
channel.close()
|
|
||||||
})
|
|
||||||
channel.on("data", (d: string) => pty && pty.write(d))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const proc = cp.spawn(command, { shell: true })
|
|
||||||
proc.stdout.on("data", (d) => channel.stdout.write(d))
|
|
||||||
proc.stderr.on("data", (d) => channel.stderr.write(d))
|
|
||||||
proc.on("exit", (exitCode) => {
|
|
||||||
channel.exit(exitCode || 0)
|
|
||||||
channel.close()
|
|
||||||
})
|
|
||||||
channel.stdin.on("data", (d: unknown) => proc.stdin.write(d))
|
|
||||||
channel.stdin.on("close", () => proc.stdin.end())
|
|
||||||
}
|
|
||||||
|
|
||||||
session.on("pty", (accept, _, info) => {
|
|
||||||
ptyInfo = info
|
|
||||||
accept && accept()
|
|
||||||
})
|
|
||||||
|
|
||||||
session.on("shell", (accept) => {
|
|
||||||
cmd(process.env.SHELL || "/usr/bin/env bash", accept())
|
|
||||||
})
|
|
||||||
|
|
||||||
session.on("exec", (accept, _, info) => {
|
|
||||||
cmd(info.command, accept())
|
|
||||||
})
|
|
||||||
|
|
||||||
session.on("sftp", fillSftpStream)
|
|
||||||
|
|
||||||
session.on("signal", (accept, _, info) => {
|
|
||||||
accept && accept()
|
|
||||||
process.kill((pty || activeProcess).pid, info.name)
|
|
||||||
})
|
|
||||||
|
|
||||||
session.on("env", (accept, _reject, info) => {
|
|
||||||
accept && accept()
|
|
||||||
env[info.key] = info.value
|
|
||||||
})
|
|
||||||
|
|
||||||
session.on("auth-agent", (accept) => {
|
|
||||||
accept()
|
|
||||||
})
|
|
||||||
|
|
||||||
session.on("window-change", (accept, reject, info) => {
|
|
||||||
if (pty) {
|
|
||||||
pty.resize(info.cols, info.rows)
|
|
||||||
accept && accept()
|
|
||||||
} else {
|
|
||||||
reject()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pipes a requested port over SSH
|
|
||||||
*/
|
|
||||||
export function forwardSshPort(
|
|
||||||
accept: () => ssh.ServerChannel,
|
|
||||||
reject: () => boolean,
|
|
||||||
info: ssh.TcpipRequestInfo,
|
|
||||||
): void {
|
|
||||||
const fwdSocket = net.createConnection(info.destPort, info.destIP)
|
|
||||||
fwdSocket.on("error", () => reject())
|
|
||||||
fwdSocket.on("connect", () => {
|
|
||||||
const channel = accept()
|
|
||||||
channel.pipe(fwdSocket)
|
|
||||||
channel.on("close", () => fwdSocket.end())
|
|
||||||
fwdSocket.pipe(channel)
|
|
||||||
fwdSocket.on("close", () => channel.close())
|
|
||||||
fwdSocket.on("error", () => channel.end())
|
|
||||||
fwdSocket.on("end", () => channel.end())
|
|
||||||
})
|
|
||||||
}
|
|
@ -44,12 +44,6 @@ export const generateCertificate = async (): Promise<{ cert: string; certKey: st
|
|||||||
return paths
|
return paths
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateSshHostKey = async (): Promise<string> => {
|
|
||||||
// Just reuse the SSL cert as the SSH host key
|
|
||||||
const { certKey } = await generateCertificate()
|
|
||||||
return certKey
|
|
||||||
}
|
|
||||||
|
|
||||||
export const generatePassword = async (length = 24): Promise<string> => {
|
export const generatePassword = async (length = 24): Promise<string> => {
|
||||||
const buffer = Buffer.alloc(Math.ceil(length / 2))
|
const buffer = Buffer.alloc(Math.ceil(length / 2))
|
||||||
await util.promisify(crypto.randomFill)(buffer)
|
await util.promisify(crypto.randomFill)(buffer)
|
||||||
|
@ -117,7 +117,6 @@ describe("cli", () => {
|
|||||||
assert.throws(() => parse(["--auth=", "--log=debug"]), /--auth requires a value/)
|
assert.throws(() => parse(["--auth=", "--log=debug"]), /--auth requires a value/)
|
||||||
assert.throws(() => parse(["--auth", "--log"]), /--auth requires a value/)
|
assert.throws(() => parse(["--auth", "--log"]), /--auth requires a value/)
|
||||||
assert.throws(() => parse(["--auth", "--invalid"]), /--auth requires a value/)
|
assert.throws(() => parse(["--auth", "--invalid"]), /--auth requires a value/)
|
||||||
assert.throws(() => parse(["--ssh-host-key"]), /--ssh-host-key requires a value/)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should error if value is invalid", () => {
|
it("should error if value is invalid", () => {
|
||||||
|
Reference in New Issue
Block a user