├── .node-version ├── packages ├── web │ ├── .gitignore │ ├── src │ │ ├── index.ts │ │ ├── index.html │ │ └── index.scss │ ├── assets │ │ └── logo.png │ ├── yarn.lock │ ├── package.json │ └── webpack.config.js ├── ide │ ├── src │ │ ├── fill │ │ │ ├── empty.ts │ │ │ ├── fs.ts │ │ │ ├── net.ts │ │ │ ├── trash.ts │ │ │ ├── child_process.ts │ │ │ ├── util.ts │ │ │ ├── webview.html │ │ │ ├── os.ts │ │ │ ├── dialog.scss │ │ │ └── notification.ts │ │ └── index.ts │ ├── yarn.lock │ └── package.json ├── vscode │ ├── .gitignore │ ├── src │ │ ├── index.ts │ │ ├── fill │ │ │ ├── graceful-fs.ts │ │ │ ├── native-watchdog.ts │ │ │ ├── package.ts │ │ │ ├── spdlog.ts │ │ │ ├── node-pty.ts │ │ │ ├── ripgrep.ts │ │ │ ├── native-keymap.ts │ │ │ ├── css.js │ │ │ ├── amd.ts │ │ │ ├── labels.ts │ │ │ ├── mouseEvent.ts │ │ │ ├── environmentService.ts │ │ │ ├── codeEditor.ts │ │ │ ├── stdioElectron.ts │ │ │ ├── dom.ts │ │ │ ├── platform.ts │ │ │ ├── vscodeTextmate.ts │ │ │ ├── menuRegistry.ts │ │ │ ├── iconv-lite.ts │ │ │ ├── workspacesService.ts │ │ │ ├── workbenchRegistry.ts │ │ │ ├── product.ts │ │ │ └── paste.ts │ │ ├── vscode.scss │ │ └── dialog.scss │ ├── test │ │ ├── test-extension.vsix │ │ └── zip.test.ts │ ├── package.json │ └── webpack.bootstrap.config.js ├── events │ ├── src │ │ ├── index.ts │ │ └── events.ts │ ├── package.json │ ├── yarn.lock │ └── test │ │ └── events.test.ts ├── logger │ ├── src │ │ ├── index.ts │ │ ├── extender.test.ts │ │ ├── logger.test.ts │ │ └── extender.ts │ ├── .npmignore │ ├── tsconfig.build.json │ ├── package.json │ ├── README.md │ └── webpack.config.js ├── requirefs │ ├── test │ │ ├── lib │ │ │ ├── scope.js │ │ │ ├── chained-3.js │ │ │ ├── individual.js │ │ │ ├── chained-1.js │ │ │ ├── chained-2.js │ │ │ ├── customModule.js │ │ │ ├── subfolder │ │ │ │ ├── oranges.js │ │ │ │ └── goingUp.js │ │ │ ├── node_modules │ │ │ │ └── frogger │ │ │ │ │ └── index.js │ │ │ ├── subfolder.js │ │ │ └── nodeResolve.js │ │ ├── .gitignore │ │ ├── requirefs.test.ts │ │ └── requirefs.bench.ts │ ├── src │ │ └── index.ts │ └── package.json ├── runner │ ├── src │ │ └── index.ts │ ├── package.json │ └── yarn.lock ├── protocol │ ├── test │ │ ├── index.ts │ │ ├── forker.js │ │ ├── server.test.ts │ │ ├── trash.test.ts │ │ ├── spdlog.test.ts │ │ ├── helpers.ts │ │ ├── node-pty.test.ts │ │ ├── util.test.ts │ │ └── child_process.test.ts │ ├── src │ │ ├── proto │ │ │ ├── index.ts │ │ │ ├── vscode.proto │ │ │ ├── vscode_pb.d.ts │ │ │ ├── client.proto │ │ │ └── node.proto │ │ ├── index.ts │ │ ├── browser │ │ │ └── modules │ │ │ │ ├── index.ts │ │ │ │ ├── trash.ts │ │ │ │ ├── node-pty.ts │ │ │ │ └── spdlog.ts │ │ ├── node │ │ │ └── modules │ │ │ │ ├── index.ts │ │ │ │ ├── trash.ts │ │ │ │ ├── spdlog.ts │ │ │ │ ├── node-pty.ts │ │ │ │ ├── net.ts │ │ │ │ ├── child_process.ts │ │ │ │ └── stream.ts │ │ └── common │ │ │ └── connection.ts │ ├── scripts │ │ └── generate_proto.sh │ ├── package.json │ └── README.md ├── disposable │ ├── src │ │ ├── index.ts │ │ └── disposable.ts │ ├── package.json │ └── yarn.lock ├── tsconfig.json ├── tunnel │ ├── package.json │ ├── yarn.lock │ └── src │ │ ├── common.ts │ │ ├── client.ts │ │ └── server.ts ├── dns │ ├── app.yaml │ ├── Dockerfile │ ├── package.json │ ├── webpack.config.js │ ├── .gcloudignore │ └── src │ │ └── dns.ts ├── app │ ├── chrome │ │ ├── icon_128.png │ │ ├── package.json │ │ ├── src │ │ │ ├── background.ts │ │ │ ├── index.html │ │ │ ├── content.ts │ │ │ └── chome.ts │ │ ├── manifest.json │ │ ├── yarn.lock │ │ └── webpack.config.js │ ├── common │ │ ├── src │ │ │ ├── fonts │ │ │ │ ├── AktivGroteskBold.eot │ │ │ │ ├── AktivGroteskBold.ttf │ │ │ │ ├── AktivGroteskBold.woff │ │ │ │ ├── AktivGroteskBold.woff2 │ │ │ │ ├── AktivGroteskMedium.eot │ │ │ │ ├── AktivGroteskMedium.ttf │ │ │ │ ├── AktivGroteskMedium.woff │ │ │ │ ├── AktivGroteskRegular.eot │ │ │ │ ├── AktivGroteskRegular.ttf │ │ │ │ ├── AktivGroteskMedium.woff2 │ │ │ │ ├── AktivGroteskRegular.woff │ │ │ │ └── AktivGroteskRegular.woff2 │ │ │ ├── storage.ts │ │ │ ├── connection.ts │ │ │ ├── tooltip.scss │ │ │ └── app.tsx │ │ └── package.json │ └── browser │ │ ├── package.json │ │ ├── webpack.config.js │ │ └── src │ │ ├── app.html │ │ ├── app.ts │ │ └── app.scss ├── ide-api │ ├── yarn.lock │ ├── README.md │ └── package.json ├── server │ ├── .gitignore │ ├── README.md │ ├── src │ │ ├── constants.ts │ │ ├── ipc.ts │ │ └── portScanner.ts │ ├── webpack.config.js │ ├── scripts │ │ └── nbin.ts │ └── package.json └── package.json ├── README.md ├── .github ├── CODEOWNERS ├── pull_request_template.md └── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── question.md │ ├── extension_bug.md │ └── bug_report.md ├── doc ├── assets │ ├── cli.png │ ├── ide.png │ ├── cros.png │ ├── release.gif │ ├── aws_ubuntu.png │ ├── chrome_warning.png │ ├── logo-horizontal.png │ └── server-password-modal.png ├── security │ ├── code-server.fail2ban.conf │ ├── fail2ban.md │ └── ssl.md ├── admin │ └── install │ │ ├── digitalocean.md │ │ └── google_cloud.md └── self-hosted │ └── cros-install.md ├── scripts ├── dummy.js ├── webpack.node.config.js ├── vstar.sh ├── test-setup.js ├── build.sh ├── install-packages.ts ├── webpack.client.config.js └── webpack.general.config.js ├── .gitignore ├── .dockerignore ├── rules ├── tsconfig.json └── src │ ├── curlyStatementNewlinesRule.ts │ └── noBlockPaddingRule.ts ├── tsconfig.json ├── deployment ├── deployment.yaml └── aws │ └── deployment.yaml ├── LICENSE ├── Dockerfile ├── .travis.yml ├── tslint.json └── package.json /.node-version: -------------------------------------------------------------------------------- 1 | 10.15.1 2 | -------------------------------------------------------------------------------- /packages/web/.gitignore: -------------------------------------------------------------------------------- 1 | out -------------------------------------------------------------------------------- /packages/ide/src/fill/empty.ts: -------------------------------------------------------------------------------- 1 | export = {}; 2 | -------------------------------------------------------------------------------- /packages/vscode/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | test/.test* -------------------------------------------------------------------------------- /packages/events/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./events"; 2 | -------------------------------------------------------------------------------- /packages/logger/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./logger"; 2 | -------------------------------------------------------------------------------- /packages/requirefs/test/lib/scope.js: -------------------------------------------------------------------------------- 1 | exports = coder.test; -------------------------------------------------------------------------------- /packages/runner/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./runner"; 2 | -------------------------------------------------------------------------------- /packages/vscode/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./client"; 2 | -------------------------------------------------------------------------------- /packages/protocol/test/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./helpers"; 2 | -------------------------------------------------------------------------------- /packages/requirefs/test/lib/chained-3.js: -------------------------------------------------------------------------------- 1 | exports.text = "moo"; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Free AI at api.airforce 2 | https://discord.gg/AJDsM7jtbq -------------------------------------------------------------------------------- /packages/disposable/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./disposable"; 2 | -------------------------------------------------------------------------------- /packages/requirefs/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./requirefs"; 2 | -------------------------------------------------------------------------------- /packages/requirefs/test/lib/individual.js: -------------------------------------------------------------------------------- 1 | exports.frog = "hi"; 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @code-asher @kylecarbs 2 | Dockerfile @nhooyr 3 | -------------------------------------------------------------------------------- /packages/requirefs/test/lib/chained-1.js: -------------------------------------------------------------------------------- 1 | exports = require("./chained-2"); -------------------------------------------------------------------------------- /packages/requirefs/test/lib/chained-2.js: -------------------------------------------------------------------------------- 1 | exports = require("./chained-3"); -------------------------------------------------------------------------------- /packages/requirefs/test/lib/customModule.js: -------------------------------------------------------------------------------- 1 | exports = require("donkey"); -------------------------------------------------------------------------------- /packages/requirefs/test/lib/subfolder/oranges.js: -------------------------------------------------------------------------------- 1 | exports.orange = "blue"; -------------------------------------------------------------------------------- /packages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/requirefs/test/.gitignore: -------------------------------------------------------------------------------- 1 | !lib/node_modules 2 | *.tar 3 | *.zip 4 | -------------------------------------------------------------------------------- /packages/requirefs/test/lib/node_modules/frogger/index.js: -------------------------------------------------------------------------------- 1 | exports.banana = "potato"; -------------------------------------------------------------------------------- /packages/requirefs/test/lib/subfolder/goingUp.js: -------------------------------------------------------------------------------- 1 | exports = require("../individual"); -------------------------------------------------------------------------------- /packages/web/src/index.ts: -------------------------------------------------------------------------------- 1 | import "./index.scss"; 2 | import "@coder/vscode"; 3 | -------------------------------------------------------------------------------- /doc/assets/cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/doc/assets/cli.png -------------------------------------------------------------------------------- /doc/assets/ide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/doc/assets/ide.png -------------------------------------------------------------------------------- /packages/logger/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tsconfig.build.json 3 | webpack.config.js 4 | yarn.lock -------------------------------------------------------------------------------- /packages/runner/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/runner", 3 | "main": "src/index.ts" 4 | } -------------------------------------------------------------------------------- /scripts/dummy.js: -------------------------------------------------------------------------------- 1 | // This is for ignoring CSS and images when running tests with Jest. 2 | -------------------------------------------------------------------------------- /doc/assets/cros.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/doc/assets/cros.png -------------------------------------------------------------------------------- /packages/tunnel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/tunnel", 3 | "main": "src/tunnel.ts" 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lib 2 | node_modules 3 | dist 4 | out 5 | .DS_Store 6 | release 7 | .vscode 8 | .cache 9 | -------------------------------------------------------------------------------- /doc/assets/release.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/doc/assets/release.gif -------------------------------------------------------------------------------- /packages/disposable/src/disposable.ts: -------------------------------------------------------------------------------- 1 | export interface IDisposable { 2 | dispose(): void; 3 | } 4 | -------------------------------------------------------------------------------- /packages/events/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/events", 3 | "main": "./src/index.ts" 4 | } 5 | -------------------------------------------------------------------------------- /packages/protocol/test/forker.js: -------------------------------------------------------------------------------- 1 | process.on("message", (data) => { 2 | process.send(data); 3 | }); 4 | -------------------------------------------------------------------------------- /packages/requirefs/test/lib/subfolder.js: -------------------------------------------------------------------------------- 1 | exports.orangeColor = require("./subfolder/oranges").orange; -------------------------------------------------------------------------------- /doc/assets/aws_ubuntu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/doc/assets/aws_ubuntu.png -------------------------------------------------------------------------------- /packages/disposable/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/disposable", 3 | "main": "src/index.ts" 4 | } 5 | -------------------------------------------------------------------------------- /packages/requirefs/test/lib/nodeResolve.js: -------------------------------------------------------------------------------- 1 | const frogger = require("frogger"); 2 | 3 | exports = frogger; -------------------------------------------------------------------------------- /packages/dns/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: nodejs10 2 | service: cdrdns 3 | network: 4 | forwarded_ports: 5 | - 53/udp -------------------------------------------------------------------------------- /packages/web/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/packages/web/assets/logo.png -------------------------------------------------------------------------------- /doc/assets/chrome_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/doc/assets/chrome_warning.png -------------------------------------------------------------------------------- /doc/assets/logo-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/doc/assets/logo-horizontal.png -------------------------------------------------------------------------------- /packages/app/chrome/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/packages/app/chrome/icon_128.png -------------------------------------------------------------------------------- /packages/vscode/src/fill/graceful-fs.ts: -------------------------------------------------------------------------------- 1 | export const gracefulify = (): void => undefined; 2 | 3 | export * from "fs"; 4 | -------------------------------------------------------------------------------- /packages/events/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/ide-api/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/ide/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/runner/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/tunnel/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/web/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /doc/assets/server-password-modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/doc/assets/server-password-modal.png -------------------------------------------------------------------------------- /packages/disposable/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/protocol/src/proto/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./client_pb"; 2 | export * from "./node_pb"; 3 | export * from "./vscode_pb"; 4 | -------------------------------------------------------------------------------- /packages/ide-api/README.md: -------------------------------------------------------------------------------- 1 | # ide-api 2 | 3 | Provides window listeners for interfacing with the IDE. 4 | 5 | Created for content-scripts. -------------------------------------------------------------------------------- /packages/vscode/test/test-extension.vsix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/packages/vscode/test/test-extension.vsix -------------------------------------------------------------------------------- /packages/tunnel/src/common.ts: -------------------------------------------------------------------------------- 1 | export enum TunnelCloseCode { 2 | Normal = 1000, 3 | Error = 4000, 4 | ConnectionRefused = 4001, 5 | } 6 | -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskBold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/packages/app/common/src/fonts/AktivGroteskBold.eot -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/packages/app/common/src/fonts/AktivGroteskBold.ttf -------------------------------------------------------------------------------- /packages/ide/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/ide", 3 | "description": "Browser-based IDE client abstraction.", 4 | "main": "src/index.ts" 5 | } 6 | -------------------------------------------------------------------------------- /packages/ide/src/fill/fs.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@coder/protocol"; 2 | import { client } from "./client"; 3 | 4 | export = client.modules[Module.Fs]; 5 | -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/packages/app/common/src/fonts/AktivGroteskBold.woff -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/packages/app/common/src/fonts/AktivGroteskBold.woff2 -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskMedium.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/packages/app/common/src/fonts/AktivGroteskMedium.eot -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskMedium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/packages/app/common/src/fonts/AktivGroteskMedium.ttf -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskMedium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/packages/app/common/src/fonts/AktivGroteskMedium.woff -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskRegular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/packages/app/common/src/fonts/AktivGroteskRegular.eot -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskRegular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/packages/app/common/src/fonts/AktivGroteskRegular.ttf -------------------------------------------------------------------------------- /packages/ide/src/fill/net.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@coder/protocol"; 2 | import { client } from "./client"; 3 | 4 | export = client.modules[Module.Net]; 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | # Docs 3 | doc/ 4 | # GitHub stuff 5 | .github 6 | .gitignore 7 | .travis.yml 8 | LICENSE 9 | README.md 10 | node_modules 11 | -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskMedium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/packages/app/common/src/fonts/AktivGroteskMedium.woff2 -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskRegular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/packages/app/common/src/fonts/AktivGroteskRegular.woff -------------------------------------------------------------------------------- /packages/app/common/src/fonts/AktivGroteskRegular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/code-server/master/packages/app/common/src/fonts/AktivGroteskRegular.woff2 -------------------------------------------------------------------------------- /packages/vscode/src/fill/native-watchdog.ts: -------------------------------------------------------------------------------- 1 | class Watchdog { 2 | public start(): void { 3 | // No action required. 4 | } 5 | } 6 | 7 | export = new Watchdog(); 8 | -------------------------------------------------------------------------------- /packages/ide/src/fill/trash.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@coder/protocol"; 2 | import { client } from "./client"; 3 | 4 | export = client.modules[Module.Trash].trash; 5 | -------------------------------------------------------------------------------- /packages/dns/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | COPY out/main.js /main.js 4 | COPY package.json /package.json 5 | RUN yarn 6 | ENV NODE_ENV production 7 | 8 | CMD ["node", "/main.js"] -------------------------------------------------------------------------------- /packages/ide/src/fill/child_process.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@coder/protocol"; 2 | import { client } from "./client"; 3 | 4 | export = client.modules[Module.ChildProcess]; 5 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/package.ts: -------------------------------------------------------------------------------- 1 | import * as packageJson from "../../../../lib/vscode/package.json"; 2 | export default { name: "vscode", version: packageJson.version }; 3 | -------------------------------------------------------------------------------- /packages/protocol/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./browser/client"; 2 | export * from "./common/connection"; 3 | export * from "./common/proxy"; 4 | export * from "./common/util"; 5 | -------------------------------------------------------------------------------- /packages/app/common/src/storage.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface StorageProvider { 3 | set(key: string, value: T): Promise; 4 | get(key: string): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/spdlog.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@coder/protocol"; 2 | import { client } from "@coder/ide/src/fill/client"; 3 | 4 | export = client.modules[Module.Spdlog]; 5 | -------------------------------------------------------------------------------- /packages/ide/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./client"; 2 | export * from "./fill/clipboard"; 3 | export * from "./fill/notification"; 4 | export * from "./retry"; 5 | export * from "./upload"; 6 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/node-pty.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@coder/protocol"; 2 | import { client } from "@coder/ide/src/fill/client"; 3 | 4 | export = client.modules[Module.NodePty]; 5 | -------------------------------------------------------------------------------- /packages/logger/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "declarationDir": "out", 5 | "declaration": true, 6 | "emitDeclarationOnly": true 7 | } 8 | } -------------------------------------------------------------------------------- /packages/ide/src/fill/util.ts: -------------------------------------------------------------------------------- 1 | export * from "../../../../node_modules/util"; 2 | import { implementation } from "../../../../node_modules/util.promisify"; 3 | 4 | export const promisify = implementation; 5 | -------------------------------------------------------------------------------- /packages/protocol/src/proto/vscode.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | // Sent when a shared process becomes active 4 | message SharedProcessActive { 5 | string socket_path = 1; 6 | string log_path = 2; 7 | } 8 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/ripgrep.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | // tslint:disable-next-line:no-any 4 | module.exports.rgPath = (global).RIPGREP_LOCATION || path.join(__dirname, "../bin/rg"); 5 | -------------------------------------------------------------------------------- /packages/protocol/src/browser/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./child_process"; 2 | export * from "./fs"; 3 | export * from "./net"; 4 | export * from "./node-pty"; 5 | export * from "./spdlog"; 6 | export * from "./trash"; 7 | -------------------------------------------------------------------------------- /packages/protocol/src/node/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./child_process"; 2 | export * from "./fs"; 3 | export * from "./net"; 4 | export * from "./node-pty"; 5 | export * from "./spdlog"; 6 | export * from "./trash"; 7 | -------------------------------------------------------------------------------- /packages/server/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | cli* 3 | !cli.ts 4 | build 5 | resources 6 | 7 | # This file is generated when the binary is created. 8 | # We want to use the parent tsconfig so we can ignore it. 9 | tsconfig.json 10 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Describe in detail the problem you had and how this PR fixes it 4 | 5 | ### Is there an open issue you can link to? 6 | 7 | -------------------------------------------------------------------------------- /packages/ide-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/ide-api", 3 | "version": "1.0.4", 4 | "typings": "api.d.ts", 5 | "author": "Coder", 6 | "license": "MIT", 7 | "description": "API for interfacing with the API created for content-scripts" 8 | } -------------------------------------------------------------------------------- /packages/app/chrome/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/chrome-app", 3 | "dependencies": { 4 | "@types/chrome": "^0.0.79" 5 | }, 6 | "scripts": { 7 | "build": "../../../node_modules/.bin/webpack --config ./webpack.config.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /rules/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "declaration": false, 6 | "rootDir": "./src", 7 | "outDir": "./dist" 8 | }, 9 | "include": [ 10 | "." 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/native-keymap.ts: -------------------------------------------------------------------------------- 1 | class NativeKeymap { 2 | public getCurrentKeyboardLayout(): null { 3 | return null; 4 | } 5 | 6 | public getKeyMap(): undefined[] { 7 | return []; 8 | } 9 | } 10 | 11 | export = new NativeKeymap(); 12 | -------------------------------------------------------------------------------- /packages/ide/src/fill/webview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Virtual Document 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/web", 3 | "scripts": { 4 | "build": "../../node_modules/.bin/cross-env UV_THREADPOOL_SIZE=100 node --max-old-space-size=32384 ../../node_modules/webpack/bin/webpack.js --config ./webpack.config.js" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/css.js: -------------------------------------------------------------------------------- 1 | module.exports = function(source) { 2 | if (this.resourcePath.endsWith(".ts")) { 3 | this.resourcePath = this.resourcePath.replace(".ts", ".css"); 4 | } 5 | return `module.exports = require("${this.resourcePath.replace(/\\/g, "\\\\")}");`; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/protocol/scripts/generate_proto.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname "$0")/.." 4 | 5 | protoc --plugin="protoc-gen-ts=./node_modules/.bin/protoc-gen-ts" --js_out="import_style=commonjs,binary:./src/proto" --ts_out="./src/proto" ./src/proto/*.proto --proto_path="./src/proto" 6 | -------------------------------------------------------------------------------- /packages/protocol/src/node/modules/trash.ts: -------------------------------------------------------------------------------- 1 | import * as trash from "trash"; 2 | 3 | // tslint:disable completed-docs 4 | 5 | export class TrashModuleProxy { 6 | public async trash(path: string, options?: trash.Options): Promise { 7 | return trash(path, options); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project. 4 | title: '' 5 | labels: 'feature' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /doc/security/code-server.fail2ban.conf: -------------------------------------------------------------------------------- 1 | # Fail2Ban filter for code-server 2 | # 3 | # 4 | 5 | [Definition] 6 | 7 | 8 | failregex = ^INFO\s+Failed login attempt\s+{\"password\":\"(\\.|[^"])*\",\"remote_address\":\"\" 9 | 10 | ignoreregex = 11 | 12 | datepattern = "timestamp":{EPOCH}}$ 13 | 14 | # Author: Dean Sheather 15 | 16 | -------------------------------------------------------------------------------- /packages/app/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/app-common", 3 | "main": "src/app.ts", 4 | "dependencies": { 5 | "material-components-web": "^0.44.0", 6 | "react": "^16.8.1", 7 | "react-dom": "^16.8.1" 8 | }, 9 | "devDependencies": { 10 | "@types/react": "^16.8.2", 11 | "@types/react-dom": "^16.8.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/dns/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/dns", 3 | "main": "out/main.js", 4 | "scripts": { 5 | "build": "../../node_modules/.bin/webpack --config ./webpack.config.js" 6 | }, 7 | "dependencies": { 8 | "node-named": "^0.0.1" 9 | }, 10 | "devDependencies": { 11 | "ip-address": "^5.8.9", 12 | "@types/ip-address": "^5.8.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question. 4 | title: '' 5 | labels: 'question' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | ## Description 12 | 13 | 14 | 15 | ## Related Issues 16 | 17 | -------------------------------------------------------------------------------- /packages/app/chrome/src/background.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // tslint:disable-next-line:no-any 4 | const chromeApp = (chrome).app; 5 | 6 | chromeApp.runtime.onLaunched.addListener(() => { 7 | chromeApp.window.create("src/index.html", { 8 | outerBounds: { 9 | width: 400, 10 | height: 500, 11 | }, 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /scripts/webpack.node.config.js: -------------------------------------------------------------------------------- 1 | const merge = require("webpack-merge"); 2 | 3 | module.exports = (options = {}) => merge( 4 | require("./webpack.general.config")({ 5 | ...options, 6 | node: true, 7 | }), { 8 | devtool: "none", 9 | mode: "production", 10 | target: "node", 11 | externals: { 12 | spdlog: "commonjs spdlog", 13 | "node-pty": "commonjs node-pty", 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /packages/server/README.md: -------------------------------------------------------------------------------- 1 | # server 2 | 3 | ## Endpoints 4 | 5 | ### `/tunnel/` 6 | 7 | Tunnels a TCP connection over WebSockets. Implemented for proxying connections from a remote machine locally. 8 | 9 | ### `/ports` 10 | 11 | Watches for open ports. Implemented for tunneling ports on the remote server. 12 | 13 | ### `/resource/` 14 | 15 | Reads files on GET. 16 | Writes files on POST. -------------------------------------------------------------------------------- /packages/app/chrome/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/logger/src/extender.test.ts: -------------------------------------------------------------------------------- 1 | import { field, logger } from "./logger"; 2 | import { createStackdriverExtender } from "./extender"; 3 | 4 | describe("Extender", () => { 5 | it("should add stackdriver extender", () => { 6 | logger.extend(createStackdriverExtender("coder-dev-1", "logging-package-tests")); 7 | }); 8 | 9 | it("should log", async () => { 10 | logger.debug("Bananas!", field("frog", { hi: "wow" })); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/amd.ts: -------------------------------------------------------------------------------- 1 | import { URI } from "vs/base/common/uri"; 2 | 3 | export const getPathFromAmdModule = (_: typeof require, relativePath: string): string => { 4 | if (process.mainModule && process.mainModule.filename) { 5 | const index = process.mainModule.filename.lastIndexOf("/"); 6 | 7 | return process.mainModule.filename.slice(0, index); 8 | } 9 | 10 | return relativePath ? URI.file(relativePath).fsPath : ""; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/app/browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/app", 3 | "scripts": { 4 | "start": "node ../../../node_modules/webpack-dev-server/bin/webpack-dev-server.js --config ./webpack.config.js", 5 | "build": "node ../../../node_modules/webpack/bin/webpack.js --config ./webpack.config.js" 6 | }, 7 | "dependencies": { 8 | "@material/checkbox": "^0.44.1", 9 | "@material/textfield": "^0.44.1", 10 | "material-components-web": "^0.44.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/dns/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const merge = require("webpack-merge"); 3 | 4 | const root = path.resolve(__dirname, "../.."); 5 | 6 | module.exports = merge( 7 | require(path.join(root, "scripts/webpack.node.config.js"))({ 8 | dirname: __dirname, 9 | name: "dns", 10 | }), { 11 | externals: { 12 | "node-named": "commonjs node-named", 13 | }, 14 | entry: [ 15 | "./packages/dns/src/dns.ts" 16 | ], 17 | }, 18 | ); 19 | -------------------------------------------------------------------------------- /packages/requirefs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "requirefs", 3 | "description": "", 4 | "main": "src/index.ts", 5 | "scripts": { 6 | "benchmark": "ts-node ./test/*.bench.ts" 7 | }, 8 | "dependencies": { 9 | "jszip": "2.6.0", 10 | "path": "0.12.7", 11 | "resolve": "1.8.1" 12 | }, 13 | "devDependencies": { 14 | "@types/benchmark": "^1.0.31", 15 | "@types/jszip": "3.1.4", 16 | "@types/resolve": "0.0.8", 17 | "benchmark": "^2.1.4", 18 | "text-encoding": "0.6.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/labels.ts: -------------------------------------------------------------------------------- 1 | import * as labels from "vs/base/common/labels"; 2 | 3 | // Disable all mnemonics for now until we implement it. 4 | const target = labels as typeof labels; 5 | target.mnemonicMenuLabel = (label: string, forceDisable?: boolean): string => { 6 | return label.replace(/\(&&\w\)|&&/g, ""); 7 | }; 8 | target.mnemonicButtonLabel = (label: string): string => { 9 | return label.replace(/\(&&\w\)|&&/g, ""); 10 | }; 11 | target.unmnemonicLabel = (label: string): string => { return label; }; 12 | -------------------------------------------------------------------------------- /packages/app/common/src/connection.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "@coder/events"; 2 | import { TunnelCloseEvent } from "@coder/tunnel/src/client"; 3 | 4 | export interface TcpHost { 5 | listen(host: string, port: number): Promise; 6 | } 7 | 8 | export interface TcpServer { 9 | readonly onConnection: Event; 10 | close(): Promise; 11 | } 12 | 13 | export interface TcpConnection { 14 | readonly onData: Event; 15 | send(data: ArrayBuffer): Promise; 16 | close(): Promise; 17 | } 18 | -------------------------------------------------------------------------------- /packages/app/browser/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const merge = require("webpack-merge"); 4 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 5 | 6 | const root = path.resolve(__dirname, "../../.."); 7 | 8 | module.exports = merge( 9 | require(path.join(root, "scripts/webpack.client.config.js"))({ 10 | dirname: __dirname, 11 | entry: path.join(__dirname, "src/app.ts"), 12 | name: "login", 13 | template: path.join(__dirname, "src/app.html"), 14 | }), { 15 | }, 16 | ); 17 | -------------------------------------------------------------------------------- /packages/protocol/src/browser/modules/trash.ts: -------------------------------------------------------------------------------- 1 | import * as trash from "trash"; 2 | import { ClientServerProxy } from "../../common/proxy"; 3 | import { TrashModuleProxy } from "../../node/modules/trash"; 4 | 5 | // tslint:disable completed-docs 6 | 7 | interface ClientTrashModuleProxy extends TrashModuleProxy, ClientServerProxy {} 8 | 9 | export class TrashModule { 10 | public constructor(private readonly proxy: ClientTrashModuleProxy) {} 11 | 12 | public trash = (path: string, options?: trash.Options): Promise => { 13 | return this.proxy.trash(path, options); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/mouseEvent.ts: -------------------------------------------------------------------------------- 1 | import * as mouse from "vs/base/browser/mouseEvent"; 2 | 3 | /** 4 | * Fix the wheel event for Firefox. 5 | */ 6 | class StandardWheelEvent extends mouse.StandardWheelEvent { 7 | public constructor(event: mouse.IMouseWheelEvent | null) { 8 | super( 9 | event, 10 | (-(event as any as MouseWheelEvent).deltaX || 0) / 3, // tslint:disable-line no-any 11 | (-(event as any as MouseWheelEvent).deltaY || 0) / 3, // tslint:disable-line no-any 12 | ); 13 | } 14 | } 15 | 16 | const target = mouse as typeof mouse; 17 | target.StandardWheelEvent = StandardWheelEvent; 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "baseUrl": ".", 6 | "rootDir": ".", 7 | "jsx": "react", 8 | "outDir": "dist", 9 | "declaration": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "resolveJsonModule": true, 13 | "experimentalDecorators": true, 14 | "importHelpers": true, 15 | "plugins": [ 16 | { 17 | "name": "typescript-tslint-plugin" 18 | } 19 | ], 20 | "paths": { 21 | "@coder/*": [ 22 | "./packages/*" 23 | ], 24 | "vs/*": [ 25 | "./lib/vscode/src/vs/*" 26 | ] 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/logger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/logger", 3 | "description": "Beautiful logging inspired by https://github.com/uber-go/zap.", 4 | "scripts": { 5 | "build": "tsc -p tsconfig.build.json && cp ./out/packages/logger/src/* ./out && rm -rf out/packages && ../../node_modules/.bin/webpack --config ./webpack.config.js", 6 | "postinstall": "if [ ! -d out ];then npm run build; fi" 7 | }, 8 | "version": "1.1.3", 9 | "main": "out/main.js", 10 | "types": "out/index.d.ts", 11 | "author": "Coder", 12 | "license": "MIT", 13 | "dependencies": { 14 | "@google-cloud/logging": "^4.5.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/extension_bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Extension Bug 3 | about: Report problems and unexpected behavior with extensions. 4 | title: '' 5 | labels: 'extension-specific' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | - `code-server` version: 12 | - OS Version: 13 | - Extension: 14 | 15 | ## Description 16 | 17 | 18 | 19 | ## Steps to Reproduce 20 | 21 | 1. 22 | 1. -------------------------------------------------------------------------------- /packages/dns/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | src 16 | 17 | # Node.js dependencies: 18 | node_modules/ -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report problems and unexpected behavior. 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | 12 | - `code-server` version: 13 | - OS Version: 14 | 15 | ## Description 16 | 17 | 18 | 19 | ## Steps to Reproduce 20 | 21 | 1. 22 | 1. -------------------------------------------------------------------------------- /packages/app/common/src/tooltip.scss: -------------------------------------------------------------------------------- 1 | .md-tooltip { 2 | position: relative; 3 | } 4 | 5 | .md-tooltip-content { 6 | position: absolute; 7 | bottom: -35px; 8 | left: 50%; 9 | padding: 7px; 10 | transform: translateX(-50%) scale(0); 11 | transition: transform 0.15s cubic-bezier(0, 0, 0.2, 1); 12 | transform-origin: top; 13 | background: rgba(67, 67, 67, 0.97); 14 | color: white; 15 | letter-spacing: 0.3px; 16 | border-radius: 3px; 17 | font-size: 12px; 18 | font-weight: 500; 19 | z-index: 2; 20 | } 21 | 22 | .md-tooltip:hover .md-tooltip-content { 23 | transform: translateX(-50%) scale(1); 24 | } 25 | -------------------------------------------------------------------------------- /packages/protocol/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/protocol", 3 | "main": "src/index.ts", 4 | "dependencies": { 5 | "express": "^4.16.4", 6 | "google-protobuf": "^3.6.1", 7 | "trash": "^4.3.0", 8 | "ws": "^6.1.2" 9 | }, 10 | "devDependencies": { 11 | "@types/google-protobuf": "^3.2.7", 12 | "@types/rimraf": "^2.0.2", 13 | "@types/text-encoding": "^0.0.35", 14 | "rimraf": "^2.6.3", 15 | "text-encoding": "^0.7.0", 16 | "ts-protoc-gen": "^0.8.0" 17 | }, 18 | "scripts": { 19 | "gen": "./scripts/generate_proto.sh" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scripts/vstar.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | # Builds a tarfile containing vscode sourcefiles neccessary for CI. 5 | # Done outside the CI and uploaded to object storage to reduce CI time. 6 | 7 | branch=1.33.1 8 | dir=/tmp/vstar 9 | outfile=/tmp/vstar-$branch.tar.gz 10 | rm -rf $dir 11 | mkdir -p $dir 12 | 13 | cd $dir 14 | git clone https://github.com/microsoft/vscode --branch $branch --single-branch --depth=1 15 | cd vscode 16 | 17 | yarn 18 | 19 | npx gulp vscode-linux-x64 --max-old-space-size=32384 20 | rm -rf extensions build out* test 21 | cd .. 22 | mv *-x64/resources/app/extensions ./extensions 23 | rm -rf *-x64 24 | tar -czvf $outfile . 25 | -------------------------------------------------------------------------------- /packages/vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coder/vscode", 3 | "description": "VS Code implementation of the browser-based IDE client.", 4 | "main": "src/index.ts", 5 | "scripts": { 6 | "build:bootstrap-fork": "../../node_modules/.bin/cross-env UV_THREADPOOL_SIZE=100 node --max-old-space-size=32384 ../../node_modules/webpack/bin/webpack.js --config ./webpack.bootstrap.config.js" 7 | }, 8 | "dependencies": { 9 | "iconv-lite": "^0.4.24", 10 | "onigasm": "^2.2.1", 11 | "string-replace-loader": "^2.1.1", 12 | "tar-stream": "^2.0.1" 13 | }, 14 | "devDependencies": { 15 | "@types/tar-stream": "^1.6.0", 16 | "vscode-textmate": "^4.0.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/logger/src/logger.test.ts: -------------------------------------------------------------------------------- 1 | import { field, logger, BrowserFormatter, Time } from "./logger"; 2 | 3 | describe("Logger", () => { 4 | it("should use server formatter", () => { 5 | logger.info("test", field("key", "value"), field("time", new Time(100, Date.now()))); 6 | logger.named("name").debug("test name"); 7 | logger.named("another name").warn("another test name"); 8 | }); 9 | 10 | it("should use browser formatter", () => { 11 | logger.formatter = new BrowserFormatter(); 12 | logger.info("test", field("key", "value"), field("time", new Time(100, Date.now()))); 13 | logger.named("name").debug("test name"); 14 | logger.named("another name").warn("another test name"); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/server/src/constants.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as os from "os"; 3 | 4 | export const isCli = typeof process.env.CLI !== "undefined" && process.env.CLI !== "false"; 5 | export const serveStatic = typeof process.env.SERVE_STATIC !== "undefined" && process.env.SERVE_STATIC !== "false"; 6 | export const buildDir = process.env.BUILD_DIR ? path.resolve(process.env.BUILD_DIR) : ""; 7 | const xdgResolve = (primary: string | undefined, fallback: string): string => { 8 | return primary ? path.resolve(primary) : path.resolve(process.env.HOME || os.homedir(), fallback); 9 | }; 10 | export const dataHome = xdgResolve(process.env.XDG_DATA_HOME, ".local/share"); 11 | export const cacheHome = xdgResolve(process.env.XDG_CACHE_HOME, ".cache"); 12 | -------------------------------------------------------------------------------- /packages/app/chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Coder", 4 | "version": "1", 5 | "icons": { 6 | "128": "icon_128.png" 7 | }, 8 | "permissions": [ 9 | "storage", 10 | "webview", 11 | "http://*/*", 12 | "https://*/*" 13 | ], 14 | "app": { 15 | "background": { 16 | "scripts": [ 17 | "out/background.js" 18 | ] 19 | }, 20 | "content": { 21 | "scripts": [ 22 | "out/content.js" 23 | ] 24 | } 25 | }, 26 | "commands": { 27 | "toggle-feature-foo": { 28 | "suggested_key": { 29 | "default": "Ctrl+W" 30 | }, 31 | "description": "Toggle feature foo", 32 | "global": true 33 | } 34 | }, 35 | "sockets": { 36 | "tcpServer": { 37 | "listen": [ 38 | "" 39 | ] 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /packages/protocol/test/server.test.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "./helpers"; 2 | 3 | describe("Server", () => { 4 | const dataDirectory = "/tmp/example"; 5 | const workingDirectory = "/working/dir"; 6 | const extensionsDirectory = "/tmp/example"; 7 | const builtInExtensionsDirectory = "/tmp/example"; 8 | const cacheDirectory = "/tmp/cache"; 9 | const client = createClient({ 10 | extensionsDirectory, 11 | builtInExtensionsDirectory, 12 | cacheDirectory, 13 | dataDirectory, 14 | workingDirectory, 15 | }); 16 | 17 | it("should get init msg", async () => { 18 | const data = await client.initData; 19 | expect(data.dataDirectory).toEqual(dataDirectory); 20 | expect(data.workingDirectory).toEqual(workingDirectory); 21 | expect(data.builtInExtensionsDirectory).toEqual(builtInExtensionsDirectory); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/protocol/test/trash.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as util from "util"; 3 | import { Module } from "../src/common/proxy"; 4 | import { createClient, Helper } from "./helpers"; 5 | 6 | // tslint:disable deprecation to use fs.exists 7 | 8 | describe("trash", () => { 9 | const client = createClient(); 10 | const trash = client.modules[Module.Trash]; 11 | const helper = new Helper("trash"); 12 | 13 | beforeAll(async () => { 14 | await helper.prepare(); 15 | }); 16 | 17 | it("should trash a file", async () => { 18 | const file = await helper.createTmpFile(); 19 | await trash.trash(file); 20 | expect(await util.promisify(fs.exists)(file)).toBeFalsy(); 21 | }); 22 | 23 | it("should dispose", (done) => { 24 | setTimeout(() => { 25 | client.dispose(); 26 | done(); 27 | }, 100); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/logger/README.md: -------------------------------------------------------------------------------- 1 | # Logger 2 | 3 | Beautiful logging inspired by https://github.com/uber-go/zap. 4 | 5 | - Built for node and the browser 6 | - Zero dependencies 7 | - Uses groups in the browser to reduce clutter 8 | 9 | ## Example Usage 10 | 11 | ```javascript 12 | import { field, logger } from "@coder/logger"; 13 | 14 | logger.info("Loading container", 15 | field("container_id", container.id_str), 16 | field("organization_id", organization.id_str)); 17 | ``` 18 | 19 | ## Formatting 20 | 21 | By default the logger uses a different formatter depending on whether it detects 22 | it is running in the browser or not. A custom formatter can be set: 23 | 24 | ```javascript 25 | import { logger, Formatter } from "@coder/logger"; 26 | 27 | class MyFormatter extends Formatter { 28 | // implementation ... 29 | } 30 | 31 | logger.formatter = new MyFormatter(); 32 | ``` 33 | -------------------------------------------------------------------------------- /packages/app/chrome/src/content.ts: -------------------------------------------------------------------------------- 1 | import { create } from "@coder/app/common/src/app"; 2 | import { tcpHost } from "./chome"; 3 | 4 | create({ 5 | storage: { 6 | get: (key: string): Promise => { 7 | return new Promise((resolve, reject): void => { 8 | try { 9 | chrome.storage.sync.get(key, (items) => { 10 | resolve(items[key]); 11 | }); 12 | } catch (ex) { 13 | reject(ex); 14 | } 15 | }); 16 | }, 17 | set: (key: string, value: T): Promise => { 18 | return new Promise((resolve, reject): void => { 19 | try { 20 | chrome.storage.sync.set({ 21 | [key]: value, 22 | }, () => { 23 | resolve(); 24 | }); 25 | } catch (ex) { 26 | reject(ex); 27 | } 28 | }); 29 | }, 30 | }, 31 | tcp: tcpHost, 32 | node: document.getElementById("main") as HTMLDivElement, 33 | }); 34 | -------------------------------------------------------------------------------- /deployment/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: code-server 5 | --- 6 | apiVersion: v1 7 | kind: Service 8 | metadata: 9 | name: code-server 10 | namespace: code-server 11 | spec: 12 | ports: 13 | - port: 8443 14 | name: https 15 | protocol: TCP 16 | selector: 17 | app: code-server 18 | type: ClusterIP 19 | --- 20 | apiVersion: extensions/v1beta1 21 | kind: Deployment 22 | metadata: 23 | labels: 24 | app: code-server 25 | name: code-server 26 | namespace: code-server 27 | spec: 28 | selector: 29 | matchLabels: 30 | app: code-server 31 | replicas: 1 32 | template: 33 | metadata: 34 | labels: 35 | app: code-server 36 | spec: 37 | containers: 38 | - image: codercom/code-server 39 | imagePullPolicy: Always 40 | name: code-server 41 | ports: 42 | - containerPort: 8443 43 | name: https 44 | -------------------------------------------------------------------------------- /packages/server/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const merge = require("webpack-merge"); 4 | 5 | const root = path.resolve(__dirname, "../.."); 6 | 7 | module.exports = merge( 8 | require(path.join(root, "scripts/webpack.node.config.js"))({ 9 | dirname: __dirname, 10 | }), { 11 | output: { 12 | filename: "cli.js", 13 | libraryTarget: "commonjs", 14 | }, 15 | node: { 16 | console: false, 17 | global: false, 18 | process: false, 19 | Buffer: false, 20 | __filename: false, 21 | __dirname: false, 22 | setImmediate: false 23 | }, 24 | externals: { 25 | "nbin": "commonjs nbin", 26 | }, 27 | entry: "./packages/server/src/cli.ts", 28 | plugins: [ 29 | new webpack.DefinePlugin({ 30 | "process.env.BUILD_DIR": `"${__dirname.replace(/\\/g, "\\\\")}"`, 31 | "process.env.CLI": `"${process.env.CLI ? "true" : "false"}"`, 32 | }), 33 | ], 34 | }, 35 | ); 36 | -------------------------------------------------------------------------------- /packages/logger/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const merge = require("webpack-merge"); 3 | 4 | module.exports = [ 5 | merge(require(path.join(__dirname, "../../scripts", "webpack.general.config.js"))(), { 6 | devtool: "none", 7 | mode: "production", 8 | target: "node", 9 | output: { 10 | path: path.join(__dirname, "out"), 11 | filename: "main.js", 12 | libraryTarget: "commonjs", 13 | }, 14 | entry: [ 15 | "./packages/logger/src/index.ts" 16 | ], 17 | }), 18 | merge(require(path.join(__dirname, "../../scripts", "webpack.general.config.js"))(), { 19 | devtool: "none", 20 | mode: "production", 21 | target: "node", 22 | output: { 23 | path: path.join(__dirname, "out"), 24 | filename: "extender.js", 25 | libraryTarget: "commonjs", 26 | }, 27 | externals: { 28 | "@google-cloud/logging": "commonjs @google-cloud/logging", 29 | }, 30 | entry: [ 31 | "./packages/logger/src/extender.ts" 32 | ], 33 | }), 34 | ]; 35 | -------------------------------------------------------------------------------- /packages/app/browser/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Authenticate: code-server 7 | 8 | 9 | 10 |
11 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /packages/app/common/src/app.tsx: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import { MDCTextField } from "@material/textfield"; 3 | import { TcpHost } from "./connection"; 4 | import { StorageProvider } from "./storage"; 5 | import "material-components-web/dist/material-components-web.css"; 6 | import "./app.scss"; 7 | import "./tooltip.scss"; 8 | 9 | import * as React from "react"; 10 | import { render } from "react-dom"; 11 | import { Main } from "./containers"; 12 | 13 | export * from "./connection"; 14 | export interface App { 15 | readonly tcp: TcpHost; 16 | readonly storage: StorageProvider; 17 | readonly node: HTMLElement; 18 | } 19 | 20 | export interface RegisteredServer { 21 | readonly host: "coder" | "self"; 22 | readonly hostname: string; 23 | readonly name: string; 24 | } 25 | 26 | export const create = async (app: App): Promise => { 27 | let servers = await app.storage.get("servers"); 28 | if (!servers) { 29 | servers = []; 30 | } 31 | 32 | render(
, app.node); 33 | }; 34 | -------------------------------------------------------------------------------- /scripts/test-setup.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const util = require("util"); 3 | 4 | // This isn't properly promisified in Jest. 5 | Object.defineProperty(fs.read, util.promisify.custom, { 6 | configurable: true, 7 | value: (...args) => { 8 | return new Promise((resolve, reject) => { 9 | args.push((error, bytesRead, buffer) => { 10 | if (error) { 11 | reject(error); 12 | } else { 13 | resolve({ bytesRead, buffer }); 14 | } 15 | }); 16 | fs.read(...args); 17 | }); 18 | }, 19 | }); 20 | 21 | global.requestAnimationFrame = (cb) => { 22 | setTimeout(cb, 0); 23 | }; 24 | 25 | // lchmod might not be available. Jest runs graceful-fs which makes this a no-op 26 | // when it doesn't exist but that doesn't seem to always run when running 27 | // multiple tests (or maybe it gets undone after a test). 28 | if (!fs.lchmod) { 29 | fs.lchmod = function (path, mode, cb) { 30 | if (cb) { 31 | process.nextTick(cb); 32 | } 33 | }; 34 | fs.lchmodSync = function () {}; 35 | } 36 | -------------------------------------------------------------------------------- /rules/src/curlyStatementNewlinesRule.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import * as Lint from "tslint"; 3 | 4 | /** 5 | * Curly statement newlines rule. 6 | */ 7 | export class Rule extends Lint.Rules.AbstractRule { 8 | public static FAILURE_STRING = "Curly statements must separate with newlines"; 9 | 10 | /** 11 | * Apply the rule. 12 | */ 13 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 14 | return this.applyWithWalker(new CurlyStatementNewlinesWalker(sourceFile, this.getOptions())); 15 | } 16 | } 17 | 18 | /** 19 | * Curly statement newlines walker. 20 | */ 21 | class CurlyStatementNewlinesWalker extends Lint.RuleWalker { 22 | /** 23 | * Visit if statements. 24 | */ 25 | public visitIfStatement(node: ts.IfStatement): void { 26 | const splitLength = node.getFullText().trim().split("\n").length; 27 | if (splitLength <= 2) { 28 | this.addFailureAt(node.getStart(), node.getWidth(), Rule.FAILURE_STRING); 29 | } 30 | 31 | super.visitIfStatement(node); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/environmentService.ts: -------------------------------------------------------------------------------- 1 | import * as paths from "./paths"; 2 | import * as environment from "vs/platform/environment/node/environmentService"; 3 | 4 | /** 5 | * Customize paths using data received from the initialization message. 6 | */ 7 | export class EnvironmentService extends environment.EnvironmentService { 8 | public get sharedIPCHandle(): string { 9 | return paths.getSocketPath() || super.sharedIPCHandle; 10 | } 11 | 12 | public get extensionsPath(): string { 13 | return paths.getExtensionsDirectory(); 14 | } 15 | 16 | public get builtinExtensionsPath(): string { 17 | return paths.getBuiltInExtensionsDirectory(); 18 | } 19 | 20 | public get extraExtensionPaths(): string[] { 21 | return paths.getExtraExtensionDirectories(); 22 | } 23 | 24 | public get extraBuiltinExtensionPaths(): string[] { 25 | return paths.getExtraBuiltinExtensionDirectories(); 26 | } 27 | } 28 | 29 | const target = environment as typeof environment; 30 | target.EnvironmentService = EnvironmentService; 31 | -------------------------------------------------------------------------------- /packages/app/chrome/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/chrome@^0.0.79": 6 | version "0.0.79" 7 | resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.79.tgz#1c83b35bd9b21b6204fb56e4816a1ea65dc013e5" 8 | integrity sha512-4+Xducpig6lpwVX65Hk8KSZwRoURHXMDbd38SDNcV8TBaw4xyJki39fjB1io2h7ip+BsyFvgTm9OxR5qneLPiA== 9 | dependencies: 10 | "@types/filesystem" "*" 11 | 12 | "@types/filesystem@*": 13 | version "0.0.29" 14 | resolved "https://registry.yarnpkg.com/@types/filesystem/-/filesystem-0.0.29.tgz#ee3748eb5be140dcf980c3bd35f11aec5f7a3748" 15 | integrity sha512-85/1KfRedmfPGsbK8YzeaQUyV1FQAvMPMTuWFQ5EkLd2w7szhNO96bk3Rh/SKmOfd9co2rCLf0Voy4o7ECBOvw== 16 | dependencies: 17 | "@types/filewriter" "*" 18 | 19 | "@types/filewriter@*": 20 | version "0.0.28" 21 | resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.28.tgz#c054e8af4d9dd75db4e63abc76f885168714d4b3" 22 | integrity sha1-wFTor02d11205jq8dviFFocU1LM= 23 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/codeEditor.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import * as editor from "vs/editor/browser/services/codeEditorServiceImpl"; 3 | import { IDecorationRenderOptions } from "vs/editor/common/editorCommon"; 4 | 5 | /** 6 | * This converts icon paths for decorations to the correct URL. 7 | */ 8 | abstract class CodeEditorServiceImpl extends editor.CodeEditorServiceImpl { 9 | public registerDecorationType(key: string, options: IDecorationRenderOptions, parentTypeKey?: string): void { 10 | super.registerDecorationType(key, options ? { 11 | ...options, 12 | gutterIconPath: options.gutterIconPath && options.gutterIconPath.scheme === "file" ? { 13 | ...options.gutterIconPath, 14 | scheme: location.protocol.replace(":", ""), 15 | authority: location.host, 16 | path: join("/resource", options.gutterIconPath.path), 17 | } :options.gutterIconPath, 18 | } : {}, parentTypeKey); 19 | } 20 | } 21 | 22 | const target = editor as typeof editor; 23 | target.CodeEditorServiceImpl = CodeEditorServiceImpl; 24 | -------------------------------------------------------------------------------- /packages/web/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | code-server 7 | 8 | 9 | 10 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /packages/app/chrome/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const merge = require("webpack-merge"); 4 | const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; 5 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 6 | const prod = process.env.NODE_ENV === "production"; 7 | 8 | module.exports = [ 9 | merge(require(path.join(__dirname, "../../../scripts", "webpack.general.config.js"))(), { 10 | devtool: "none", 11 | mode: "development", 12 | target: "web", 13 | output: { 14 | path: path.join(__dirname, "out"), 15 | filename: "background.js", 16 | }, 17 | entry: [ 18 | "./packages/app/chrome/src/background.ts" 19 | ], 20 | plugins: [ 21 | ] 22 | }), 23 | merge(require(path.join(__dirname, "../../../scripts", "webpack.general.config.js"))(), { 24 | devtool: "none", 25 | mode: "development", 26 | target: "web", 27 | output: { 28 | path: path.join(__dirname, "out"), 29 | filename: "content.js", 30 | }, 31 | entry: [ 32 | "./packages/app/chrome/src/content.ts" 33 | ], 34 | plugins: [ 35 | ] 36 | }), 37 | ]; 38 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/stdioElectron.ts: -------------------------------------------------------------------------------- 1 | import { StdioIpcHandler } from "@coder/server/src/ipc"; 2 | import { IpcRenderer } from "electron"; 3 | 4 | // TODO: Commenting out for now since the electron fill includes the client code 5 | // and tries to connect to the web socket. The fill also likely wouldn't work 6 | // since it assumes it is running on the client. Could we proxy all methods to 7 | // the client? It might not matter since we intercept everything before sending 8 | // to the shared process. 9 | // export * from "@coder/ide/src/fill/electron"; 10 | 11 | class StdioIpcRenderer extends StdioIpcHandler implements IpcRenderer { 12 | // tslint:disable-next-line no-any 13 | public sendTo(_windowId: number, _channel: string, ..._args: any[]): void { 14 | throw new Error("Method not implemented."); 15 | } 16 | 17 | // tslint:disable-next-line no-any 18 | public sendToHost(_channel: string, ..._args: any[]): void { 19 | throw new Error("Method not implemented."); 20 | } 21 | 22 | public eventNames(): string[] { 23 | return super.eventNames() as string[]; 24 | } 25 | } 26 | 27 | export const ipcRenderer = new StdioIpcRenderer(); 28 | -------------------------------------------------------------------------------- /packages/protocol/test/spdlog.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as util from "util"; 3 | import { Module } from "../src/common/proxy"; 4 | import { createClient, Helper } from "./helpers"; 5 | 6 | describe("spdlog", () => { 7 | const client = createClient(); 8 | const spdlog = client.modules[Module.Spdlog]; 9 | const helper = new Helper("spdlog"); 10 | 11 | beforeAll(async () => { 12 | await helper.prepare(); 13 | }); 14 | 15 | it("should log to a file", async () => { 16 | const file = await helper.createTmpFile(); 17 | const logger = new spdlog.RotatingLogger("test logger", file, 10000, 10); 18 | logger.trace("trace"); 19 | logger.debug("debug"); 20 | logger.info("info"); 21 | logger.warn("warn"); 22 | logger.error("error"); 23 | logger.critical("critical"); 24 | logger.flush(); 25 | await new Promise((resolve): number | NodeJS.Timer => setTimeout(resolve, 1000)); 26 | expect(await util.promisify(fs.readFile)(file, "utf8")) 27 | .toContain("critical"); 28 | }); 29 | 30 | it("should dispose", (done) => { 31 | setTimeout(() => { 32 | client.dispose(); 33 | done(); 34 | }, 100); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019 Coder Technologies Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/protocol/src/common/connection.ts: -------------------------------------------------------------------------------- 1 | import * as jspb from "google-protobuf"; 2 | 3 | export interface SendableConnection { 4 | send(data: Buffer | Uint8Array): void; 5 | } 6 | 7 | export interface ReadWriteConnection extends SendableConnection { 8 | onMessage(cb: (data: Uint8Array | Buffer) => void): void; 9 | onClose(cb: () => void): void; 10 | onDown(cb: () => void): void; 11 | onUp(cb: () => void): void; 12 | close(): void; 13 | } 14 | 15 | export enum OperatingSystem { 16 | Windows, 17 | Linux, 18 | Mac, 19 | } 20 | 21 | export interface InitData { 22 | readonly os: OperatingSystem; 23 | readonly dataDirectory: string; 24 | readonly workingDirectory: string; 25 | readonly homeDirectory: string; 26 | readonly tmpDirectory: string; 27 | readonly shell: string; 28 | readonly extensionsDirectory: string; 29 | readonly builtInExtensionsDirectory: string; 30 | readonly extraExtensionDirectories: string[]; 31 | readonly extraBuiltinExtensionDirectories: string[]; 32 | readonly env: jspb.Map; 33 | } 34 | 35 | export interface SharedProcessData { 36 | readonly socketPath: string; 37 | readonly logPath: string; 38 | } 39 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/dom.ts: -------------------------------------------------------------------------------- 1 | import * as dom from "vs/base/browser/dom"; 2 | import { IDisposable } from "vs/base/common/lifecycle"; 3 | 4 | // Firefox has no implementation of toElement. 5 | if (!("toElement" in MouseEvent.prototype)) { 6 | Object.defineProperty(MouseEvent.prototype, "toElement", { 7 | get: function (): EventTarget | null { 8 | // @ts-ignore 9 | const event = this as MouseEvent; 10 | switch (event.type) { 11 | case "mouseup": 12 | case "focusin": 13 | case "mousenter": 14 | case "mouseover": 15 | case "dragenter": 16 | return event.target; 17 | default: 18 | return event.relatedTarget; 19 | } 20 | }, 21 | }); 22 | } 23 | 24 | const _addDisposableListener = dom.addDisposableListener; 25 | // tslint:disable-next-line no-any 26 | const addDisposableListener = (node: Element | Window | Document, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable => { 27 | return _addDisposableListener(node, type === "mousewheel" ? "wheel" : type, handler, useCapture); 28 | }; 29 | 30 | const target = dom as typeof dom; 31 | target.addDisposableListener = addDisposableListener; 32 | -------------------------------------------------------------------------------- /packages/protocol/src/proto/vscode_pb.d.ts: -------------------------------------------------------------------------------- 1 | // package: 2 | // file: vscode.proto 3 | 4 | import * as jspb from "google-protobuf"; 5 | 6 | export class SharedProcessActive extends jspb.Message { 7 | getSocketPath(): string; 8 | setSocketPath(value: string): void; 9 | 10 | getLogPath(): string; 11 | setLogPath(value: string): void; 12 | 13 | serializeBinary(): Uint8Array; 14 | toObject(includeInstance?: boolean): SharedProcessActive.AsObject; 15 | static toObject(includeInstance: boolean, msg: SharedProcessActive): SharedProcessActive.AsObject; 16 | static extensions: {[key: number]: jspb.ExtensionFieldInfo}; 17 | static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; 18 | static serializeBinaryToWriter(message: SharedProcessActive, writer: jspb.BinaryWriter): void; 19 | static deserializeBinary(bytes: Uint8Array): SharedProcessActive; 20 | static deserializeBinaryFromReader(message: SharedProcessActive, reader: jspb.BinaryReader): SharedProcessActive; 21 | } 22 | 23 | export namespace SharedProcessActive { 24 | export type AsObject = { 25 | socketPath: string, 26 | logPath: string, 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /packages/server/scripts/nbin.ts: -------------------------------------------------------------------------------- 1 | import { Binary } from "@coder/nbin"; 2 | import * as fs from "fs"; 3 | import * as os from "os"; 4 | import * as path from "path"; 5 | import { platform } from "../../../build/platform"; 6 | 7 | const target = `${platform()}-${os.arch()}`; 8 | const rootDir = path.join(__dirname, ".."); 9 | const bin = new Binary({ 10 | mainFile: path.join(rootDir, "out", "cli.js"), 11 | target: platform() === "darwin" ? "darwin" : platform() === "musl" ? "alpine" : "linux", 12 | }); 13 | bin.writeFiles(path.join(rootDir, "build", "**")); 14 | bin.writeFiles(path.join(rootDir, "out", "**")); 15 | [ 16 | // Native modules. These are marked as externals in the webpack config. 17 | "spdlog", 18 | "node-pty", 19 | 20 | // These are spdlog's dependencies. 21 | "mkdirp", "bindings", 22 | ].forEach((name) => { 23 | bin.writeModule(path.join(__dirname, "../../../node_modules", name)); 24 | }); 25 | bin.build().then((binaryData) => { 26 | const outputPath = path.join(__dirname, "..", `cli-${target}`); 27 | fs.writeFileSync(outputPath, binaryData); 28 | fs.chmodSync(outputPath, "755"); 29 | }).catch((ex) => { 30 | // tslint:disable-next-line:no-console 31 | console.error(ex); 32 | process.exit(1); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/protocol/src/proto/client.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | import "node.proto"; 3 | import "vscode.proto"; 4 | 5 | // Messages that the client can send to the server. 6 | message ClientMessage { 7 | oneof msg { 8 | // node.proto 9 | Method method = 20; 10 | Ping ping = 21; 11 | } 12 | } 13 | 14 | // Messages that the server can send to the client. 15 | message ServerMessage { 16 | oneof msg { 17 | // node.proto 18 | Method.Fail fail = 13; 19 | Method.Success success = 14; 20 | Event event = 19; 21 | Callback callback = 22; 22 | Pong pong = 18; 23 | 24 | WorkingInit init = 16; 25 | 26 | // vscode.proto 27 | SharedProcessActive shared_process_active = 17; 28 | } 29 | } 30 | 31 | message WorkingInit { 32 | string home_directory = 1; 33 | string tmp_directory = 2; 34 | string data_directory = 3; 35 | string working_directory = 4; 36 | enum OperatingSystem { 37 | Windows = 0; 38 | Linux = 1; 39 | Mac = 2; 40 | } 41 | OperatingSystem operating_system = 5; 42 | string shell = 6; 43 | string builtin_extensions_dir = 7; 44 | string extensions_directory = 8; 45 | repeated string extra_extension_directories = 9; 46 | repeated string extra_builtin_extension_directories = 10; 47 | 48 | map env = 11; 49 | } 50 | -------------------------------------------------------------------------------- /packages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "postinstall": "../node_modules/.bin/ts-node ../scripts/install-packages.ts", 4 | "test": "jest" 5 | }, 6 | "devDependencies": { 7 | "@types/jest": "^23.3.12", 8 | "jest": "^23.6.0", 9 | "ts-jest": "^23.10.5" 10 | }, 11 | "dependencies": { 12 | "xmlhttprequest": "1.8.0" 13 | }, 14 | "jest": { 15 | "globals": { 16 | "ts-jest": { 17 | "diagnostics": false 18 | } 19 | }, 20 | "moduleFileExtensions": [ 21 | "ts", 22 | "tsx", 23 | "js", 24 | "json" 25 | ], 26 | "setupFiles": [ 27 | "/../scripts/test-setup.js" 28 | ], 29 | "moduleNameMapper": { 30 | "^.+\\.(s?css|png|svg)$": "/../scripts/dummy.js", 31 | "@coder/ide/src/fill/evaluation": "/ide/src/fill/evaluation", 32 | "@coder/ide/src/fill/client": "/ide/src/fill/client", 33 | "@coder/(.*)/test": "/$1/test", 34 | "@coder/(.*)": "/$1/src", 35 | "vs/(.*)": "/../lib/vscode/src/vs/$1", 36 | "vszip": "/../lib/vscode/src/vs/base/node/zip.ts" 37 | }, 38 | "transform": { 39 | "^.+\\.tsx?$": "ts-jest" 40 | }, 41 | "testPathIgnorePatterns": [ 42 | "/node_modules/", 43 | "/logger/" 44 | ], 45 | "testRegex": ".*\\.test\\.tsx?" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "main": "./out/cli.js", 4 | "bin": "./out/cli.js", 5 | "files": [], 6 | "scripts": { 7 | "start": "node --max-old-space-size=32384 --require ts-node/register --require tsconfig-paths/register src/cli.ts", 8 | "build": "rm -rf ./out && ../../node_modules/.bin/cross-env CLI=true UV_THREADPOOL_SIZE=100 node --max-old-space-size=32384 ../../node_modules/webpack/bin/webpack.js --config ./webpack.config.js", 9 | "build:binary": "ts-node scripts/nbin.ts" 10 | }, 11 | "dependencies": { 12 | "@coder/nbin": "^1.1.2", 13 | "commander": "^2.19.0", 14 | "express": "^4.16.4", 15 | "express-static-gzip": "^1.1.3", 16 | "httpolyglot": "^0.1.2", 17 | "mime-types": "^2.1.21", 18 | "node-netstat": "^1.6.0", 19 | "pem": "^1.14.1", 20 | "promise.prototype.finally": "^3.1.0", 21 | "safe-compare": "^1.1.4", 22 | "ws": "^6.1.2", 23 | "xhr2": "^0.1.4" 24 | }, 25 | "devDependencies": { 26 | "@types/commander": "^2.12.2", 27 | "@types/express": "^4.16.0", 28 | "@types/fs-extra": "^5.0.4", 29 | "@types/mime-types": "^2.1.0", 30 | "@types/opn": "^5.1.0", 31 | "@types/pem": "^1.9.4", 32 | "@types/safe-compare": "^1.1.0", 33 | "@types/ws": "^6.0.1", 34 | "fs-extra": "^7.0.1", 35 | "opn": "^5.4.0", 36 | "string-replace-webpack-plugin": "^0.1.3", 37 | "ts-node": "^7.0.1", 38 | "tsconfig-paths": "^3.7.0", 39 | "typescript": "^3.2.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/platform.ts: -------------------------------------------------------------------------------- 1 | import * as os from "os"; 2 | import * as platform from "vs/base/common/platform"; 3 | import * as browser from "vs/base/browser/browser"; 4 | 5 | // tslint:disable no-any to override const 6 | 7 | // Use en instead of en-US since that's vscode default and it uses 8 | // that to determine whether to output aliases which will be redundant. 9 | if (platform.locale === "en-US") { 10 | (platform as any).locale = "en"; 11 | } 12 | if (platform.language === "en-US") { 13 | (platform as any).language = "en"; 14 | } 15 | 16 | // Use the server's platform instead of the client's. For example, this affects 17 | // how VS Code handles paths (and more) because different platforms give 18 | // different results. We'll have to counter for things that shouldn't change, 19 | // like keybindings. 20 | (platform as any).isLinux = os.platform() === "linux"; 21 | (platform as any).isWindows = os.platform() === "win32"; 22 | (platform as any).isMacintosh = os.platform() === "darwin"; 23 | (platform as any).platform = os.platform() === "linux" ? platform.Platform.Linux : os.platform() === "win32" ? platform.Platform.Windows : platform.Platform.Mac; 24 | 25 | // This is used for keybindings, and in one place to choose between \r\n and \n 26 | // (which we change to use platform.isWindows instead). 27 | (platform as any).OS = (browser.isMacintosh ? platform.OperatingSystem.Macintosh : (browser.isWindows ? platform.OperatingSystem.Windows : platform.OperatingSystem.Linux)); 28 | -------------------------------------------------------------------------------- /doc/security/fail2ban.md: -------------------------------------------------------------------------------- 1 | # Protecting code-server from bruteforce attempts 2 | 3 | code-server outputs all failed login attempts, along with the IP address, 4 | provided password, user agent and timestamp by default. When using a reverse 5 | proxy such as Nginx or Apache, the remote address may appear to be `127.0.0.1` 6 | or a similar address unless the `--trust-proxy` argument is provided to 7 | code-server. 8 | 9 | When used with the `--trust-proxy` argument, code-server will use the last IP in 10 | `X-Forwarded-For` (if provided) instead of the remote socket address. Ensure 11 | that you are setting this value in your reverse proxy: 12 | 13 | Nginx: 14 | ``` 15 | location / { 16 | ... 17 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 18 | ... 19 | } 20 | ``` 21 | 22 | Apache: 23 | ``` 24 | 25 | ... 26 | SetEnvIf X-Forwarded-For "^.*\..*\..*\..*" forwarded 27 | ... 28 | 29 | ``` 30 | 31 | It is extremely important that if you enable `--trust-proxy` you ensure your 32 | code-server instance is not accessible from the internet (block it in your 33 | firewall). 34 | 35 | ## Fail2Ban 36 | 37 | Fail2Ban allows for automatically banning and logging repeated failed 38 | authentication attempts for many applications through regex filters. A working 39 | filter for code-server can be found in `./code-server.fail2ban.conf`. Once this 40 | is installed and configured correctly, repeated failed login attempts should 41 | automatically be banned from connecting to your server. 42 | 43 | -------------------------------------------------------------------------------- /packages/tunnel/src/client.ts: -------------------------------------------------------------------------------- 1 | import { Event, Emitter } from "@coder/events"; 2 | import { TunnelCloseCode } from "./common"; 3 | 4 | export interface TunnelCloseEvent { 5 | readonly code: TunnelCloseCode; 6 | readonly reason: string; 7 | } 8 | 9 | export interface ClientConnection { 10 | readonly onData: Event; 11 | readonly onClose: Event; 12 | send(data: ArrayBuffer): void; 13 | } 14 | 15 | export const forward = (connectionUrl: string): Promise => { 16 | return new Promise((resolve, reject): void => { 17 | const socket = new WebSocket(connectionUrl); 18 | const closeEmitter = new Emitter(); 19 | const dataEmitter = new Emitter(); 20 | const connection: ClientConnection = { 21 | get onClose(): Event { 22 | return closeEmitter.event; 23 | }, 24 | get onData(): Event { 25 | return dataEmitter.event; 26 | }, 27 | send(data: ArrayBuffer): void { 28 | socket.send(data); 29 | }, 30 | }; 31 | socket.binaryType = "arraybuffer"; 32 | socket.addEventListener("message", (event) => { 33 | dataEmitter.emit(event.data); 34 | }); 35 | socket.addEventListener("error", (event) => { 36 | reject("uncertain"); 37 | }); 38 | socket.addEventListener("open", () => { 39 | resolve(connection); 40 | }); 41 | socket.addEventListener("close", (event) => { 42 | closeEmitter.emit({ 43 | code: event.code, 44 | reason: event.reason, 45 | }); 46 | }); 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | # Build using a Docker container using the specified image and version. 5 | function docker_build() { 6 | local image="${1}" ; shift 7 | local version="${1}" ; shift 8 | 9 | local containerId 10 | containerId=$(docker create --network=host --rm -it -v "$(pwd)"/.cache:/src/.cache "${image}") 11 | docker start "${containerId}" 12 | docker exec "${containerId}" mkdir -p /src 13 | 14 | function docker_exec() { 15 | docker exec "${containerId}" bash -c "$@" 16 | } 17 | 18 | docker cp ./. "${containerId}":/src 19 | docker_exec "cd /src && yarn" 20 | docker_exec "cd /src && npm rebuild" 21 | docker_exec "cd /src && NODE_ENV=production VERSION=${version} yarn task build:server:binary" 22 | docker_exec "cd /src && yarn task package ${version}" 23 | docker cp "${containerId}":/src/release/. ./release/ 24 | 25 | docker stop "${containerId}" 26 | } 27 | 28 | function main() { 29 | local version=${VERSION:-} 30 | local ostype=${OSTYPE:-} 31 | 32 | if [[ -z "${version}" ]] ; then 33 | >&2 echo "Must set VERSION environment variable" 34 | exit 1 35 | fi 36 | 37 | if [[ "${ostype}" == "darwin"* ]]; then 38 | NODE_ENV=production VERSION="${version}" yarn task build:server:binary 39 | yarn task package "${version}" 40 | else 41 | local image 42 | if [[ "$TARGET" == "alpine" ]]; then 43 | image="codercom/nbin-alpine" 44 | else 45 | image="codercom/nbin-centos" 46 | fi 47 | docker_build "${image}" "${version}" 48 | fi 49 | } 50 | 51 | main "$@" 52 | -------------------------------------------------------------------------------- /scripts/install-packages.ts: -------------------------------------------------------------------------------- 1 | import { exec, execSync } from "child_process"; 2 | import { existsSync, readdirSync } from "fs"; 3 | import * as os from "os"; 4 | import { join, resolve } from "path"; 5 | import { logger, field } from "../packages/logger/src/logger"; 6 | 7 | /** 8 | * Install dependencies for a single package. 9 | */ 10 | const doInstall = (pkg: string, path: string): Promise => { 11 | logger.info(`Installing "${pkg}" dependencies...`); 12 | 13 | return new Promise((resolve): void => { 14 | exec("yarn --network-concurrency 1", { 15 | cwd: path, 16 | maxBuffer: 1024 * 1024 * 10, 17 | }, (error, stdout, stderr) => { 18 | if (error) { 19 | logger.error( 20 | `Failed to install "${pkg}" dependencies`, 21 | field("error", error), 22 | field("stdout", stdout), 23 | field("stderr", stderr), 24 | ); 25 | process.exit(1); 26 | } 27 | 28 | logger.info(`Successfully grabbed \"${pkg}\" dependencies!`); 29 | resolve(); 30 | }); 31 | }); 32 | }; 33 | 34 | /** 35 | * Install dependencies for all packages. 36 | */ 37 | const handlePackages = async (dir: string): Promise => { 38 | const dirs = readdirSync(dir); 39 | for (let i = 0; i < dirs.length; i++) { 40 | const pkg = dirs[i]; 41 | const pkgDir = join(dir, pkg); 42 | const pkgJsonPath = join(pkgDir, "package.json"); 43 | if (existsSync(pkgJsonPath)) { 44 | const ip = await doInstall(pkg, pkgDir); 45 | } 46 | } 47 | }; 48 | 49 | handlePackages(resolve(__dirname, "..", "packages")).then(() => { 50 | return handlePackages(resolve(__dirname, "..", "packages", "app")); 51 | }); 52 | -------------------------------------------------------------------------------- /packages/ide/src/fill/os.ts: -------------------------------------------------------------------------------- 1 | import { OperatingSystem, InitData } from "@coder/protocol"; 2 | import { client } from "./client"; 3 | 4 | class OS { 5 | private _homedir: string | undefined; 6 | private _tmpdir: string | undefined; 7 | private _platform: NodeJS.Platform | undefined; 8 | 9 | public constructor() { 10 | client.initData.then((d) => this.initialize(d)); 11 | } 12 | 13 | public homedir(): string { 14 | if (typeof this._homedir === "undefined") { 15 | throw new Error("trying to access homedir before it has been set"); 16 | } 17 | 18 | return this._homedir; 19 | } 20 | 21 | public tmpdir(): string { 22 | if (typeof this._tmpdir === "undefined") { 23 | throw new Error("trying to access tmpdir before it has been set"); 24 | } 25 | 26 | return this._tmpdir; 27 | } 28 | 29 | public initialize(data: InitData): void { 30 | this._homedir = data.homeDirectory; 31 | this._tmpdir = data.tmpDirectory; 32 | switch (data.os) { 33 | case OperatingSystem.Windows: this._platform = "win32"; break; 34 | case OperatingSystem.Mac: this._platform = "darwin"; break; 35 | default: this._platform = "linux"; break; 36 | } 37 | process.platform = this._platform; 38 | process.env = {}; 39 | data.env.forEach((v, k) => { 40 | process.env[k] = v; 41 | }); 42 | } 43 | 44 | public release(): string { 45 | return "Unknown"; 46 | } 47 | 48 | public platform(): NodeJS.Platform { 49 | if (typeof this._platform === "undefined") { 50 | throw new Error("trying to access platform before it has been set"); 51 | } 52 | 53 | return this._platform; 54 | } 55 | } 56 | 57 | export = new OS(); 58 | -------------------------------------------------------------------------------- /packages/logger/src/extender.ts: -------------------------------------------------------------------------------- 1 | import * as gcl from "@google-cloud/logging"; 2 | import { Extender, logger, field } from "./logger"; 3 | 4 | export const createStackdriverExtender = (projectId: string, logId: string): Extender => { 5 | enum GcpLogSeverity { 6 | DEFAULT = 0, 7 | DEBUG = 100, 8 | INFO = 200, 9 | NOTICE = 300, 10 | WARNING = 400, 11 | ERROR = 500, 12 | CRITICAL = 600, 13 | ALERT = 700, 14 | EMERGENCY = 800, 15 | } 16 | 17 | const logging = new gcl.Logging({ 18 | autoRetry: true, 19 | projectId, 20 | }); 21 | 22 | const log = logging.log(logId); 23 | const convertSeverity = (severity: "trace" | "info" | "warn" | "debug" | "error"): GcpLogSeverity => { 24 | switch (severity) { 25 | case "trace": 26 | case "debug": 27 | return GcpLogSeverity.DEBUG; 28 | case "info": 29 | return GcpLogSeverity.INFO; 30 | case "error": 31 | return GcpLogSeverity.ERROR; 32 | case "warn": 33 | return GcpLogSeverity.WARNING; 34 | } 35 | }; 36 | 37 | return (options): void => { 38 | const severity = convertSeverity(options.type); 39 | // tslint:disable-next-line:no-any 40 | const metadata = {} as any; 41 | if (options.fields) { 42 | options.fields.forEach((f) => { 43 | if (!f) { 44 | return; 45 | } 46 | metadata[f.identifier] = f.value; 47 | }); 48 | } 49 | 50 | const entry = log.entry({ 51 | // tslint:disable-next-line:no-any 52 | severity: severity as any, 53 | }, { 54 | ...metadata, 55 | message: options.message, 56 | }); 57 | 58 | log.write(entry).catch((ex) => { 59 | logger.named("GCP").error("Failed to log", field("error", ex)); 60 | }); 61 | }; 62 | 63 | }; 64 | -------------------------------------------------------------------------------- /deployment/aws/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: code-server 5 | --- 6 | apiVersion: v1 7 | kind: Service 8 | metadata: 9 | name: code-server 10 | namespace: code-server 11 | spec: 12 | ports: 13 | - port: 8443 14 | name: https 15 | protocol: TCP 16 | selector: 17 | app: code-server 18 | type: ClusterIP 19 | --- 20 | kind: StorageClass 21 | apiVersion: storage.k8s.io/v1 22 | metadata: 23 | name: gp2 24 | annotations: 25 | storageclass.kubernetes.io/is-default-class: "true" 26 | provisioner: kubernetes.io/aws-ebs 27 | parameters: 28 | type: gp2 29 | fsType: ext4 30 | --- 31 | kind: PersistentVolumeClaim 32 | apiVersion: v1 33 | metadata: 34 | name: code-store 35 | namespace: code-server 36 | spec: 37 | accessModes: 38 | - ReadWriteOnce 39 | resources: 40 | requests: 41 | storage: 60Gi 42 | --- 43 | apiVersion: extensions/v1beta1 44 | kind: Deployment 45 | metadata: 46 | labels: 47 | app: code-server 48 | name: code-server 49 | namespace: code-server 50 | spec: 51 | selector: 52 | matchLabels: 53 | app: code-server 54 | replicas: 1 55 | template: 56 | metadata: 57 | labels: 58 | app: code-server 59 | spec: 60 | containers: 61 | - image: codercom/code-server 62 | imagePullPolicy: Always 63 | name: code-servery 64 | ports: 65 | - containerPort: 8443 66 | name: https 67 | volumeMounts: 68 | - name: code-server-storage 69 | mountPath: /go/src 70 | volumes: 71 | - name: code-server-storage 72 | persistentVolumeClaim: 73 | claimName: code-store 74 | 75 | -------------------------------------------------------------------------------- /packages/app/browser/src/app.ts: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import { MDCTextField } from "@material/textfield"; 3 | //@ts-ignore 4 | import { MDCCheckbox } from "@material/checkbox"; 5 | import "material-components-web/dist/material-components-web.css"; 6 | import "./app.scss"; 7 | 8 | document.querySelectorAll(".mdc-text-field").forEach((d) => new MDCTextField(d)); 9 | document.querySelectorAll(".mdc-checkbox").forEach((d) => new MDCCheckbox(d)); 10 | 11 | window.addEventListener("message", (event) => { 12 | if (event.data === "app") { 13 | document.body.classList.add("in-app"); 14 | 15 | const back = document.querySelector(".back")!; 16 | back.addEventListener("click", () => { 17 | (event.source as Window).postMessage("back", event.origin); 18 | }); 19 | } 20 | }); 21 | 22 | const password = document.getElementById("password") as HTMLInputElement; 23 | const form = document.getElementById("login-form") as HTMLFormElement; 24 | 25 | if (!form) { 26 | throw new Error("No password form found"); 27 | } 28 | 29 | form.addEventListener("submit", (e) => { 30 | e.preventDefault(); 31 | document.cookie = `password=${password.value}; ` 32 | + `path=${location.pathname.replace(/\/login\/?$/, "/")}; ` 33 | + `domain=${location.hostname}`; 34 | location.reload(); 35 | }); 36 | 37 | /** 38 | * Notify user on load of page if previous password was unsuccessful 39 | */ 40 | const reg = new RegExp(`password=(\\w+);?`); 41 | const matches = document.cookie.match(reg); 42 | const errorDisplay = document.getElementById("error-display") as HTMLDivElement; 43 | 44 | if (document.referrer === document.location.href && matches) { 45 | errorDisplay.innerText = "Password is incorrect!"; 46 | } 47 | -------------------------------------------------------------------------------- /packages/requirefs/test/requirefs.test.ts: -------------------------------------------------------------------------------- 1 | import { RequireFS } from "../src/requirefs"; 2 | import { TestCaseArray, isMac } from "./requirefs.util"; 3 | 4 | const toTest = new TestCaseArray(); 5 | 6 | describe("requirefs", () => { 7 | for (let i = 0; i < toTest.length(); i++) { 8 | const testCase = toTest.byID(i); 9 | if (!isMac && testCase.name === "gtar") { 10 | break; 11 | } 12 | if (isMac && testCase.name === "tar") { 13 | break; 14 | } 15 | 16 | describe(testCase.name, () => { 17 | let rfs: RequireFS; 18 | beforeAll(async () => { 19 | rfs = await testCase.rfs; 20 | }); 21 | 22 | it("should parse individual module", () => { 23 | expect(rfs.require("./individual.js").frog).toEqual("hi"); 24 | }); 25 | 26 | it("should parse chained modules", () => { 27 | expect(rfs.require("./chained-1").text).toEqual("moo"); 28 | }); 29 | 30 | it("should parse through subfolders", () => { 31 | expect(rfs.require("./subfolder").orangeColor).toEqual("blue"); 32 | }); 33 | 34 | it("should be able to move up directories", () => { 35 | expect(rfs.require("./subfolder/goingUp").frog).toEqual("hi"); 36 | }); 37 | 38 | it("should resolve node_modules", () => { 39 | expect(rfs.require("./nodeResolve").banana).toEqual("potato"); 40 | }); 41 | 42 | it("should access global scope", () => { 43 | // tslint:disable-next-line no-any for testing 44 | (window as any).coder = { 45 | test: "hi", 46 | }; 47 | expect(rfs.require("./scope")).toEqual("hi"); 48 | }); 49 | 50 | it("should find custom module", () => { 51 | rfs.provide("donkey", "ok"); 52 | expect(rfs.require("./customModule")).toEqual("ok"); 53 | }); 54 | }); 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /packages/ide/src/fill/dialog.scss: -------------------------------------------------------------------------------- 1 | .msgbox { 2 | padding-top: 25px; 3 | padding-left: 40px; 4 | padding-right: 40px; 5 | padding-bottom: 25px; 6 | background: #242424; 7 | -webkit-box-shadow: 0px 0px 10px -3px rgba(0,0,0,0.75); 8 | -moz-box-shadow: 0px 0px 10px -3px rgba(0,0,0,0.75); 9 | box-shadow: 0px 0px 10px -3px rgba(0,0,0,0.75); 10 | border-radius: 3px; 11 | } 12 | 13 | .msgbox.input { 14 | max-width: 500px; 15 | width: 100%; 16 | } 17 | 18 | .msgbox > .input { 19 | background: #141414; 20 | border: none; 21 | box-sizing: border-box; 22 | padding: 10px; 23 | width: 100%; 24 | } 25 | 26 | .msgbox > .msg { 27 | font-size: 16px; 28 | font-weight: bold; 29 | } 30 | 31 | .msgbox > .detail { 32 | font-size: 14px; 33 | margin: 5px 0; 34 | } 35 | 36 | .msgbox > .errors { 37 | margin-top: 20px; 38 | } 39 | 40 | .msgbox > .errors { 41 | color: #f44747; 42 | } 43 | 44 | .msgbox > .button-wrapper { 45 | display: flex; 46 | flex-direction: row; 47 | justify-content: space-between; 48 | margin-top: 20px; 49 | } 50 | 51 | .msgbox > .button-wrapper > button { 52 | flex: 1; 53 | border-radius: 2px; 54 | padding: 10px; 55 | color: white; 56 | background: #3d3d3d; 57 | border: 0px; 58 | cursor: pointer; 59 | opacity: 0.8; 60 | } 61 | 62 | .msgbox > .button-wrapper > button:hover { 63 | opacity: 1; 64 | } 65 | 66 | .msgbox > .button-wrapper > button:not(:last-child) { 67 | margin-right: 8px; 68 | } 69 | 70 | .msgbox-overlay { 71 | align-items: center; 72 | background: rgba(0, 0, 0, 0.4); 73 | bottom: 0; 74 | display: flex; 75 | justify-content: center; 76 | left: 0; 77 | opacity: 0; 78 | position: absolute; 79 | right: 0; 80 | top: 0; 81 | transition: 300ms opacity ease; 82 | z-index: 15; 83 | } 84 | -------------------------------------------------------------------------------- /packages/tunnel/src/server.ts: -------------------------------------------------------------------------------- 1 | import * as net from "net"; 2 | import { TunnelCloseCode } from "./common"; 3 | 4 | export interface WS { 5 | addEventListener(event: "message", cb: (event: { 6 | // tslint:disable-next-line:no-any 7 | readonly data: any; 8 | }) => void): void; 9 | addEventListener(event: "close", cb: () => void): void; 10 | binaryType: string; 11 | close(code: number, reason?: string): void; 12 | // tslint:disable-next-line:no-any 13 | send(data: any): void; 14 | } 15 | 16 | export const handle = async (socket: WS, port: number): Promise => { 17 | const hosts = [ 18 | "127.0.0.1", 19 | "::", // localhost 20 | ]; 21 | 22 | let localSocket: net.Socket | undefined; 23 | for (let i = 0; i < hosts.length; i++) { 24 | if (localSocket) { 25 | break; 26 | } 27 | localSocket = await new Promise((resolve, reject): void => { 28 | const socket = net.connect({ 29 | host: hosts[i], 30 | port, 31 | }, () => { 32 | // Connected 33 | resolve(socket); 34 | }); 35 | socket.on("error", (err: Error & { readonly code: string }) => { 36 | if (err.code === "ECONNREFUSED") { 37 | resolve(undefined); 38 | } 39 | }); 40 | }); 41 | } 42 | if (!localSocket) { 43 | socket.close(TunnelCloseCode.ConnectionRefused); 44 | 45 | return; 46 | } 47 | socket.binaryType = "arraybuffer"; 48 | socket.addEventListener("message", (event) => localSocket!.write(Buffer.from(event.data))); 49 | socket.addEventListener("close", () => localSocket!.end()); 50 | localSocket.on("data", (data) => socket.send(data)); 51 | localSocket.on("error", (err) => socket.close(TunnelCloseCode.Error, err.toString())); 52 | localSocket.on("close", () => socket.close(TunnelCloseCode.Normal)); 53 | }; 54 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.15.1 2 | 3 | # Install VS Code's deps. These are the only two it seems we need. 4 | RUN apt-get update && apt-get install -y \ 5 | libxkbfile-dev \ 6 | libsecret-1-dev 7 | 8 | # Ensure latest yarn. 9 | RUN npm install -g yarn@1.13 10 | 11 | WORKDIR /src 12 | COPY . . 13 | 14 | # In the future, we can use https://github.com/yarnpkg/rfcs/pull/53 to make yarn use the node_modules 15 | # directly which should be fast as it is slow because it populates its own cache every time. 16 | RUN yarn && NODE_ENV=production yarn task build:server:binary 17 | 18 | # We deploy with ubuntu so that devs have a familiar environment. 19 | FROM ubuntu:18.04 20 | 21 | RUN apt-get update && apt-get install -y \ 22 | openssl \ 23 | net-tools \ 24 | git \ 25 | locales \ 26 | sudo \ 27 | dumb-init \ 28 | vim \ 29 | curl \ 30 | wget 31 | 32 | RUN locale-gen en_US.UTF-8 33 | # We unfortunately cannot use update-locale because docker will not use the env variables 34 | # configured in /etc/default/locale so we need to set it manually. 35 | ENV LC_ALL=en_US.UTF-8 36 | 37 | RUN adduser --gecos '' --disabled-password coder && \ 38 | echo "coder ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd 39 | 40 | USER coder 41 | # We create first instead of just using WORKDIR as when WORKDIR creates, the user is root. 42 | RUN mkdir -p /home/coder/project 43 | 44 | WORKDIR /home/coder/project 45 | 46 | # This assures we have a volume mounted even if the user forgot to do bind mount. 47 | # So that they do not lose their data if they delete the container. 48 | VOLUME [ "/home/coder/project" ] 49 | 50 | COPY --from=0 /src/packages/server/cli-linux-x64 /usr/local/bin/code-server 51 | EXPOSE 8443 52 | 53 | ENTRYPOINT ["dumb-init", "code-server"] 54 | -------------------------------------------------------------------------------- /packages/requirefs/test/requirefs.bench.ts: -------------------------------------------------------------------------------- 1 | import * as benchmark from "benchmark"; 2 | import { performance } from "perf_hooks"; 3 | import { TestCaseArray, isMac } from "./requirefs.util"; 4 | 5 | const files = [ 6 | "./individual.js", "./chained-1", "./subfolder", 7 | "./subfolder/goingUp", "./nodeResolve", 8 | ]; 9 | const toBench = new TestCaseArray(); 10 | 11 | // Limits the amount of time taken for each test, 12 | // but increases uncertainty. 13 | benchmark.options.maxTime = 0.5; 14 | 15 | let suite = new benchmark.Suite(); 16 | let _start = 0; 17 | const addMany = (names: string[]): benchmark.Suite => { 18 | for (let name of names) { 19 | for (let file of files) { 20 | suite = suite.add(`${name} -> ${file}`, async () => { 21 | let rfs = await toBench.byName(name).rfs; 22 | rfs.require(file); 23 | }); 24 | } 25 | } 26 | _start = performance.now(); 27 | return suite; 28 | } 29 | // Returns mean time per operation, in microseconds (10^-6s). 30 | const mean = (c: any): number => { 31 | return Number((c.stats.mean * 10e+5).toFixed(5)); 32 | }; 33 | 34 | // Swap out the tar command for gtar, when on MacOS. 35 | let testNames = ["zip", "bsdtar", isMac ? "gtar" : "tar"]; 36 | addMany(testNames).on("cycle", (event: benchmark.Event) => { 37 | console.log(String(event.target) + ` (~${mean(event.target)} μs/op)`); 38 | }).on("complete", () => { 39 | const slowest = suite.filter("slowest").shift(); 40 | const fastest = suite.filter("fastest").shift(); 41 | console.log(`===\nFastest is ${fastest.name} with ~${mean(fastest)} μs/op`); 42 | if (slowest.name !== fastest.name) { 43 | console.log(`Slowest is ${slowest.name} with ~${mean(slowest)} μs/op`); 44 | } 45 | const d = ((performance.now() - _start)/1000).toFixed(2); 46 | console.log(`Benchmark took ${d} s`); 47 | }) 48 | .run({ "async": true }); 49 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/vscodeTextmate.ts: -------------------------------------------------------------------------------- 1 | import * as vscodeTextmate from "../../../../lib/vscode/node_modules/vscode-textmate"; 2 | 3 | const target = vscodeTextmate as typeof vscodeTextmate; 4 | 5 | const ctx = (require as any).context("../../../../lib/extensions", true, /.*\.tmLanguage.json$/); 6 | // Maps grammar scope to loaded grammar 7 | const scopeToGrammar = {} as any; 8 | 9 | ctx.keys().forEach((key: string) => { 10 | const value = ctx(key); 11 | if (value.scopeName) { 12 | scopeToGrammar[value.scopeName] = value; 13 | } 14 | }); 15 | 16 | target.Registry = class Registry extends vscodeTextmate.Registry { 17 | public constructor(opts: vscodeTextmate.RegistryOptions) { 18 | super({ 19 | ...opts, 20 | getOnigLib: (): Promise => { 21 | return new Promise((res, rej) => { 22 | const onigasm = require("onigasm"); 23 | const wasmUrl = require("!!file-loader!onigasm/lib/onigasm.wasm"); 24 | 25 | return fetch(wasmUrl).then(resp => resp.arrayBuffer()).then(buffer => { 26 | return onigasm.loadWASM(buffer); 27 | }).then(() => { 28 | res({ 29 | createOnigScanner: function (patterns) { return new onigasm.OnigScanner(patterns); }, 30 | createOnigString: function (s) { return new onigasm.OnigString(s); }, 31 | }); 32 | }).catch(reason => rej(reason)); 33 | }); 34 | }, 35 | loadGrammar: async (scopeName: string) => { 36 | if (scopeToGrammar[scopeName]) { 37 | return scopeToGrammar[scopeName]; 38 | } 39 | 40 | return opts.loadGrammar(scopeName); 41 | }, 42 | }); 43 | } 44 | }; 45 | 46 | enum StandardTokenType { 47 | Other = 0, 48 | Comment = 1, 49 | String = 2, 50 | RegEx = 4, 51 | } 52 | 53 | // tslint:disable-next-line no-any to override const 54 | (target as any).StandardTokenType = StandardTokenType; 55 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/menuRegistry.ts: -------------------------------------------------------------------------------- 1 | import { IDisposable } from "vs/base/common/lifecycle"; 2 | import * as actions from "vs/platform/actions/common/actions"; 3 | import { CloseWorkspaceAction } from "vs/workbench/browser/actions/workspaceActions"; 4 | import { OpenProcessExplorer } from "vs/workbench/contrib/issue/electron-browser/issueActions"; 5 | import { ToggleDevToolsAction } from "vs/workbench/electron-browser/actions/developerActions"; 6 | import { OpenPrivacyStatementUrlAction, OpenRequestFeatureUrlAction, OpenTwitterUrlAction } from "vs/workbench/electron-browser/actions/helpActions"; 7 | import { CloseCurrentWindowAction, NewWindowAction, ShowAboutDialogAction } from "vs/workbench/electron-browser/actions/windowActions"; 8 | import { REVEAL_IN_OS_COMMAND_ID } from "vs/workbench/contrib/files/browser/fileCommands"; 9 | 10 | const toSkip = [ 11 | ToggleDevToolsAction.ID, 12 | OpenTwitterUrlAction.ID, 13 | OpenPrivacyStatementUrlAction.ID, 14 | ShowAboutDialogAction.ID, 15 | OpenProcessExplorer.ID, 16 | OpenRequestFeatureUrlAction.ID, 17 | NewWindowAction.ID, 18 | CloseCurrentWindowAction.ID, 19 | CloseWorkspaceAction.ID, 20 | REVEAL_IN_OS_COMMAND_ID, 21 | 22 | // Unfortunately referenced as a string 23 | "update.showCurrentReleaseNotes", 24 | "workbench.action.openIssueReporter", 25 | ]; 26 | 27 | // Intercept appending menu items so we can skip items that won't work. 28 | const originalAppend = actions.MenuRegistry.appendMenuItem.bind(actions.MenuRegistry); 29 | actions.MenuRegistry.appendMenuItem = (id: actions.MenuId, item: actions.IMenuItem | actions.ISubmenuItem): IDisposable => { 30 | if (actions.isIMenuItem(item)) { 31 | if (toSkip.indexOf(item.command.id) !== -1) { 32 | // Skip instantiation 33 | return { 34 | dispose: (): void => undefined, 35 | }; 36 | } 37 | } 38 | 39 | return originalAppend(id, item); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/iconv-lite.ts: -------------------------------------------------------------------------------- 1 | import * as iconv from "../../node_modules/iconv-lite"; 2 | import { Transform, TransformCallback } from "stream"; 3 | 4 | class IconvLiteDecoderStream extends Transform { 5 | // tslint:disable-next-line no-any 6 | private conv: any; 7 | private encoding: string; 8 | 9 | public constructor(options: { encoding: string }) { 10 | super(options); 11 | // tslint:disable-next-line no-any 12 | this.conv = (iconv as any).getDecoder(options.encoding, undefined); 13 | this.encoding = options.encoding; 14 | } 15 | 16 | // tslint:disable-next-line no-any 17 | public _transform(chunk: any, _encoding: string, done: TransformCallback): void { 18 | if (!Buffer.isBuffer(chunk)) { 19 | return done(new Error("Iconv decoding stream needs buffers as its input.")); 20 | } 21 | try { 22 | const res = this.conv.write(chunk); 23 | if (res && res.length) { 24 | this.push(res, this.encoding); 25 | } 26 | done(); 27 | } catch (error) { 28 | done(error); 29 | } 30 | } 31 | 32 | public _flush(done: TransformCallback): void { 33 | try { 34 | const res = this.conv.end(); 35 | if (res && res.length) { 36 | this.push(res, this.encoding); 37 | } 38 | done(); 39 | } catch (error) { 40 | done(error); 41 | } 42 | } 43 | 44 | // tslint:disable-next-line no-any 45 | public collect(cb: (error: Error | null, response?: any) => void): this { 46 | let res = ""; 47 | this.on("error", cb); 48 | this.on("data", (chunk) => res += chunk); 49 | this.on("end", () => { 50 | cb(null, res); 51 | }); 52 | 53 | return this; 54 | } 55 | } 56 | 57 | const decodeStream = (encoding: string): NodeJS.ReadWriteStream => { 58 | return new IconvLiteDecoderStream({ encoding }); 59 | }; 60 | 61 | const target = iconv as typeof iconv; 62 | target.decodeStream = decodeStream; 63 | 64 | export = target; 65 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/workspacesService.ts: -------------------------------------------------------------------------------- 1 | import { URI } from "vs/base/common/uri"; 2 | import { IEnvironmentService } from "vs/platform/environment/common/environment"; 3 | import { ILogService } from "vs/platform/log/common/log"; 4 | import { IWorkspaceFolderCreationData, IWorkspaceIdentifier, IWorkspacesService } from "vs/platform/workspaces/common/workspaces"; 5 | import { WorkspacesMainService } from "vs/platform/workspaces/electron-main/workspacesMainService"; 6 | import * as workspacesIpc from "vs/platform/workspaces/node/workspacesIpc"; 7 | import { workbench } from "../workbench"; 8 | 9 | /** 10 | * Instead of going to the shared process, we'll directly run these methods on 11 | * the client. This setup means we can only control the current window. 12 | */ 13 | class WorkspacesService implements IWorkspacesService { 14 | // tslint:disable-next-line:no-any 15 | public _serviceBrand: any; 16 | 17 | public createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[] | undefined): Promise { 18 | const mainService = new WorkspacesMainService( 19 | workbench.serviceCollection.get(IEnvironmentService) as IEnvironmentService, 20 | workbench.serviceCollection.get(ILogService) as ILogService, 21 | ); 22 | 23 | // lib/vscode/src/vs/platform/workspaces/node/workspacesIpc.ts 24 | const rawFolders: IWorkspaceFolderCreationData[] = folders!; 25 | if (Array.isArray(rawFolders)) { 26 | folders = rawFolders.map(rawFolder => { 27 | return { 28 | uri: URI.revive(rawFolder.uri), // convert raw URI back to real URI 29 | name: rawFolder.name!, 30 | } as IWorkspaceFolderCreationData; 31 | }); 32 | } 33 | 34 | return mainService.createUntitledWorkspace(folders); 35 | } 36 | } 37 | 38 | const target = workspacesIpc as typeof workspacesIpc; 39 | // @ts-ignore TODO: don't ignore it. 40 | target.WorkspacesChannelClient = WorkspacesService; 41 | -------------------------------------------------------------------------------- /packages/protocol/src/node/modules/spdlog.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { EventEmitter } from "events"; 3 | import * as spdlog from "spdlog"; 4 | import { ServerProxy } from "../../common/proxy"; 5 | 6 | // tslint:disable completed-docs 7 | 8 | export class RotatingLoggerProxy extends ServerProxy { 9 | public constructor(private readonly logger: spdlog.RotatingLogger) { 10 | super({ 11 | bindEvents: [], 12 | doneEvents: ["dispose"], 13 | instance: new EventEmitter(), 14 | }); 15 | } 16 | 17 | public async trace (message: string): Promise { this.logger.trace(message); } 18 | public async debug (message: string): Promise { this.logger.debug(message); } 19 | public async info (message: string): Promise { this.logger.info(message); } 20 | public async warn (message: string): Promise { this.logger.warn(message); } 21 | public async error (message: string): Promise { this.logger.error(message); } 22 | public async critical (message: string): Promise { this.logger.critical(message); } 23 | public async setLevel (level: number): Promise { this.logger.setLevel(level); } 24 | public async clearFormatters (): Promise { this.logger.clearFormatters(); } 25 | public async flush (): Promise { this.logger.flush(); } 26 | public async drop (): Promise { this.logger.drop(); } 27 | 28 | public async dispose(): Promise { 29 | await this.flush(); 30 | this.instance.emit("dispose"); 31 | await super.dispose(); 32 | } 33 | } 34 | 35 | export class SpdlogModuleProxy { 36 | public async createLogger(name: string, filePath: string, fileSize: number, fileCount: number): Promise { 37 | return new RotatingLoggerProxy(new (require("spdlog") as typeof import("spdlog")).RotatingLogger(name, filePath, fileSize, fileCount)); 38 | } 39 | 40 | public async setAsyncMode(bufferSize: number, flushInterval: number): Promise { 41 | require("spdlog").setAsyncMode(bufferSize, flushInterval); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/server/src/ipc.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | import { ChildProcess } from "child_process"; 3 | 4 | export interface IpcMessage { 5 | readonly event: string; 6 | readonly args: any[]; // tslint:disable-line no-any 7 | } 8 | 9 | export class StdioIpcHandler extends EventEmitter { 10 | private isListening: boolean = false; 11 | 12 | public constructor( 13 | private readonly childProcess?: ChildProcess, 14 | ) { 15 | super(); 16 | } 17 | 18 | // tslint:disable-next-line no-any 19 | public on(event: string, cb: (...args: any[]) => void): this { 20 | this.listen(); 21 | 22 | return super.on(event, cb); 23 | } 24 | 25 | // tslint:disable-next-line no-any 26 | public once(event: string, cb: (...args: any[]) => void): this { 27 | this.listen(); 28 | 29 | return super.once(event, cb); 30 | } 31 | 32 | // tslint:disable-next-line no-any 33 | public addListener(event: string, cb: (...args: any[]) => void): this { 34 | this.listen(); 35 | 36 | return super.addListener(event, cb); 37 | } 38 | 39 | // tslint:disable-next-line no-any 40 | public send(event: string, ...args: any[]): void { 41 | const msg: IpcMessage = { 42 | event, 43 | args, 44 | }; 45 | const d = JSON.stringify(msg); 46 | if (this.childProcess) { 47 | this.childProcess.stdin.write(d + "\n"); 48 | } else { 49 | process.stdout.write(d); 50 | } 51 | } 52 | 53 | private listen(): void { 54 | if (this.isListening) { 55 | return; 56 | } 57 | // tslint:disable-next-line no-any 58 | const onData = (data: any): void => { 59 | try { 60 | const d = JSON.parse(data.toString()) as IpcMessage; 61 | this.emit(d.event, ...d.args); 62 | } catch (ex) { 63 | if (!this.childProcess) { 64 | process.stderr.write(`Failed to parse incoming data: ${ex.message}`); 65 | } 66 | } 67 | }; 68 | if (this.childProcess) { 69 | this.childProcess.stdout.resume(); 70 | this.childProcess.stdout.on("data", onData); 71 | } else { 72 | process.stdin.resume(); 73 | process.stdin.on("data", onData); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /packages/vscode/src/vscode.scss: -------------------------------------------------------------------------------- 1 | // These use -webkit-margin-before/after which don't work. 2 | .monaco-workbench > .part > .title > .title-label h2, 3 | .monaco-panel-view .panel > .panel-header h3.title { 4 | margin-top: 0; 5 | margin-bottom: 0; 6 | } 7 | 8 | .monaco-icon-label > .monaco-icon-label-description-container { 9 | margin-right: auto; 10 | } 11 | 12 | .monaco-icon-label > .decorations-wrapper { 13 | display: flex; 14 | flex-direction: row; 15 | padding-right: 12px; 16 | } 17 | 18 | .monaco-icon-label::after { 19 | margin-left: initial; 20 | } 21 | 22 | // We don't have rating data. 23 | .extension-ratings { 24 | display: none !important; 25 | } 26 | 27 | // Using @supports to keep the Firefox fixes completely separate from vscode's 28 | // CSS that is tailored for Chrome. 29 | @supports (-moz-appearance:none) { 30 | // Fix buttons getting cut off on notifications. 31 | .monaco-workbench .notifications-list-container .notification-list-item .notification-list-item-buttons-container .monaco-button.monaco-text-button { 32 | max-width: 100%; 33 | width: auto; 34 | } 35 | 36 | .monaco-shell .screen-reader-detected-explanation .buttons a, 37 | .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink, 38 | .monaco-workbench .notifications-list-container .notification-list-item .notification-list-item-buttons-container .monaco-button { 39 | max-width: -moz-fit-content; 40 | } 41 | 42 | .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit, 43 | .explorer-viewlet .panel-header .count, 44 | .extensions-viewlet > .extensions .extension > .details > .header-container > .header > .version, 45 | .debug-viewlet .debug-call-stack .stack-frame .label { 46 | min-width: -moz-fit-content; 47 | } 48 | } 49 | 50 | .window-appicon { 51 | background-image: url(./vscode-coder.svg) !important; 52 | background-size: 56px !important; 53 | width: 56px !important; 54 | margin-right: 4px; 55 | } 56 | 57 | .window-controls-container { 58 | display: none !important; 59 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10.15.1 4 | services: 5 | - docker 6 | matrix: 7 | include: 8 | - os: linux 9 | dist: trusty 10 | env: 11 | - VSCODE_VERSION="1.33.1" MAJOR_VERSION="1" VERSION="$MAJOR_VERSION.$TRAVIS_BUILD_NUMBER-vsc$VSCODE_VERSION" TARGET="centos" 12 | - os: linux 13 | dist: trusty 14 | env: 15 | - VSCODE_VERSION="1.33.1" MAJOR_VERSION="1" VERSION="$MAJOR_VERSION.$TRAVIS_BUILD_NUMBER-vsc$VSCODE_VERSION" TARGET="alpine" 16 | - os: osx 17 | env: 18 | - VSCODE_VERSION="1.33.1" MAJOR_VERSION="1" VERSION="$MAJOR_VERSION.$TRAVIS_BUILD_NUMBER-vsc$VSCODE_VERSION" 19 | before_install: 20 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install libxkbfile-dev 21 | libsecret-1-dev; fi 22 | - npm install -g yarn@1.12.3 23 | script: 24 | - scripts/build.sh 25 | before_deploy: 26 | - echo "$VERSION" "$TRAVIS_COMMIT" 27 | - git config --local user.name "$USER_NAME" 28 | - git config --local user.email "$USER_EMAIL" 29 | - git tag "$VERSION" "$TRAVIS_COMMIT" 30 | deploy: 31 | provider: releases 32 | file_glob: true 33 | draft: true 34 | tag_name: "$VERSION" 35 | target_commitish: "$TRAVIS_COMMIT" 36 | name: "$VERSION" 37 | skip_cleanup: true 38 | api_key: 39 | secure: YL/x24KjYjgYXPcJWk3FV7FGxI79Mh6gBECQEcdlf3fkLEoKFVgzHBoUNWrFPzyR4tgLyWNAgcpD9Lkme1TRWTom7UPjXcwMNyLcLa+uec7ciSAnYD9ntLTpiCuPDD1u0LtRGclSi/EHQ+F8YVq+HZJpXTsJeAmOmihma3GVbGKSZr+BRum+0YZSG4w+o4TOlYzw/4bLWS52MogZcwpjd+hemBbgXLuGU2ziKv2vEKCZFbEeA16II4x1WLI4mutDdCeh7+3aLzGLwDa49NxtsVYNjyNFF75JhCTCNA55e2YMiLz9Uq69IXe/mi5F7xUaFfhIqqLNyKBnKeEOzu3dYnc+8n3LjnQ+00PmkF05nx9kBn3UfV1kwQGh6QbyDmTtBP07rtUMyI14aeQqHjxsaVRdMnwj9Q2DjXRr8UDqESZF0rmK3pHCXS2fBhIzLE8tLVW5Heiba2pQRFMHMZW+KBE97FzcFh7is90Ait3T8enfcd/PWFPYoBejDAdjwxwOkezh5N5ZkYquEfDYuWrFi6zRFCktsruaAcA+xGtTf9oilBBzUqu8Ie+YFWH5me83xakcblJWdaW/D2rLJAJH3m6LFm8lBqyUgDX5t/etob6CpDuYHu5D1J3XINOj/+aLAcadq6qlh70PMZS3zYffUu3JlzaD2amlSHIT8b5YXFc= 40 | file: 41 | - release/*.tar.gz 42 | - release/*.zip 43 | on: 44 | branch: master 45 | cache: 46 | yarn: true 47 | timeout: 1000 48 | directories: 49 | - .cache 50 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": "./rules/dist", 3 | "rules": { 4 | "only-arrow-functions": true, 5 | "curly-statement-newlines": true, 6 | "no-block-padding": true, 7 | "adjacent-overload-signatures": true, 8 | "align": true, 9 | "await-promise": [true, "Thenable"], 10 | "class-name": true, 11 | "eofline": true, 12 | "import-spacing": true, 13 | "indent": [true, "tabs"], 14 | "no-angle-bracket-type-assertion": false, 15 | "no-bitwise": false, 16 | "no-any": true, 17 | "newline-before-return": true, 18 | "no-console": true, 19 | "no-duplicate-imports": true, 20 | "no-consecutive-blank-lines": true, 21 | "no-empty": true, 22 | "no-floating-promises": true, 23 | "no-return-await": true, 24 | "no-var-keyword": true, 25 | "no-trailing-whitespace": true, 26 | "no-redundant-jsdoc": true, 27 | "no-implicit-dependencies": false, 28 | "no-boolean-literal-compare": true, 29 | "prefer-readonly": true, 30 | "deprecation": true, 31 | "semicolon": true, 32 | "one-line": [ 33 | true, 34 | "check-catch", 35 | "check-finally", 36 | "check-else", 37 | "check-whitespace", 38 | "check-open-brace" 39 | ], 40 | "completed-docs": { 41 | "options": [ 42 | true, 43 | "enums", 44 | "functions", 45 | "methods", 46 | "classes" 47 | ], 48 | "severity": "warning" 49 | }, 50 | "no-unused-expression": [ 51 | true, 52 | "allow-fast-null-checks" 53 | ], 54 | "curly": [ 55 | true 56 | ], 57 | "quotemark": [ 58 | true, 59 | "double", 60 | "avoid-escape", 61 | "avoid-template" 62 | ], 63 | "trailing-comma": [ 64 | true, 65 | { 66 | "multiline": "always", 67 | "singleline": "never", 68 | "esSpecCompliant": true 69 | } 70 | ], 71 | "space-before-function-paren": [ 72 | false, 73 | "always" 74 | ], 75 | "member-access": [ 76 | true, 77 | "check-accessor", 78 | "check-constructor", 79 | "check-parameter-property" 80 | ], 81 | "typedef": [ 82 | true, 83 | "call-signature", 84 | "arrow-call-signature", 85 | "parameter", 86 | "property-declaration" 87 | ] 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/workbenchRegistry.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "@coder/logger"; 2 | import { IDisposable } from "vs/base/common/lifecycle"; 3 | import { Registry } from "vs/platform/registry/common/platform"; 4 | import { IWorkbenchActionRegistry, Extensions } from "vs/workbench/common/actions"; 5 | import { SyncActionDescriptor } from "vs/platform/actions/common/actions"; 6 | import { ContextKeyExpr } from "vs/platform/contextkey/common/contextkey"; 7 | import { ToggleDevToolsAction } from "vs/workbench/electron-browser/actions/developerActions"; 8 | import { TerminalPasteAction } from "vs/workbench/contrib/terminal/browser/terminalActions"; 9 | import { KEYBINDING_CONTEXT_TERMINAL_FOCUS } from "vs/workbench/contrib/terminal/common/terminal"; 10 | import { KeyCode, KeyMod } from "vs/base/common/keyCodes"; 11 | import { workbench } from "../workbench"; 12 | 13 | // Intercept adding workbench actions so we can skip actions that won't work or 14 | // modify actions that need different conditions, keybindings, etc. 15 | const registry = Registry.as(Extensions.WorkbenchActions); 16 | const originalRegister = registry.registerWorkbenchAction.bind(registry); 17 | registry.registerWorkbenchAction = (descriptor: SyncActionDescriptor, alias: string, category?: string, when?: ContextKeyExpr): IDisposable => { 18 | switch (descriptor.id) { 19 | case ToggleDevToolsAction.ID: // There appears to be no way to toggle this programmatically. 20 | logger.debug(`Skipping unsupported workbench action ${descriptor.id}`); 21 | 22 | return { 23 | dispose: (): void => undefined, 24 | }; 25 | 26 | case TerminalPasteAction.ID: // Modify the Windows keybinding and add our context key. 27 | // tslint:disable-next-line no-any override private 28 | (descriptor as any)._keybindings = { 29 | primary: KeyMod.CtrlCmd | KeyCode.KEY_V, 30 | linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_V }, 31 | win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_V }, 32 | mac: { primary: 0 }, 33 | }; 34 | // tslint:disable-next-line no-any override private 35 | (descriptor as any)._keybindingContext = ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, workbench.clipboardContextKey); 36 | } 37 | 38 | return originalRegister(descriptor, alias, category, when); 39 | }; 40 | -------------------------------------------------------------------------------- /packages/protocol/src/node/modules/node-pty.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { EventEmitter } from "events"; 3 | import * as pty from "node-pty"; 4 | import { ServerProxy } from "../../common/proxy"; 5 | import { withEnv } from "../../common/util"; 6 | 7 | // tslint:disable completed-docs 8 | 9 | /** 10 | * Server-side IPty proxy. 11 | */ 12 | export class NodePtyProcessProxy extends ServerProxy { 13 | public constructor(private readonly process: pty.IPty) { 14 | super({ 15 | bindEvents: ["process", "data", "exit"], 16 | doneEvents: ["exit"], 17 | instance: new EventEmitter(), 18 | }); 19 | 20 | this.process.on("data", (data) => this.instance.emit("data", data)); 21 | this.process.on("exit", (exitCode, signal) => this.instance.emit("exit", exitCode, signal)); 22 | 23 | let name = process.process; 24 | setTimeout(() => { // Need to wait for the caller to listen to the event. 25 | this.instance.emit("process", name); 26 | }, 1); 27 | const timer = setInterval(() => { 28 | if (process.process !== name) { 29 | name = process.process; 30 | this.instance.emit("process", name); 31 | } 32 | }, 200); 33 | 34 | this.process.on("exit", () => clearInterval(timer)); 35 | } 36 | 37 | public async getPid(): Promise { 38 | return this.process.pid; 39 | } 40 | 41 | public async getProcess(): Promise { 42 | return this.process.process; 43 | } 44 | 45 | public async kill(signal?: string): Promise { 46 | this.process.kill(signal); 47 | } 48 | 49 | public async resize(columns: number, rows: number): Promise { 50 | this.process.resize(columns, rows); 51 | } 52 | 53 | public async write(data: string): Promise { 54 | this.process.write(data); 55 | } 56 | 57 | public async dispose(): Promise { 58 | this.process.kill(); 59 | setTimeout(() => this.process.kill("SIGKILL"), 5000); // Double tap. 60 | await super.dispose(); 61 | } 62 | } 63 | 64 | /** 65 | * Server-side node-pty proxy. 66 | */ 67 | export class NodePtyModuleProxy { 68 | public async spawn(file: string, args: string[] | string, options: pty.IPtyForkOptions): Promise { 69 | return new NodePtyProcessProxy(require("node-pty").spawn(file, args, withEnv(options))); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/ide/src/fill/notification.ts: -------------------------------------------------------------------------------- 1 | import { logger, field } from "@coder/logger"; 2 | 3 | export interface INotificationHandle { 4 | close(): void; 5 | updateMessage(message: string): void; 6 | updateButtons(buttons: INotificationButton[]): void; 7 | } 8 | 9 | export enum Severity { 10 | Ignore = 0, 11 | Info = 1, 12 | Warning = 2, 13 | Error = 3, 14 | } 15 | 16 | export interface INotificationButton { 17 | label: string; 18 | run(): void; 19 | } 20 | 21 | /** 22 | * Optional notification service. 23 | */ 24 | export interface INotificationService { 25 | error(error: Error): void; 26 | prompt(severity: Severity, message: string, buttons: INotificationButton[], onCancel: () => void): INotificationHandle; 27 | } 28 | 29 | export interface IProgress { 30 | /** 31 | * Report progress, which should be the completed percentage from 0 to 100. 32 | */ 33 | report(progress: number): void; 34 | } 35 | 36 | export interface IProgressService { 37 | /** 38 | * Start a new progress bar that resolves & disappears when the task finishes. 39 | */ 40 | start(title: string, task: (progress: IProgress) => Promise, onCancel: () => void): Promise; 41 | } 42 | 43 | /** 44 | * Console-based notification service. 45 | */ 46 | export class NotificationService implements INotificationService { 47 | public error(error: Error): void { 48 | logger.error(error.message, field("error", error)); 49 | } 50 | 51 | public prompt(severity: Severity, message: string, _buttons: INotificationButton[], _onCancel: () => void): INotificationHandle { 52 | switch (severity) { 53 | case Severity.Info: logger.info(message); break; 54 | case Severity.Warning: logger.warn(message); break; 55 | case Severity.Error: logger.error(message); break; 56 | } 57 | 58 | return { 59 | close: (): void => undefined, 60 | updateMessage: (): void => undefined, 61 | updateButtons: (): void => undefined, 62 | }; 63 | } 64 | } 65 | 66 | /** 67 | * Console-based progress service. 68 | */ 69 | export class ProgressService implements IProgressService { 70 | public start(title: string, task: (progress: IProgress) => Promise): Promise { 71 | logger.info(title); 72 | 73 | return task({ 74 | report: (progress): void => { 75 | logger.info(`${title} progress: ${progress}`); 76 | }, 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/product.ts: -------------------------------------------------------------------------------- 1 | import { InitData } from "@coder/protocol"; 2 | import { IProductConfiguration } from "vs/platform/product/node/product"; 3 | 4 | class Product implements IProductConfiguration { 5 | public nameShort = "code-server"; 6 | public nameLong = "code-server"; 7 | public documentationUrl = "https://code.visualstudio.com/docs"; 8 | public keyboardShortcutsUrlMac = "https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf"; 9 | public keyboardShortcutsUrlLinux = "https://code.visualstudio.com/shortcuts/keyboard-shortcuts-linux.pdf"; 10 | public keyboardShortcutsUrlWin = "https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf"; 11 | public introductoryVideosUrl = "https://code.visualstudio.com/docs/getstarted/introvideos"; 12 | public tipsAndTricksUrl = "https://code.visualstudio.com/docs/getstarted/tips-and-tricks"; 13 | public twitterUrl = "https://twitter.com/code"; 14 | public licenseUrl = "https://github.com/cdr/code-server/blob/master/LICENSE"; 15 | public aiConfig = process.env.DISABLE_TELEMETRY ? undefined! : { 16 | // Only needed so vscode can see that content exists for this value. 17 | // We override the application insights module. 18 | asimovKey: "content", 19 | }; 20 | public enableTelemetry = process.env.DISABLE_TELEMETRY ? false : true; 21 | 22 | private _dataFolderName: string | undefined; 23 | public get dataFolderName(): string { 24 | if (!this._dataFolderName) { 25 | throw new Error("trying to access data folder name before it has been set"); 26 | } 27 | 28 | return this._dataFolderName; 29 | } 30 | 31 | // tslint:disable-next-line:no-any 32 | public extensionsGallery: any = { 33 | get serviceUrl(): string { 34 | return process.env.SERVICE_URL || "https://v1.extapi.coder.com"; 35 | }, 36 | 37 | get itemUrl(): string { 38 | return process.env.ITEM_URL || ""; 39 | }, 40 | 41 | }; 42 | 43 | public extensionExecutionEnvironments = { 44 | "wayou.vscode-todo-highlight": "worker", 45 | "vscodevim.vim": "worker", 46 | "coenraads.bracket-pair-colorizer": "worker", 47 | }; 48 | 49 | public fetchUrl = ""; 50 | 51 | public initialize(_data: InitData): void { 52 | // Nothing at the moment; dataFolderName isn't used since we override the 53 | // extension path. 54 | } 55 | } 56 | 57 | export default new Product(); 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kibibit/code-server", 3 | "repository": "https://github.com/kibibit/code-server", 4 | "author": "Coder", 5 | "license": "MIT", 6 | "description": "Run VS Code remotely.", 7 | "scripts": { 8 | "build:rules": "cd ./rules && tsc -p .", 9 | "packages:install": "cd ./packages && yarn", 10 | "postinstall": "npm-run-all --parallel packages:install build:rules", 11 | "start": "cd ./packages/server && yarn start", 12 | "task": "ts-node -r tsconfig-paths/register build/tasks.ts", 13 | "test": "cd ./packages && yarn test" 14 | }, 15 | "devDependencies": { 16 | "@types/fs-extra": "^5.0.4", 17 | "@types/node": "^10.12.18", 18 | "@types/tar": "^4.0.0", 19 | "@types/trash": "^4.3.1", 20 | "cache-loader": "^2.0.1", 21 | "cross-env": "^5.2.0", 22 | "crypto-browserify": "^3.12.0", 23 | "css-loader": "^2.1.0", 24 | "file-loader": "^3.0.1", 25 | "fork-ts-checker-webpack-plugin": "^0.5.2", 26 | "fs-extra": "^7.0.1", 27 | "happypack": "^5.0.1", 28 | "html-webpack-plugin": "^3.2.0", 29 | "http-browserify": "^1.7.0", 30 | "ignore-loader": "^0.1.2", 31 | "mini-css-extract-plugin": "^0.5.0", 32 | "node-sass": "^4.11.0", 33 | "npm-run-all": "^4.1.5", 34 | "path-browserify": "^1.0.0", 35 | "preload-webpack-plugin": "^3.0.0-beta.2", 36 | "sass-loader": "^7.1.0", 37 | "string-replace-loader": "^2.1.1", 38 | "style-loader": "^0.23.1", 39 | "tar": "^4.4.8", 40 | "terser-webpack-plugin": "^1.2.3", 41 | "ts-loader": "^5.3.3", 42 | "ts-node": "^7.0.1", 43 | "tsconfig-paths": "^3.8.0", 44 | "tslib": "^1.9.3", 45 | "tslint": "^5.12.1", 46 | "typescript": "^3.2.2", 47 | "typescript-tslint-plugin": "^0.2.1", 48 | "uglifyjs-webpack-plugin": "^2.1.1", 49 | "url-loader": "^1.1.2", 50 | "util": "^0.11.1", 51 | "webpack": "^4.28.4", 52 | "webpack-bundle-analyzer": "^3.0.3", 53 | "webpack-cli": "^3.2.1", 54 | "webpack-dev-middleware": "^3.5.0", 55 | "webpack-dev-server": "^3.1.14", 56 | "webpack-hot-middleware": "^2.24.3", 57 | "webpack-pwa-manifest": "^4.0.0", 58 | "workbox-webpack-plugin": "^4.1.0", 59 | "write-file-webpack-plugin": "^4.5.0" 60 | }, 61 | "resolutions": { 62 | "bindings": "1.3.0" 63 | }, 64 | "dependencies": { 65 | "node-loader": "^0.6.0", 66 | "node-pty": "0.8.1", 67 | "spdlog": "0.8.1", 68 | "webpack-merge": "^4.2.1" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/vscode/test/zip.test.ts: -------------------------------------------------------------------------------- 1 | import * as zip from "../src/fill/zip"; 2 | import * as path from "path"; 3 | import * as fs from "fs"; 4 | import * as cp from "child_process"; 5 | import { CancellationToken } from "vs/base/common/cancellation"; 6 | 7 | // tslint:disable-next-line:no-any 8 | jest.mock("vs/nls", () => ({ "localize": (...args: any): string => `${JSON.stringify(args)}` })); 9 | 10 | describe("zip", () => { 11 | const tarPath = path.resolve(__dirname, "./test-extension.tar"); 12 | const vsixPath = path.resolve(__dirname, "./test-extension.vsix"); 13 | const extractPath = path.resolve(__dirname, "./.test-extension"); 14 | 15 | beforeEach(() => { 16 | if (!fs.existsSync(extractPath) || path.dirname(extractPath) !== __dirname) { 17 | return; 18 | } 19 | cp.execSync(`rm -rf '${extractPath}'`); 20 | }); 21 | 22 | const resolveExtract = async (archivePath: string): Promise => { 23 | expect(fs.existsSync(archivePath)).toEqual(true); 24 | await expect(zip.extract( 25 | archivePath, 26 | extractPath, 27 | { sourcePath: "extension", overwrite: true }, 28 | CancellationToken.None, 29 | )).resolves.toBe(undefined); 30 | expect(fs.existsSync(extractPath)).toEqual(true); 31 | }; 32 | 33 | // tslint:disable-next-line:no-any 34 | const extract = (archivePath: string): () => any => { 35 | // tslint:disable-next-line:no-any 36 | return async (): Promise => { 37 | await resolveExtract(archivePath); 38 | expect(fs.existsSync(path.resolve(extractPath, ".vsixmanifest"))).toEqual(true); 39 | expect(fs.existsSync(path.resolve(extractPath, "package.json"))).toEqual(true); 40 | }; 41 | }; 42 | it("should extract from tarred VSIX", extract(tarPath), 2000); 43 | it("should extract from zipped VSIX", extract(vsixPath), 2000); 44 | 45 | // tslint:disable-next-line:no-any 46 | const buffer = (archivePath: string): () => any => { 47 | // tslint:disable-next-line:no-any 48 | return async (): Promise => { 49 | await resolveExtract(archivePath); 50 | const manifestPath = path.resolve(extractPath, ".vsixmanifest"); 51 | expect(fs.existsSync(manifestPath)).toEqual(true); 52 | const manifestBuf = fs.readFileSync(manifestPath); 53 | expect(manifestBuf.length).toBeGreaterThan(0); 54 | await expect(zip.buffer(archivePath, "extension.vsixmanifest")).resolves.toEqual(manifestBuf); 55 | }; 56 | }; 57 | it("should buffer tarred VSIX", buffer(tarPath), 2000); 58 | it("should buffer zipped VSIX", buffer(vsixPath), 2000); 59 | }); 60 | -------------------------------------------------------------------------------- /packages/protocol/README.md: -------------------------------------------------------------------------------- 1 | # Protocol 2 | 3 | This module provides a way for the browser to run Node modules like `fs`, `net`, 4 | etc. 5 | 6 | ## Internals 7 | 8 | ### Server-side proxies 9 | The server-side proxies are regular classes that call native Node functions. The 10 | only thing special about them is that they must return promises and they must 11 | return serializable values. 12 | 13 | The only exception to the promise rule are event-related methods such as 14 | `onEvent` and `onDone` (these are synchronous). The server will simply 15 | immediately bind and push all events it can to the client. It doesn't wait for 16 | the client to start listening. This prevents issues with the server not 17 | receiving the client's request to start listening in time. 18 | 19 | However, there is a way to specify events that should not bind immediately and 20 | should wait for the client to request it, because some events (like `data` on a 21 | stream) cannot be bound immediately (because doing so changes how the stream 22 | behaves). 23 | 24 | ### Client-side proxies 25 | Client-side proxies are `Proxy` instances. They simply make remote calls for any 26 | method you call on it. The only exception is for events. Each client proxy has a 27 | local emitter which it uses in place of a remote call (this allows the call to 28 | be completed synchronously on the client). Then when an event is received from 29 | the server, it gets emitted on that local emitter. 30 | 31 | When an event is listened to, the proxy also notifies the server so it can start 32 | listening in case it isn't already (see the `data` example above). This only 33 | works for events that only fire after they are bound. 34 | 35 | ### Client-side fills 36 | The client-side fills implement the Node API and make calls to the server-side 37 | proxies using the client-side proxies. 38 | 39 | When a proxy returns a proxy (for example `fs.createWriteStream`), that proxy is 40 | a promise (since communicating with the server is asynchronous). We have to 41 | return the fill from `fs.createWriteStream` synchronously, so that means the 42 | fill has to contain a proxy promise. To eliminate the need for calling `then` 43 | and to keep the code looking clean every time you use the proxy, the proxy is 44 | itself wrapped in another proxy which just calls the method after a `then`. This 45 | works since all the methods return promises (aside from the event methods, but 46 | those are not used by the fills directly—they are only used internally to 47 | forward events to the fill if it is an event emitter). 48 | -------------------------------------------------------------------------------- /packages/protocol/src/node/modules/net.ts: -------------------------------------------------------------------------------- 1 | import * as net from "net"; 2 | import { ServerProxy } from "../../common/proxy"; 3 | import { DuplexProxy } from "./stream"; 4 | 5 | // tslint:disable completed-docs no-any 6 | 7 | export class NetSocketProxy extends DuplexProxy { 8 | public constructor(socket: net.Socket) { 9 | super(socket, ["connect", "lookup", "timeout"]); 10 | } 11 | 12 | public async connect(options: number | string | net.SocketConnectOpts, host?: string): Promise { 13 | this.instance.connect(options as any, host as any); 14 | } 15 | 16 | public async unref(): Promise { 17 | this.instance.unref(); 18 | } 19 | 20 | public async ref(): Promise { 21 | this.instance.ref(); 22 | } 23 | 24 | public async dispose(): Promise { 25 | this.instance.end(); 26 | this.instance.destroy(); 27 | this.instance.unref(); 28 | await super.dispose(); 29 | } 30 | } 31 | 32 | export class NetServerProxy extends ServerProxy { 33 | public constructor(instance: net.Server) { 34 | super({ 35 | bindEvents: ["close", "error", "listening"], 36 | doneEvents: ["close"], 37 | instance, 38 | }); 39 | } 40 | 41 | public async listen(handle?: net.ListenOptions | number | string, hostname?: string | number, backlog?: number): Promise { 42 | this.instance.listen(handle, hostname as any, backlog as any); 43 | } 44 | 45 | public async ref(): Promise { 46 | this.instance.ref(); 47 | } 48 | 49 | public async unref(): Promise { 50 | this.instance.unref(); 51 | } 52 | 53 | public async close(): Promise { 54 | this.instance.close(); 55 | } 56 | 57 | public async onConnection(cb: (proxy: NetSocketProxy) => void): Promise { 58 | this.instance.on("connection", (socket) => cb(new NetSocketProxy(socket))); 59 | } 60 | 61 | public async dispose(): Promise { 62 | this.instance.close(); 63 | this.instance.removeAllListeners(); 64 | } 65 | } 66 | 67 | export class NetModuleProxy { 68 | public async createSocket(options?: net.SocketConstructorOpts): Promise { 69 | return new NetSocketProxy(new net.Socket(options)); 70 | } 71 | 72 | public async createConnection(target: string | number | net.NetConnectOpts, host?: string): Promise { 73 | return new NetSocketProxy(net.createConnection(target as any, host)); 74 | } 75 | 76 | public async createServer(options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean }): Promise { 77 | return new NetServerProxy(net.createServer(options)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/protocol/test/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as os from "os"; 3 | import * as path from "path"; 4 | import * as rimraf from "rimraf"; 5 | import * as util from "util"; 6 | import { IDisposable } from "@coder/disposable"; 7 | import { Emitter } from "@coder/events"; 8 | import { Client } from "../src/browser/client"; 9 | import { Server, ServerOptions } from "../src/node/server"; 10 | 11 | // So we only make the directory once when running multiple tests. 12 | let mkdirPromise: Promise | undefined; 13 | 14 | export class Helper { 15 | private i = 0; 16 | public coderDir: string; 17 | private baseDir = path.join(os.tmpdir(), "coder"); 18 | 19 | public constructor(directoryName: string) { 20 | if (!directoryName.trim()) { 21 | throw new Error("no directory name"); 22 | } 23 | 24 | this.coderDir = path.join(this.baseDir, directoryName); 25 | } 26 | 27 | public tmpFile(): string { 28 | return path.join(this.coderDir, `${this.i++}`); 29 | } 30 | 31 | public async createTmpFile(): Promise { 32 | const tf = this.tmpFile(); 33 | await util.promisify(fs.writeFile)(tf, ""); 34 | 35 | return tf; 36 | } 37 | 38 | public async prepare(): Promise { 39 | if (!mkdirPromise) { 40 | mkdirPromise = util.promisify(fs.mkdir)(this.baseDir).catch((error) => { 41 | if (error.code !== "EEXIST" && error.code !== "EISDIR") { 42 | throw error; 43 | } 44 | }); 45 | } 46 | await mkdirPromise; 47 | await util.promisify(rimraf)(this.coderDir); 48 | await util.promisify(fs.mkdir)(this.coderDir); 49 | } 50 | } 51 | 52 | export const createClient = (serverOptions?: ServerOptions): Client => { 53 | const s2c = new Emitter(); 54 | const c2s = new Emitter(); 55 | const closeCallbacks = void>>[]; 56 | 57 | // tslint:disable-next-line no-unused-expression 58 | new Server({ 59 | close: (): void => closeCallbacks.forEach((cb) => cb()), 60 | onDown: (_cb: () => void): void => undefined, 61 | onUp: (_cb: () => void): void => undefined, 62 | onClose: (cb: () => void): number => closeCallbacks.push(cb), 63 | onMessage: (cb): IDisposable => c2s.event((d) => cb(d)), 64 | send: (data): NodeJS.Timer => setTimeout(() => s2c.emit(data), 0), 65 | }, serverOptions); 66 | 67 | const client = new Client({ 68 | close: (): void => closeCallbacks.forEach((cb) => cb()), 69 | onDown: (_cb: () => void): void => undefined, 70 | onUp: (_cb: () => void): void => undefined, 71 | onClose: (cb: () => void): number => closeCallbacks.push(cb), 72 | onMessage: (cb): IDisposable => s2c.event((d) => cb(d)), 73 | send: (data): NodeJS.Timer => setTimeout(() => c2s.emit(data), 0), 74 | }); 75 | 76 | return client; 77 | }; 78 | -------------------------------------------------------------------------------- /packages/protocol/src/browser/modules/node-pty.ts: -------------------------------------------------------------------------------- 1 | import * as pty from "node-pty"; 2 | import { ClientProxy, ClientServerProxy } from "../../common/proxy"; 3 | import { NodePtyModuleProxy, NodePtyProcessProxy } from "../../node/modules/node-pty"; 4 | 5 | // tslint:disable completed-docs 6 | 7 | interface ClientNodePtyProcessProxy extends NodePtyProcessProxy, ClientServerProxy {} 8 | 9 | export class NodePtyProcess extends ClientProxy implements pty.IPty { 10 | private _pid = -1; 11 | private _process = ""; 12 | private lastCols: number | undefined; 13 | private lastRows: number | undefined; 14 | 15 | public constructor( 16 | private readonly moduleProxy: ClientNodePtyModuleProxy, 17 | private readonly file: string, 18 | private readonly args: string[] | string, 19 | private readonly options: pty.IPtyForkOptions, 20 | ) { 21 | super(moduleProxy.spawn(file, args, options)); 22 | this.on("process", (process) => this._process = process); 23 | } 24 | 25 | protected initialize(proxyPromise: Promise): ClientNodePtyProcessProxy { 26 | const proxy = super.initialize(proxyPromise); 27 | this.catch(this.proxy.getPid().then((p) => this._pid = p)); 28 | this.catch(this.proxy.getProcess().then((p) => this._process = p)); 29 | 30 | return proxy; 31 | } 32 | 33 | public get pid(): number { 34 | return this._pid; 35 | } 36 | 37 | public get process(): string { 38 | return this._process; 39 | } 40 | 41 | public resize(columns: number, rows: number): void { 42 | this.lastCols = columns; 43 | this.lastRows = rows; 44 | 45 | this.catch(this.proxy.resize(columns, rows)); 46 | } 47 | 48 | public write(data: string): void { 49 | this.catch(this.proxy.write(data)); 50 | } 51 | 52 | public kill(signal?: string): void { 53 | this.catch(this.proxy.kill(signal)); 54 | } 55 | 56 | protected handleDisconnect(): void { 57 | this._process += " (disconnected)"; 58 | this.emit("data", "\r\n\nLost connection...\r\n\n"); 59 | this.initialize(this.moduleProxy.spawn(this.file, this.args, { 60 | ...this.options, 61 | cols: this.lastCols || this.options.cols, 62 | rows: this.lastRows || this.options.rows, 63 | })); 64 | } 65 | } 66 | 67 | type NodePty = typeof pty; 68 | 69 | interface ClientNodePtyModuleProxy extends NodePtyModuleProxy, ClientServerProxy { 70 | spawn(file: string, args: string[] | string, options: pty.IPtyForkOptions): Promise; 71 | } 72 | 73 | export class NodePtyModule implements NodePty { 74 | public constructor(private readonly proxy: ClientNodePtyModuleProxy) {} 75 | 76 | public spawn = (file: string, args: string[] | string, options: pty.IPtyForkOptions): pty.IPty => { 77 | return new NodePtyProcess(this.proxy, file, args, options); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /scripts/webpack.client.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | const merge = require("webpack-merge"); 4 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 5 | const PreloadWebpackPlugin = require("preload-webpack-plugin"); 6 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 7 | const WebpackPwaManifest = require("webpack-pwa-manifest"); 8 | const { GenerateSW } = require("workbox-webpack-plugin"); 9 | 10 | const root = path.join(__dirname, ".."); 11 | const prod = process.env.NODE_ENV === "production" || process.env.CI === "true"; 12 | const cachePattern = /\.(?:png|jpg|jpeg|svg|css|js|ttf|woff|eot|woff2|wasm)$/; 13 | 14 | module.exports = (options = {}) => merge( 15 | require("./webpack.general.config")(options), { 16 | devtool: prod ? "none" : "cheap-module-eval-source-map", 17 | mode: prod ? "production" : "development", 18 | entry: prod ? options.entry : [ 19 | "webpack-hot-middleware/client?reload=true&quiet=true", 20 | options.entry, 21 | ], 22 | module: { 23 | rules: [{ 24 | test: /\.s?css$/, 25 | // This is required otherwise it'll fail to resolve CSS in common. 26 | include: root, 27 | use: [{ 28 | loader: MiniCssExtractPlugin.loader, 29 | }, { 30 | loader: "css-loader", 31 | }, { 32 | loader: "sass-loader", 33 | }], 34 | }, { 35 | test: /\.(png|ttf|woff|eot|woff2)$/, 36 | use: [{ 37 | loader: "file-loader", 38 | options: { 39 | name: "[path][name].[ext]", 40 | }, 41 | }], 42 | }, { 43 | test: /\.svg$/, 44 | loader: 'url-loader' 45 | }], 46 | }, 47 | plugins: [ 48 | new MiniCssExtractPlugin({ 49 | chunkFilename: `${options.name || "client"}.[name].[hash:6].css`, 50 | filename: `${options.name || "client"}.[name].[hash:6].css` 51 | }), 52 | new HtmlWebpackPlugin({ 53 | template: options.template 54 | }), 55 | new PreloadWebpackPlugin({ 56 | rel: "preload", 57 | as: "script" 58 | }), 59 | new WebpackPwaManifest({ 60 | name: "Coder", 61 | short_name: "Coder", 62 | description: "Run VS Code on a remote server", 63 | background_color: "#e5e5e5", 64 | crossorigin: "use-credentials", 65 | icons: [{ 66 | src: path.join(root, "packages/web/assets/logo.png"), 67 | sizes: [96, 128, 192, 256, 384], 68 | }], 69 | }) 70 | ].concat(prod ? [ 71 | new GenerateSW({ 72 | importWorkboxFrom: "local", 73 | include: [cachePattern], 74 | runtimeCaching: [{ 75 | urlPattern: cachePattern, 76 | handler: "StaleWhileRevalidate", 77 | options: { 78 | cacheName: "code-server", 79 | expiration: { 80 | maxAgeSeconds: 86400, 81 | }, 82 | cacheableResponse: { 83 | statuses: [0, 200], 84 | }, 85 | }, 86 | }, 87 | ]}), 88 | ] : [new webpack.HotModuleReplacementPlugin()]), 89 | target: "web" 90 | }); 91 | -------------------------------------------------------------------------------- /doc/security/ssl.md: -------------------------------------------------------------------------------- 1 | # Generate a self-signed certificate 🔒 2 | 3 | code-server has the ability to secure your connection between client and server using SSL/TSL certificates. By default, the server will start with an unencrypted connection. We recommend Self-signed TLS/SSL certificates for personal use of code-server or within an organization. 4 | 5 | This guide will show you how to create a self-signed certificate and start code-server using your certificate/key. 6 | 7 | ## TLS / HTTPS 8 | 9 | You can specify any location that you want to save the certificate and key. In this example, we will navigate to the root directory, create a folder called `certs` and cd into it. 10 | 11 | ```shell 12 | mkdir ~/certs && cd ~/certs 13 | ``` 14 | 15 | If you don't already have a TLS certificate and key, you can generate them with the command below. They will be placed in `~/certs` 16 | 17 | ```shell 18 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ~/certs/MyKey.key -out ~/certs/MyCertificate.crt 19 | ``` 20 | 21 | You will be prompted to add some identifying information about your organization 22 | ```shell 23 | You are about to be asked to enter information that will be incorporated 24 | into your certificate request. 25 | What you are about to enter is what is called a Distinguished Name or a DN. 26 | There are quite a few fields but you can leave some blank 27 | For some fields there will be a default value, 28 | If you enter '.', the field will be left blank. 29 | ----- 30 | Country Name (2 letter code) [AU]:US 31 | State or Province Name (full name) [Some-State]:TX 32 | Locality Name (eg, city) []:Austin 33 | Organization Name (eg, company) [Coder Technologies]:Coder 34 | Organizational Unit Name (eg, section) []:Docs 35 | Common Name (e.g. server FQDN or YOUR name) []:hostname.example.com 36 | Email Address []:admin@example.com 37 | ``` 38 | >If you already have a TLS certificate and key, you can simply reference them in the `--cert` and `--cert-key` flags when launching code-server 39 | 40 | 41 | ## Starting code-server with certificate and key 42 | 43 | 1. At the end of the path to your binary, add the following flags followed by the path to your certificate and key like so. Then press enter to run code-server. 44 | ```shell 45 | ./code-server --cert=~/certs/MyCertificate.crt --cert-key=~/certs/MyKey.key 46 | ``` 47 | 2. After that you will be running a secure code-server. 48 | 49 | > You will know your connection is secure if the lines `WARN No certificate specified. This could be insecure. WARN Documentation on securing your setup: https://coder.com/docs` no longer appear. 50 | 51 | ## Other options 52 | 53 | For larger organizations you may wish to rely on a Certificate Authority as opposed to a self-signed certificate. For more information on generating free and open certificates for your site, please check out EFF's [certbot](https://certbot.eff.org/). Certbot is a cli to generate certificates using [LetsEncrypt](https://letsencrypt.org/). 54 | -------------------------------------------------------------------------------- /packages/vscode/src/fill/paste.ts: -------------------------------------------------------------------------------- 1 | import * as nls from "vs/nls"; 2 | import { Action } from "vs/base/common/actions"; 3 | import { TERMINAL_COMMAND_ID } from "vs/workbench/contrib/terminal/common/terminalCommands"; 4 | import { ITerminalService } from "vs/workbench/contrib/terminal/common/terminal"; 5 | import * as actions from "vs/workbench/contrib/terminal/browser/terminalActions"; 6 | import * as instance from "vs/workbench/contrib/terminal/browser/terminalInstance"; 7 | import { client } from "../client"; 8 | 9 | const getLabel = (key: string, enabled: boolean): string => { 10 | return enabled 11 | ? nls.localize(key, "Paste") 12 | : nls.localize(`${key}WithKeybind`, "Paste (must use keybind)"); 13 | }; 14 | 15 | export class PasteAction extends Action { 16 | private static readonly KEY = "paste"; 17 | 18 | public constructor() { 19 | super( 20 | "editor.action.clipboardPasteAction", 21 | getLabel(PasteAction.KEY, client.clipboard.isEnabled), 22 | undefined, 23 | client.clipboard.isEnabled, 24 | async (): Promise => client.clipboard.paste(), 25 | ); 26 | 27 | client.clipboard.onPermissionChange((enabled) => { 28 | this.label = getLabel(PasteAction.KEY, enabled); 29 | this.enabled = enabled; 30 | }); 31 | } 32 | } 33 | 34 | class TerminalPasteAction extends Action { 35 | private static readonly KEY = "workbench.action.terminal.paste"; 36 | 37 | public static readonly ID = TERMINAL_COMMAND_ID.PASTE; 38 | public static readonly LABEL = nls.localize("workbench.action.terminal.paste", "Paste into Active Terminal"); 39 | public static readonly SHORT_LABEL = getLabel(TerminalPasteAction.KEY, client.clipboard.isEnabled); 40 | 41 | public constructor( 42 | id: string, label: string, 43 | @ITerminalService private terminalService: ITerminalService, 44 | ) { 45 | super(id, label); 46 | client.clipboard.onPermissionChange((enabled) => { 47 | this._setLabel(getLabel(TerminalPasteAction.KEY, enabled)); 48 | }); 49 | this._setLabel(getLabel(TerminalPasteAction.KEY, client.clipboard.isEnabled)); 50 | } 51 | 52 | public run(): Promise { 53 | const instance = this.terminalService.getActiveOrCreateInstance(); 54 | if (instance) { 55 | // tslint:disable-next-line no-any it will return a promise (see below) 56 | return (instance as any).paste(); 57 | } 58 | 59 | return Promise.resolve(); 60 | } 61 | } 62 | 63 | class TerminalInstance extends instance.TerminalInstance { 64 | public async paste(): Promise { 65 | this.focus(); 66 | if (client.clipboard.isEnabled) { 67 | const text = await client.clipboard.readText(); 68 | this.sendText(text, false); 69 | } else { 70 | document.execCommand("paste"); 71 | } 72 | } 73 | } 74 | 75 | const actionsTarget = actions as typeof actions; 76 | // @ts-ignore TODO: don't ignore it. 77 | actionsTarget.TerminalPasteAction = TerminalPasteAction; 78 | 79 | const instanceTarget = instance as typeof instance; 80 | instanceTarget.TerminalInstance = TerminalInstance; 81 | -------------------------------------------------------------------------------- /packages/protocol/src/browser/modules/spdlog.ts: -------------------------------------------------------------------------------- 1 | import * as spdlog from "spdlog"; 2 | import { ClientProxy, ClientServerProxy } from "../../common/proxy"; 3 | import { RotatingLoggerProxy, SpdlogModuleProxy } from "../../node/modules/spdlog"; 4 | 5 | // tslint:disable completed-docs 6 | 7 | interface ClientRotatingLoggerProxy extends RotatingLoggerProxy, ClientServerProxy {} 8 | 9 | class RotatingLogger extends ClientProxy implements spdlog.RotatingLogger { 10 | public constructor( 11 | private readonly moduleProxy: ClientSpdlogModuleProxy, 12 | private readonly name: string, 13 | private readonly filename: string, 14 | private readonly filesize: number, 15 | private readonly filecount: number, 16 | ) { 17 | super(moduleProxy.createLogger(name, filename, filesize, filecount)); 18 | } 19 | 20 | public trace (message: string): void { this.catch(this.proxy.trace(message)); } 21 | public debug (message: string): void { this.catch(this.proxy.debug(message)); } 22 | public info (message: string): void { this.catch(this.proxy.info(message)); } 23 | public warn (message: string): void { this.catch(this.proxy.warn(message)); } 24 | public error (message: string): void { this.catch(this.proxy.error(message)); } 25 | public critical (message: string): void { this.catch(this.proxy.critical(message)); } 26 | public setLevel (level: number): void { this.catch(this.proxy.setLevel(level)); } 27 | public clearFormatters (): void { this.catch(this.proxy.clearFormatters()); } 28 | public flush (): void { this.catch(this.proxy.flush()); } 29 | public drop (): void { this.catch(this.proxy.drop()); } 30 | 31 | protected handleDisconnect(): void { 32 | this.initialize(this.moduleProxy.createLogger(this.name, this.filename, this.filesize, this.filecount)); 33 | } 34 | } 35 | 36 | interface ClientSpdlogModuleProxy extends SpdlogModuleProxy, ClientServerProxy { 37 | createLogger(name: string, filePath: string, fileSize: number, fileCount: number): Promise; 38 | } 39 | 40 | export class SpdlogModule { 41 | public readonly RotatingLogger: typeof spdlog.RotatingLogger; 42 | 43 | public constructor(private readonly proxy: ClientSpdlogModuleProxy) { 44 | this.RotatingLogger = class extends RotatingLogger { 45 | public constructor(name: string, filename: string, filesize: number, filecount: number) { 46 | super(proxy, name, filename, filesize, filecount); 47 | } 48 | }; 49 | } 50 | 51 | public setAsyncMode = (bufferSize: number, flushInterval: number): Promise => { 52 | return this.proxy.setAsyncMode(bufferSize, flushInterval); 53 | } 54 | 55 | public createRotatingLogger(name: string, filename: string, filesize: number, filecount: number): RotatingLogger { 56 | return new RotatingLogger(this.proxy, name, filename, filesize, filecount); 57 | } 58 | 59 | public createRotatingLoggerAsync(name: string, filename: string, filesize: number, filecount: number): Promise { 60 | return Promise.resolve(this.createRotatingLogger(name, filename, filesize, filecount)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /doc/admin/install/digitalocean.md: -------------------------------------------------------------------------------- 1 | # Deploy on DigitalOcean 2 | 3 | This tutorial shows you how to deploy `code-server` to a single node running on DigitalOcean. 4 | 5 | If you're just starting out, we recommend [installing code-server locally](../../self-hosted/index.md). It takes only a few minutes and lets you try out all of the features. 6 | 7 | --- 8 | 9 | ## Use the "Create Droplets" wizard 10 | 11 | [Open your DigitalOcean dashboard](https://cloud.digitalocean.com/droplets/new) to create a new droplet 12 | 13 | - **Choose an image -** Select the **Distributions** tab and then choose Ubuntu 14 | - **Choose a size -** We recommend at least 4GB RAM and 2 CPU, more depending on team size and number of repositories/languages enabled. 15 | - Launch your instance 16 | - Open a terminal on your computer and SSH into your instance 17 | > example: ssh root@203.0.113.0 18 | - Once in the SSH session, visit code-server [releases page](https://github.com/cdr/code-server/releases/) and copy the link to the download for the latest linux release 19 | - Find the latest Linux release from this URL: 20 | ``` 21 | https://github.com/cdr/code-server/releases/latest 22 | ``` 23 | - Replace {version} in the following command with the version found on the releases page and run it (or just copy the download URL from the releases page): 24 | ``` 25 | wget https://github.com/cdr/code-server/releases/download/{version}/code-server{version}-linux-x64.tar.gz 26 | ``` 27 | - Extract the downloaded tar.gz file with this command, for example: 28 | ``` 29 | tar -xvzf code-server{version}-linux-x64.tar.gz 30 | ``` 31 | - Navigate to extracted directory with this command: 32 | ``` 33 | cd code-server{version}-linux-x64 34 | ``` 35 | - If you run into any permission errors when attempting to run the binary: 36 | ``` 37 | chmod +x code-server 38 | ``` 39 | > To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../../security/ssl.md) 40 | - Finally start the code-server 41 | ``` 42 | ./code-server 43 | ``` 44 | > For instructions on how to keep the server running after you end your SSH session please checkout [how to use systemd](https://www.linode.com/docs/quick-answers/linux/start-service-at-boot/) to start linux based services if they are killed 45 | - Open your browser and visit `https://$public_ip:8443/` (where `$public_ip` is your Digital Ocean instance's public IP address). You will be greeted with a page similar to the following screenshot. Code-server is using a self-signed SSL certificate for easy setup. In Chrome/Chromium, click **"Advanced"** then click **"proceed anyway"**. In Firefox, click **Advanced**, then **Add Exception**, then finally **Confirm Security Exception**. 46 | 47 | --- 48 | > NOTE: If you get stuck or need help, [file an issue](https://github.com/cdr/code-server/issues/new?&title=Improve+self-hosted+quickstart+guide), [tweet (@coderhq)](https://twitter.com/coderhq) or [email](mailto:support@coder.com?subject=Self-hosted%20quickstart%20guide). 49 | -------------------------------------------------------------------------------- /packages/protocol/test/node-pty.test.ts: -------------------------------------------------------------------------------- 1 | import { IPty } from "node-pty"; 2 | import { Module } from "../src/common/proxy"; 3 | import { createClient } from "./helpers"; 4 | 5 | describe("node-pty", () => { 6 | const client = createClient(); 7 | const pty = client.modules[Module.NodePty]; 8 | 9 | /** 10 | * Returns a function that when called returns a promise that resolves with 11 | * the next chunk of data from the process. 12 | */ 13 | const promisifyData = (proc: IPty): (() => Promise) => { 14 | // Use a persistent callback instead of creating it in the promise since 15 | // otherwise we could lose data that comes in while no promise is listening. 16 | let onData: (() => void) | undefined; 17 | let buffer: string | undefined; 18 | proc.on("data", (data) => { 19 | // Remove everything that isn't a letter, number, or $ to avoid issues 20 | // with ANSI escape codes printing inside the test output. 21 | buffer = (buffer || "") + data.toString().replace(/[^a-zA-Z0-9$]/g, ""); 22 | if (onData) { 23 | onData(); 24 | } 25 | }); 26 | 27 | return (): Promise => new Promise((resolve): void => { 28 | onData = (): void => { 29 | if (typeof buffer !== "undefined") { 30 | const data = buffer; 31 | buffer = undefined; 32 | onData = undefined; 33 | resolve(data); 34 | } 35 | }; 36 | onData(); 37 | }); 38 | }; 39 | 40 | it("should create shell", async () => { 41 | // Setting the config file to something that shouldn't exist so the test 42 | // isn't affected by custom configuration. 43 | const proc = pty.spawn("/bin/bash", ["--rcfile", "/tmp/test/nope/should/not/exist"], { 44 | cols: 100, 45 | rows: 10, 46 | }); 47 | 48 | const getData = promisifyData(proc); 49 | 50 | // Wait for [hostname@user]$ 51 | let data = ""; 52 | while (!data.includes("$")) { 53 | data = await getData(); 54 | } 55 | 56 | proc.kill(); 57 | 58 | await new Promise((resolve): void => { 59 | proc.on("exit", resolve); 60 | }); 61 | }); 62 | 63 | it("should resize", async () => { 64 | // Requires the `tput lines` cmd to be available. 65 | // Setting the config file to something that shouldn't exist so the test 66 | // isn't affected by custom configuration. 67 | const proc = pty.spawn("/bin/bash", ["--rcfile", "/tmp/test/nope/should/not/exist"], { 68 | cols: 10, 69 | rows: 912, 70 | }); 71 | 72 | const getData = promisifyData(proc); 73 | 74 | proc.write("tput lines\n"); 75 | 76 | let data = ""; 77 | while (!data.includes("912")) { 78 | data = await getData(); 79 | } 80 | proc.resize(10, 219); 81 | proc.write("tput lines\n"); 82 | 83 | while (!data.includes("219")) { 84 | data = await getData(); 85 | } 86 | 87 | proc.kill(); 88 | await new Promise((resolve): void => { 89 | proc.on("exit", resolve); 90 | }); 91 | }); 92 | 93 | it("should dispose", (done) => { 94 | setTimeout(() => { 95 | client.dispose(); 96 | done(); 97 | }, 100); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /packages/events/src/events.ts: -------------------------------------------------------------------------------- 1 | import { IDisposable } from "@coder/disposable"; 2 | 3 | export interface Event { 4 | (listener: (value: T) => void): IDisposable; 5 | (id: number | string, listener: (value: T) => void): IDisposable; 6 | } 7 | 8 | /** 9 | * Emitter typecasts for a single event type. You can optionally use IDs, but 10 | * using undefined with IDs will not work. If you emit without an ID, *all* 11 | * listeners regardless of their ID (or lack thereof) will receive the event. 12 | * Similarly, if you listen without an ID you will get *all* events for any or 13 | * no ID. 14 | */ 15 | export class Emitter { 16 | private listeners = void>>[]; 17 | private readonly idListeners = new Map void>>(); 18 | 19 | public get event(): Event { 20 | return (id: number | string | ((value: T) => void), cb?: (value: T) => void): IDisposable => { 21 | if (typeof id !== "function") { 22 | if (this.idListeners.has(id)) { 23 | this.idListeners.get(id)!.push(cb!); 24 | } else { 25 | this.idListeners.set(id, [cb!]); 26 | } 27 | 28 | return { 29 | dispose: (): void => { 30 | if (this.idListeners.has(id)) { 31 | const cbs = this.idListeners.get(id)!; 32 | const i = cbs.indexOf(cb!); 33 | if (i !== -1) { 34 | cbs.splice(i, 1); 35 | } 36 | } 37 | }, 38 | }; 39 | } 40 | 41 | cb = id; 42 | this.listeners.push(cb); 43 | 44 | return { 45 | dispose: (): void => { 46 | const i = this.listeners.indexOf(cb!); 47 | if (i !== -1) { 48 | this.listeners.splice(i, 1); 49 | } 50 | }, 51 | }; 52 | }; 53 | } 54 | 55 | /** 56 | * Emit an event with a value. 57 | */ 58 | public emit(value: T): void; 59 | public emit(id: number | string, value: T): void; 60 | public emit(id: number | string | T, value?: T): void { 61 | if ((typeof id === "number" || typeof id === "string") && typeof value !== "undefined") { 62 | if (this.idListeners.has(id)) { 63 | this.idListeners.get(id)!.forEach((cb) => cb(value!)); 64 | } 65 | this.listeners.forEach((cb) => cb(value!)); 66 | } else { 67 | this.idListeners.forEach((cbs) => cbs.forEach((cb) => cb((id as T)!))); 68 | this.listeners.forEach((cb) => cb((id as T)!)); 69 | } 70 | } 71 | 72 | /** 73 | * Dispose the current events. 74 | */ 75 | public dispose(): void; 76 | public dispose(id: number | string): void; 77 | public dispose(id?: number | string): void { 78 | if (typeof id !== "undefined") { 79 | this.idListeners.delete(id); 80 | } else { 81 | this.listeners = []; 82 | this.idListeners.clear(); 83 | } 84 | } 85 | 86 | public get counts(): { [key: string]: number } { 87 | const counts = <{ [key: string]: number }>{}; 88 | if (this.listeners.length > 0) { 89 | counts["n/a"] = this.listeners.length; 90 | } 91 | this.idListeners.forEach((cbs, id) => { 92 | if (cbs.length > 0) { 93 | counts[`${id}`] = cbs.length; 94 | } 95 | }); 96 | 97 | return counts; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /packages/protocol/src/proto/node.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | enum Module { 4 | ChildProcess = 0; 5 | Fs = 1; 6 | Net = 2; 7 | NodePty = 3; 8 | Spdlog = 4; 9 | Trash = 5; 10 | } 11 | 12 | message Argument { 13 | message ErrorValue { 14 | string message = 1; 15 | string stack = 2; 16 | string code = 3; 17 | } 18 | 19 | message BufferValue { 20 | bytes data = 1; 21 | } 22 | 23 | message ObjectValue { 24 | map data = 1; 25 | } 26 | 27 | message ArrayValue { 28 | repeated Argument data = 1; 29 | } 30 | 31 | message ProxyValue { 32 | uint64 id = 1; 33 | } 34 | 35 | message FunctionValue { 36 | uint64 id = 1; 37 | } 38 | 39 | message NullValue {} 40 | 41 | message UndefinedValue {} 42 | 43 | message DateValue { 44 | string date = 1; 45 | } 46 | 47 | oneof msg { 48 | ErrorValue error = 1; 49 | BufferValue buffer = 2; 50 | ObjectValue object = 3; 51 | ArrayValue array = 4; 52 | ProxyValue proxy = 5; 53 | FunctionValue function = 6; 54 | NullValue null = 7; 55 | UndefinedValue undefined = 8; 56 | double number = 9; 57 | string string = 10; 58 | bool boolean = 11; 59 | DateValue date = 12; 60 | } 61 | } 62 | 63 | // Call a remote method. 64 | message Method { 65 | // A proxy identified by a unique name like "fs". 66 | message Named { 67 | uint64 id = 1; 68 | Module module = 2; 69 | string method = 3; 70 | repeated Argument args = 4; 71 | } 72 | 73 | // A general proxy identified by an ID like WriteStream. 74 | message Numbered { 75 | uint64 id = 1; 76 | uint64 proxy_id = 2; 77 | string method = 3; 78 | repeated Argument args = 4; 79 | } 80 | 81 | // Remote method failed. 82 | message Fail { 83 | uint64 id = 1; 84 | Argument response = 2; 85 | } 86 | 87 | // Remote method succeeded. 88 | message Success { 89 | uint64 id = 1; 90 | Argument response = 2; 91 | } 92 | 93 | oneof msg { 94 | Method.Named named_proxy = 1; 95 | Method.Numbered numbered_proxy = 2; 96 | } 97 | } 98 | 99 | message Callback { 100 | // A remote callback for uniquely named proxy. 101 | message Named { 102 | Module module = 1; 103 | uint64 callback_id = 2; 104 | repeated Argument args = 3; 105 | } 106 | 107 | // A remote callback for a numbered proxy. 108 | message Numbered { 109 | uint64 proxy_id = 1; 110 | uint64 callback_id = 2; 111 | repeated Argument args = 3; 112 | } 113 | 114 | oneof msg { 115 | Callback.Named named_callback = 1; 116 | Callback.Numbered numbered_callback = 2; 117 | } 118 | } 119 | 120 | message Event { 121 | // Emit an event on a uniquely named proxy. 122 | message Named { 123 | Module module = 1; 124 | string event = 2; 125 | repeated Argument args = 3; 126 | } 127 | 128 | // Emit an event on a numbered proxy. 129 | message Numbered { 130 | uint64 proxy_id = 1; 131 | string event = 2; 132 | repeated Argument args = 3; 133 | } 134 | 135 | oneof msg { 136 | Event.Named named_event = 1; 137 | Event.Numbered numbered_event = 2; 138 | } 139 | } 140 | 141 | message Ping {} 142 | 143 | message Pong {} 144 | -------------------------------------------------------------------------------- /packages/web/src/index.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | width: 100%; 5 | } 6 | 7 | #overlay { 8 | background: rgba(0, 0, 0, 0.2); 9 | bottom: 0; 10 | left: 0; 11 | position: absolute; 12 | right: 0; 13 | top: 0; 14 | } 15 | 16 | #overlay { 17 | align-items: center; 18 | background-color: #252526; 19 | bottom: 0; 20 | display: flex; 21 | flex-direction: column; 22 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 23 | justify-content: center; 24 | left: 0; 25 | opacity: 1; 26 | position: absolute; 27 | right: 0; 28 | top: 0; 29 | transition: 150ms opacity ease; 30 | z-index: 2; 31 | } 32 | 33 | #overlay > .message { 34 | color: white; 35 | margin-top: 10px; 36 | opacity: 0.5; 37 | } 38 | 39 | #overlay.error > .message { 40 | color: white; 41 | opacity: 0.3; 42 | } 43 | 44 | #overlay > .activitybar { 45 | background-color: rgb(44, 44, 44); 46 | bottom: 0; 47 | height: 100%; 48 | left: 0; 49 | position: absolute; 50 | top: 0; 51 | width: 50px; 52 | } 53 | 54 | #overlay > .activitybar svg { 55 | fill: white; 56 | margin-left: 2px; 57 | margin-top: 2px; 58 | opacity: 0.3; 59 | } 60 | 61 | #overlay.error > #status { 62 | opacity: 0; 63 | } 64 | 65 | #overlay>.statusbar { 66 | background-color: rgb(0, 122, 204); 67 | bottom: 0; 68 | cursor: default; 69 | height: 22px; 70 | left: 0; 71 | position: absolute; 72 | right: 0; 73 | } 74 | 75 | #logo { 76 | transform-style: preserve-3d; 77 | } 78 | 79 | #logo > svg { 80 | fill: rgb(0, 122, 204); 81 | opacity: 1; 82 | width: 100px; 83 | } 84 | 85 | #status { 86 | background: rgba(255, 255, 255, 0.1); 87 | border-radius: 5px; 88 | box-shadow: 0px 2px 10px -2px rgba(0, 0, 0, 0.75); 89 | color: white; 90 | font-size: 0.9em; 91 | margin-top: 15px; 92 | min-width: 100px; 93 | position: relative; 94 | transition: 300ms opacity ease; 95 | } 96 | 97 | #progress { 98 | background: rgba(0, 0, 0, 0.2); 99 | border-bottom-left-radius: 5px; 100 | border-bottom-right-radius: 5px; 101 | bottom: 0; 102 | height: 3px; 103 | left: 0; 104 | overflow: hidden; 105 | position: absolute; 106 | right: 0; 107 | } 108 | 109 | @-moz-keyframes statusProgress { 110 | 0% { 111 | background-position: 0% 50% 112 | } 113 | 114 | 50% { 115 | background-position: 100% 50% 116 | } 117 | 118 | 100% { 119 | background-position: 0% 50% 120 | } 121 | } 122 | 123 | @keyframes statusProgress { 124 | 0% { 125 | background-position: 0% 50% 126 | } 127 | 128 | 50% { 129 | background-position: 100% 50% 130 | } 131 | 132 | 100% { 133 | background-position: 0% 50% 134 | } 135 | } 136 | 137 | #fill { 138 | animation: statusProgress 2s ease infinite; 139 | background-size: 400% 400%; 140 | background: linear-gradient(270deg, #007acc, #0016cc); 141 | height: 100%; 142 | transition: 500ms width ease; 143 | width: 0%; 144 | } 145 | 146 | .reload-button { 147 | background-color: #007acc; 148 | border-radius: 2px; 149 | cursor: pointer; 150 | margin-top: 10px; 151 | padding: 6px 10px; 152 | } 153 | -------------------------------------------------------------------------------- /scripts/webpack.general.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const os = require("os"); 3 | const environment = process.env.NODE_ENV || "development"; 4 | const HappyPack = require("happypack"); 5 | const webpack = require("webpack"); 6 | const TerserPlugin = require("terser-webpack-plugin"); 7 | 8 | const root = path.join(__dirname, ".."); 9 | 10 | module.exports = (options = {}) => ({ 11 | context: root, 12 | devtool: "none", 13 | externals: { 14 | fsevents: "fsevents", 15 | }, 16 | output: { 17 | path: path.join(options.dirname || __dirname, "out"), 18 | chunkFilename: `${options.name || "general"}.[name].[hash:6].js`, 19 | filename: `${options.name || "general"}.[name].[hash:6].js` 20 | }, 21 | module: { 22 | rules: [{ 23 | loader: "string-replace-loader", 24 | test: /\.(j|t)s/, 25 | options: { 26 | multiple: [{ 27 | // These will be handled by file-loader. Must be a fully formed URI. 28 | // The !! prefix causes it to ignore other loaders. 29 | search: "require\\.toUrl\\(", 30 | replace: `${ 31 | options.node 32 | ? "'file://'" 33 | : "location.protocol + '//' + location.host + location.pathname.replace(/\\/$/,'')" 34 | } + '/' + require('!!file-loader?name=[path][name].[ext]!' + `, 35 | flags: "g", 36 | }, { 37 | search: "require\\.__\\$__nodeRequire", 38 | replace: "require", 39 | flags: "g", 40 | }, { 41 | search: "\\.attributes\\[([^\\]]+)\\] = ([^;]+)", 42 | replace: ".setAttribute($1, $2)", 43 | flags: "g", 44 | }], 45 | }, 46 | }, { 47 | test: /\.node$/, 48 | use: "node-loader", 49 | }, { 50 | use: [{ 51 | loader: "happypack/loader?id=ts", 52 | }], 53 | test: /(^.?|\.[^d]|[^.]d|[^.][^d])\.tsx?$/, 54 | }, { 55 | test: /\.wasm$/, 56 | type: "javascript/auto", 57 | }], 58 | }, 59 | resolve: { 60 | alias: { 61 | "@coder": path.join(root, "packages"), 62 | }, 63 | extensions: [".js", ".jsx", ".ts", ".tsx", ".json", ".css"], 64 | mainFiles: [ 65 | "index", 66 | "src/index", 67 | ], 68 | }, 69 | resolveLoader: { 70 | modules: [ 71 | path.join(root, "node_modules"), 72 | ], 73 | }, 74 | plugins: [ 75 | new HappyPack({ 76 | id: "ts", 77 | threads: Math.max(os.cpus().length - 1, 1), 78 | loaders: [{ 79 | path: "cache-loader", 80 | query: { 81 | cacheDirectory: path.join(__dirname, "..", ".cache"), 82 | }, 83 | }, { 84 | path: "ts-loader", 85 | query: { 86 | happyPackMode: true, 87 | compilerOptions: options.typescriptCompilerOptions, 88 | }, 89 | }], 90 | }), 91 | new webpack.DefinePlugin({ 92 | "process.env.NODE_ENV": `"${environment}"`, 93 | "process.env.VERSION": `"${process.env.VERSION || ""}"`, 94 | }), 95 | ], 96 | optimization: { 97 | minimizer: [ 98 | new TerserPlugin({ 99 | cache: path.join(__dirname, "..", ".cache", "terser"), 100 | parallel: true, 101 | }), 102 | ], 103 | }, 104 | stats: { 105 | all: false, // Fallback for options not defined. 106 | errors: true, 107 | warnings: true, 108 | }, 109 | }); 110 | -------------------------------------------------------------------------------- /packages/app/chrome/src/chome.ts: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import { TcpHost, TcpServer, TcpConnection } from "@coder/app/common/src/app"; 3 | import { Event, Emitter } from "@coder/events/src"; 4 | 5 | export const tcpHost: TcpHost = { 6 | listen(host: string, port: number): Promise { 7 | const socketApi: { 8 | readonly tcpServer: { 9 | create(props: {}, cb: (createInfo: { readonly socketId: number }) => void): void; 10 | listen(socketId: number, address: string, port: number, callback: (result: number) => void): void; 11 | disconnect(socketId: number, callback: () => void): void; 12 | 13 | readonly onAccept: { 14 | addListener(callback: (info: { readonly socketId: number; readonly clientSocketId: number }) => void): void; 15 | }; 16 | }; 17 | readonly tcp: { 18 | readonly onReceive: { 19 | addListener(callback: (info: { readonly socketId: number; readonly data: ArrayBuffer; }) => void): void; 20 | }; 21 | close(socketId: number, callback?: () => void): void; 22 | send(socketId: number, data: ArrayBuffer, callback?: () => void): void; 23 | setPaused(socketId: number, value: boolean): void; 24 | }; 25 | // tslint:disable-next-line:no-any 26 | } = (chrome).sockets; 27 | 28 | return new Promise((resolve, reject): void => { 29 | socketApi.tcpServer.create({}, (createInfo) => { 30 | const serverSocketId = createInfo.socketId; 31 | socketApi.tcpServer.listen(serverSocketId, host, port, (result) => { 32 | if (result < 0) { 33 | return reject("Failed to listen: " + chrome.runtime.lastError); 34 | } 35 | 36 | const connectionEmitter = new Emitter(); 37 | 38 | socketApi.tcpServer.onAccept.addListener((info) => { 39 | if (info.socketId !== serverSocketId) { 40 | return; 41 | } 42 | 43 | const dataEmitter = new Emitter(); 44 | 45 | socketApi.tcp.onReceive.addListener((recvInfo) => { 46 | if (recvInfo.socketId !== info.clientSocketId) { 47 | return; 48 | } 49 | 50 | dataEmitter.emit(recvInfo.data); 51 | }); 52 | 53 | socketApi.tcp.setPaused(info.clientSocketId, false); 54 | 55 | connectionEmitter.emit({ 56 | send: (data): Promise => { 57 | return new Promise((res): void => { 58 | socketApi.tcp.send(info.clientSocketId, data, () => { 59 | res(); 60 | }); 61 | }); 62 | }, 63 | close: (): Promise => { 64 | return new Promise((res): void => { 65 | socketApi.tcp.close(info.clientSocketId, () => { 66 | res(); 67 | }); 68 | }); 69 | }, 70 | get onData(): Event { 71 | return dataEmitter.event; 72 | }, 73 | }); 74 | }); 75 | 76 | resolve({ 77 | get onConnection(): Event { 78 | return connectionEmitter.event; 79 | }, 80 | close: (): Promise => { 81 | return new Promise((res): void => { 82 | socketApi.tcpServer.disconnect(serverSocketId, () => { 83 | res(); 84 | }); 85 | }); 86 | }, 87 | }); 88 | }); 89 | }); 90 | }); 91 | }, 92 | }; 93 | -------------------------------------------------------------------------------- /packages/vscode/webpack.bootstrap.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const merge = require("webpack-merge"); 3 | 4 | const root = path.resolve(__dirname, "../.."); 5 | const fills = path.join(root, "packages/ide/src/fill"); 6 | const vsFills = path.join(root, "packages/vscode/src/fill"); 7 | 8 | module.exports = merge( 9 | require(path.join(root, "scripts/webpack.node.config.js"))({ 10 | dirname: __dirname, 11 | typescriptCompilerOptions: { 12 | target: "es6", 13 | }, 14 | }), { 15 | entry: path.join(root, "lib/vscode/src/bootstrap-fork.js"), 16 | mode: "development", 17 | output: { 18 | chunkFilename: "[name].bundle.js", 19 | publicPath: "/", 20 | filename: "bootstrap-fork.js", 21 | libraryTarget: "commonjs", 22 | globalObject: "this", 23 | }, 24 | // Due to the dynamic `require.context` we add to `loader.js` Webpack tries 25 | // to include way too much. We can modify what Webpack imports in this case 26 | // (I believe), but for now ignore some things. 27 | module: { 28 | rules: [{ 29 | test: /\.(txt|d\.ts|perf\.data\.js|jxs|scpt|exe|sh|less|html|s?css|qwoff|md|svg|png|ttf|woff|eot|woff2)$/, 30 | use: [{ 31 | loader: "ignore-loader", 32 | }], 33 | }, { 34 | test: /test|tsconfig/, 35 | use: [{ 36 | loader: "ignore-loader", 37 | }], 38 | }, { 39 | // The only thing we need in electron-browser is the shared process (including contrib). 40 | test: /((\\|\/)vs(\\|\/)code(\\|\/)electron-main(\\|\/))|((\\|\/)test(\\|\/))|(OSSREADME\.json$)|\/browser\/|\/electron-browser\/(?!sharedProcess\/).+\//, 41 | use: [{ 42 | loader: "ignore-loader", 43 | }], 44 | }], 45 | noParse: /(\\|\/)test(\\|\/)|\.test\.jsx?|\.test\.tsx?|tsconfig.+\.json$/, 46 | }, 47 | resolve: { 48 | alias: { 49 | "gc-signals": path.join(fills, "empty.ts"), 50 | "node-pty": path.resolve(fills, "empty.ts"), 51 | "windows-mutex": path.resolve(fills, "empty.ts"), 52 | "windows-process-tree": path.resolve(fills, "empty.ts"), 53 | "vscode-windows-registry": path.resolve(fills, "empty.ts"), 54 | "vscode-windows-ca-certs": path.resolve(fills, "empty.ts"), 55 | "vscode-sqlite3": path.resolve(fills, "empty.ts"), 56 | "vs/base/browser/browser": path.resolve(fills, "empty.ts"), 57 | 58 | "applicationinsights": path.join(vsFills, "applicationInsights.ts"), 59 | "electron": path.join(vsFills, "stdioElectron.ts"), 60 | "vscode-ripgrep": path.join(vsFills, "ripgrep.ts"), 61 | "native-keymap": path.join(vsFills, "native-keymap.ts"), 62 | "native-watchdog": path.join(vsFills, "native-watchdog.ts"), 63 | "vs/base/common/amd": path.resolve(vsFills, "amd.ts"), 64 | "vs/base/node/paths": path.join(vsFills, "paths.ts"), 65 | "vs/platform/product/node/package": path.resolve(vsFills, "package.ts"), 66 | "vs/platform/product/node/product": path.resolve(vsFills, "product.ts"), 67 | "vs/base/node/zip": path.resolve(vsFills, "zip.ts"), 68 | "vszip": path.resolve(root, "lib/vscode/src/vs/base/node/zip.ts"), 69 | "vs": path.resolve(root, "lib/vscode/src/vs"), 70 | }, 71 | }, 72 | resolveLoader: { 73 | alias: { 74 | "vs/css": path.resolve(vsFills, "css.js"), 75 | }, 76 | }, 77 | } 78 | ); 79 | -------------------------------------------------------------------------------- /packages/events/test/events.test.ts: -------------------------------------------------------------------------------- 1 | import { Emitter } from "../src/events"; 2 | 3 | describe("Event", () => { 4 | const emitter = new Emitter(); 5 | 6 | it("should listen to global event", () => { 7 | const fn = jest.fn(); 8 | const d = emitter.event(fn); 9 | emitter.emit(10); 10 | expect(fn).toHaveBeenCalledWith(10); 11 | d.dispose(); 12 | }); 13 | 14 | it("should listen to id event", () => { 15 | const fn = jest.fn(); 16 | const d = emitter.event(0, fn); 17 | emitter.emit(0, 5); 18 | expect(fn).toHaveBeenCalledWith(5); 19 | d.dispose(); 20 | }); 21 | 22 | it("should listen to string id event", () => { 23 | const fn = jest.fn(); 24 | const d = emitter.event("string", fn); 25 | emitter.emit("string", 55); 26 | expect(fn).toHaveBeenCalledWith(55); 27 | d.dispose(); 28 | }); 29 | 30 | it("should not listen wrong id event", () => { 31 | const fn = jest.fn(); 32 | const d = emitter.event(1, fn); 33 | emitter.emit(0, 5); 34 | emitter.emit(1, 6); 35 | expect(fn).toHaveBeenCalledWith(6); 36 | expect(fn).toHaveBeenCalledTimes(1); 37 | d.dispose(); 38 | }); 39 | 40 | it("should listen to id event globally", () => { 41 | const fn = jest.fn(); 42 | const d = emitter.event(fn); 43 | emitter.emit(1, 11); 44 | expect(fn).toHaveBeenCalledWith(11); 45 | d.dispose(); 46 | }); 47 | 48 | it("should listen to global event", () => { 49 | const fn = jest.fn(); 50 | const d = emitter.event(3, fn); 51 | emitter.emit(14); 52 | expect(fn).toHaveBeenCalledWith(14); 53 | d.dispose(); 54 | }); 55 | 56 | it("should listen to id event multiple times", () => { 57 | const fn = jest.fn(); 58 | const disposers = [ 59 | emitter.event(934, fn), 60 | emitter.event(934, fn), 61 | emitter.event(934, fn), 62 | emitter.event(934, fn), 63 | ]; 64 | emitter.emit(934, 324); 65 | expect(fn).toHaveBeenCalledTimes(4); 66 | expect(fn).toHaveBeenCalledWith(324); 67 | disposers.forEach((d) => d.dispose()); 68 | }); 69 | 70 | it("should dispose individually", () => { 71 | const fn = jest.fn(); 72 | const d = emitter.event(fn); 73 | 74 | const fn2 = jest.fn(); 75 | const d2 = emitter.event(1, fn2); 76 | 77 | d.dispose(); 78 | 79 | emitter.emit(12); 80 | emitter.emit(1, 12); 81 | 82 | expect(fn).not.toBeCalled(); 83 | expect(fn2).toBeCalledTimes(2); 84 | 85 | d2.dispose(); 86 | 87 | emitter.emit(12); 88 | emitter.emit(1, 12); 89 | 90 | expect(fn).not.toBeCalled(); 91 | expect(fn2).toBeCalledTimes(2); 92 | }); 93 | 94 | it("should dispose by id", () => { 95 | const fn = jest.fn(); 96 | emitter.event(fn); 97 | 98 | const fn2 = jest.fn(); 99 | emitter.event(1, fn2); 100 | 101 | emitter.dispose(1); 102 | 103 | emitter.emit(12); 104 | emitter.emit(1, 12); 105 | 106 | expect(fn).toBeCalledTimes(2); 107 | expect(fn2).not.toBeCalled(); 108 | }); 109 | 110 | it("should dispose all", () => { 111 | const fn = jest.fn(); 112 | emitter.event(fn); 113 | emitter.event(1, fn); 114 | 115 | emitter.dispose(); 116 | 117 | emitter.emit(12); 118 | emitter.emit(1, 12); 119 | 120 | expect(fn).not.toBeCalled(); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /packages/protocol/test/util.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as util from "util"; 3 | import { argumentToProto, protoToArgument } from "../src/common/util"; 4 | 5 | describe("Convert", () => { 6 | it("should convert nothing", () => { 7 | expect(protoToArgument()).toBeUndefined(); 8 | }); 9 | 10 | it("should convert null", () => { 11 | expect(protoToArgument(argumentToProto(null))).toBeNull(); 12 | }); 13 | 14 | it("should convert undefined", () => { 15 | expect(protoToArgument(argumentToProto(undefined))).toBeUndefined(); 16 | }); 17 | 18 | it("should convert string", () => { 19 | expect(protoToArgument(argumentToProto("test"))).toBe("test"); 20 | }); 21 | 22 | it("should convert number", () => { 23 | expect(protoToArgument(argumentToProto(10))).toBe(10); 24 | }); 25 | 26 | it("should convert boolean", () => { 27 | expect(protoToArgument(argumentToProto(true))).toBe(true); 28 | expect(protoToArgument(argumentToProto(false))).toBe(false); 29 | }); 30 | 31 | it("should convert error", () => { 32 | const error = new Error("message"); 33 | const convertedError = protoToArgument(argumentToProto(error)); 34 | 35 | expect(convertedError instanceof Error).toBeTruthy(); 36 | expect(convertedError.message).toBe("message"); 37 | }); 38 | 39 | it("should convert buffer", async () => { 40 | const buffer = await util.promisify(fs.readFile)(__filename); 41 | expect(buffer instanceof Buffer).toBeTruthy(); 42 | 43 | const convertedBuffer = protoToArgument(argumentToProto(buffer)); 44 | expect(convertedBuffer instanceof Buffer).toBeTruthy(); 45 | expect(convertedBuffer.toString()).toBe(buffer.toString()); 46 | }); 47 | 48 | it("should convert proxy", () => { 49 | let i = 0; 50 | const proto = argumentToProto( 51 | { onEvent: (): void => undefined }, 52 | undefined, 53 | () => i++, 54 | ); 55 | 56 | const proxy = protoToArgument(proto, undefined, (id) => { 57 | return { 58 | id: `created: ${id}`, 59 | dispose: (): Promise => Promise.resolve(), 60 | onDone: (): Promise => Promise.resolve(), 61 | onEvent: (): Promise => Promise.resolve(), 62 | }; 63 | }); 64 | 65 | expect(proxy.id).toBe("created: 0"); 66 | }); 67 | 68 | it("should convert function", () => { 69 | const fn = jest.fn(); 70 | // tslint:disable-next-line no-any 71 | const map = new Map void>(); 72 | let i = 0; 73 | const proto = argumentToProto( 74 | fn, 75 | (f) => { 76 | map.set(i++, f); 77 | 78 | return i - 1; 79 | }, 80 | ); 81 | 82 | const remoteFn = protoToArgument(proto, (id, args) => { 83 | map.get(id)!(...args); 84 | }); 85 | 86 | remoteFn("a", "b", 1); 87 | 88 | expect(fn).toHaveBeenCalledWith("a", "b", 1); 89 | }); 90 | 91 | it("should convert array", () => { 92 | const array = ["a", "b", 1, [1, "a"], null, undefined]; 93 | expect(protoToArgument(argumentToProto(array))).toEqual(array); 94 | }); 95 | 96 | it("should convert object", () => { 97 | const obj = { a: "test" }; 98 | // const obj = { "a": 1, "b": [1, "a"], test: null, test2: undefined }; 99 | expect(protoToArgument(argumentToProto(obj))).toEqual(obj); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /packages/protocol/test/child_process.test.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess } from "child_process"; 2 | import * as path from "path"; 3 | import { Readable } from "stream"; 4 | import * as util from "util"; 5 | import { createClient } from "@coder/protocol/test"; 6 | import { Module } from "../src/common/proxy"; 7 | 8 | describe("child_process", () => { 9 | const client = createClient(); 10 | const cp = client.modules[Module.ChildProcess]; 11 | 12 | const getStdout = async (proc: ChildProcess): Promise => { 13 | return new Promise((r): Readable => proc.stdout!.once("data", r)) 14 | .then((s) => s.toString()); 15 | }; 16 | 17 | describe("exec", () => { 18 | it("should get exec stdout", async () => { 19 | await expect(util.promisify(cp.exec)("echo test", { encoding: "utf8" })) 20 | .resolves.toEqual({ 21 | stdout: "test\n", 22 | stderr: "", 23 | }); 24 | }); 25 | }); 26 | 27 | describe("spawn", () => { 28 | it("should get spawn stdout", async () => { 29 | const proc = cp.spawn("echo", ["test"]); 30 | await expect(Promise.all([ 31 | getStdout(proc), 32 | new Promise((r): ChildProcess => proc.on("exit", r)), 33 | ]).then((values) => values[0])).resolves.toEqual("test\n"); 34 | }); 35 | 36 | it("should cat", async () => { 37 | const proc = cp.spawn("cat", []); 38 | expect(proc.pid).toBe(-1); 39 | proc.stdin!.write("banana"); 40 | await expect(getStdout(proc)).resolves.toBe("banana"); 41 | 42 | proc.stdin!.end(); 43 | proc.kill(); 44 | 45 | expect(proc.pid).toBeGreaterThan(-1); 46 | await new Promise((r): ChildProcess => proc.on("exit", r)); 47 | }); 48 | 49 | it("should print env", async () => { 50 | const proc = cp.spawn("env", [], { 51 | env: { hi: "donkey" }, 52 | }); 53 | 54 | await expect(getStdout(proc)).resolves.toContain("hi=donkey\n"); 55 | }); 56 | 57 | it("should eval", async () => { 58 | const proc = cp.spawn("node", ["-e", "console.log('foo')"]); 59 | await expect(getStdout(proc)).resolves.toContain("foo"); 60 | }); 61 | }); 62 | 63 | describe("fork", () => { 64 | it("should echo messages", async () => { 65 | const proc = cp.fork(path.join(__dirname, "forker.js")); 66 | 67 | proc.send({ bananas: true }); 68 | 69 | await expect(new Promise((r): ChildProcess => proc.on("message", r))) 70 | .resolves.toMatchObject({ 71 | bananas: true, 72 | }); 73 | 74 | proc.kill(); 75 | 76 | await new Promise((r): ChildProcess => proc.on("exit", r)); 77 | }); 78 | }); 79 | 80 | it("should dispose", (done) => { 81 | setTimeout(() => { 82 | client.dispose(); 83 | done(); 84 | }, 100); 85 | }); 86 | 87 | it("should disconnect", async () => { 88 | const client = createClient(); 89 | const cp = client.modules[Module.ChildProcess]; 90 | const proc = cp.fork(path.join(__dirname, "forker.js")); 91 | const fn = jest.fn(); 92 | proc.on("error", fn); 93 | 94 | proc.send({ bananas: true }); 95 | await expect(new Promise((r): ChildProcess => proc.on("message", r))) 96 | .resolves.toMatchObject({ 97 | bananas: true, 98 | }); 99 | 100 | client.dispose(); 101 | expect(fn).toHaveBeenCalledWith(new Error("disconnected")); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /packages/app/browser/src/app.scss: -------------------------------------------------------------------------------- 1 | @import url("https://use.typekit.net/vzk7ygg.css"); 2 | 3 | html, body { 4 | background-color: #FFFFFF; 5 | min-height: 100%; 6 | } 7 | 8 | body { 9 | font-family: 'aktiv-grotesk'; 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | height: calc(100vh - 20px); 14 | margin: 0; 15 | padding: 10px; 16 | --mdc-theme-primary: #AAADA1; 17 | --mdc-theme-secondary: #AAADA1; 18 | 19 | &.in-app { 20 | .back { 21 | pointer-events: all; 22 | opacity: 1; 23 | } 24 | } 25 | } 26 | 27 | .login { 28 | box-shadow: 0 18px 80px 10px rgba(69, 65, 78, 0.08); 29 | max-width: 328px; 30 | width: 100%; 31 | padding: 40px; 32 | border-radius: 5px; 33 | position: relative; 34 | color: #575962; 35 | 36 | .title { 37 | margin-bottom: 0px; 38 | font-size: 12px; 39 | font-weight: 500; 40 | letter-spacing: 1.5px; 41 | line-height: 15px; 42 | margin-bottom: 5px; 43 | margin-top: 0px; 44 | text-align: center; 45 | text-transform: uppercase; 46 | } 47 | 48 | .subtitle { 49 | text-align: center; 50 | margin: 0; 51 | font-size: 19px; 52 | font-weight: bold; 53 | line-height: 25px; 54 | margin-bottom: 45px; 55 | } 56 | 57 | .mdc-text-field { 58 | width: 100%; 59 | background: none !important; 60 | 61 | &::before { 62 | background: none !important; 63 | } 64 | } 65 | 66 | .mdc-form-field { 67 | text-align: left; 68 | font-size: 12px; 69 | color: #797E84; 70 | margin-top: 16px; 71 | } 72 | 73 | .mdc-button { 74 | border-radius: 24px; 75 | padding-left: 75px; 76 | padding-right: 75px; 77 | padding-top: 15px; 78 | padding-bottom: 15px; 79 | height: 48px; 80 | margin: 0 auto; 81 | display: block; 82 | box-shadow: 0 12px 17px 2px rgba(171,173,163,0.14), 0 5px 22px 4px rgba(171,173,163,0.12), 0 7px 8px -4px rgba(171,173,163,0.2); 83 | margin-top: 40px; 84 | } 85 | } 86 | 87 | .mdc-text-field--focused:not(.mdc-text-field--disabled) .mdc-floating-label { 88 | color: var(--mdc-theme-primary); 89 | } 90 | 91 | .mdc-floating-label--float-above { 92 | transform: translateY(-70%) scale(0.75); 93 | } 94 | 95 | .mdc-text-field:not(.mdc-text-field--disabled):not(.mdc-text-field--outlined):not(.mdc-text-field--textarea) .mdc-text-field__input, .mdc-text-field:not(.mdc-text-field--disabled):not(.mdc-text-field--outlined):not(.mdc-text-field--textarea) .mdc-text-field__input:hover { 96 | border-bottom-color: #EBEDF2; 97 | } 98 | 99 | .back { 100 | position: absolute; 101 | top: -50px; 102 | left: -50px; 103 | font-weight: bold; 104 | opacity: 0; 105 | pointer-events: none; 106 | 107 | // transition: 500ms opacity ease; 108 | } 109 | 110 | #error-display { 111 | box-sizing: border-box; 112 | color: #bb2d0f; 113 | font-size: 14px; 114 | font-weight: 400; 115 | letter-spacing: 0.3px; 116 | line-height: 12px; 117 | padding: 8px; 118 | padding-bottom: 0; 119 | padding-top: 20px; 120 | text-align: center; 121 | } 122 | -------------------------------------------------------------------------------- /packages/server/src/portScanner.ts: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import * as netstat from "node-netstat"; 3 | import { Event, Emitter } from "@coder/events"; 4 | import { logger } from "@coder/logger"; 5 | 6 | export interface PortScanner { 7 | readonly ports: ReadonlyArray; 8 | 9 | readonly onAdded: Event>; 10 | readonly onRemoved: Event>; 11 | 12 | dispose(): void; 13 | } 14 | 15 | /** 16 | * Creates a disposable port scanner. 17 | * Will scan local ports and emit events when ports are added or removed. 18 | * Currently only scans TCP ports. 19 | */ 20 | export const createPortScanner = (scanInterval: number = 5000): PortScanner => { 21 | const ports = new Map(); 22 | 23 | const addEmitter = new Emitter(); 24 | const removeEmitter = new Emitter(); 25 | 26 | const scan = (onCompleted: (err?: Error) => void): void => { 27 | const scanTime = Date.now(); 28 | const added: number[] = []; 29 | netstat({ 30 | done: (err: Error): void => { 31 | const removed: number[] = []; 32 | ports.forEach((value, key) => { 33 | if (value !== scanTime) { 34 | // Remove port 35 | removed.push(key); 36 | ports.delete(key); 37 | } 38 | }); 39 | if (removed.length > 0) { 40 | removeEmitter.emit(removed); 41 | } 42 | 43 | if (added.length > 0) { 44 | addEmitter.emit(added); 45 | } 46 | 47 | onCompleted(err); 48 | }, 49 | filter: { 50 | state: "LISTEN", 51 | }, 52 | }, (data: { 53 | readonly protocol: string; 54 | readonly local: { 55 | readonly port: number; 56 | readonly address: string; 57 | }; 58 | }) => { 59 | // https://en.wikipedia.org/wiki/Registered_port 60 | if (data.local.port <= 1023 || data.local.port >= 49151) { 61 | return; 62 | } 63 | // Only forward TCP ports 64 | if (!data.protocol.startsWith("tcp")) { 65 | return; 66 | } 67 | 68 | if (!ports.has(data.local.port)) { 69 | added.push(data.local.port); 70 | } 71 | ports.set(data.local.port, scanTime); 72 | }); 73 | }; 74 | 75 | let lastTimeout: NodeJS.Timer | undefined; 76 | let disposed: boolean = false; 77 | 78 | const doInterval = (): void => { 79 | logger.trace("scanning ports"); 80 | scan((error) => { 81 | if (error) { 82 | if ((error as NodeJS.ErrnoException).code === "ENOENT") { 83 | logger.warn("Port scanning will not be available because netstat is not installed"); 84 | } else { 85 | logger.warn(`Port scanning will not be available: ${error.message}`); 86 | } 87 | disposed = true; 88 | } else if (!disposed) { 89 | lastTimeout = setTimeout(doInterval, scanInterval); 90 | } 91 | }); 92 | }; 93 | 94 | doInterval(); 95 | 96 | return { 97 | get ports(): number[] { 98 | return Array.from(ports.keys()); 99 | }, 100 | get onAdded(): Event { 101 | return addEmitter.event; 102 | }, 103 | get onRemoved(): Event { 104 | return removeEmitter.event; 105 | }, 106 | dispose(): void { 107 | if (typeof lastTimeout !== "undefined") { 108 | clearTimeout(lastTimeout); 109 | } 110 | disposed = true; 111 | }, 112 | }; 113 | }; 114 | -------------------------------------------------------------------------------- /packages/protocol/src/node/modules/child_process.ts: -------------------------------------------------------------------------------- 1 | import * as cp from "child_process"; 2 | import { ServerProxy } from "../../common/proxy"; 3 | import { withEnv } from "../../common/util"; 4 | import { WritableProxy, ReadableProxy } from "./stream"; 5 | 6 | // tslint:disable completed-docs 7 | 8 | export type ForkProvider = (modulePath: string, args?: string[], options?: cp.ForkOptions) => cp.ChildProcess; 9 | 10 | export class ChildProcessProxy extends ServerProxy { 11 | public constructor(instance: cp.ChildProcess) { 12 | super({ 13 | bindEvents: ["close", "disconnect", "error", "exit", "message"], 14 | doneEvents: ["close"], 15 | instance, 16 | }); 17 | } 18 | 19 | public async kill(signal?: string): Promise { 20 | this.instance.kill(signal); 21 | } 22 | 23 | public async disconnect(): Promise { 24 | this.instance.disconnect(); 25 | } 26 | 27 | public async ref(): Promise { 28 | this.instance.ref(); 29 | } 30 | 31 | public async unref(): Promise { 32 | this.instance.unref(); 33 | } 34 | 35 | // tslint:disable-next-line no-any 36 | public async send(message: any): Promise { 37 | return new Promise((resolve, reject): void => { 38 | this.instance.send(message, (error) => { 39 | if (error) { 40 | reject(error); 41 | } else { 42 | resolve(); 43 | } 44 | }); 45 | }); 46 | } 47 | 48 | public async getPid(): Promise { 49 | return this.instance.pid; 50 | } 51 | 52 | public async dispose(): Promise { 53 | this.instance.kill(); 54 | setTimeout(() => this.instance.kill("SIGKILL"), 5000); // Double tap. 55 | await super.dispose(); 56 | } 57 | } 58 | 59 | export interface ChildProcessProxies { 60 | childProcess: ChildProcessProxy; 61 | stdin?: WritableProxy | null; 62 | stdout?: ReadableProxy | null; 63 | stderr?: ReadableProxy | null; 64 | } 65 | 66 | export class ChildProcessModuleProxy { 67 | public constructor(private readonly forkProvider?: ForkProvider) {} 68 | 69 | public async exec( 70 | command: string, 71 | options?: { encoding?: string | null } & cp.ExecOptions | null, 72 | callback?: ((error: cp.ExecException | null, stdin: string | Buffer, stdout: string | Buffer) => void), 73 | ): Promise { 74 | return this.returnProxies(cp.exec(command, options && withEnv(options), callback)); 75 | } 76 | 77 | public async fork(modulePath: string, args?: string[], options?: cp.ForkOptions): Promise { 78 | return this.returnProxies((this.forkProvider || cp.fork)(modulePath, args, withEnv(options))); 79 | } 80 | 81 | public async spawn(command: string, args?: string[], options?: cp.SpawnOptions): Promise { 82 | return this.returnProxies(cp.spawn(command, args, withEnv(options))); 83 | } 84 | 85 | private returnProxies(process: cp.ChildProcess): ChildProcessProxies { 86 | return { 87 | childProcess: new ChildProcessProxy(process), 88 | stdin: process.stdin && new WritableProxy(process.stdin), 89 | // Child processes streams appear to immediately flow so we need to bind 90 | // to the data event right away. 91 | stdout: process.stdout && new ReadableProxy(process.stdout, ["data"]), 92 | stderr: process.stderr && new ReadableProxy(process.stderr, ["data"]), 93 | }; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /doc/self-hosted/cros-install.md: -------------------------------------------------------------------------------- 1 | # Installng code-server in your ChromiumOS/ChromeOS/CloudReady machine 2 | 3 | This guide will show you how to install code-server into your CrOS machine. 4 | 5 | ## Using Crostini 6 | 7 | One of the easier ways to run code-server is via [Crostini](https://www.aboutchromebooks.com/tag/project-crostini/), the Linux apps support feature in CrOS. Make sure you have enough RAM, HDD space and your CPU has VT-x/ AMD-V support. If your chromebook has this, then you are qualified to use Crostini. 8 | 9 | If you are running R69, you might want to enable this on [Chrome Flags](chrome://flags/#enable-experimental-crostini-ui). If you run R72, however, this is already enabled for you. 10 | 11 | After checking your prerequisites, follow the steps in [the self-host install guide](index.md) on installing code-server. Once done, make sure code-server works by running it. After running it, simply go to `penguin.linux.test:8443` to access code-server. Now you should be greeted with this screen. If you did, congratulations, you have installed code-server in your Chromebook! 12 | 13 | ![code-server on Chromebook](../assets/cros.png) 14 | 15 | Alternatively, if you ran code-server in another container and you need the IP for that specific container, simply go to Termina's shell via `crosh` and type `vsh termina`. 16 | 17 | ```bash 18 | Loading extra module: /usr/share/crosh/dev.d/50-crosh.sh 19 | Welcome to crosh, the Chrome OS developer shell. 20 | 21 | If you got here by mistake, don't panic! Just close this tab and carry on. 22 | 23 | Type 'help' for a list of commands. 24 | 25 | If you want to customize the look/behavior, you can use the options page. 26 | Load it by using the Ctrl+Shift+P keyboard shortcut. 27 | 28 | crosh> vsh termina 29 | (termina) chronos@localhost ~ $ 30 | ``` 31 | While in termina, run `lxc list`. It should output the list of running containers. 32 | 33 | ```bash 34 | (termina) chronos@localhost ~ $ lxc list 35 | +---------+---------+-----------------------+------+------------+-----------+ 36 | | NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS | 37 | +---------+---------+-----------------------+------+------------+-----------+ 38 | | penguin | RUNNING | 100.115.92.199 (eth0) | | PERSISTENT | 0 | 39 | +---------+---------+-----------------------+------+------------+-----------+ 40 | (termina) chronos@localhost ~ $ 41 | ``` 42 | 43 | For this example, we show the default `penguin` container, which is exposed on `eth0` at 100.115.92.199. Simply enter the IP of the container where the code-server runs to Chrome. 44 | 45 | ## Using Crouton 46 | 47 | [Crouton](https://github.com/dnschneid/crouton) is one of the old ways to get a running full Linux via `chroot` on a Chromebook. To use crouton, enable developer mode and go to `crosh`. This time, run `shell`, which should drop you to `bash`. 48 | 49 | Make sure you downloaded `crouton`, if so, go ahead and run it under `~/Downloads`. After installing your chroot container via crouton, go ahead and enter `enter-chroot` to enter your container. 50 | 51 | Follow the instructions set in [the self-host install guide](index.md) to install code-server. After that is done, run `code-server` and verify it works by going to `localhost:8443`. 52 | 53 | > At this point in writing, `localhost` seems to work in this method. However, the author is not sure if it applies still to newer Chromebooks. 54 | -------------------------------------------------------------------------------- /rules/src/noBlockPaddingRule.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import * as Lint from "tslint"; 3 | 4 | /** 5 | * Rule for disallowing blank lines around the content of blocks. 6 | */ 7 | export class Rule extends Lint.Rules.AbstractRule { 8 | public static BEFORE_FAILURE_STRING = "Blocks must not start with blank lines"; 9 | public static AFTER_FAILURE_STRING = "Blocks must not end with blank lines"; 10 | 11 | /** 12 | * Apply the rule. 13 | */ 14 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 15 | return this.applyWithWalker(new NoBlockPaddingWalker(sourceFile, this.getOptions())); 16 | } 17 | } 18 | 19 | /** 20 | * Walker for checking block padding. 21 | */ 22 | class NoBlockPaddingWalker extends Lint.RuleWalker { 23 | /** 24 | * Apply this rule to interfaces. 25 | */ 26 | public visitInterfaceDeclaration(node: ts.InterfaceDeclaration): void { 27 | this.visitBlockNode(node); 28 | super.visitInterfaceDeclaration(node); 29 | } 30 | 31 | /** 32 | * Apply this rule to classes. 33 | */ 34 | public visitClassDeclaration(node: ts.ClassDeclaration): void { 35 | this.visitBlockNode(node); 36 | super.visitClassDeclaration(node); 37 | } 38 | 39 | /** 40 | * Add failures to blank lines surrounding a block's content. 41 | */ 42 | private visitBlockNode(node: ts.ClassDeclaration | ts.InterfaceDeclaration): void { 43 | const sourceFile = node.getSourceFile(); 44 | const children = node.getChildren(); 45 | 46 | const openBraceIndex = children.findIndex((n) => n.kind === ts.SyntaxKind.OpenBraceToken); 47 | if (openBraceIndex !== -1) { 48 | const nextToken = children[openBraceIndex + 1]; 49 | if (nextToken) { 50 | const startLine = this.getStartIncludingComments(sourceFile, nextToken); 51 | const openBraceToken = children[openBraceIndex]; 52 | if (ts.getLineAndCharacterOfPosition(sourceFile, openBraceToken.getEnd()).line + 1 < startLine) { 53 | this.addFailureAt(openBraceToken.getEnd(), openBraceToken.getEnd(), Rule.BEFORE_FAILURE_STRING); 54 | } 55 | } 56 | } 57 | 58 | const closeBraceIndex = children.findIndex((n) => n.kind === ts.SyntaxKind.CloseBraceToken); 59 | if (closeBraceIndex >= 2) { 60 | const previousToken = children[closeBraceIndex - 1]; 61 | if (previousToken) { 62 | let endLine = ts.getLineAndCharacterOfPosition(sourceFile, previousToken.getEnd()).line; 63 | const closeBraceToken = children[closeBraceIndex]; 64 | if (this.getStartIncludingComments(sourceFile, closeBraceToken) > endLine + 1) { 65 | this.addFailureAt(closeBraceToken.getStart(), closeBraceToken.getStart(), Rule.AFTER_FAILURE_STRING); 66 | } 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * getStart() doesn't account for comments while this does. 73 | */ 74 | private getStartIncludingComments(sourceFile: ts.SourceFile, node: ts.Node): number { 75 | // This gets the line the node starts on without counting comments. 76 | let startLine = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart()).line; 77 | 78 | // Adjust the start line for the comments. 79 | const comments = ts.getLeadingCommentRanges(sourceFile.text, node.pos) || []; 80 | comments.forEach((c) => { 81 | const commentStartLine = ts.getLineAndCharacterOfPosition(sourceFile, c.pos).line; 82 | if (commentStartLine < startLine) { 83 | startLine = commentStartLine; 84 | } 85 | }); 86 | 87 | return startLine; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /doc/admin/install/google_cloud.md: -------------------------------------------------------------------------------- 1 | # Deploy on Google Cloud 2 | 3 | This tutorial shows you how to deploy `code-server` to a single node running on Google Cloud. 4 | 5 | If you're just starting out, we recommend [installing code-server locally](../../self-hosted/index.md). It takes only a few minutes and lets you try out all of the features. 6 | 7 | --- 8 | 9 | ## Deploy to Google Cloud VM 10 | > Pre-requisite: Please [set up Google Cloud SDK](https://cloud.google.com/sdk/docs/) on your local machine 11 | 12 | - [Open your Google Cloud console](https://console.cloud.google.com/compute/instances) to create a new VM instance and click **Create Instance** 13 | - Choose an appropriate machine type (we recommend 2 vCPU and 7.5 GB RAM, more depending on team size and number of repositories/languages enabled) 14 | - Choose Ubuntu 16.04 LTS as your boot disk 15 | - Expand the "Management, security, disks, networking, sole tenancy" section, go to the "Networking" tab, then under network tags add "code-server" 16 | - Create your VM, and **take note** of its public IP address. 17 | - Visit "VPC network" in the console and go to "Firewall rules". Create a new firewall rule called "http-8443". Under "Target tags" add "code-server", and under "Protocols and ports" tick "Specified protocols and ports" and "tcp". Beside "tcp", add "8443", then create the rule. 18 | - Copy the link to download the latest Linux binary from our [releases page](https://github.com/cdr/code-server/releases) 19 | 20 | --- 21 | 22 | ## Final Steps 23 | 24 | - SSH into your Google Cloud VM 25 | ``` 26 | gcloud compute ssh --zone [region] [instance name] 27 | ``` 28 | 29 | - Find the latest Linux release from this URL: 30 | ``` 31 | https://github.com/cdr/code-server/releases/latest 32 | ``` 33 | 34 | - Replace {version} in the following command with the version found on the releases page and run it (or just copy the download URL from the releases page): 35 | ``` 36 | wget https://github.com/cdr/code-server/releases/download/{version}/code-server{version}-linux-x64.tar.gz 37 | ``` 38 | 39 | - Extract the downloaded tar.gz file with this command, for example: 40 | ``` 41 | tar -xvzf code-server{version}-linux-x64.tar.gz 42 | ``` 43 | 44 | - Navigate to extracted directory with this command: 45 | ``` 46 | cd code-server{version}-linux-x64 47 | ``` 48 | 49 | - Make the binary executable if you run into any errors regarding permission: 50 | ``` 51 | chmod +x code-server 52 | ``` 53 | 54 | > To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../../security/ssl.md) 55 | 56 | - Start the code-server 57 | ``` 58 | ./code-server 59 | ``` 60 | - Open your browser and visit `https://$public_ip:8443/` (where `$public_ip` is your Compute Engine instance's public IP address). You will be greeted with a page similar to the following screenshot. Code-server is using a self-signed SSL certificate for easy setup. In Chrome/Chromium, click **"Advanced"** then click **"proceed anyway"**. In Firefox, click **Advanced**, then **Add Exception**, then finally **Confirm Security Exception**. 61 | 62 | > For instructions on how to keep the server running after you end your SSH session please checkout [how to use systemd](https://www.linode.com/docs/quick-answers/linux/start-service-at-boot/) to start linux based services if they are killed 63 | 64 | --- 65 | 66 | > NOTE: If you get stuck or need help, [file an issue](https://github.com/cdr/code-server/issues/new?&title=Improve+self-hosted+quickstart+guide), [tweet (@coderhq)](https://twitter.com/coderhq) or [email](mailto:support@coder.com?subject=Self-hosted%20quickstart%20guide). 67 | -------------------------------------------------------------------------------- /packages/dns/src/dns.ts: -------------------------------------------------------------------------------- 1 | import { field, logger } from "@coder/logger"; 2 | import * as http from "http"; 3 | //@ts-ignore 4 | import * as named from "node-named"; 5 | import * as ip from "ip-address"; 6 | import { words, wordKeys } from "./words"; 7 | 8 | import * as dgram from "dgram"; 9 | 10 | const oldCreate = dgram.createSocket; 11 | 12 | // tslint:disable-next-line:no-any 13 | (dgram).createSocket = (_: any, callback: any): dgram.Socket => { 14 | return oldCreate("udp4", callback); 15 | }; 16 | 17 | interface DnsQuery { 18 | name(): string; 19 | // tslint:disable-next-line:no-any 20 | addAnswer(domain: string, target: any, ttl: number): void; 21 | } 22 | 23 | const dnsServer: { 24 | listen(port: number, host: string, callback: () => void): void; 25 | on(event: "query", callback: (query: DnsQuery) => void): void; 26 | send(query: DnsQuery): void; 27 | } = named.createServer(); 28 | 29 | const isDev = process.env.NODE_ENV !== "production"; 30 | const dnsPort = isDev ? 9999 : 53; 31 | dnsServer.listen(dnsPort, "0.0.0.0", () => { 32 | logger.info("DNS server started", field("port", dnsPort)); 33 | }); 34 | 35 | dnsServer.on("query", (query) => { 36 | const domain = query.name(); 37 | const reqParts = domain.split("."); 38 | if (reqParts.length < 2) { 39 | dnsServer.send(query); 40 | logger.info("Invalid request", field("request", domain)); 41 | 42 | return; 43 | } 44 | const allWords = reqParts.shift()!; 45 | if (allWords.length > 16) { 46 | dnsServer.send(query); 47 | logger.info("Invalid request", field("request", domain)); 48 | 49 | return; 50 | } 51 | const wordParts = allWords.split(/(?=[A-Z])/); 52 | const ipParts: string[] = []; 53 | // Should be left with HowAreYouNow 54 | for (let i = 0; i < wordParts.length; i++) { 55 | const part = wordParts[i]; 56 | if (part.length > 4) { 57 | dnsServer.send(query); 58 | logger.info("Words too long", field("request", domain)); 59 | 60 | return; 61 | } 62 | const ipPart = words[part.toLowerCase()]; 63 | if (typeof ipPart === "undefined") { 64 | dnsServer.send(query); 65 | logger.info("Word not found in index", field("part", part), field("request", domain)); 66 | 67 | return; 68 | } 69 | ipParts.push(ipPart.toString()); 70 | } 71 | 72 | const address = new ip.Address4(ipParts.join(".")); 73 | 74 | if (address.isValid()) { 75 | logger.info("Responded with valid address query", field("address", address.address), field("request", domain)); 76 | query.addAnswer(domain, new named.ARecord(address.address), 99999); 77 | } else { 78 | logger.warn("Received invalid request", field("request", domain)); 79 | } 80 | 81 | dnsServer.send(query); 82 | }); 83 | 84 | const httpServer = http.createServer((request, response) => { 85 | const remoteAddr = request.connection.remoteAddress; 86 | if (!remoteAddr) { 87 | response.writeHead(422); 88 | response.end(); 89 | 90 | return; 91 | } 92 | const hostHeader = request.headers.host; 93 | if (!hostHeader) { 94 | response.writeHead(422); 95 | response.end(); 96 | 97 | return; 98 | } 99 | const host = remoteAddr.split(".").map(p => wordKeys[Number.parseInt(p, 10)]).map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(""); 100 | logger.info("Resolved host", field("remote-addr", remoteAddr), field("host", host)); 101 | response.writeHead(200); 102 | response.write(`${host}.${hostHeader}`); 103 | response.end(); 104 | }); 105 | 106 | const httpPort = isDev ? 3000 : 80; 107 | httpServer.listen(httpPort, "0.0.0.0", () => { 108 | logger.info("HTTP server started", field("port", httpPort)); 109 | }); 110 | -------------------------------------------------------------------------------- /packages/web/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const merge = require("webpack-merge"); 3 | 4 | const root = path.resolve(__dirname, "../.."); 5 | const fills = path.join(root, "packages/ide/src/fill"); 6 | const vsFills = path.join(root, "packages/vscode/src/fill"); 7 | 8 | module.exports = merge( 9 | require(path.join(root, "scripts/webpack.client.config.js"))({ 10 | dirname: __dirname, 11 | entry: path.join(root, "packages/web/src/index.ts"), 12 | name: "ide", 13 | template: path.join(root, "packages/web/src/index.html"), 14 | typescriptCompilerOptions: { 15 | "target": "es5", 16 | "lib": ["dom", "esnext"], 17 | }, 18 | }, 19 | ), { 20 | node: { 21 | module: "empty", 22 | crypto: "empty", 23 | tls: "empty", 24 | }, 25 | resolve: { 26 | alias: { 27 | "gc-signals": path.join(fills, "empty.ts"), 28 | "selenium-webdriver": path.join(fills, "empty.ts"), 29 | "vscode": path.join(fills, "empty.ts"), 30 | "vscode-fsevents": path.join(fills, "empty.ts"), 31 | "vscode-windows-registry": path.resolve(fills, "empty.ts"), 32 | "vsda": path.join(fills, "empty.ts"), 33 | "windows-foreground-love": path.join(fills, "empty.ts"), 34 | "windows-mutex": path.join(fills, "empty.ts"), 35 | "windows-process-tree": path.join(fills, "empty.ts"), 36 | "vscode-sqlite3": path.join(fills, "empty.ts"), 37 | "tls": path.join(fills, "empty.ts"), 38 | "native-is-elevated": path.join(fills, "empty.ts"), 39 | "dns": path.join(fills, "empty.ts"), 40 | "console": path.join(fills, "empty.ts"), 41 | "readline": path.join(fills, "empty.ts"), 42 | "oniguruma": path.join(fills, "empty.ts"), 43 | 44 | // Webpack includes path-browserify but not the latest version, so 45 | // path.posix and path.parse are undefined (among other things possibly). 46 | // Also if we don't provide the full path, the code in vscode will import 47 | // from vscode's node_modules which is the wrong version. 48 | "path": path.join(fills, "path.js"), 49 | "crypto": "crypto-browserify", 50 | "http": "http-browserify", 51 | 52 | "child_process": path.join(fills, "child_process.ts"), 53 | "os": path.join(fills, "os.ts"), 54 | "fs": path.join(fills, "fs.ts"), 55 | "net": path.join(fills, "net.ts"), 56 | "util": path.join(fills, "util.ts"), 57 | "trash": path.join(fills, "trash.ts"), 58 | "electron": path.join(fills, "electron.ts"), 59 | 60 | "native-keymap": path.join(vsFills, "native-keymap.ts"), 61 | "node-pty": path.join(vsFills, "node-pty.ts"), 62 | "graceful-fs": path.join(vsFills, "graceful-fs.ts"), 63 | "spdlog": path.join(vsFills, "spdlog.ts"), 64 | "native-watchdog": path.join(vsFills, "native-watchdog.ts"), 65 | "iconv-lite": path.join(vsFills, "iconv-lite.ts"), 66 | 67 | // This seems to be in the wrong place? 68 | "vs/workbench/contrib/codeEditor/electron-browser/media/WordWrap_16x.svg": "vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/WordWrap_16x.svg", 69 | 70 | "vs/platform/windows/electron-browser/windowsService": path.join(vsFills, "windowsService.ts"), 71 | "vs/base/node/paths": path.join(vsFills, "paths.ts"), 72 | "vs/base/common/amd": path.join(vsFills, "amd.ts"), 73 | "vs/platform/product/node/package": path.resolve(vsFills, "package.ts"), 74 | "vs/platform/product/node/product": path.resolve(vsFills, "product.ts"), 75 | "vs/base/node/zip": path.resolve(vsFills, "zip.ts"), 76 | "vszip": path.resolve(root, "lib/vscode/src/vs/base/node/zip.ts"), 77 | "vs": path.join(root, "lib", "vscode", "src", "vs"), 78 | }, 79 | }, 80 | resolveLoader: { 81 | alias: { 82 | "vs/css": path.join(vsFills, "css.js"), 83 | }, 84 | }, 85 | }); 86 | -------------------------------------------------------------------------------- /packages/vscode/src/dialog.scss: -------------------------------------------------------------------------------- 1 | .dialog { 2 | --primary: #2A2E37; 3 | --border: black; 4 | --faded: #a0a1a5; 5 | --disabled: #888; 6 | --header-background: #161616; 7 | --header-foreground: white; 8 | --list-active-selection-background: rgb(0, 120, 160); 9 | --list-active-selection-foreground: white; 10 | --list-hover-background: rgb(36, 39, 46); 11 | font-family: inherit; 12 | box-shadow: 0 18px 80px 10px rgba(0, 0, 0, 0.1); 13 | background-color: var(--primary); 14 | display: flex; 15 | flex-direction: column; 16 | user-select: none; 17 | overflow: hidden; 18 | border-radius: 5px; 19 | 20 | .monaco-tl-twistie { 21 | display: none; 22 | } 23 | 24 | .title { 25 | background-color: var(--header-background); 26 | color: var(--header-foreground); 27 | padding: 1px; 28 | font-size: 11px; 29 | font-weight: normal; 30 | text-transform: uppercase; 31 | white-space: nowrap; 32 | padding: 5px 10px; 33 | } 34 | 35 | .nav { 36 | display: flex; 37 | flex-direction: row; 38 | padding: 4px; 39 | border-bottom: 1px solid var(--border); 40 | } 41 | 42 | .path { 43 | display: flex; 44 | flex-direction: row; 45 | 46 | .path-part { 47 | padding: 5px; 48 | border-radius: 3px; 49 | font-size: 1.02em; 50 | cursor: pointer; 51 | 52 | &:not(:first-child) { 53 | margin-left: 5px; 54 | } 55 | 56 | &.active { 57 | font-weight: bold; 58 | color: var(--list-active-selection-foreground); 59 | } 60 | } 61 | } 62 | 63 | .dialog-grid { 64 | display: grid; 65 | grid-template-columns: 2fr 0.2fr 0.8fr; 66 | } 67 | 68 | .headings { 69 | padding: 8px; 70 | font-size: 12px; 71 | } 72 | 73 | .file-area { 74 | flex: 1; 75 | display: flex; 76 | flex-direction: column; 77 | overflow: hidden; 78 | 79 | .dialog-entry { 80 | cursor: pointer; 81 | font-size: 1.02em; 82 | padding: 0px 8px; 83 | 84 | .dialog-entry-info { 85 | display: flex; 86 | flex-direction: row; 87 | } 88 | 89 | .dialog-entry-icon { 90 | width: 16px; 91 | height: 19px; 92 | margin-right: 5px; 93 | } 94 | 95 | .dialog-entry-size { 96 | text-align: right; 97 | } 98 | 99 | .dialog-entry-mtime { 100 | padding-left: 8px; 101 | } 102 | 103 | &:hover { 104 | background-color: var(--list-hover-background); 105 | } 106 | 107 | &.active { 108 | background-color: var(--list-active-selection-background); 109 | color: var(--list-active-selection-foreground); 110 | } 111 | 112 | &.disabled, &.disabled:hover { 113 | background-color: var(--primary); 114 | color: var(--disabled); 115 | cursor: initial; 116 | } 117 | } 118 | } 119 | 120 | .buttons { 121 | display: flex; 122 | flex-direction: row; 123 | padding: 10px; 124 | position: relative; 125 | background: var(--primary); 126 | border-top: 1px solid var(--border); 127 | 128 | button:first-child { 129 | margin-left: auto; 130 | margin-right: 10px; 131 | } 132 | 133 | button { 134 | background: transparent; 135 | outline: none; 136 | border: 0; 137 | color: var(--faded); 138 | padding: 10px; 139 | padding-left: 18px; 140 | padding-right: 18px; 141 | transition: 150ms background ease, 150ms color ease; 142 | cursor: pointer; 143 | border-radius: 5px; 144 | 145 | &:hover { 146 | background: var(--titlebar); 147 | color: white; 148 | } 149 | } 150 | 151 | button[disabled], button[disabled]:hover { 152 | color: var(--disabled); 153 | cursor: initial; 154 | } 155 | } 156 | } 157 | 158 | .monaco-shell .monaco-tree.focused.no-focused-item:focus:before, .monaco-shell .monaco-list:not(.element-focused):focus:before { 159 | display: none; 160 | } 161 | -------------------------------------------------------------------------------- /packages/protocol/src/node/modules/stream.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | import * as stream from "stream"; 3 | import { ServerProxy } from "../../common/proxy"; 4 | 5 | // tslint:disable completed-docs no-any 6 | 7 | export class WritableProxy extends ServerProxy { 8 | public constructor(instance: T, bindEvents: string[] = [], delayedEvents?: string[]) { 9 | super({ 10 | bindEvents: ["close", "drain", "error", "finish"].concat(bindEvents), 11 | doneEvents: ["close"], 12 | delayedEvents, 13 | instance, 14 | }); 15 | } 16 | 17 | public async destroy(): Promise { 18 | this.instance.destroy(); 19 | } 20 | 21 | public async end(data?: any, encoding?: string): Promise { 22 | return new Promise((resolve): void => { 23 | this.instance.end(data, encoding, () => { 24 | resolve(); 25 | }); 26 | }); 27 | } 28 | 29 | public async setDefaultEncoding(encoding: string): Promise { 30 | this.instance.setDefaultEncoding(encoding); 31 | } 32 | 33 | public async write(data: any, encoding?: string): Promise { 34 | return new Promise((resolve, reject): void => { 35 | this.instance.write(data, encoding, (error) => { 36 | if (error) { 37 | reject(error); 38 | } else { 39 | resolve(); 40 | } 41 | }); 42 | }); 43 | } 44 | 45 | public async dispose(): Promise { 46 | this.instance.end(); 47 | await super.dispose(); 48 | } 49 | } 50 | 51 | /** 52 | * This noise is because we can't do multiple extends and we also can't seem to 53 | * do `extends WritableProxy implement ReadableProxy` (for `DuplexProxy`). 54 | */ 55 | export interface IReadableProxy extends ServerProxy { 56 | pipe

