mirror of https://github.com/coder/code-server.git
Merge branch 'main' into vscode-1.56
This commit is contained in:
commit
ab122daa93
|
@ -43,10 +43,6 @@ jobs:
|
||||||
if: steps.cache-yarn.outputs.cache-hit != 'true'
|
if: steps.cache-yarn.outputs.cache-hit != 'true'
|
||||||
run: yarn --frozen-lockfile
|
run: yarn --frozen-lockfile
|
||||||
|
|
||||||
- name: Audit for vulnerabilities
|
|
||||||
run: yarn _audit
|
|
||||||
if: success()
|
|
||||||
|
|
||||||
- name: Run yarn fmt
|
- name: Run yarn fmt
|
||||||
run: yarn fmt
|
run: yarn fmt
|
||||||
if: success()
|
if: success()
|
||||||
|
@ -63,6 +59,34 @@ jobs:
|
||||||
run: yarn coverage
|
run: yarn coverage
|
||||||
if: success()
|
if: success()
|
||||||
|
|
||||||
|
audit-ci:
|
||||||
|
name: Run audit-ci
|
||||||
|
needs: prebuild
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install Node.js v12
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: "12"
|
||||||
|
|
||||||
|
- name: Fetch dependencies from cache
|
||||||
|
id: cache-yarn
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: "**/node_modules"
|
||||||
|
key: yarn-build-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.cache-yarn.outputs.cache-hit != 'true'
|
||||||
|
run: yarn --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Audit for vulnerabilities
|
||||||
|
run: yarn _audit
|
||||||
|
if: success()
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
needs: prebuild
|
needs: prebuild
|
||||||
|
|
|
@ -1,12 +1,28 @@
|
||||||
# Security Policy
|
# Security Policy
|
||||||
|
|
||||||
|
The code-server team (and Coder, the organization) care a lot about keeping the project secure and safe for end-users.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
We use a combination of tools to help us stay on top of vulnerabilities.
|
||||||
|
|
||||||
|
- [dependabot](https://dependabot.com/)
|
||||||
|
- Submits pull requests to upgrade dependencies. We use dependabot's version upgrades as well as security updates.
|
||||||
|
- code-scanning
|
||||||
|
- [CodeQL](https://securitylab.github.com/tools/codeql/)
|
||||||
|
- Semantic code analysis engine that runs on a regular schedule (see `codeql-analysis.yml`)
|
||||||
|
- [trivy](https://github.com/aquasecurity/trivy)
|
||||||
|
- Comprehensive vulnerability scanner that runs on PRs into the default branch and scans both our container image and repository code (see `trivy-scan-repo` and `trivy-scan-image` jobs in `ci.yaml`)
|
||||||
|
- [`audit-ci`](https://github.com/IBM/audit-ci)
|
||||||
|
- Audits npm and Yarn dependencies in CI (see "Audit for vulnerabilities" step in `ci.yaml`) on PRs into the default branch and fails CI if moderate or higher vulnerabilities(see the `audit.sh` script) are present.
|
||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
Coder sponsors development and maintenance of the code-server project. We will fix security issues within 90 days of receiving a report, and publish the fix in a subsequent release. The code-server project does not provide backports or patch releases for security issues at this time.
|
Coder sponsors development and maintenance of the code-server project. We will fix security issues within 90 days of receiving a report, and publish the fix in a subsequent release. The code-server project does not provide backports or patch releases for security issues at this time.
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | ------------------ |
|
| ----------------------------------------------------- | ------------------ |
|
||||||
| 3.9.3 | :white_check_mark: |
|
| [Latest](https://github.com/cdr/code-server/releases) | :white_check_mark: |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
|
|
@ -195,7 +195,7 @@
|
||||||
"tsec": "0.1.4",
|
"tsec": "0.1.4",
|
||||||
"typescript": "^4.3.0-dev.20210426",
|
"typescript": "^4.3.0-dev.20210426",
|
||||||
"typescript-formatter": "7.1.0",
|
"typescript-formatter": "7.1.0",
|
||||||
"underscore": "^1.8.2",
|
"underscore": "^1.12.1",
|
||||||
"vinyl": "^2.0.0",
|
"vinyl": "^2.0.0",
|
||||||
"vinyl-fs": "^3.0.0",
|
"vinyl-fs": "^3.0.0",
|
||||||
"vscode-debugprotocol": "1.47.0",
|
"vscode-debugprotocol": "1.47.0",
|
||||||
|
|
|
@ -8915,16 +8915,11 @@ unc-path-regex@^0.1.2:
|
||||||
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
|
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
|
||||||
integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo=
|
integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo=
|
||||||
|
|
||||||
underscore@^1.8.2:
|
underscore@^1.8.2, underscore@~1.8.3:
|
||||||
version "1.13.1"
|
version "1.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1"
|
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1"
|
||||||
integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==
|
integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==
|
||||||
|
|
||||||
underscore@~1.8.3:
|
|
||||||
version "1.8.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
|
|
||||||
integrity sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=
|
|
||||||
|
|
||||||
undertaker-registry@^1.0.0:
|
undertaker-registry@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/undertaker-registry/-/undertaker-registry-1.0.1.tgz#5e4bda308e4a8a2ae584f9b9a4359a499825cc50"
|
resolved "https://registry.yarnpkg.com/undertaker-registry/-/undertaker-registry-1.0.1.tgz#5e4bda308e4a8a2ae584f9b9a4359a499825cc50"
|
||||||
|
|
|
@ -144,7 +144,7 @@
|
||||||
"clover"
|
"clover"
|
||||||
],
|
],
|
||||||
"coveragePathIgnorePatterns": [
|
"coveragePathIgnorePatterns": [
|
||||||
"out"
|
"/out"
|
||||||
],
|
],
|
||||||
"coverageThreshold": {
|
"coverageThreshold": {
|
||||||
"global": {
|
"global": {
|
||||||
|
|
|
@ -6,6 +6,11 @@ import * as path from "path"
|
||||||
import { Args as VsArgs } from "../../typings/ipc"
|
import { Args as VsArgs } from "../../typings/ipc"
|
||||||
import { canConnect, generateCertificate, generatePassword, humanPath, paths } from "./util"
|
import { canConnect, generateCertificate, generatePassword, humanPath, paths } from "./util"
|
||||||
|
|
||||||
|
export enum Feature {
|
||||||
|
/** Web socket compression. */
|
||||||
|
PermessageDeflate = "permessage-deflate",
|
||||||
|
}
|
||||||
|
|
||||||
export enum AuthType {
|
export enum AuthType {
|
||||||
Password = "password",
|
Password = "password",
|
||||||
None = "none",
|
None = "none",
|
||||||
|
@ -35,6 +40,7 @@ export interface Args extends VsArgs {
|
||||||
"cert-key"?: string
|
"cert-key"?: string
|
||||||
"disable-telemetry"?: boolean
|
"disable-telemetry"?: boolean
|
||||||
"disable-update-check"?: boolean
|
"disable-update-check"?: boolean
|
||||||
|
enable?: string[]
|
||||||
help?: boolean
|
help?: boolean
|
||||||
host?: string
|
host?: string
|
||||||
json?: boolean
|
json?: boolean
|
||||||
|
@ -128,6 +134,9 @@ const options: Options<Required<Args>> = {
|
||||||
"Disable update check. Without this flag, code-server checks every 6 hours against the latest github release and \n" +
|
"Disable update check. Without this flag, code-server checks every 6 hours against the latest github release and \n" +
|
||||||
"then notifies you once every week that a new release is available.",
|
"then notifies you once every week that a new release is available.",
|
||||||
},
|
},
|
||||||
|
// --enable can be used to enable experimental features. These features
|
||||||
|
// provide no guarantees.
|
||||||
|
enable: { type: "string[]" },
|
||||||
help: { type: "boolean", short: "h", description: "Show this output." },
|
help: { type: "boolean", short: "h", description: "Show this output." },
|
||||||
json: { type: "boolean" },
|
json: { type: "boolean" },
|
||||||
open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." },
|
open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." },
|
||||||
|
|
|
@ -20,3 +20,4 @@ export const version = pkg.version || "development"
|
||||||
export const commit = pkg.commit || "development"
|
export const commit = pkg.commit || "development"
|
||||||
export const rootPath = path.resolve(__dirname, "../..")
|
export const rootPath = path.resolve(__dirname, "../..")
|
||||||
export const tmpdir = path.join(os.tmpdir(), "code-server")
|
export const tmpdir = path.join(os.tmpdir(), "code-server")
|
||||||
|
export const isDevMode = commit === "development"
|
||||||
|
|
|
@ -1,13 +1,5 @@
|
||||||
import { field, logger } from "@coder/logger"
|
import { logger } from "@coder/logger"
|
||||||
import * as cp from "child_process"
|
|
||||||
import http from "http"
|
|
||||||
import * as path from "path"
|
|
||||||
import { CliMessage, OpenCommandPipeArgs } from "../../typings/ipc"
|
|
||||||
import { plural } from "../common/util"
|
|
||||||
import { createApp, ensureAddress } from "./app"
|
|
||||||
import {
|
import {
|
||||||
AuthType,
|
|
||||||
DefaultedArgs,
|
|
||||||
optionDescriptions,
|
optionDescriptions,
|
||||||
parse,
|
parse,
|
||||||
readConfigFile,
|
readConfigFile,
|
||||||
|
@ -15,149 +7,11 @@ import {
|
||||||
shouldOpenInExistingInstance,
|
shouldOpenInExistingInstance,
|
||||||
shouldRunVsCodeCli,
|
shouldRunVsCodeCli,
|
||||||
} from "./cli"
|
} from "./cli"
|
||||||
import { coderCloudBind } from "./coder_cloud"
|
|
||||||
import { commit, version } from "./constants"
|
import { commit, version } from "./constants"
|
||||||
|
import { openInExistingInstance, runCodeServer, runVsCodeCli } from "./main"
|
||||||
import * as proxyAgent from "./proxy_agent"
|
import * as proxyAgent from "./proxy_agent"
|
||||||
import { register } from "./routes"
|
|
||||||
import { humanPath, isFile, open } from "./util"
|
|
||||||
import { isChild, wrapper } from "./wrapper"
|
import { isChild, wrapper } from "./wrapper"
|
||||||
|
|
||||||
export const runVsCodeCli = (args: DefaultedArgs): void => {
|
|
||||||
logger.debug("forking vs code cli...")
|
|
||||||
const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], {
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
vscode.once("message", (message: any) => {
|
|
||||||
logger.debug("got message from VS Code", field("message", message))
|
|
||||||
if (message.type !== "ready") {
|
|
||||||
logger.error("Unexpected response waiting for ready response", field("type", message.type))
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
const send: CliMessage = { type: "cli", args }
|
|
||||||
vscode.send(send)
|
|
||||||
})
|
|
||||||
vscode.once("error", (error) => {
|
|
||||||
logger.error("Got error from VS Code", field("error", error))
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
vscode.on("exit", (code) => process.exit(code || 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openInExistingInstance = async (args: DefaultedArgs, socketPath: string): Promise<void> => {
|
|
||||||
const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = {
|
|
||||||
type: "open",
|
|
||||||
folderURIs: [],
|
|
||||||
fileURIs: [],
|
|
||||||
forceReuseWindow: args["reuse-window"],
|
|
||||||
forceNewWindow: args["new-window"],
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < args._.length; i++) {
|
|
||||||
const fp = path.resolve(args._[i])
|
|
||||||
if (await isFile(fp)) {
|
|
||||||
pipeArgs.fileURIs.push(fp)
|
|
||||||
} else {
|
|
||||||
pipeArgs.folderURIs.push(fp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pipeArgs.forceNewWindow && pipeArgs.fileURIs.length > 0) {
|
|
||||||
logger.error("--new-window can only be used with folder paths")
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pipeArgs.folderURIs.length === 0 && pipeArgs.fileURIs.length === 0) {
|
|
||||||
logger.error("Please specify at least one file or folder")
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const vscode = http.request(
|
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
method: "POST",
|
|
||||||
socketPath,
|
|
||||||
},
|
|
||||||
(response) => {
|
|
||||||
response.on("data", (message) => {
|
|
||||||
logger.debug("got message from VS Code", field("message", message.toString()))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
vscode.on("error", (error: unknown) => {
|
|
||||||
logger.error("got error from VS Code", field("error", error))
|
|
||||||
})
|
|
||||||
vscode.write(JSON.stringify(pipeArgs))
|
|
||||||
vscode.end()
|
|
||||||
}
|
|
||||||
|
|
||||||
const main = async (args: DefaultedArgs): Promise<void> => {
|
|
||||||
logger.info(`code-server ${version} ${commit}`)
|
|
||||||
|
|
||||||
logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`)
|
|
||||||
logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`)
|
|
||||||
|
|
||||||
if (args.auth === AuthType.Password && !args.password && !args["hashed-password"]) {
|
|
||||||
throw new Error(
|
|
||||||
"Please pass in a password via the config file or environment variable ($PASSWORD or $HASHED_PASSWORD)",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [app, wsApp, server] = await createApp(args)
|
|
||||||
const serverAddress = ensureAddress(server)
|
|
||||||
await register(app, wsApp, server, args)
|
|
||||||
|
|
||||||
logger.info(`Using config file ${humanPath(args.config)}`)
|
|
||||||
logger.info(`HTTP server listening on ${serverAddress} ${args.link ? "(randomized by --link)" : ""}`)
|
|
||||||
|
|
||||||
if (args.auth === AuthType.Password) {
|
|
||||||
logger.info(" - Authentication is enabled")
|
|
||||||
if (args.usingEnvPassword) {
|
|
||||||
logger.info(" - Using password from $PASSWORD")
|
|
||||||
} else if (args.usingEnvHashedPassword) {
|
|
||||||
logger.info(" - Using password from $HASHED_PASSWORD")
|
|
||||||
} else {
|
|
||||||
logger.info(` - Using password from ${humanPath(args.config)}`)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.info(` - Authentication is disabled ${args.link ? "(disabled by --link)" : ""}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.cert) {
|
|
||||||
logger.info(` - Using certificate for HTTPS: ${humanPath(args.cert.value)}`)
|
|
||||||
} else {
|
|
||||||
logger.info(` - Not serving HTTPS ${args.link ? "(disabled by --link)" : ""}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args["proxy-domain"].length > 0) {
|
|
||||||
logger.info(` - ${plural(args["proxy-domain"].length, "Proxying the following domain")}:`)
|
|
||||||
args["proxy-domain"].forEach((domain) => logger.info(` - *.${domain}`))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.link) {
|
|
||||||
try {
|
|
||||||
await coderCloudBind(serverAddress.replace(/^https?:\/\//, ""), args.link.value)
|
|
||||||
logger.info(" - Connected to cloud agent")
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(err.message)
|
|
||||||
wrapper.exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!args.socket && args.open) {
|
|
||||||
// The web socket doesn't seem to work if browsing with 0.0.0.0.
|
|
||||||
const openAddress = serverAddress.replace("://0.0.0.0", "://localhost")
|
|
||||||
try {
|
|
||||||
await open(openAddress)
|
|
||||||
logger.info(`Opened ${openAddress}`)
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Failed to open", field("address", openAddress), field("error", error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function entry(): Promise<void> {
|
async function entry(): Promise<void> {
|
||||||
proxyAgent.monkeyPatch(false)
|
proxyAgent.monkeyPatch(false)
|
||||||
|
|
||||||
|
@ -170,7 +24,8 @@ async function entry(): Promise<void> {
|
||||||
if (isChild(wrapper)) {
|
if (isChild(wrapper)) {
|
||||||
const args = await wrapper.handshake()
|
const args = await wrapper.handshake()
|
||||||
wrapper.preventExit()
|
wrapper.preventExit()
|
||||||
return main(args)
|
await runCodeServer(args)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const cliArgs = parse(process.argv.slice(2))
|
const cliArgs = parse(process.argv.slice(2))
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
import { field, logger } from "@coder/logger"
|
||||||
|
import * as cp from "child_process"
|
||||||
|
import http from "http"
|
||||||
|
import * as path from "path"
|
||||||
|
import { CliMessage, OpenCommandPipeArgs } from "../../typings/ipc"
|
||||||
|
import { plural } from "../common/util"
|
||||||
|
import { createApp, ensureAddress } from "./app"
|
||||||
|
import { AuthType, DefaultedArgs, Feature } from "./cli"
|
||||||
|
import { coderCloudBind } from "./coder_cloud"
|
||||||
|
import { commit, version } from "./constants"
|
||||||
|
import { register } from "./routes"
|
||||||
|
import { humanPath, isFile, open } from "./util"
|
||||||
|
|
||||||
|
export const runVsCodeCli = (args: DefaultedArgs): void => {
|
||||||
|
logger.debug("forking vs code cli...")
|
||||||
|
const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], {
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
vscode.once("message", (message: any) => {
|
||||||
|
logger.debug("got message from VS Code", field("message", message))
|
||||||
|
if (message.type !== "ready") {
|
||||||
|
logger.error("Unexpected response waiting for ready response", field("type", message.type))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
const send: CliMessage = { type: "cli", args }
|
||||||
|
vscode.send(send)
|
||||||
|
})
|
||||||
|
vscode.once("error", (error) => {
|
||||||
|
logger.error("Got error from VS Code", field("error", error))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
vscode.on("exit", (code) => process.exit(code || 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const openInExistingInstance = async (args: DefaultedArgs, socketPath: string): Promise<void> => {
|
||||||
|
const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = {
|
||||||
|
type: "open",
|
||||||
|
folderURIs: [],
|
||||||
|
fileURIs: [],
|
||||||
|
forceReuseWindow: args["reuse-window"],
|
||||||
|
forceNewWindow: args["new-window"],
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < args._.length; i++) {
|
||||||
|
const fp = path.resolve(args._[i])
|
||||||
|
if (await isFile(fp)) {
|
||||||
|
pipeArgs.fileURIs.push(fp)
|
||||||
|
} else {
|
||||||
|
pipeArgs.folderURIs.push(fp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pipeArgs.forceNewWindow && pipeArgs.fileURIs.length > 0) {
|
||||||
|
logger.error("--new-window can only be used with folder paths")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pipeArgs.folderURIs.length === 0 && pipeArgs.fileURIs.length === 0) {
|
||||||
|
logger.error("Please specify at least one file or folder")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const vscode = http.request(
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
method: "POST",
|
||||||
|
socketPath,
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
response.on("data", (message) => {
|
||||||
|
logger.debug("got message from VS Code", field("message", message.toString()))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
vscode.on("error", (error: unknown) => {
|
||||||
|
logger.error("got error from VS Code", field("error", error))
|
||||||
|
})
|
||||||
|
vscode.write(JSON.stringify(pipeArgs))
|
||||||
|
vscode.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const runCodeServer = async (args: DefaultedArgs): Promise<http.Server> => {
|
||||||
|
logger.info(`code-server ${version} ${commit}`)
|
||||||
|
|
||||||
|
logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`)
|
||||||
|
logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`)
|
||||||
|
|
||||||
|
if (args.auth === AuthType.Password && !args.password && !args["hashed-password"]) {
|
||||||
|
throw new Error(
|
||||||
|
"Please pass in a password via the config file or environment variable ($PASSWORD or $HASHED_PASSWORD)",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [app, wsApp, server] = await createApp(args)
|
||||||
|
const serverAddress = ensureAddress(server)
|
||||||
|
await register(app, wsApp, server, args)
|
||||||
|
|
||||||
|
logger.info(`Using config file ${humanPath(args.config)}`)
|
||||||
|
logger.info(`HTTP server listening on ${serverAddress} ${args.link ? "(randomized by --link)" : ""}`)
|
||||||
|
if (args.auth === AuthType.Password) {
|
||||||
|
logger.info(" - Authentication is enabled")
|
||||||
|
if (args.usingEnvPassword) {
|
||||||
|
logger.info(" - Using password from $PASSWORD")
|
||||||
|
} else if (args.usingEnvHashedPassword) {
|
||||||
|
logger.info(" - Using password from $HASHED_PASSWORD")
|
||||||
|
} else {
|
||||||
|
logger.info(` - Using password from ${humanPath(args.config)}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info(` - Authentication is disabled ${args.link ? "(disabled by --link)" : ""}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.cert) {
|
||||||
|
logger.info(` - Using certificate for HTTPS: ${humanPath(args.cert.value)}`)
|
||||||
|
} else {
|
||||||
|
logger.info(` - Not serving HTTPS ${args.link ? "(disabled by --link)" : ""}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args["proxy-domain"].length > 0) {
|
||||||
|
logger.info(` - ${plural(args["proxy-domain"].length, "Proxying the following domain")}:`)
|
||||||
|
args["proxy-domain"].forEach((domain) => logger.info(` - *.${domain}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.link) {
|
||||||
|
await coderCloudBind(serverAddress.replace(/^https?:\/\//, ""), args.link.value)
|
||||||
|
logger.info(" - Connected to cloud agent")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.enable && args.enable.length > 0) {
|
||||||
|
logger.info("Enabling the following experimental features:")
|
||||||
|
args.enable.forEach((feature) => {
|
||||||
|
if (Object.values(Feature).includes(feature as Feature)) {
|
||||||
|
logger.info(` - "${feature}"`)
|
||||||
|
} else {
|
||||||
|
logger.error(` X "${feature}" (unknown feature)`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// TODO: Could be nice to add wrapping to the logger?
|
||||||
|
logger.info(
|
||||||
|
" The code-server project does not provide stability guarantees or commit to fixing bugs relating to these experimental features. When filing bug reports, please ensure that you can reproduce the bug with all experimental features turned off.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args.socket && args.open) {
|
||||||
|
// The web socket doesn't seem to work if browsing with 0.0.0.0.
|
||||||
|
const openAddress = serverAddress.replace("://0.0.0.0", "://localhost")
|
||||||
|
try {
|
||||||
|
await open(openAddress)
|
||||||
|
logger.info(`Opened ${openAddress}`)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to open", field("address", openAddress), field("error", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return server
|
||||||
|
}
|
|
@ -7,7 +7,8 @@ import * as ipc from "../../../typings/ipc"
|
||||||
import { Emitter } from "../../common/emitter"
|
import { Emitter } from "../../common/emitter"
|
||||||
import { HttpCode, HttpError } from "../../common/http"
|
import { HttpCode, HttpError } from "../../common/http"
|
||||||
import { getFirstString } from "../../common/util"
|
import { getFirstString } from "../../common/util"
|
||||||
import { commit, rootPath, version } from "../constants"
|
import { Feature } from "../cli"
|
||||||
|
import { isDevMode, rootPath, version } from "../constants"
|
||||||
import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http"
|
import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http"
|
||||||
import { getMediaMime, pathToFsPath } from "../util"
|
import { getMediaMime, pathToFsPath } from "../util"
|
||||||
import { VscodeProvider } from "../vscode"
|
import { VscodeProvider } from "../vscode"
|
||||||
|
@ -31,7 +32,7 @@ router.get("/", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
return await vscode.initialize({ args: req.args, remoteAuthority: req.headers.host || "" }, req.query)
|
return await vscode.initialize({ args: req.args, remoteAuthority: req.headers.host || "" }, req.query)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const devMessage = commit === "development" ? "It might not have finished compiling." : ""
|
const devMessage = isDevMode ? "It might not have finished compiling." : ""
|
||||||
throw new Error(`VS Code failed to load. ${devMessage} ${error.message}`)
|
throw new Error(`VS Code failed to load. ${devMessage} ${error.message}`)
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
|
@ -44,7 +45,7 @@ router.get("/", async (req, res) => {
|
||||||
req,
|
req,
|
||||||
// Uncomment prod blocks if not in development. TODO: Would this be
|
// Uncomment prod blocks if not in development. TODO: Would this be
|
||||||
// better as a build step? Or maintain two HTML files again?
|
// better as a build step? Or maintain two HTML files again?
|
||||||
commit !== "development" ? content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "") : content,
|
!isDevMode ? content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "") : content,
|
||||||
{
|
{
|
||||||
authed: req.args.auth !== "none",
|
authed: req.args.auth !== "none",
|
||||||
disableTelemetry: !!req.args["disable-telemetry"],
|
disableTelemetry: !!req.args["disable-telemetry"],
|
||||||
|
@ -209,14 +210,21 @@ wsRouter.ws("/", ensureAuthenticated, async (req) => {
|
||||||
`Sec-WebSocket-Accept: ${reply}`,
|
`Sec-WebSocket-Accept: ${reply}`,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// See if the browser reports it supports web socket compression.
|
||||||
// TODO: Parse this header properly.
|
// TODO: Parse this header properly.
|
||||||
const extensions = req.headers["sec-websocket-extensions"]
|
const extensions = req.headers["sec-websocket-extensions"]
|
||||||
const permessageDeflate = extensions ? extensions.includes("permessage-deflate") : false
|
const isCompressionSupported = extensions ? extensions.includes("permessage-deflate") : false
|
||||||
if (permessageDeflate) {
|
|
||||||
|
// TODO: For now we only use compression if the user enables it.
|
||||||
|
const isCompressionEnabled = !!req.args.enable?.includes(Feature.PermessageDeflate)
|
||||||
|
|
||||||
|
const useCompression = isCompressionEnabled && isCompressionSupported
|
||||||
|
if (useCompression) {
|
||||||
|
// This response header tells the browser the server supports compression.
|
||||||
responseHeaders.push("Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=15")
|
responseHeaders.push("Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=15")
|
||||||
}
|
}
|
||||||
|
|
||||||
req.ws.write(responseHeaders.join("\r\n") + "\r\n\r\n")
|
req.ws.write(responseHeaders.join("\r\n") + "\r\n\r\n")
|
||||||
|
|
||||||
await vscode.sendWebsocket(req.ws, req.query, permessageDeflate)
|
await vscode.sendWebsocket(req.ws, req.query, useCompression)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { test, expect } from "@playwright/test"
|
import { expect, test } from "@playwright/test"
|
||||||
import * as cp from "child_process"
|
import * as cp from "child_process"
|
||||||
import * as fs from "fs"
|
import * as fs from "fs"
|
||||||
// import { tmpdir } from "os"
|
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import util from "util"
|
import util from "util"
|
||||||
import { STORAGE, tmpdir } from "../utils/constants"
|
import { STORAGE } from "../utils/constants"
|
||||||
|
import { tmpdir } from "../utils/helpers"
|
||||||
import { CodeServer } from "./models/CodeServer"
|
import { CodeServer } from "./models/CodeServer"
|
||||||
|
|
||||||
test.describe("Integrated Terminal", () => {
|
test.describe("Integrated Terminal", () => {
|
||||||
|
|
|
@ -39,6 +39,10 @@ describe("parser", () => {
|
||||||
it("should parse all available options", () => {
|
it("should parse all available options", () => {
|
||||||
expect(
|
expect(
|
||||||
parse([
|
parse([
|
||||||
|
"--enable",
|
||||||
|
"feature1",
|
||||||
|
"--enable",
|
||||||
|
"feature2",
|
||||||
"--bind-addr=192.169.0.1:8080",
|
"--bind-addr=192.169.0.1:8080",
|
||||||
"--auth",
|
"--auth",
|
||||||
"none",
|
"none",
|
||||||
|
@ -82,6 +86,7 @@ describe("parser", () => {
|
||||||
cert: {
|
cert: {
|
||||||
value: path.resolve("baz"),
|
value: path.resolve("baz"),
|
||||||
},
|
},
|
||||||
|
enable: ["feature1", "feature2"],
|
||||||
"extensions-dir": path.resolve("foo"),
|
"extensions-dir": path.resolve("foo"),
|
||||||
"extra-builtin-extensions-dir": [path.resolve("bazzle")],
|
"extra-builtin-extensions-dir": [path.resolve("bazzle")],
|
||||||
"extra-extensions-dir": [path.resolve("nozzle")],
|
"extra-extensions-dir": [path.resolve("nozzle")],
|
||||||
|
|
|
@ -1,68 +1,76 @@
|
||||||
import * as fs from "fs"
|
import { createLoggerMock } from "../utils/helpers"
|
||||||
import { commit, getPackageJson, version } from "../../src/node/constants"
|
|
||||||
import { tmpdir } from "../../test/utils/constants"
|
|
||||||
import { loggerModule } from "../utils/helpers"
|
|
||||||
|
|
||||||
// jest.mock is hoisted above the imports so we must use `require` here.
|
|
||||||
jest.mock("@coder/logger", () => require("../utils/helpers").loggerModule)
|
|
||||||
|
|
||||||
describe("constants", () => {
|
describe("constants", () => {
|
||||||
describe("getPackageJson", () => {
|
let constants: typeof import("../../src/node/constants")
|
||||||
afterEach(() => {
|
|
||||||
jest.clearAllMocks()
|
describe("with package.json defined", () => {
|
||||||
|
const loggerModule = createLoggerMock()
|
||||||
|
const mockPackageJson = {
|
||||||
|
name: "mock-code-server",
|
||||||
|
description: "Run VS Code on a remote server.",
|
||||||
|
repository: "https://github.com/cdr/code-server",
|
||||||
|
version: "1.0.0",
|
||||||
|
commit: "f6b2be2838f4afb217c2fd8f03eafedd8d55ef9b",
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.mock("@coder/logger", () => loggerModule)
|
||||||
|
jest.mock("../../package.json", () => mockPackageJson, { virtual: true })
|
||||||
|
constants = require("../../src/node/constants")
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
jest.restoreAllMocks()
|
jest.clearAllMocks()
|
||||||
|
jest.resetModules()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should log a warning if package.json not found", () => {
|
it("should provide the commit", () => {
|
||||||
const expectedErrorMessage = "Cannot find module './package.json' from 'src/node/constants.ts'"
|
expect(constants.commit).toBe(mockPackageJson.commit)
|
||||||
|
|
||||||
getPackageJson("./package.json")
|
|
||||||
|
|
||||||
expect(loggerModule.logger.warn).toHaveBeenCalled()
|
|
||||||
expect(loggerModule.logger.warn).toHaveBeenCalledWith(expectedErrorMessage)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should find the package.json", () => {
|
|
||||||
// the function calls require from src/node/constants
|
|
||||||
// so to get the root package.json we need to use ../../
|
|
||||||
const packageJson = getPackageJson("../../package.json")
|
|
||||||
expect(Object.keys(packageJson).length).toBeGreaterThan(0)
|
|
||||||
expect(packageJson.name).toBe("code-server")
|
|
||||||
expect(packageJson.description).toBe("Run VS Code on a remote server.")
|
|
||||||
expect(packageJson.repository).toBe("https://github.com/cdr/code-server")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
describe("version", () => {
|
|
||||||
it("should return the package.json version", () => {
|
it("should return the package.json version", () => {
|
||||||
// Source: https://gist.github.com/jhorsman/62eeea161a13b80e39f5249281e17c39#gistcomment-2896416
|
expect(constants.version).toBe(mockPackageJson.version)
|
||||||
const validSemVar = new RegExp("^(0|[1-9]d*).(0|[1-9]d*).(0|[1-9]d*)")
|
})
|
||||||
const isValidSemVar = validSemVar.test(version)
|
|
||||||
expect(version).not.toBe(null)
|
describe("getPackageJson", () => {
|
||||||
expect(isValidSemVar).toBe(true)
|
it("should log a warning if package.json not found", () => {
|
||||||
|
const expectedErrorMessage = "Cannot find module './package.json' from 'src/node/constants.ts'"
|
||||||
|
|
||||||
|
constants.getPackageJson("./package.json")
|
||||||
|
|
||||||
|
expect(loggerModule.logger.warn).toHaveBeenCalled()
|
||||||
|
expect(loggerModule.logger.warn).toHaveBeenCalledWith(expectedErrorMessage)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should find the package.json", () => {
|
||||||
|
// the function calls require from src/node/constants
|
||||||
|
// so to get the root package.json we need to use ../../
|
||||||
|
const packageJson = constants.getPackageJson("../../package.json")
|
||||||
|
expect(packageJson).toStrictEqual(mockPackageJson)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("commit", () => {
|
describe("with incomplete package.json", () => {
|
||||||
it("should return 'development' if commit is undefined", () => {
|
const mockPackageJson = {
|
||||||
// In development, the commit is not stored in our package.json
|
name: "mock-code-server",
|
||||||
// But when we build code-server and release it, it is
|
}
|
||||||
expect(commit).toBe("development")
|
|
||||||
})
|
beforeAll(() => {
|
||||||
})
|
jest.mock("../../package.json", () => mockPackageJson, { virtual: true })
|
||||||
})
|
constants = require("../../src/node/constants")
|
||||||
|
})
|
||||||
describe("test constants", () => {
|
|
||||||
describe("tmpdir", () => {
|
afterAll(() => {
|
||||||
it("should return a temp directory", async () => {
|
jest.clearAllMocks()
|
||||||
const testName = "temp-dir"
|
jest.resetModules()
|
||||||
const pathToTempDir = await tmpdir(testName)
|
})
|
||||||
|
|
||||||
expect(pathToTempDir).toContain(testName)
|
it("version should return 'development'", () => {
|
||||||
|
expect(constants.version).toBe("development")
|
||||||
await fs.promises.rmdir(pathToTempDir)
|
})
|
||||||
|
it("commit should return 'development'", () => {
|
||||||
|
expect(constants.commit).toBe("development")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { promises as fs } from "fs"
|
||||||
|
import { tmpdir } from "../../test/utils/helpers"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is for testing test helpers (not core code).
|
||||||
|
*/
|
||||||
|
describe("test helpers", () => {
|
||||||
|
it("should return a temp directory", async () => {
|
||||||
|
const testName = "temp-dir"
|
||||||
|
const pathToTempDir = await tmpdir(testName)
|
||||||
|
expect(pathToTempDir).toContain(testName)
|
||||||
|
expect(fs.access(pathToTempDir)).resolves.toStrictEqual(undefined)
|
||||||
|
})
|
||||||
|
})
|
|
@ -37,7 +37,7 @@ describe("proxy", () => {
|
||||||
e.get("/wsup", (req, res) => {
|
e.get("/wsup", (req, res) => {
|
||||||
res.json("asher is the best")
|
res.json("asher is the best")
|
||||||
})
|
})
|
||||||
;[, , codeServer] = await integration.setup(["--auth=none"], "")
|
codeServer = await integration.setup(["--auth=none"], "")
|
||||||
const resp = await codeServer.fetch(proxyPath)
|
const resp = await codeServer.fetch(proxyPath)
|
||||||
expect(resp.status).toBe(200)
|
expect(resp.status).toBe(200)
|
||||||
const json = await resp.json()
|
const json = await resp.json()
|
||||||
|
@ -48,7 +48,7 @@ describe("proxy", () => {
|
||||||
e.get(absProxyPath, (req, res) => {
|
e.get(absProxyPath, (req, res) => {
|
||||||
res.json("joe is the best")
|
res.json("joe is the best")
|
||||||
})
|
})
|
||||||
;[, , codeServer] = await integration.setup(["--auth=none"], "")
|
codeServer = await integration.setup(["--auth=none"], "")
|
||||||
const resp = await codeServer.fetch(absProxyPath)
|
const resp = await codeServer.fetch(absProxyPath)
|
||||||
expect(resp.status).toBe(200)
|
expect(resp.status).toBe(200)
|
||||||
const json = await resp.json()
|
const json = await resp.json()
|
||||||
|
@ -62,7 +62,7 @@ describe("proxy", () => {
|
||||||
e.post("/finale", (req, res) => {
|
e.post("/finale", (req, res) => {
|
||||||
res.json("redirect success")
|
res.json("redirect success")
|
||||||
})
|
})
|
||||||
;[, , codeServer] = await integration.setup(["--auth=none"], "")
|
codeServer = await integration.setup(["--auth=none"], "")
|
||||||
const resp = await codeServer.fetch(proxyPath, {
|
const resp = await codeServer.fetch(proxyPath, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
})
|
})
|
||||||
|
@ -78,7 +78,7 @@ describe("proxy", () => {
|
||||||
e.post(finalePath, (req, res) => {
|
e.post(finalePath, (req, res) => {
|
||||||
res.json("redirect success")
|
res.json("redirect success")
|
||||||
})
|
})
|
||||||
;[, , codeServer] = await integration.setup(["--auth=none"], "")
|
codeServer = await integration.setup(["--auth=none"], "")
|
||||||
const resp = await codeServer.fetch(absProxyPath, {
|
const resp = await codeServer.fetch(absProxyPath, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
})
|
})
|
||||||
|
@ -91,7 +91,7 @@ describe("proxy", () => {
|
||||||
e.post("/wsup", (req, res) => {
|
e.post("/wsup", (req, res) => {
|
||||||
res.json(req.body)
|
res.json(req.body)
|
||||||
})
|
})
|
||||||
;[, , codeServer] = await integration.setup(["--auth=none"], "")
|
codeServer = await integration.setup(["--auth=none"], "")
|
||||||
const resp = await codeServer.fetch(proxyPath, {
|
const resp = await codeServer.fetch(proxyPath, {
|
||||||
method: "post",
|
method: "post",
|
||||||
body: JSON.stringify("coder is the best"),
|
body: JSON.stringify("coder is the best"),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { JSDOM } from "jsdom"
|
import { JSDOM } from "jsdom"
|
||||||
import { registerServiceWorker } from "../../src/browser/register"
|
import { registerServiceWorker } from "../../src/browser/register"
|
||||||
import { loggerModule } from "../utils/helpers"
|
import { createLoggerMock } from "../utils/helpers"
|
||||||
import { LocationLike } from "./util.test"
|
import { LocationLike } from "./util.test"
|
||||||
|
|
||||||
describe("register", () => {
|
describe("register", () => {
|
||||||
|
@ -21,6 +21,7 @@ describe("register", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const loggerModule = createLoggerMock()
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
jest.mock("@coder/logger", () => loggerModule)
|
jest.mock("@coder/logger", () => loggerModule)
|
||||||
|
@ -75,6 +76,7 @@ describe("register", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("when navigator and serviceWorker are NOT defined", () => {
|
describe("when navigator and serviceWorker are NOT defined", () => {
|
||||||
|
const loggerModule = createLoggerMock()
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
jest.mock("@coder/logger", () => loggerModule)
|
jest.mock("@coder/logger", () => loggerModule)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as httpserver from "../utils/httpserver"
|
import * as httpserver from "../../utils/httpserver"
|
||||||
import * as integration from "../utils/integration"
|
import * as integration from "../../utils/integration"
|
||||||
|
|
||||||
describe("health", () => {
|
describe("health", () => {
|
||||||
let codeServer: httpserver.HttpServer | undefined
|
let codeServer: httpserver.HttpServer | undefined
|
||||||
|
@ -12,7 +12,7 @@ describe("health", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("/healthz", async () => {
|
it("/healthz", async () => {
|
||||||
;[, , codeServer] = await integration.setup(["--auth=none"], "")
|
codeServer = await integration.setup(["--auth=none"], "")
|
||||||
const resp = await codeServer.fetch("/healthz")
|
const resp = await codeServer.fetch("/healthz")
|
||||||
expect(resp.status).toBe(200)
|
expect(resp.status).toBe(200)
|
||||||
const json = await resp.json()
|
const json = await resp.json()
|
||||||
|
@ -20,7 +20,7 @@ describe("health", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("/healthz (websocket)", async () => {
|
it("/healthz (websocket)", async () => {
|
||||||
;[, , codeServer] = await integration.setup(["--auth=none"], "")
|
codeServer = await integration.setup(["--auth=none"], "")
|
||||||
const ws = codeServer.ws("/healthz")
|
const ws = codeServer.ws("/healthz")
|
||||||
const message = await new Promise((resolve, reject) => {
|
const message = await new Promise((resolve, reject) => {
|
||||||
ws.on("error", console.error)
|
ws.on("error", console.error)
|
|
@ -0,0 +1,136 @@
|
||||||
|
import { promises as fs } from "fs"
|
||||||
|
import * as path from "path"
|
||||||
|
import { tmpdir } from "../../utils/helpers"
|
||||||
|
import * as httpserver from "../../utils/httpserver"
|
||||||
|
import * as integration from "../../utils/integration"
|
||||||
|
|
||||||
|
describe("/static", () => {
|
||||||
|
let _codeServer: httpserver.HttpServer | undefined
|
||||||
|
function codeServer(): httpserver.HttpServer {
|
||||||
|
if (!_codeServer) {
|
||||||
|
throw new Error("tried to use code-server before setting it up")
|
||||||
|
}
|
||||||
|
return _codeServer
|
||||||
|
}
|
||||||
|
|
||||||
|
let testFile: string | undefined
|
||||||
|
let testFileContent: string | undefined
|
||||||
|
let nonExistentTestFile: string | undefined
|
||||||
|
|
||||||
|
// The static endpoint expects a commit and then the full path of the file.
|
||||||
|
// The commit is just for cache busting so we can use anything we want. `-`
|
||||||
|
// and `development` are specially recognized in that they will cause the
|
||||||
|
// static endpoint to avoid sending cache headers.
|
||||||
|
const commit = "-"
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const testDir = await tmpdir("static")
|
||||||
|
testFile = path.join(testDir, "test")
|
||||||
|
testFileContent = "static file contents"
|
||||||
|
nonExistentTestFile = path.join(testDir, "i-am-not-here")
|
||||||
|
await fs.writeFile(testFile, testFileContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (_codeServer) {
|
||||||
|
await _codeServer.close()
|
||||||
|
_codeServer = undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function commonTests() {
|
||||||
|
it("should return a 404 when a commit and file are not provided", async () => {
|
||||||
|
const resp = await codeServer().fetch("/static")
|
||||||
|
expect(resp.status).toBe(404)
|
||||||
|
|
||||||
|
const content = await resp.json()
|
||||||
|
expect(content).toStrictEqual({ error: "Not Found" })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return a 404 when a file is not provided", async () => {
|
||||||
|
const resp = await codeServer().fetch(`/static/${commit}`)
|
||||||
|
expect(resp.status).toBe(404)
|
||||||
|
|
||||||
|
const content = await resp.json()
|
||||||
|
expect(content).toStrictEqual({ error: "Not Found" })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("disabled authentication", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
_codeServer = await integration.setup(["--auth=none"], "")
|
||||||
|
})
|
||||||
|
|
||||||
|
commonTests()
|
||||||
|
|
||||||
|
it("should return a 404 for a nonexistent file", async () => {
|
||||||
|
const resp = await codeServer().fetch(`/static/${commit}/${nonExistentTestFile}`)
|
||||||
|
expect(resp.status).toBe(404)
|
||||||
|
|
||||||
|
const content = await resp.json()
|
||||||
|
expect(content.error).toMatch("ENOENT")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return a 200 and file contents for an existent file", async () => {
|
||||||
|
const resp = await codeServer().fetch(`/static/${commit}${testFile}`)
|
||||||
|
expect(resp.status).toBe(200)
|
||||||
|
|
||||||
|
const content = await resp.text()
|
||||||
|
expect(content).toStrictEqual(testFileContent)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("enabled authentication", () => {
|
||||||
|
// Store whatever might be in here so we can restore it afterward.
|
||||||
|
// TODO: We should probably pass this as an argument somehow instead of
|
||||||
|
// manipulating the environment.
|
||||||
|
const previousEnvPassword = process.env.PASSWORD
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
process.env.PASSWORD = "test"
|
||||||
|
_codeServer = await integration.setup(["--auth=password"], "")
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env.PASSWORD = previousEnvPassword
|
||||||
|
})
|
||||||
|
|
||||||
|
commonTests()
|
||||||
|
|
||||||
|
describe("inside code-server root", () => {
|
||||||
|
it("should return a 404 for a nonexistent file", async () => {
|
||||||
|
const resp = await codeServer().fetch(`/static/${commit}/${__filename}-does-not-exist`)
|
||||||
|
expect(resp.status).toBe(404)
|
||||||
|
|
||||||
|
const content = await resp.json()
|
||||||
|
expect(content.error).toMatch("ENOENT")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return a 200 and file contents for an existent file", async () => {
|
||||||
|
const resp = await codeServer().fetch(`/static/${commit}${__filename}`)
|
||||||
|
expect(resp.status).toBe(200)
|
||||||
|
|
||||||
|
const content = await resp.text()
|
||||||
|
expect(content).toStrictEqual(await fs.readFile(__filename, "utf8"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("outside code-server root", () => {
|
||||||
|
it("should return a 401 for a nonexistent file", async () => {
|
||||||
|
const resp = await codeServer().fetch(`/static/${commit}/${nonExistentTestFile}`)
|
||||||
|
expect(resp.status).toBe(401)
|
||||||
|
|
||||||
|
const content = await resp.json()
|
||||||
|
expect(content).toStrictEqual({ error: "Unauthorized" })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return a 401 for an existent file", async () => {
|
||||||
|
const resp = await codeServer().fetch(`/static/${commit}${testFile}`)
|
||||||
|
expect(resp.status).toBe(401)
|
||||||
|
|
||||||
|
const content = await resp.json()
|
||||||
|
expect(content).toStrictEqual({ error: "Unauthorized" })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -11,7 +11,7 @@ import {
|
||||||
trimSlashes,
|
trimSlashes,
|
||||||
normalize,
|
normalize,
|
||||||
} from "../../src/common/util"
|
} from "../../src/common/util"
|
||||||
import { loggerModule } from "../utils/helpers"
|
import { createLoggerMock } from "../utils/helpers"
|
||||||
|
|
||||||
const dom = new JSDOM()
|
const dom = new JSDOM()
|
||||||
global.document = dom.window.document
|
global.document = dom.window.document
|
||||||
|
@ -229,6 +229,8 @@ describe("util", () => {
|
||||||
jest.restoreAllMocks()
|
jest.restoreAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const loggerModule = createLoggerMock()
|
||||||
|
|
||||||
it("should log an error with the message and stack trace", () => {
|
it("should log an error with the message and stack trace", () => {
|
||||||
const message = "You don't have access to that folder."
|
const message = "You don't have access to that folder."
|
||||||
const error = new Error(message)
|
const error = new Error(message)
|
||||||
|
|
|
@ -1,14 +1,3 @@
|
||||||
import * as fs from "fs"
|
|
||||||
import * as os from "os"
|
|
||||||
import * as path from "path"
|
|
||||||
|
|
||||||
export const CODE_SERVER_ADDRESS = process.env.CODE_SERVER_ADDRESS || "http://localhost:8080"
|
export const CODE_SERVER_ADDRESS = process.env.CODE_SERVER_ADDRESS || "http://localhost:8080"
|
||||||
export const PASSWORD = process.env.PASSWORD || "e45432jklfdsab"
|
export const PASSWORD = process.env.PASSWORD || "e45432jklfdsab"
|
||||||
export const STORAGE = process.env.STORAGE || ""
|
export const STORAGE = process.env.STORAGE || ""
|
||||||
|
|
||||||
export async function tmpdir(testName: string): Promise<string> {
|
|
||||||
const dir = path.join(os.tmpdir(), "code-server")
|
|
||||||
await fs.promises.mkdir(dir, { recursive: true })
|
|
||||||
|
|
||||||
return await fs.promises.mkdtemp(path.join(dir, `test-${testName}-`), { encoding: "utf8" })
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,11 +1,32 @@
|
||||||
export const loggerModule = {
|
import * as fs from "fs"
|
||||||
field: jest.fn(),
|
import * as os from "os"
|
||||||
level: 2,
|
import * as path from "path"
|
||||||
logger: {
|
|
||||||
debug: jest.fn(),
|
/**
|
||||||
error: jest.fn(),
|
* Return a mock of @coder/logger.
|
||||||
info: jest.fn(),
|
*/
|
||||||
trace: jest.fn(),
|
export function createLoggerMock() {
|
||||||
warn: jest.fn(),
|
return {
|
||||||
},
|
field: jest.fn(),
|
||||||
|
level: 2,
|
||||||
|
logger: {
|
||||||
|
debug: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
trace: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a uniquely named temporary directory.
|
||||||
|
*
|
||||||
|
* These directories are placed under a single temporary code-server directory.
|
||||||
|
*/
|
||||||
|
export async function tmpdir(testName: string): Promise<string> {
|
||||||
|
const dir = path.join(os.tmpdir(), "code-server/tests")
|
||||||
|
await fs.promises.mkdir(dir, { recursive: true })
|
||||||
|
|
||||||
|
return await fs.promises.mkdtemp(path.join(dir, `${testName}-`), { encoding: "utf8" })
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,15 @@
|
||||||
import * as express from "express"
|
import { parse, parseConfigFile, setDefaults } from "../../src/node/cli"
|
||||||
import { createApp } from "../../src/node/app"
|
import { runCodeServer } from "../../src/node/main"
|
||||||
import { parse, setDefaults, parseConfigFile, DefaultedArgs } from "../../src/node/cli"
|
|
||||||
import { register } from "../../src/node/routes"
|
|
||||||
import * as httpserver from "./httpserver"
|
import * as httpserver from "./httpserver"
|
||||||
|
|
||||||
export async function setup(
|
export async function setup(argv: string[], configFile?: string): Promise<httpserver.HttpServer> {
|
||||||
argv: string[],
|
argv = ["--bind-addr=localhost:0", "--log=warn", ...argv]
|
||||||
configFile?: string,
|
|
||||||
): Promise<[express.Application, express.Application, httpserver.HttpServer, DefaultedArgs]> {
|
|
||||||
argv = ["--bind-addr=localhost:0", ...argv]
|
|
||||||
|
|
||||||
const cliArgs = parse(argv)
|
const cliArgs = parse(argv)
|
||||||
const configArgs = parseConfigFile(configFile || "", "test/integration.ts")
|
const configArgs = parseConfigFile(configFile || "", "test/integration.ts")
|
||||||
const args = await setDefaults(cliArgs, configArgs)
|
const args = await setDefaults(cliArgs, configArgs)
|
||||||
|
|
||||||
const [app, wsApp, server] = await createApp(args)
|
const server = await runCodeServer(args)
|
||||||
await register(app, wsApp, server, args)
|
|
||||||
|
|
||||||
return [app, wsApp, new httpserver.HttpServer(server), args]
|
return new httpserver.HttpServer(server)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3462,9 +3462,9 @@ lodash.sortby@^4.7.0:
|
||||||
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
||||||
|
|
||||||
lodash@^4.17.19:
|
lodash@^4.17.19:
|
||||||
version "4.17.20"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
|
||||||
lru-cache@^6.0.0:
|
lru-cache@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
|
|
Loading…
Reference in New Issue