2021-06-04 01:37:46 +02:00
|
|
|
import { logger } from "@coder/logger"
|
|
|
|
import * as argon2 from "argon2"
|
2020-02-04 20:27:46 +01:00
|
|
|
import * as cp from "child_process"
|
|
|
|
import * as crypto from "crypto"
|
2020-08-04 22:51:39 +02:00
|
|
|
import envPaths from "env-paths"
|
2021-03-15 22:15:24 +01:00
|
|
|
import { promises as fs } from "fs"
|
2020-09-15 23:51:43 +02:00
|
|
|
import * as net from "net"
|
2020-02-04 20:27:46 +01:00
|
|
|
import * as os from "os"
|
|
|
|
import * as path from "path"
|
2021-06-04 01:37:46 +02:00
|
|
|
import safeCompare from "safe-compare"
|
2020-02-04 20:27:46 +01:00
|
|
|
import * as util from "util"
|
2020-05-10 07:35:42 +02:00
|
|
|
import xdgBasedir from "xdg-basedir"
|
2021-09-30 05:14:56 +02:00
|
|
|
import { vsRootPath } from "./constants"
|
2020-02-04 20:27:46 +01:00
|
|
|
|
2021-05-11 01:27:47 +02:00
|
|
|
export interface Paths {
|
2020-05-10 07:35:42 +02:00
|
|
|
data: string
|
|
|
|
config: string
|
2021-05-11 01:17:30 +02:00
|
|
|
runtime: string
|
2020-05-10 07:35:42 +02:00
|
|
|
}
|
|
|
|
|
2021-06-23 22:57:16 +02:00
|
|
|
// From https://github.com/chalk/ansi-regex
|
|
|
|
const pattern = [
|
2021-09-11 15:10:47 +02:00
|
|
|
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
|
2021-06-23 22:57:16 +02:00
|
|
|
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))",
|
|
|
|
].join("|")
|
|
|
|
const re = new RegExp(pattern, "g")
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Split stdout on newlines and strip ANSI codes.
|
|
|
|
*/
|
|
|
|
export const onLine = (proc: cp.ChildProcess, callback: (strippedLine: string, originalLine: string) => void): void => {
|
|
|
|
let buffer = ""
|
|
|
|
if (!proc.stdout) {
|
|
|
|
throw new Error("no stdout")
|
|
|
|
}
|
|
|
|
proc.stdout.setEncoding("utf8")
|
|
|
|
proc.stdout.on("data", (d) => {
|
|
|
|
const data = buffer + d
|
|
|
|
const split = data.split("\n")
|
|
|
|
const last = split.length - 1
|
|
|
|
|
|
|
|
for (let i = 0; i < last; ++i) {
|
|
|
|
callback(split[i].replace(re, ""), split[i])
|
|
|
|
}
|
|
|
|
|
|
|
|
// The last item will either be an empty string (the data ended with a
|
|
|
|
// newline) or a partial line (did not end with a newline) and we must
|
|
|
|
// wait to parse it until we get a full line.
|
|
|
|
buffer = split[last]
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-05-10 07:35:42 +02:00
|
|
|
export const paths = getEnvPaths()
|
|
|
|
|
2020-05-10 10:15:29 +02:00
|
|
|
/**
|
|
|
|
* Gets the config and data paths for the current platform/configuration.
|
|
|
|
* On MacOS this function gets the standard XDG directories instead of using the native macOS
|
|
|
|
* ones. Most CLIs do this as in practice only GUI apps use the standard macOS directories.
|
|
|
|
*/
|
2021-05-11 01:17:30 +02:00
|
|
|
export function getEnvPaths(): Paths {
|
2021-05-11 01:27:47 +02:00
|
|
|
const paths = envPaths("code-server", { suffix: "" })
|
|
|
|
const append = (p: string): string => path.join(p, "code-server")
|
|
|
|
switch (process.platform) {
|
|
|
|
case "darwin":
|
|
|
|
return {
|
|
|
|
// envPaths uses native directories so force Darwin to use the XDG spec
|
|
|
|
// to align with other CLI tools.
|
|
|
|
data: xdgBasedir.data ? append(xdgBasedir.data) : paths.data,
|
|
|
|
config: xdgBasedir.config ? append(xdgBasedir.config) : paths.config,
|
|
|
|
// Fall back to temp if there is no runtime dir.
|
|
|
|
runtime: xdgBasedir.runtime ? append(xdgBasedir.runtime) : paths.temp,
|
|
|
|
}
|
|
|
|
case "win32":
|
|
|
|
return {
|
|
|
|
data: paths.data,
|
|
|
|
config: paths.config,
|
|
|
|
// Windows doesn't have a runtime dir.
|
|
|
|
runtime: paths.temp,
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return {
|
|
|
|
data: paths.data,
|
|
|
|
config: paths.config,
|
|
|
|
// Fall back to temp if there is no runtime dir.
|
|
|
|
runtime: xdgBasedir.runtime ? append(xdgBasedir.runtime) : paths.temp,
|
|
|
|
}
|
2020-02-04 20:27:46 +01:00
|
|
|
}
|
2019-07-26 00:39:43 +02:00
|
|
|
}
|
2019-07-23 22:38:00 +02:00
|
|
|
|
2020-05-10 10:15:29 +02:00
|
|
|
/**
|
|
|
|
* humanPath replaces the home directory in p with ~.
|
|
|
|
* Makes it more readable.
|
|
|
|
*
|
|
|
|
* @param p
|
|
|
|
*/
|
|
|
|
export function humanPath(p?: string): string {
|
|
|
|
if (!p) {
|
|
|
|
return ""
|
|
|
|
}
|
2020-05-10 07:35:42 +02:00
|
|
|
return p.replace(os.homedir(), "~")
|
|
|
|
}
|
2020-02-04 20:27:46 +01:00
|
|
|
|
2020-10-30 10:26:40 +01:00
|
|
|
export const generateCertificate = async (hostname: string): Promise<{ cert: string; certKey: string }> => {
|
|
|
|
const certPath = path.join(paths.data, `${hostname.replace(/\./g, "_")}.crt`)
|
|
|
|
const certKeyPath = path.join(paths.data, `${hostname.replace(/\./g, "_")}.key`)
|
2020-10-30 09:13:22 +01:00
|
|
|
|
2021-03-15 22:15:24 +01:00
|
|
|
// Try generating the certificates if we can't access them (which probably
|
|
|
|
// means they don't exist).
|
|
|
|
try {
|
|
|
|
await Promise.all([fs.access(certPath), fs.access(certKeyPath)])
|
|
|
|
} catch (error) {
|
2020-02-04 20:27:46 +01:00
|
|
|
// Require on demand so openssl isn't required if you aren't going to
|
|
|
|
// generate certificates.
|
|
|
|
const pem = require("pem") as typeof import("pem")
|
|
|
|
const certs = await new Promise<import("pem").CertificateCreationResult>((resolve, reject): void => {
|
2020-10-30 09:35:08 +01:00
|
|
|
pem.createCertificate(
|
|
|
|
{
|
|
|
|
selfSigned: true,
|
2020-10-30 10:26:40 +01:00
|
|
|
commonName: hostname,
|
2020-10-30 09:35:08 +01:00
|
|
|
config: `
|
|
|
|
[req]
|
|
|
|
req_extensions = v3_req
|
|
|
|
|
|
|
|
[ v3_req ]
|
2020-10-30 10:42:42 +01:00
|
|
|
basicConstraints = CA:true
|
2020-10-30 09:35:08 +01:00
|
|
|
extendedKeyUsage = serverAuth
|
|
|
|
subjectAltName = @alt_names
|
|
|
|
|
|
|
|
[alt_names]
|
2020-10-30 10:26:40 +01:00
|
|
|
DNS.1 = ${hostname}
|
2020-10-30 09:35:08 +01:00
|
|
|
`,
|
|
|
|
},
|
|
|
|
(error, result) => {
|
|
|
|
return error ? reject(error) : resolve(result)
|
|
|
|
},
|
|
|
|
)
|
2020-02-04 20:27:46 +01:00
|
|
|
})
|
2021-03-15 22:15:24 +01:00
|
|
|
await fs.mkdir(paths.data, { recursive: true })
|
2020-10-30 09:13:22 +01:00
|
|
|
await Promise.all([fs.writeFile(certPath, certs.certificate), fs.writeFile(certKeyPath, certs.serviceKey)])
|
|
|
|
}
|
2021-03-15 22:15:24 +01:00
|
|
|
|
2020-10-30 09:13:22 +01:00
|
|
|
return {
|
|
|
|
cert: certPath,
|
|
|
|
certKey: certKeyPath,
|
2020-02-04 20:27:46 +01:00
|
|
|
}
|
2019-09-04 23:57:23 +02:00
|
|
|
}
|
|
|
|
|
2020-02-04 20:27:46 +01:00
|
|
|
export const generatePassword = async (length = 24): Promise<string> => {
|
|
|
|
const buffer = Buffer.alloc(Math.ceil(length / 2))
|
|
|
|
await util.promisify(crypto.randomFill)(buffer)
|
|
|
|
return buffer.toString("hex").substring(0, length)
|
|
|
|
}
|
2019-07-12 22:21:00 +02:00
|
|
|
|
2021-05-20 01:17:32 +02:00
|
|
|
/**
|
|
|
|
* Used to hash the password.
|
|
|
|
*/
|
2021-06-02 21:47:22 +02:00
|
|
|
export const hash = async (password: string): Promise<string> => {
|
|
|
|
try {
|
|
|
|
return await argon2.hash(password)
|
2021-09-30 05:14:56 +02:00
|
|
|
} catch (error: any) {
|
2021-06-02 21:47:22 +02:00
|
|
|
logger.error(error)
|
|
|
|
return ""
|
|
|
|
}
|
2021-05-20 01:17:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Used to verify if the password matches the hash
|
|
|
|
*/
|
2021-06-02 21:47:22 +02:00
|
|
|
export const isHashMatch = async (password: string, hash: string) => {
|
2021-06-30 21:29:12 +02:00
|
|
|
if (password === "" || hash === "" || !hash.startsWith("$")) {
|
2021-06-08 00:45:11 +02:00
|
|
|
return false
|
|
|
|
}
|
2021-06-02 21:47:22 +02:00
|
|
|
try {
|
|
|
|
return await argon2.verify(hash, password)
|
2021-09-30 05:14:56 +02:00
|
|
|
} catch (error: any) {
|
2021-06-30 21:29:12 +02:00
|
|
|
throw new Error(error)
|
2021-06-02 21:47:22 +02:00
|
|
|
}
|
2021-05-20 01:17:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Used to hash the password using the sha256
|
|
|
|
* algorithm. We only use this to for checking
|
|
|
|
* the hashed-password set in the config.
|
|
|
|
*
|
|
|
|
* Kept for legacy reasons.
|
|
|
|
*/
|
|
|
|
export const hashLegacy = (str: string): string => {
|
|
|
|
return crypto.createHash("sha256").update(str).digest("hex")
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Used to check if the password matches the hash using
|
|
|
|
* the hashLegacy function
|
|
|
|
*/
|
|
|
|
export const isHashLegacyMatch = (password: string, hashPassword: string) => {
|
|
|
|
const hashedWithLegacy = hashLegacy(password)
|
|
|
|
return safeCompare(hashedWithLegacy, hashPassword)
|
2020-02-04 20:27:46 +01:00
|
|
|
}
|
|
|
|
|
2021-06-10 15:09:38 +02:00
|
|
|
export type PasswordMethod = "SHA256" | "ARGON2" | "PLAIN_TEXT"
|
2021-06-03 00:17:39 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Used to determine the password method.
|
|
|
|
*
|
|
|
|
* There are three options for the return value:
|
|
|
|
* 1. "SHA256" -> the legacy hashing algorithm
|
|
|
|
* 2. "ARGON2" -> the newest hashing algorithm
|
|
|
|
* 3. "PLAIN_TEXT" -> regular ol' password with no hashing
|
|
|
|
*
|
|
|
|
* @returns {PasswordMethod} "SHA256" | "ARGON2" | "PLAIN_TEXT"
|
|
|
|
*/
|
|
|
|
export function getPasswordMethod(hashedPassword: string | undefined): PasswordMethod {
|
|
|
|
if (!hashedPassword) {
|
|
|
|
return "PLAIN_TEXT"
|
|
|
|
}
|
|
|
|
|
|
|
|
// This is the new hashing algorithm
|
|
|
|
if (hashedPassword.includes("$argon")) {
|
|
|
|
return "ARGON2"
|
|
|
|
}
|
|
|
|
|
|
|
|
// This is the legacy hashing algorithm
|
|
|
|
return "SHA256"
|
|
|
|
}
|
|
|
|
|
2021-06-03 01:09:46 +02:00
|
|
|
type PasswordValidation = {
|
|
|
|
isPasswordValid: boolean
|
|
|
|
hashedPassword: string
|
|
|
|
}
|
|
|
|
|
|
|
|
type HandlePasswordValidationArgs = {
|
|
|
|
/** The PasswordMethod */
|
|
|
|
passwordMethod: PasswordMethod
|
|
|
|
/** The password provided by the user */
|
|
|
|
passwordFromRequestBody: string
|
|
|
|
/** The password set in PASSWORD or config */
|
|
|
|
passwordFromArgs: string | undefined
|
|
|
|
/** The hashed-password set in HASHED_PASSWORD or config */
|
|
|
|
hashedPasswordFromArgs: string | undefined
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if a password is valid and also returns the hash
|
|
|
|
* using the PasswordMethod
|
|
|
|
*/
|
2021-06-08 00:45:11 +02:00
|
|
|
export async function handlePasswordValidation({
|
|
|
|
passwordMethod,
|
|
|
|
passwordFromArgs,
|
|
|
|
passwordFromRequestBody,
|
|
|
|
hashedPasswordFromArgs,
|
|
|
|
}: HandlePasswordValidationArgs): Promise<PasswordValidation> {
|
2021-06-03 01:09:46 +02:00
|
|
|
const passwordValidation = <PasswordValidation>{
|
|
|
|
isPasswordValid: false,
|
|
|
|
hashedPassword: "",
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (passwordMethod) {
|
|
|
|
case "PLAIN_TEXT": {
|
|
|
|
const isValid = passwordFromArgs ? safeCompare(passwordFromRequestBody, passwordFromArgs) : false
|
|
|
|
passwordValidation.isPasswordValid = isValid
|
|
|
|
|
|
|
|
const hashedPassword = await hash(passwordFromRequestBody)
|
|
|
|
passwordValidation.hashedPassword = hashedPassword
|
|
|
|
break
|
|
|
|
}
|
|
|
|
case "SHA256": {
|
|
|
|
const isValid = isHashLegacyMatch(passwordFromRequestBody, hashedPasswordFromArgs || "")
|
|
|
|
passwordValidation.isPasswordValid = isValid
|
|
|
|
|
|
|
|
passwordValidation.hashedPassword = hashedPasswordFromArgs || (await hashLegacy(passwordFromRequestBody))
|
|
|
|
break
|
|
|
|
}
|
|
|
|
case "ARGON2": {
|
|
|
|
const isValid = await isHashMatch(passwordFromRequestBody, hashedPasswordFromArgs || "")
|
|
|
|
passwordValidation.isPasswordValid = isValid
|
|
|
|
|
|
|
|
passwordValidation.hashedPassword = hashedPasswordFromArgs || ""
|
|
|
|
break
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
return passwordValidation
|
|
|
|
}
|
|
|
|
|
2021-06-03 02:23:57 +02:00
|
|
|
export type IsCookieValidArgs = {
|
|
|
|
passwordMethod: PasswordMethod
|
|
|
|
cookieKey: string
|
|
|
|
hashedPasswordFromArgs: string | undefined
|
|
|
|
passwordFromArgs: string | undefined
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Checks if a req.cookies.key is valid using the PasswordMethod */
|
2021-06-08 00:45:11 +02:00
|
|
|
export async function isCookieValid({
|
|
|
|
passwordFromArgs = "",
|
|
|
|
cookieKey,
|
|
|
|
hashedPasswordFromArgs = "",
|
|
|
|
passwordMethod,
|
|
|
|
}: IsCookieValidArgs): Promise<boolean> {
|
2021-06-03 02:23:57 +02:00
|
|
|
let isValid = false
|
2021-06-08 00:45:11 +02:00
|
|
|
switch (passwordMethod) {
|
2021-06-03 02:23:57 +02:00
|
|
|
case "PLAIN_TEXT":
|
|
|
|
isValid = await isHashMatch(passwordFromArgs, cookieKey)
|
|
|
|
break
|
|
|
|
case "ARGON2":
|
|
|
|
case "SHA256":
|
|
|
|
isValid = safeCompare(cookieKey, hashedPasswordFromArgs)
|
|
|
|
break
|
|
|
|
default:
|
|
|
|
break
|
|
|
|
}
|
|
|
|
return isValid
|
|
|
|
}
|
|
|
|
|
2021-06-07 23:46:59 +02:00
|
|
|
/** Ensures that the input is sanitized by checking
|
|
|
|
* - it's a string
|
|
|
|
* - greater than 0 characters
|
|
|
|
* - trims whitespace
|
|
|
|
*/
|
|
|
|
export function sanitizeString(str: string): string {
|
|
|
|
// Very basic sanitization of string
|
|
|
|
// Credit: https://stackoverflow.com/a/46719000/3015595
|
|
|
|
return typeof str === "string" && str.trim().length > 0 ? str.trim() : ""
|
|
|
|
}
|
|
|
|
|
2020-02-04 20:27:46 +01:00
|
|
|
const mimeTypes: { [key: string]: string } = {
|
|
|
|
".aac": "audio/x-aac",
|
|
|
|
".avi": "video/x-msvideo",
|
|
|
|
".bmp": "image/bmp",
|
|
|
|
".css": "text/css",
|
|
|
|
".flv": "video/x-flv",
|
|
|
|
".gif": "image/gif",
|
|
|
|
".html": "text/html",
|
|
|
|
".ico": "image/x-icon",
|
|
|
|
".jpe": "image/jpg",
|
|
|
|
".jpeg": "image/jpg",
|
|
|
|
".jpg": "image/jpg",
|
|
|
|
".js": "application/javascript",
|
|
|
|
".json": "application/json",
|
|
|
|
".m1v": "video/mpeg",
|
|
|
|
".m2a": "audio/mpeg",
|
|
|
|
".m2v": "video/mpeg",
|
|
|
|
".m3a": "audio/mpeg",
|
|
|
|
".mid": "audio/midi",
|
|
|
|
".midi": "audio/midi",
|
|
|
|
".mk3d": "video/x-matroska",
|
|
|
|
".mks": "video/x-matroska",
|
|
|
|
".mkv": "video/x-matroska",
|
|
|
|
".mov": "video/quicktime",
|
|
|
|
".movie": "video/x-sgi-movie",
|
|
|
|
".mp2": "audio/mpeg",
|
|
|
|
".mp2a": "audio/mpeg",
|
|
|
|
".mp3": "audio/mpeg",
|
|
|
|
".mp4": "video/mp4",
|
|
|
|
".mp4a": "audio/mp4",
|
|
|
|
".mp4v": "video/mp4",
|
|
|
|
".mpe": "video/mpeg",
|
|
|
|
".mpeg": "video/mpeg",
|
|
|
|
".mpg": "video/mpeg",
|
|
|
|
".mpg4": "video/mp4",
|
|
|
|
".mpga": "audio/mpeg",
|
|
|
|
".oga": "audio/ogg",
|
|
|
|
".ogg": "audio/ogg",
|
|
|
|
".ogv": "video/ogg",
|
|
|
|
".png": "image/png",
|
|
|
|
".psd": "image/vnd.adobe.photoshop",
|
|
|
|
".qt": "video/quicktime",
|
|
|
|
".spx": "audio/ogg",
|
|
|
|
".svg": "image/svg+xml",
|
|
|
|
".tga": "image/x-tga",
|
|
|
|
".tif": "image/tiff",
|
|
|
|
".tiff": "image/tiff",
|
|
|
|
".txt": "text/plain",
|
|
|
|
".wav": "audio/x-wav",
|
|
|
|
".wasm": "application/wasm",
|
|
|
|
".webm": "video/webm",
|
|
|
|
".webp": "image/webp",
|
|
|
|
".wma": "audio/x-ms-wma",
|
|
|
|
".wmv": "video/x-ms-wmv",
|
|
|
|
".woff": "application/font-woff",
|
|
|
|
}
|
2019-11-07 22:43:10 +01:00
|
|
|
|
2019-07-12 22:21:00 +02:00
|
|
|
export const getMediaMime = (filePath?: string): string => {
|
2020-02-04 20:27:46 +01:00
|
|
|
return (filePath && mimeTypes[path.extname(filePath)]) || "text/plain"
|
|
|
|
}
|
2019-07-15 20:31:05 +02:00
|
|
|
|
|
|
|
export const isWsl = async (): Promise<boolean> => {
|
2020-02-04 20:27:46 +01:00
|
|
|
return (
|
2020-05-14 09:17:17 +02:00
|
|
|
(process.platform === "linux" && os.release().toLowerCase().indexOf("microsoft") !== -1) ||
|
2020-02-04 20:27:46 +01:00
|
|
|
(await fs.readFile("/proc/version", "utf8")).toLowerCase().indexOf("microsoft") !== -1
|
|
|
|
)
|
|
|
|
}
|
2019-07-15 20:31:05 +02:00
|
|
|
|
2020-02-04 20:27:46 +01:00
|
|
|
/**
|
2021-10-28 22:27:17 +02:00
|
|
|
* Try opening an address using whatever the system has set for opening URLs.
|
2020-02-04 20:27:46 +01:00
|
|
|
*/
|
2021-10-28 22:27:17 +02:00
|
|
|
export const open = async (address: URL | string): Promise<void> => {
|
|
|
|
if (typeof address === "string") {
|
|
|
|
throw new Error("Cannot open socket paths")
|
|
|
|
}
|
|
|
|
// Web sockets do not seem to work if browsing with 0.0.0.0.
|
|
|
|
const url = new URL(address)
|
|
|
|
if (url.hostname === "0.0.0.0") {
|
|
|
|
url.hostname = "localhost"
|
|
|
|
}
|
2020-02-04 20:27:46 +01:00
|
|
|
const args = [] as string[]
|
|
|
|
const options = {} as cp.SpawnOptions
|
|
|
|
const platform = (await isWsl()) ? "wsl" : process.platform
|
|
|
|
let command = platform === "darwin" ? "open" : "xdg-open"
|
|
|
|
if (platform === "win32" || platform === "wsl") {
|
|
|
|
command = platform === "wsl" ? "cmd.exe" : "cmd"
|
|
|
|
args.push("/c", "start", '""', "/b")
|
2021-10-28 22:27:17 +02:00
|
|
|
url.search = url.search.replace(/&/g, "^&")
|
2020-02-04 20:27:46 +01:00
|
|
|
}
|
2021-10-28 22:27:17 +02:00
|
|
|
const proc = cp.spawn(command, [...args, url.toString()], options)
|
2021-01-20 19:30:09 +01:00
|
|
|
await new Promise<void>((resolve, reject) => {
|
2020-02-04 20:27:46 +01:00
|
|
|
proc.on("error", reject)
|
|
|
|
proc.on("close", (code) => {
|
|
|
|
return code !== 0 ? reject(new Error(`Failed to open with code ${code}`)) : resolve()
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
2019-07-17 02:26:05 +02:00
|
|
|
|
2020-02-04 20:27:46 +01:00
|
|
|
/**
|
|
|
|
* For iterating over an enum's values.
|
|
|
|
*/
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2019-08-10 01:50:05 +02:00
|
|
|
export const enumToArray = (t: any): string[] => {
|
2020-02-04 20:27:46 +01:00
|
|
|
const values = [] as string[]
|
|
|
|
for (const k in t) {
|
|
|
|
values.push(t[k])
|
|
|
|
}
|
|
|
|
return values
|
|
|
|
}
|
2019-08-10 01:50:05 +02:00
|
|
|
|
2020-02-04 20:27:46 +01:00
|
|
|
/**
|
|
|
|
* For displaying all allowed options in an enum.
|
|
|
|
*/
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2019-08-10 01:50:05 +02:00
|
|
|
export const buildAllowedMessage = (t: any): string => {
|
2020-02-04 20:27:46 +01:00
|
|
|
const values = enumToArray(t)
|
|
|
|
return `Allowed value${values.length === 1 ? " is" : "s are"} ${values.map((t) => `'${t}'`).join(", ")}`
|
|
|
|
}
|
|
|
|
|
|
|
|
export const isObject = <T extends object>(obj: T): obj is T => {
|
|
|
|
return !Array.isArray(obj) && typeof obj === "object" && obj !== null
|
|
|
|
}
|
2019-07-31 22:22:05 +02:00
|
|
|
|
2020-09-15 23:51:43 +02:00
|
|
|
/**
|
|
|
|
* Return a promise that resolves with whether the socket path is active.
|
|
|
|
*/
|
|
|
|
export function canConnect(path: string): Promise<boolean> {
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
const socket = net.connect(path)
|
|
|
|
socket.once("error", () => resolve(false))
|
|
|
|
socket.once("connect", () => {
|
|
|
|
socket.destroy()
|
|
|
|
resolve(true)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
2020-11-03 21:40:06 +01:00
|
|
|
|
|
|
|
export const isFile = async (path: string): Promise<boolean> => {
|
|
|
|
try {
|
|
|
|
const stat = await fs.stat(path)
|
|
|
|
return stat.isFile()
|
|
|
|
} catch (error) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
2021-06-30 00:28:44 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Escapes any HTML string special characters, like &, <, >, ", and '.
|
|
|
|
*
|
|
|
|
* Source: https://stackoverflow.com/a/6234804/3015595
|
|
|
|
**/
|
|
|
|
export function escapeHtml(unsafe: string): string {
|
|
|
|
return unsafe
|
|
|
|
.replace(/&/g, "&")
|
|
|
|
.replace(/</g, "<")
|
|
|
|
.replace(/>/g, ">")
|
|
|
|
.replace(/"/g, """)
|
2021-06-30 19:37:08 +02:00
|
|
|
.replace(/'/g, "'")
|
2021-06-30 00:28:44 +02:00
|
|
|
}
|
2021-09-13 23:35:12 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A helper function which returns a boolean indicating whether
|
|
|
|
* the given error is a NodeJS.ErrnoException by checking if
|
|
|
|
* it has a .code property.
|
|
|
|
*/
|
|
|
|
export function isNodeJSErrnoException(error: unknown): error is NodeJS.ErrnoException {
|
2021-10-30 01:03:57 +02:00
|
|
|
return error !== undefined && (error as NodeJS.ErrnoException).code !== undefined
|
2021-09-13 23:35:12 +02:00
|
|
|
}
|
2021-09-30 05:14:56 +02:00
|
|
|
|
|
|
|
// TODO: Replace with proper templating system.
|
|
|
|
export const escapeJSON = (value: cp.Serializable) => JSON.stringify(value).replace(/"/g, """)
|
|
|
|
|
|
|
|
type AMDModule<T> = { [exportName: string]: T }
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Loads AMD module, typically from a compiled VSCode bundle.
|
|
|
|
*
|
|
|
|
* @deprecated This should be gradually phased out as code-server migrates to lib/vscode
|
|
|
|
* @param amdPath Path to module relative to lib/vscode
|
|
|
|
* @param exportName Given name of export in the file
|
|
|
|
*/
|
|
|
|
export const loadAMDModule = async <T>(amdPath: string, exportName: string): Promise<T> => {
|
2021-11-10 06:28:31 +01:00
|
|
|
// Set default remote native node modules path, if unset
|
|
|
|
process.env["VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH"] =
|
|
|
|
process.env["VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH"] || path.join(vsRootPath, "remote", "node_modules")
|
|
|
|
|
|
|
|
require(path.join(vsRootPath, "out/bootstrap-node")).injectNodeModuleLookupPath(
|
|
|
|
process.env["VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH"],
|
|
|
|
)
|
|
|
|
|
2021-09-30 05:14:56 +02:00
|
|
|
const module = await new Promise<AMDModule<T>>((resolve, reject) => {
|
|
|
|
require(path.join(vsRootPath, "out/bootstrap-amd")).load(amdPath, resolve, reject)
|
|
|
|
})
|
|
|
|
|
|
|
|
return module[exportName] as T
|
|
|
|
}
|