diff --git a/build/tasks.ts b/build/tasks.ts index cd4d312ba..06199fa82 100644 --- a/build/tasks.ts +++ b/build/tasks.ts @@ -67,21 +67,21 @@ const buildServerBinaryCopy = register("build:server:binary:copy", async (runner } fse.copySync(defaultExtensionsPath, path.join(cliBuildPath, "extensions")); fs.writeFileSync(path.join(cliBuildPath, "bootstrap-fork.js.gz"), zlib.gzipSync(fs.readFileSync(bootstrapForkPath))); - const cpDir = (dir: string, subdir: "auth" | "unauth", rootPath: string): void => { + const cpDir = (dir: string, rootPath: string, subdir?: "login"): void => { const stat = fs.statSync(dir); if (stat.isDirectory()) { const paths = fs.readdirSync(dir); - paths.forEach((p) => cpDir(path.join(dir, p), subdir, rootPath)); + paths.forEach((p) => cpDir(path.join(dir, p), rootPath, subdir)); } else if (stat.isFile()) { - const newPath = path.join(cliBuildPath, "web", subdir, path.relative(rootPath, dir)); + const newPath = path.join(cliBuildPath, "web", subdir || "", path.relative(rootPath, dir)); fse.mkdirpSync(path.dirname(newPath)); fs.writeFileSync(newPath + ".gz", zlib.gzipSync(fs.readFileSync(dir))); } else { // Nothing } }; - cpDir(webOutputPath, "auth", webOutputPath); - cpDir(browserAppOutputPath, "unauth", browserAppOutputPath); + cpDir(webOutputPath, webOutputPath); + cpDir(browserAppOutputPath, browserAppOutputPath, "login"); fse.mkdirpSync(path.join(cliBuildPath, "dependencies")); fse.copySync(ripgrepPath, path.join(cliBuildPath, "dependencies", "rg")); }); diff --git a/packages/app/browser/src/app.ts b/packages/app/browser/src/app.ts index 9e377845f..5fa8a9cb1 100644 --- a/packages/app/browser/src/app.ts +++ b/packages/app/browser/src/app.ts @@ -28,7 +28,9 @@ if (!form) { form.addEventListener("submit", (e) => { e.preventDefault(); - document.cookie = `password=${password.value}`; + document.cookie = `password=${password.value}; ` + + `path=${location.pathname.replace(/\/login\/?$/, "/")}; ` + + `domain=${location.hostname}`; location.reload(); }); diff --git a/packages/app/browser/webpack.config.js b/packages/app/browser/webpack.config.js index 4c7d24f98..05028ca99 100644 --- a/packages/app/browser/webpack.config.js +++ b/packages/app/browser/webpack.config.js @@ -7,11 +7,10 @@ const root = path.resolve(__dirname, "../../.."); module.exports = merge( require(path.join(root, "scripts/webpack.client.config.js"))({ - entry: path.join(root, "packages/app/browser/src/app.ts"), - template: path.join(root, "packages/app/browser/src/app.html"), + dirname: __dirname, + entry: path.join(__dirname, "src/app.ts"), + name: "login", + template: path.join(__dirname, "src/app.html"), }), { - output: { - path: path.join(__dirname, "out"), - }, }, ); diff --git a/packages/dns/webpack.config.js b/packages/dns/webpack.config.js index 99308ddce..1d69f59b3 100644 --- a/packages/dns/webpack.config.js +++ b/packages/dns/webpack.config.js @@ -5,15 +5,12 @@ const root = path.resolve(__dirname, "../.."); module.exports = merge( require(path.join(root, "scripts/webpack.node.config.js"))({ - // Options. + dirname: __dirname, + name: "dns", }), { externals: { "node-named": "commonjs node-named", }, - output: { - path: path.join(__dirname, "out"), - filename: "main.js", - }, entry: [ "./packages/dns/src/dns.ts" ], diff --git a/packages/server/src/cli.ts b/packages/server/src/cli.ts index 6544a1e6c..45d2d5530 100644 --- a/packages/server/src/cli.ts +++ b/packages/server/src/cli.ts @@ -216,13 +216,6 @@ const bold = (text: string | number): string | number => { allowHttp: options.allowHttp, bypassAuth: options.noAuth, registerMiddleware: (app): void => { - app.use((req, res, next) => { - res.on("finish", () => { - logger.trace(`\u001B[1m${req.method} ${res.statusCode} \u001B[0m${req.url}`, field("host", req.hostname), field("ip", req.ip)); - }); - - next(); - }); // If we're not running from the binary and we aren't serving the static // pre-built version, use webpack to serve the web files. if (!isCli && !serveStatic) { diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index 9555b1d7f..1b112bfe1 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -18,6 +18,7 @@ import * as os from "os"; import * as path from "path"; import * as pem from "pem"; import * as util from "util"; +import * as url from "url"; import * as ws from "ws"; import { buildDir } from "./constants"; import { createPortScanner } from "./portScanner"; @@ -140,13 +141,13 @@ export const createApp = async (options: CreateAppOptions): Promise<{ }; const portScanner = createPortScanner(); - wss.on("connection", (ws, req) => { + wss.on("connection", async (ws, req) => { if (req.url && req.url.startsWith("/tunnel")) { try { const rawPort = req.url.split("/").pop(); const port = Number.parseInt(rawPort!, 10); - handleTunnel(ws, port); + await handleTunnel(ws, port); } catch (ex) { ws.close(TunnelCloseCode.Error, ex.toString()); } @@ -189,31 +190,70 @@ export const createApp = async (options: CreateAppOptions): Promise<{ new Server(connection, options.serverOptions); }); + const redirect = ( + req: express.Request, res: express.Response, + to: string = "", from: string = "", + code: number = 302, protocol: string = req.protocol, + ): void => { + const currentUrl = `${protocol}://${req.headers.host}${req.originalUrl}`; + const newUrl = url.parse(currentUrl); + if (from && newUrl.pathname) { + newUrl.pathname = newUrl.pathname.replace(new RegExp(`\/${from}\/?$`), "/"); + } + if (to) { + newUrl.pathname = (newUrl.pathname || "").replace(/\/$/, "") + `/${to}`; + } + newUrl.path = undefined; // Path is not necessary for format(). + const newUrlString = url.format(newUrl); + logger.trace(`Redirecting from ${currentUrl} to ${newUrlString}`); + + return res.redirect(code, newUrlString); + }; + const baseDir = buildDir || path.join(__dirname, ".."); - const authStaticFunc = expressStaticGzip(path.join(baseDir, "build/web/auth")); - const unauthStaticFunc = expressStaticGzip(path.join(baseDir, "build/web/unauth")); + const staticGzip = expressStaticGzip(path.join(baseDir, "build/web")); + app.use((req, res, next) => { + logger.trace(`\u001B[1m${req.method} ${res.statusCode} \u001B[0m${req.originalUrl}`, field("host", req.hostname), field("ip", req.ip)); + + // Force HTTPS unless allowing HTTP. if (!isEncrypted(req.socket) && !options.allowHttp) { - return res.redirect(301, `https://${req.headers.host!}${req.path}`); + return redirect(req, res, "", "", 301, "https"); } - if (isAuthed(req)) { - // We can serve the actual VSCode bin - authStaticFunc(req, res, next); - } else { - // Serve only the unauthed version - unauthStaticFunc(req, res, next); - } - }); - // @ts-ignore - app.use((err, req, res, next) => { next(); }); - app.get("/ping", (req, res) => { + + // @ts-ignore + app.use((err, _req, _res, next) => { + logger.error(err.message); + next(); + }); + + // If not authenticated, redirect to the login page. + app.get("/", (req, res, next) => { + if (!isAuthed(req)) { + return redirect(req, res, "login"); + } + next(); + }); + + // If already authenticated, redirect back to the root. + app.get("/login", (req, res, next) => { + if (isAuthed(req)) { + return redirect(req, res, "", "login"); + } + next(); + }); + + // For getting general server data. + app.get("/ping", (_req, res) => { res.json({ hostname: os.hostname(), }); }); + + // For getting a resource on disk. app.get("/resource/:url(*)", async (req, res) => { if (!ensureAuthed(req, res)) { return; @@ -254,6 +294,8 @@ export const createApp = async (options: CreateAppOptions): Promise<{ res.end(); } }); + + // For writing a resource to disk. app.post("/resource/:url(*)", async (req, res) => { if (!ensureAuthed(req, res)) { return; @@ -282,6 +324,9 @@ export const createApp = async (options: CreateAppOptions): Promise<{ } }); + // Everything else just pulls from the static build directory. + app.use(staticGzip); + return { express: app, server, diff --git a/packages/server/webpack.config.js b/packages/server/webpack.config.js index c3d50561f..bd2768e51 100644 --- a/packages/server/webpack.config.js +++ b/packages/server/webpack.config.js @@ -6,11 +6,10 @@ const root = path.resolve(__dirname, "../.."); module.exports = merge( require(path.join(root, "scripts/webpack.node.config.js"))({ - // Config options. + dirname: __dirname, }), { output: { filename: "cli.js", - path: path.join(__dirname, "out"), libraryTarget: "commonjs", }, node: { diff --git a/packages/vscode/webpack.bootstrap.config.js b/packages/vscode/webpack.bootstrap.config.js index 91a5ade63..38156a6b1 100644 --- a/packages/vscode/webpack.bootstrap.config.js +++ b/packages/vscode/webpack.bootstrap.config.js @@ -7,6 +7,7 @@ const vsFills = path.join(root, "packages/vscode/src/fill"); module.exports = merge( require(path.join(root, "scripts/webpack.node.config.js"))({ + dirname: __dirname, typescriptCompilerOptions: { target: "es6", }, @@ -15,7 +16,6 @@ module.exports = merge( mode: "development", output: { chunkFilename: "[name].bundle.js", - path: path.resolve(__dirname, "out"), publicPath: "/", filename: "bootstrap-fork.js", libraryTarget: "commonjs", diff --git a/packages/web/src/index.html b/packages/web/src/index.html index 37a8f81a4..4e8473b56 100644 --- a/packages/web/src/index.html +++ b/packages/web/src/index.html @@ -23,15 +23,15 @@ return; } document.body.style.background = bg; - })(); - - // Check that service workers are registered - if ("serviceWorker" in navigator) { - // Use the window load event to keep the page load performant - window.addEventListener("load", () => { - navigator.serviceWorker.register("/service-worker.js"); - }); - } + })(); + + // Check that service workers are registered + if ("serviceWorker" in navigator) { + // Use the window load event to keep the page load performant + window.addEventListener("load", () => { + navigator.serviceWorker.register("/service-worker.js"); + }); + } - \ No newline at end of file + diff --git a/packages/web/webpack.config.js b/packages/web/webpack.config.js index c2a1e5817..7d312035a 100644 --- a/packages/web/webpack.config.js +++ b/packages/web/webpack.config.js @@ -7,7 +7,9 @@ const vsFills = path.join(root, "packages/vscode/src/fill"); module.exports = merge( require(path.join(root, "scripts/webpack.client.config.js"))({ + dirname: __dirname, entry: path.join(root, "packages/web/src/index.ts"), + name: "ide", template: path.join(root, "packages/web/src/index.html"), typescriptCompilerOptions: { "target": "es5", @@ -15,11 +17,6 @@ module.exports = merge( }, }, ), { - output: { - chunkFilename: "[name]-[hash:6].bundle.js", - path: path.join(__dirname, "out"), - filename: "[hash:6].bundle.js", - }, node: { module: "empty", crypto: "empty", diff --git a/scripts/webpack.client.config.js b/scripts/webpack.client.config.js index 3e27fbe15..31ab2a7e0 100644 --- a/scripts/webpack.client.config.js +++ b/scripts/webpack.client.config.js @@ -7,102 +7,82 @@ const HtmlWebpackPlugin = require("html-webpack-plugin"); const WebpackPwaManifest = require("webpack-pwa-manifest"); const { GenerateSW } = require("workbox-webpack-plugin"); -// const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); - const root = path.join(__dirname, ".."); const prod = process.env.NODE_ENV === "production" || process.env.CI === "true"; +const cachePattern = /\.(?:png|jpg|jpeg|svg|css|js|ttf|woff|eot|woff2|wasm)$/; module.exports = (options = {}) => merge( - require("./webpack.general.config")(options), { - devtool: prod ? "none" : "cheap-module-eval-source-map", - mode: prod ? "production" : "development", - entry: prod ? options.entry : [ - "webpack-hot-middleware/client?reload=true&quiet=true", - options.entry, - ], - module: { - rules: [{ - test: /\.s?css$/, - // This is required otherwise it'll fail to resolve CSS in common. - include: root, - use: [{ - loader: MiniCssExtractPlugin.loader, - }, { - loader: "css-loader", - }, { - loader: "sass-loader", - }], - }, { - test: /\.(png|ttf|woff|eot|woff2)$/, - use: [{ - loader: "file-loader", - options: { - name: "[path][name].[ext]", - }, - }], - }, { - test: /\.svg$/, - loader: 'url-loader' - }], - }, - plugins: [ - new MiniCssExtractPlugin({ - filename: "[name].css", - chunkFilename: "[id].css" - }), - new HtmlWebpackPlugin({ - template: options.template - }), - new PreloadWebpackPlugin({ - rel: "preload", - as: "script" - }), - new WebpackPwaManifest({ - name: "Coder", - short_name: "Coder", - description: "Run VS Code on a remote server", - background_color: "#e5e5e5", - icons: [ - { - src: path.join(root, "packages/web/assets/logo.png"), - sizes: [96, 128, 192, 256, 384] - } - ] - }) - ].concat(prod ? [ - new GenerateSW({ - exclude: [/\.map$/, /^manifest.*\.js$/, /\.html$/], - runtimeCaching: [ - { - urlPattern: new RegExp("^(?!.*(html))"), - handler: "StaleWhileRevalidate", - options: { - cacheName: "code-server", - expiration: { - maxAgeSeconds: 86400 - }, - cacheableResponse: { - statuses: [0, 200] - } - } - } - // Network first caching is also possible. - /*{ - urlPattern: new RegExp("^(?!.*(html))"), - handler: "NetworkFirst", - options: { - networkTimeoutSeconds: 4, - cacheName: "code-server", - expiration: { - maxAgeSeconds: 86400, - }, - cacheableResponse: { - statuses: [0, 200], - }, - }, - }*/ - ] - }) - ] : [new webpack.HotModuleReplacementPlugin()]), - target: "web" - }); + require("./webpack.general.config")(options), { + devtool: prod ? "none" : "cheap-module-eval-source-map", + mode: prod ? "production" : "development", + entry: prod ? options.entry : [ + "webpack-hot-middleware/client?reload=true&quiet=true", + options.entry, + ], + module: { + rules: [{ + test: /\.s?css$/, + // This is required otherwise it'll fail to resolve CSS in common. + include: root, + use: [{ + loader: MiniCssExtractPlugin.loader, + }, { + loader: "css-loader", + }, { + loader: "sass-loader", + }], + }, { + test: /\.(png|ttf|woff|eot|woff2)$/, + use: [{ + loader: "file-loader", + options: { + name: "[path][name].[ext]", + }, + }], + }, { + test: /\.svg$/, + loader: 'url-loader' + }], + }, + plugins: [ + new MiniCssExtractPlugin({ + chunkFilename: `${options.name || "client"}.[name].[hash:6].css`, + filename: `${options.name || "client"}.[name].[hash:6].css` + }), + new HtmlWebpackPlugin({ + template: options.template + }), + new PreloadWebpackPlugin({ + rel: "preload", + as: "script" + }), + new WebpackPwaManifest({ + name: "Coder", + short_name: "Coder", + description: "Run VS Code on a remote server", + background_color: "#e5e5e5", + icons: [{ + src: path.join(root, "packages/web/assets/logo.png"), + sizes: [96, 128, 192, 256, 384], + }], + }) + ].concat(prod ? [ + new GenerateSW({ + include: [cachePattern], + runtimeCaching: [{ + urlPattern: cachePattern, + handler: "StaleWhileRevalidate", + options: { + cacheName: "code-server", + expiration: { + maxAgeSeconds: 86400, + }, + cacheableResponse: { + statuses: [0, 200], + }, + }, + }, + ]}), + ] : [new webpack.HotModuleReplacementPlugin()]), + target: "web" +}); diff --git a/scripts/webpack.general.config.js b/scripts/webpack.general.config.js index ffc34f2fd..d855dc81b 100644 --- a/scripts/webpack.general.config.js +++ b/scripts/webpack.general.config.js @@ -13,6 +13,11 @@ module.exports = (options = {}) => ({ externals: { fsevents: "fsevents", }, + output: { + path: path.join(options.dirname || __dirname, "out"), + chunkFilename: `${options.name || "general"}.[name].[hash:6].js`, + filename: `${options.name || "general"}.[name].[hash:6].js` + }, module: { rules: [{ loader: "string-replace-loader",