(destination: P, options?: { end?: boolean; }): Promise; 57 | setEncoding(encoding: string): Promise; 58 | } 59 | 60 | export class ReadableProxy extends ServerProxy implements IReadableProxy { 61 | public constructor(instance: T, bindEvents: string[] = []) { 62 | super({ 63 | bindEvents: ["close", "end", "error"].concat(bindEvents), 64 | doneEvents: ["close"], 65 | delayedEvents: ["data"], 66 | instance, 67 | }); 68 | } 69 | 70 | public async pipe

(destination: P, options?: { end?: boolean; }): Promise { 71 | this.instance.pipe(destination.instance, options); 72 | // `pipe` switches the stream to flowing mode and makes data start emitting. 73 | await this.bindDelayedEvent("data"); 74 | } 75 | 76 | public async destroy(): Promise { 77 | this.instance.destroy(); 78 | } 79 | 80 | public async setEncoding(encoding: string): Promise { 81 | this.instance.setEncoding(encoding); 82 | } 83 | 84 | public async dispose(): Promise { 85 | this.instance.destroy(); 86 | await super.dispose(); 87 | } 88 | } 89 | 90 | export class DuplexProxy extends WritableProxy implements IReadableProxy { 91 | public constructor(stream: T, bindEvents: string[] = []) { 92 | super(stream, ["end"].concat(bindEvents), ["data"]); 93 | } 94 | 95 | public async pipe

(destination: P, options?: { end?: boolean; }): Promise { 96 | this.instance.pipe(destination.instance, options); 97 | // `pipe` switches the stream to flowing mode and makes data start emitting. 98 | await this.bindDelayedEvent("data"); 99 | } 100 | 101 | public async setEncoding(encoding: string): Promise { 102 | this.instance.setEncoding(encoding); 103 | } 104 | 105 | public async dispose(): Promise { 106 | this.instance.destroy(); 107 | await super.dispose(); 108 | } 109 | } 110 | --------------------------------------------------------------------------------