From 2b2aa9a211e238d21af73aaf1108bbc87f189726 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 11 Jul 2019 17:12:52 -0500 Subject: [PATCH] Add https server --- .travis.yml | 6 +- main.js | 2 +- package.json | 23 +-- scripts/ci.bash | 18 ++- scripts/{nbin-loader.js => nbin-shim.js} | 0 scripts/tasks.bash | 136 ++++++++++++----- channel.ts => src/channel.ts | 12 +- cli.ts => src/cli.ts | 42 +++++- connection.ts => src/connection.ts | 6 +- insights.ts => src/insights.ts | 0 protocol.ts => src/protocol.ts | 0 server.ts => src/server.ts | 138 +++++++++++------- tar.ts => src/tar.ts | 5 +- upload.ts => src/upload.ts | 0 .../uriTransformerHttp.js | 0 src/uriTransformerHttps.js | 3 + src/util.ts | 60 ++++++++ typings/httpolyglot.d.ts | 7 + uriTransformerHttps.js | 3 - yarn.lock | 130 ++++++++++------- 20 files changed, 405 insertions(+), 186 deletions(-) rename scripts/{nbin-loader.js => nbin-shim.js} (100%) rename channel.ts => src/channel.ts (95%) rename cli.ts => src/cli.ts (79%) rename connection.ts => src/connection.ts (96%) rename insights.ts => src/insights.ts (100%) rename protocol.ts => src/protocol.ts (100%) rename server.ts => src/server.ts (83%) rename tar.ts => src/tar.ts (99%) rename upload.ts => src/upload.ts (100%) rename uriTransformer.js => src/uriTransformerHttp.js (100%) create mode 100644 src/uriTransformerHttps.js create mode 100644 src/util.ts create mode 100644 typings/httpolyglot.d.ts delete mode 100644 uriTransformerHttps.js diff --git a/.travis.yml b/.travis.yml index 929137ca0..5c4e3c554 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ matrix: - os: linux dist: trusty env: - - VSCODE_VERSION="1.36.1" MAJOR_VERSION="2" VERSION="$MAJOR_VERSION.$TRAVIS_BUILD_NUMBER" TARGET="centos" + - VSCODE_VERSION="1.36.1" MAJOR_VERSION="2" VERSION="$MAJOR_VERSION.$TRAVIS_BUILD_NUMBER" TARGET="linux" - os: linux dist: trusty env: @@ -16,10 +16,6 @@ matrix: - os: osx env: - VSCODE_VERSION="1.36.1" MAJOR_VERSION="2" VERSION="$MAJOR_VERSION.$TRAVIS_BUILD_NUMBER" -before_install: -- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install -y libxkbfile-dev libsecret-1-dev; fi -- npm install -g yarn@1.12.3 -- npm install -g @coder/nbin script: - scripts/ci.bash before_deploy: diff --git a/main.js b/main.js index 765f41812..afb6a1d99 100644 --- a/main.js +++ b/main.js @@ -1 +1 @@ -require("../../bootstrap-amd").load("vs/server/cli"); +require("../../bootstrap-amd").load("vs/server/src/cli"); diff --git a/package.json b/package.json index d6d2f165a..e80fba063 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,30 @@ { "license": "MIT", "scripts": { - "preinstall": "cd ../../../ && yarn", - "postinstall": "rm -rf node_modules/@types/node # I keep getting type conflicts", - "start": "nodemon ../../../out/vs/server/main.js --watch ../../../out --verbose", - "watch": "cd ../../../ && yarn watch", + "ensure-in-vscode": "bash ./scripts/tasks.bash ensure-in-vscode", + "preinstall": "yarn ensure-in-vscode && cd ../../../ && yarn || true", + "postinstall": "rm -rf node_modules/@types/node", + "start": "yarn ensure-in-vscode && nodemon ../../../out/vs/server/main.js --watch ../../../out --verbose", + "watch": "yarn ensure-in-vscode && cd ../../../ && yarn watch", "build": "bash ./scripts/tasks.bash build", "package": "bash ./scripts/tasks.bash package", "vstar": "bash ./scripts/tasks.bash vstar", "binary": "bash ./scripts/tasks.bash binary", - "patch:generate": "cd ../../../ && git diff --staged > ./src/vs/server/scripts/vscode.patch", - "patch:apply": "cd ../../../ && git apply ./src/vs/server/scripts/vscode.patch" + "patch:generate": "yarn ensure-in-vscode && cd ../../../ && git diff --staged > ./src/vs/server/scripts/vscode.patch", + "patch:apply": "yarn ensure-in-vscode && cd ../../../ && git apply ./src/vs/server/scripts/vscode.patch" }, "devDependencies": { + "@types/pem": "^1.9.5", + "@types/safe-compare": "^1.1.0", "@types/tar-stream": "^1.6.1", "nodemon": "^1.19.1" }, - "dependencies": { - "tar-stream": "^2.1.0" - }, "resolutions": { "@types/node": "^10.12.12" + }, + "dependencies": { + "httpolyglot": "^0.1.2", + "pem": "^1.14.2", + "safe-compare": "^1.1.4" } } diff --git a/scripts/ci.bash b/scripts/ci.bash index cacc30d27..8a64f0c00 100755 --- a/scripts/ci.bash +++ b/scripts/ci.bash @@ -3,11 +3,21 @@ set -euo pipefail # Build using a Docker container. function docker-build() { + local image="codercom/nbin-${target}" + if [[ "${target}" == "linux" ]] ; then + image="codercom/nbin-centos" + fi + local containerId containerId=$(docker create --network=host --rm -it -v "$(pwd)"/.cache:/src/.cache "${image}") docker start "${containerId}" docker exec "${containerId}" mkdir -p /src + # TODO: temporary as long as we are rebuilding modules. + if [[ "${image}" == "codercom/nbin-alpine" ]] ; then + docker exec "${containerId}" apk add libxkbfile-dev libsecret-dev + fi + function docker-exec() { local command="${1}" ; shift local args="'${codeServerVersion}' '${vscodeVersion}' '${target}' '${arch}'" @@ -57,14 +67,6 @@ function main() { target=darwin local-build else - local image - if [[ "${target}" == alpine ]]; then - image=codercom/nbin-alpine - target=musl - else - image=codercom/nbin-centos - target=linux - fi docker-build fi } diff --git a/scripts/nbin-loader.js b/scripts/nbin-shim.js similarity index 100% rename from scripts/nbin-loader.js rename to scripts/nbin-shim.js diff --git a/scripts/tasks.bash b/scripts/tasks.bash index b1ee8cc1d..ed0ae546e 100755 --- a/scripts/tasks.bash +++ b/scripts/tasks.bash @@ -20,39 +20,55 @@ function exit-if-ci() { # Copy code-server into VS Code along with its dependencies. function copy-server() { + log "Applying patch" + cd "${vscodeSourcePath}" + git reset --hard + git clean -fd + git apply "${rootPath}/scripts/vscode.patch" + local serverPath="${vscodeSourcePath}/src/vs/server" rm -rf "${serverPath}" mkdir -p "${serverPath}" - log "Copying server code" + log "Copying code-server code" - cp "${rootPath}"/*.{ts,js} "${serverPath}" + cp -r "${rootPath}/src" "${serverPath}" + cp -r "${rootPath}/typings" "${serverPath}" + cp "${rootPath}/main.js" "${serverPath}" cp "${rootPath}/package.json" "${serverPath}" cp "${rootPath}/yarn.lock" "${serverPath}" if [[ -d "${rootPath}/node_modules" ]] ; then - log "Copying dependencies" + log "Copying code-server build dependencies" cp -r "${rootPath}/node_modules" "${serverPath}" else - log "Installing dependencies" + log "Installing code-server build dependencies" cd "${serverPath}" # Ignore scripts to avoid also installing VS Code dependencies which has # already been done. yarn --ignore-scripts rm -r node_modules/@types/node # I keep getting type conflicts fi + + # TODO: Duplicate identifier issue. There must be a better way to fix this. + if [[ "${target}" == "darwin" ]] ; then + rm "${serverPath}/node_modules/fsevents/node_modules/safe-buffer/index.d.ts" + fi } -# Prepend the nbin loading code which allows the code to find files within -# the binary. +# Prepend the nbin shim which enables finding files within the binary. function prepend-loader() { local filePath="${codeServerBuildPath}/${1}" ; shift - cat "${rootPath}/scripts/nbin-loader.js" "${filePath}" > "${filePath}.temp" + cat "${rootPath}/scripts/nbin-shim.js" "${filePath}" > "${filePath}.temp" mv "${filePath}.temp" "${filePath}" # Using : as the delimiter so the escaping here is easier to read. # ${parameter/pattern/string}, so the pattern is /: (if the pattern starts # with / it matches all instances) and the string is \\: (results in \:). - sed -i "s:{{ROOT_PATH}}:${codeServerBuildPath//:/\\:}:g" "${filePath}" + if [[ "${target}" == "darwin" ]] ; then + sed -i "" -e "s:{{ROOT_PATH}}:${codeServerBuildPath//:/\\:}:g" "${filePath}" + else + sed -i "s:{{ROOT_PATH}}:${codeServerBuildPath//:/\\:}:g" "${filePath}" + fi } # Copy code-server into VS Code then build it. @@ -63,10 +79,7 @@ function build-code-server() { # (basically just want to skip extensions, target our server code, and get # the same type of build you get with the vscode-linux-x64-min task). # Something like: yarn gulp "vscode-server-${target}-${arch}-min" - cd "${vscodeSourcePath}" - git reset --hard - git clean -fd - git apply "${rootPath}/scripts/vscode.patch" + log "Building code-server" yarn gulp compile-client rm -rf "${codeServerBuildPath}" @@ -78,9 +91,22 @@ function build-code-server() { node "${rootPath}/scripts/merge.js" "${vscodeBuildPath}/resources/app/package.json" "${rootPath}/scripts/package.json" "${codeServerBuildPath}/package.json" "${json}" node "${rootPath}/scripts/merge.js" "${vscodeBuildPath}/resources/app/product.json" "${rootPath}/scripts/product.json" "${codeServerBuildPath}/product.json" cp -r "${vscodeSourcePath}/out" "${codeServerBuildPath}" - rm -rf "${codeServerBuildPath}/out/vs/server/node_modules" + rm -rf "${codeServerBuildPath}/out/vs/server/typings" + + # Rebuild to make sure the native modules work since at the moment all the + # pre-built packages are from one Linux system. This means you must build on + # the target system. + log "Installing remote dependencies" + cd "${vscodeSourcePath}/remote" + if [[ "${target}" != "linux" ]] ; then + yarn --production --force + fi cp -r "${vscodeSourcePath}/remote/node_modules" "${codeServerBuildPath}" + # Only keep the production dependencies. + cd "${codeServerBuildPath}/out/vs/server" + yarn --production --ignore-scripts + prepend-loader "out/vs/server/main.js" prepend-loader "out/bootstrap-fork.js" @@ -105,11 +131,9 @@ function build-vscode() { if [[ ! -d "${vscodeSourcePath}/node_modules" ]] ; then exit-if-ci log "Installing VS Code dependencies" - yarn - # Not entirely sure why but there seem to be problems with native modules. - # Also vscode-ripgrep keeps complaining after the rebuild that the - # node_modules directory doesn't exist, so we're ignoring that for now. - npm rebuild || true + # Not entirely sure why but there seem to be problems with native modules + # so rebuild them. + yarn --force # Keep just what we need to keep the pre-built archive smaller. rm -rf "${vscodeSourcePath}/test" @@ -138,7 +162,7 @@ function download-vscode() { cd "${buildPath}" if command -v wget &> /dev/null ; then log "Attempting to download ${tarName} with wget" - wget "${vsSourceUrl}" --quiet + wget "${vsSourceUrl}" --quiet --output-document "${tarName}" else log "Attempting to download ${tarName} with curl" curl "${vsSourceUrl}" --silent --fail --output "${tarName}" @@ -152,13 +176,20 @@ function download-vscode() { function prepare-vscode() { if [[ ! -d "${vscodeBuildPath}" || ! -d "${vscodeSourcePath}" ]] ; then mkdir -p "${buildPath}" + # TODO: for now everything uses the Linux build and we rebuild the modules. + # This means you must build on the target system. local tarName="vstar-${vscodeVersion}-${target}-${arch}.tar.gz" - local vsSourceUrl="https://codesrv-ci.cdr.sh/${tarName}" + local linuxTarName="vstar-${vscodeVersion}-linux-${arch}.tar.gz" + local linuxVscodeBuildName="vscode-${vscodeVersion}-linux-${arch}-built" + local vsSourceUrl="https://codesrv-ci.cdr.sh/${linuxTarName}" if download-vscode ; then cd "${buildPath}" rm -rf "${vscodeBuildPath}" tar -xzf "${tarName}" rm "${tarName}" + if [[ "${target}" != "linux" ]] ; then + mv "${linuxVscodeBuildName}" "${vscodeBuildName}" + fi elif [[ -n "${ci}" ]] ; then log "Pre-built VS Code ${vscodeVersion}-${target}-${arch} does not exist" "error" exit 1 @@ -199,12 +230,12 @@ function package-task() { cp "${vscodeSourcePath}/ThirdPartyNotices.txt" "${archivePath}" cd "${releasePath}" - if [[ "${target}" == "linux" ]] ; then - tar -czf "${binaryName}.tar.gz" "${binaryName}" - log "Archive: ${archivePath}.tar.gz" - else + if [[ "${target}" == "darwin" ]] ; then zip -r "${binaryName}.zip" "${binaryName}" log "Archive: ${archivePath}.zip" + else + tar -czf "${binaryName}.tar.gz" "${binaryName}" + log "Archive: ${archivePath}.tar.gz" fi } @@ -221,19 +252,54 @@ function binary-task() { log "Binary: ${buildPath}/${binaryName}" } +# Check if it looks like we are inside VS Code. +function in-vscode () { + log "Checking if we are inside VS Code" + local dir="${1}" ; shift + + local maybeVscode + local dirName + maybeVscode="$(realpath "${dir}/../../..")" + dirName="$(basename "${maybeVscode}")" + + if [[ "${dirName}" != "vscode" ]] ; then + return 1 + fi + if [[ ! -f "${maybeVscode}/package.json" ]] ; then + return 1 + fi + if ! grep '"name": "code-oss-dev"' "${maybeVscode}/package.json" --quiet ; then + return 1 + fi + + return 0 +} + +function ensure-in-vscode-task() { + if ! in-vscode "${rootPath}"; then + log "Not in vscode" "error" + exit 1 + fi + exit 0 +} + function main() { + local relativeRootPath + local rootPath + relativeRootPath="$(dirname "${0}")/.." + rootPath="$(realpath "${relativeRootPath}")" + local task="${1}" ; shift + if [[ "${task}" == "ensure-in-vscode" ]] ; then + ensure-in-vscode-task + fi + local codeServerVersion="${1}" ; shift local vscodeVersion="${1}" ; shift local target="${1}" ; shift local arch="${1}" ; shift local ci="${CI:-}" - local relativeRootPath - local rootPath - relativeRootPath="$(dirname "${0}")/.." - rootPath="$(realpath "${relativeRootPath}")" - # This lets you build in a separate directory since building within this # directory while developing makes it hard to keep developing since compiling # will compile everything in the build directory as well. @@ -241,15 +307,9 @@ function main() { # If we're inside a vscode directory, assume we want to develop. In that case # we should set an OUT directory and not build in this directory. - if [[ "${outPath}" == "${rootPath}" ]] ; then - local maybeVscode - local dirName - maybeVscode="$(realpath "${outPath}/../../..")" - dirName="$(basename "${maybeVscode}")" - if [[ "${dirName}" == "vscode" ]] ; then - log "Set the OUT environment variable to something outside ${maybeVscode}" "error" - exit 1 - fi + if in-vscode "${outPath}" ; then + log "Set the OUT environment variable to something outside of VS Code" "error" + exit 1 fi local releasePath="${outPath}/release" diff --git a/channel.ts b/src/channel.ts similarity index 95% rename from channel.ts rename to src/channel.ts index eb50a340c..b463e5f0c 100644 --- a/channel.ts +++ b/src/channel.ts @@ -6,7 +6,7 @@ import { Emitter, Event } from "vs/base/common/event"; import { IDisposable } from "vs/base/common/lifecycle"; import { OS } from "vs/base/common/platform"; import { URI, UriComponents } from "vs/base/common/uri"; -import { URITransformer, IRawURITransformer, transformOutgoingURIs } from "vs/base/common/uriIpc"; +import { transformOutgoingURIs } from "vs/base/common/uriIpc"; import { IServerChannel } from "vs/base/parts/ipc/common/ipc"; import { IDiagnosticInfo } from "vs/platform/diagnostics/common/diagnosticsService"; import { IEnvironmentService } from "vs/platform/environment/common/environment"; @@ -19,6 +19,8 @@ import { IRemoteAgentEnvironment } from "vs/platform/remote/common/remoteAgentEn import { ExtensionScanner, ExtensionScannerInput } from "vs/workbench/services/extensions/node/extensionPoints"; import { DiskFileSystemProvider } from "vs/workbench/services/files/node/diskFileSystemProvider"; +import { getUriTransformer } from "vs/server/src/util"; + /** * Extend the file provider to allow unwatching. */ @@ -262,11 +264,3 @@ export class ExtensionEnvironmentChannel implements IServerChannel { throw new Error("not implemented"); } } - -export const uriTransformerPath = getPathFromAmdModule(require, "vs/server/uriTransformer"); - -export const getUriTransformer = (remoteAuthority: string): URITransformer => { - const rawURITransformerFactory = require.__$__nodeRequire(uriTransformerPath); - const rawURITransformer = rawURITransformerFactory(remoteAuthority); - return new URITransformer(rawURITransformer); -}; diff --git a/cli.ts b/src/cli.ts similarity index 79% rename from cli.ts rename to src/cli.ts index 270c58f42..c94cb717d 100644 --- a/cli.ts +++ b/src/cli.ts @@ -1,12 +1,15 @@ import * as os from "os"; + import { validatePaths } from "vs/code/node/paths"; import { parseMainProcessArgv } from "vs/platform/environment/node/argvHelper"; import { ParsedArgs } from "vs/platform/environment/common/environment"; import { buildHelpMessage, buildVersionMessage, options } from "vs/platform/environment/node/argv"; import product from "vs/platform/product/node/product"; import pkg from "vs/platform/product/node/package"; -import { MainServer, WebviewServer } from "vs/server/server"; -import "vs/server/tar"; + +import { MainServer, WebviewServer } from "vs/server/src/server"; +import "vs/server/src/tar"; +import { generateCertificate } from "vs/server/src/util"; interface Args extends ParsedArgs { "allow-http"?: boolean; @@ -111,14 +114,41 @@ const main = async (): Promise => { return process.exit(0); } - const webviewServer = new WebviewServer(typeof args["webview-port"] !== "undefined" && parseInt(args["webview-port"], 10) || 8444); - const server = new MainServer(typeof args.port !== "undefined" && parseInt(args.port, 10) || 8443, webviewServer, args); + const options = { + host: args["host"] + || (args["no-auth"] || args["allow-http"] ? "localhost" : "0.0.0.0"), + allowHttp: args["allow-http"], + cert: args["cert"], + certKey: args["cert"], + }; + + if (!options.allowHttp && (!options.cert || !options.certKey)) { + const { cert, certKey } = await generateCertificate(); + options.cert = cert; + options.certKey = certKey; + } + + const webviewPort = typeof args["webview-port"] !== "undefined" + && parseInt(args["webview-port"], 10) || 8444; + const webviewServer = new WebviewServer({ + ...options, + port: webviewPort, + socket: args["webview-socket"], + }); + + const port = typeof args.port !== "undefined" && parseInt(args.port, 10) || 8443; + const server = new MainServer({ + ...options, + port, + socket: args.socket, + }, webviewServer, args); + const [webviewAddress, serverAddress] = await Promise.all([ webviewServer.listen(), server.listen() ]); - console.log(`Main server serving ${serverAddress}`); - console.log(`Webview server serving ${webviewAddress}`); + console.log(`Main server listening on ${serverAddress}`); + console.log(`Webview server listening on ${webviewAddress}`); }; main().catch((error) => { diff --git a/connection.ts b/src/connection.ts similarity index 96% rename from connection.ts rename to src/connection.ts index 8d1d7c2ea..3c29a97c3 100644 --- a/connection.ts +++ b/src/connection.ts @@ -6,10 +6,10 @@ import { Emitter } from "vs/base/common/event"; import { ISocket } from "vs/base/parts/ipc/common/ipc.net"; import { NodeSocket, WebSocketNodeSocket } from "vs/base/parts/ipc/node/ipc.net"; import { ILogService } from "vs/platform/log/common/log"; -import { uriTransformerPath } from "vs/server/channel"; import { IExtHostReadyMessage, IExtHostSocketMessage } from "vs/workbench/services/extensions/common/extensionHostProtocol"; -import { Protocol } from "vs/server/protocol"; +import { Protocol } from "vs/server/src/protocol"; +import { uriTransformerPath } from "vs/server/src/util"; export abstract class Connection { private readonly _onClose = new Emitter(); @@ -126,7 +126,7 @@ export class ExtensionHostConnection extends Connection { getPathFromAmdModule(require, "bootstrap-fork"), [ "--type=extensionHost", - `--uriTransformerPath=${uriTransformerPath}` + `--uriTransformerPath=${uriTransformerPath()}` ], { env: { diff --git a/insights.ts b/src/insights.ts similarity index 100% rename from insights.ts rename to src/insights.ts diff --git a/protocol.ts b/src/protocol.ts similarity index 100% rename from protocol.ts rename to src/protocol.ts diff --git a/server.ts b/src/server.ts similarity index 83% rename from server.ts rename to src/server.ts index 349d8e08f..0f134dddc 100644 --- a/server.ts +++ b/src/server.ts @@ -1,7 +1,9 @@ import * as fs from "fs"; import * as http from "http"; +import * as https from "https"; import * as net from "net"; import * as path from "path"; +import * as tls from "tls"; import * as util from "util"; import * as url from "url"; @@ -44,9 +46,10 @@ import { RemoteExtensionLogFileName } from "vs/workbench/services/remote/common/ // import { TelemetryService } from "vs/workbench/services/telemetry/electron-browser/telemetryService"; import { IWorkbenchConstructionOptions } from "vs/workbench/workbench.web.api"; -import { Connection, ManagementConnection, ExtensionHostConnection } from "vs/server/connection"; -import { ExtensionEnvironmentChannel, FileProviderChannel, getUriTransformer } from "vs/server/channel"; -import { Protocol } from "vs/server/protocol"; +import { Connection, ManagementConnection, ExtensionHostConnection } from "vs/server/src/connection"; +import { ExtensionEnvironmentChannel, FileProviderChannel , } from "vs/server/src/channel"; +import { Protocol } from "vs/server/src/protocol"; +import { getUriTransformer, useHttpsTransformer } from "vs/server/src/util"; export enum HttpCode { Ok = 200, @@ -76,71 +79,51 @@ export class HttpError extends Error { } } +export interface ServerOptions { + readonly port: number; + readonly host: string; + readonly socket?: string; + readonly allowHttp?: boolean; + readonly cert?: string; + readonly certKey?: string; +} + export abstract class Server { // The underlying web server. - protected readonly server: http.Server; + protected readonly server: http.Server | https.Server; protected rootPath = path.resolve(__dirname, "../../.."); private listenPromise: Promise | undefined; - public constructor(private readonly port: number) { - this.server = http.createServer(async (request, response): Promise => { - try { - if (request.method !== "GET") { - throw new HttpError( - `Unsupported method ${request.method}`, - HttpCode.BadRequest, - ); - } - - const parsedUrl = url.parse(request.url || "", true); - - const fullPath = decodeURIComponent(parsedUrl.pathname || "/"); - const match = fullPath.match(/^(\/?[^/]*)(.*)$/); - const [, base, requestPath] = match - ? match.map((p) => p !== "/" ? p.replace(/\/$/, "") : p) - : ["", "", ""]; - - const { content, headers, code } = await this.handleRequest( - base, requestPath, parsedUrl, request, - ); - response.writeHead(code || HttpCode.Ok, { - "Cache-Control": "max-age=86400", - // TODO: ETag? - ...headers, - }); - response.end(content); - } catch (error) { - if (error.code === "ENOENT" || error.code === "EISDIR") { - error = new HttpError("Not found", HttpCode.NotFound); - } - response.writeHead(typeof error.code === "number" ? error.code : 500); - response.end(error.message); - } - }); + public constructor(private readonly options: ServerOptions) { + if (this.options.cert && this.options.certKey) { + useHttpsTransformer(); + const httpolyglot = require.__$__nodeRequire(path.resolve(__dirname, "../node_modules/httpolyglot/lib/index")) as typeof import("httpolyglot"); + this.server = httpolyglot.createServer({ + cert: fs.readFileSync(this.options.cert), + key: fs.readFileSync(this.options.certKey), + }, this.onRequest); + } else { + this.server = http.createServer(this.onRequest); + } } public listen(): Promise { if (!this.listenPromise) { this.listenPromise = new Promise((resolve, reject) => { this.server.on("error", reject); - this.server.listen(this.port, () => { - resolve(this.address()); - }); + const onListen = () => resolve(this.address(this.server, this.options.allowHttp)); + if (this.options.socket) { + this.server.listen(this.options.socket, onListen); + } else { + this.server.listen(this.options.port, this.options.host, onListen); + } }); } return this.listenPromise; } - public address(): string { - const address = this.server.address(); - const endpoint = typeof address !== "string" - ? ((address.address === "::" ? "localhost" : address.address) + ":" + address.port) - : address; - return `http://${endpoint}`; - } - protected abstract handleRequest( base: string, requestPath: string, @@ -162,6 +145,57 @@ export abstract class Server { }, }; } + + private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise => { + const secure = (request.connection as tls.TLSSocket).encrypted; + if (!this.options.allowHttp && !secure) { + response.writeHead(302, { + Location: "https://" + request.headers.host + request.url, + }); + return response.end(); + } + + try { + if (request.method !== "GET") { + throw new HttpError( + `Unsupported method ${request.method}`, + HttpCode.BadRequest, + ); + } + + const parsedUrl = url.parse(request.url || "", true); + + const fullPath = decodeURIComponent(parsedUrl.pathname || "/"); + const match = fullPath.match(/^(\/?[^/]*)(.*)$/); + const [, base, requestPath] = match + ? match.map((p) => p !== "/" ? p.replace(/\/$/, "") : p) + : ["", "", ""]; + + const { content, headers, code } = await this.handleRequest( + base, requestPath, parsedUrl, request, + ); + response.writeHead(code || HttpCode.Ok, { + "Cache-Control": "max-age=86400", + // TODO: ETag? + ...headers, + }); + response.end(content); + } catch (error) { + if (error.code === "ENOENT" || error.code === "EISDIR") { + error = new HttpError("Not found", HttpCode.NotFound); + } + response.writeHead(typeof error.code === "number" ? error.code : 500); + response.end(error.message); + } + } + + private address(server: net.Server, http?: boolean): string { + const address = server.address(); + const endpoint = typeof address !== "string" + ? ((address.address === "::" ? "localhost" : address.address) + ":" + address.port) + : address; + return `${http ? "http" : "https"}://${endpoint}`; + } } export class MainServer extends Server { @@ -179,11 +213,11 @@ export class MainServer extends Server { private readonly services = new ServiceCollection(); public constructor( - port: number, + options: ServerOptions, private readonly webviewServer: WebviewServer, args: ParsedArgs, ) { - super(port); + super(options); this.server.on("upgrade", async (request, socket) => { const protocol = this.createProtocol(request, socket); diff --git a/tar.ts b/src/tar.ts similarity index 99% rename from tar.ts rename to src/tar.ts index b513e145a..b8030143f 100644 --- a/tar.ts +++ b/src/tar.ts @@ -1,9 +1,10 @@ -import * as nls from "vs/nls"; -import * as vszip from "vs/base/node/zip"; import * as fs from "fs"; import * as path from "path"; import * as tarStream from "tar-stream"; import { promisify } from "util"; + +import * as nls from "vs/nls"; +import * as vszip from "vs/base/node/zip"; import { CancellationToken } from "vs/base/common/cancellation"; import { mkdirp } from "vs/base/node/pfs"; diff --git a/upload.ts b/src/upload.ts similarity index 100% rename from upload.ts rename to src/upload.ts diff --git a/uriTransformer.js b/src/uriTransformerHttp.js similarity index 100% rename from uriTransformer.js rename to src/uriTransformerHttp.js diff --git a/src/uriTransformerHttps.js b/src/uriTransformerHttps.js new file mode 100644 index 000000000..bddfe6465 --- /dev/null +++ b/src/uriTransformerHttps.js @@ -0,0 +1,3 @@ +module.exports = (remoteAuthority) => { + return require("./uriTransformerHttp")(remoteAuthority, true); +}; diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 000000000..cec97b41c --- /dev/null +++ b/src/util.ts @@ -0,0 +1,60 @@ +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import * as util from "util"; + +import { getPathFromAmdModule } from "vs/base/common/amd"; +import { URITransformer, IRawURITransformer } from "vs/base/common/uriIpc"; +import { mkdirp } from "vs/base/node/pfs"; + +export const tmpdir = path.join(os.tmpdir(), "code-server"); + +export const generateCertificate = async (): Promise<{ cert: string, certKey: string }> => { + const paths = { + cert: path.join(tmpdir, "self-signed.cert"), + certKey: path.join(tmpdir, "self-signed.key"), + }; + + const exists = await Promise.all([ + util.promisify(fs.exists)(paths.cert), + util.promisify(fs.exists)(paths.certKey), + ]); + + await mkdirp(tmpdir); + + if (!exists[0] || !exists[1]) { + const pem = require.__$__nodeRequire(path.resolve(__dirname, "../node_modules/pem/lib/pem")) as typeof import("pem"); + const certs = await new Promise((resolve, reject): void => { + pem.createCertificate({ selfSigned: true }, (error, result) => { + if (error) { + return reject(error); + } + resolve(result); + }); + }); + await Promise.all([ + util.promisify(fs.writeFile)(paths.cert, certs.certificate), + util.promisify(fs.writeFile)(paths.certKey, certs.serviceKey), + ]); + } + + return paths; +}; + +let secure: boolean; +export const useHttpsTransformer = (): void => { + secure = true; +}; + +export const uriTransformerPath = (): string => { + return getPathFromAmdModule( + require, + "vs/server/src/uriTransformerHttp" + (secure ? "s": ""), + ); +}; + +export const getUriTransformer = (remoteAuthority: string): URITransformer => { + const rawURITransformerFactory = require.__$__nodeRequire(uriTransformerPath()); + const rawURITransformer = rawURITransformerFactory(remoteAuthority); + return new URITransformer(rawURITransformer); +}; diff --git a/typings/httpolyglot.d.ts b/typings/httpolyglot.d.ts new file mode 100644 index 000000000..aeb2fc05a --- /dev/null +++ b/typings/httpolyglot.d.ts @@ -0,0 +1,7 @@ +declare module "httpolyglot" { + import * as http from "http"; + import * as https from "https"; + + function createServer(requestListener?: (req: http.IncomingMessage, res: http.ServerResponse) => void): http.Server; + function createServer(options: https.ServerOptions, requestListener?: (req: http.IncomingMessage, res: http.ServerResponse) => void): https.Server; +} diff --git a/uriTransformerHttps.js b/uriTransformerHttps.js deleted file mode 100644 index 3b4713ba0..000000000 --- a/uriTransformerHttps.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = (remoteAuthority) => { - return require("./uriTransformer")(remoteAuthority, true); -}; diff --git a/yarn.lock b/yarn.lock index 9acdbbaa8..1284158c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,18 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.12.tgz#0eec3155a46e6c4db1f27c3e588a205f767d622f" integrity sha512-QcAKpaO6nhHLlxWBvpc4WeLrTvPqlHOvaj0s5GriKkA1zq+bsFBPpfYCvQhLqLgYlIko8A9YrPdaMHCo5mBcpg== +"@types/pem@^1.9.5": + version "1.9.5" + resolved "https://registry.yarnpkg.com/@types/pem/-/pem-1.9.5.tgz#cd5548b5e0acb4b41a9e21067e9fcd8c57089c99" + integrity sha512-C0txxEw8B7DCoD85Ko7SEvzUogNd5VDJ5/YBG8XUcacsOGqxr5Oo4g3OUAfdEDUbhXanwUoVh/ZkMFw77FGPQQ== + dependencies: + "@types/node" "*" + +"@types/safe-compare@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/safe-compare/-/safe-compare-1.1.0.tgz#47ed9b9ca51a3a791b431cd59b28f47fa9bf1224" + integrity sha512-1ri+LJhh0gRxIa37IpGytdaW7yDEHeJniBSMD1BmitS07R1j63brcYCzry+l0WJvGdEKQNQ7DYXO2epgborWPw== + "@types/tar-stream@^1.6.1": version "1.6.1" resolved "https://registry.yarnpkg.com/@types/tar-stream/-/tar-stream-1.6.1.tgz#67d759068ff781d976cad978893bb7a334ec8809" @@ -122,13 +134,6 @@ binary-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.12.0.tgz#c2d780f53d45bba8317a8902d4ceeaf3a6385b14" integrity sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg== -bl@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88" - integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A== - dependencies: - readable-stream "^3.0.1" - boxen@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" @@ -166,6 +171,24 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" +buffer-alloc-unsafe@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" + integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== + +buffer-alloc@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" + integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== + dependencies: + buffer-alloc-unsafe "^1.1.0" + buffer-fill "^1.0.0" + +buffer-fill@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" + integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -200,6 +223,11 @@ chalk@^2.0.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +charenc@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + chokidar@^2.1.5: version "2.1.6" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.6.tgz#b6cad653a929e244ce8a834244164d241fa954c5" @@ -322,6 +350,11 @@ cross-spawn@^5.0.1: shebang-command "^1.2.0" which "^1.2.9" +crypt@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + crypto-random-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" @@ -395,12 +428,10 @@ duplexer3@^0.1.4: resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= -end-of-stream@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" - integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== - dependencies: - once "^1.4.0" +es6-promisify@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-6.0.1.tgz#6edaa45f3bd570ffe08febce66f7116be4b1cdb6" + integrity sha512-J3ZkwbEnnO+fGAKrjVpeUAnZshAdfZvbhQpqfIH9kSAspReRC4nJnu8ewm55b4y9ElyeuhCTzJD0XiH8Tsbhlw== escape-string-regexp@^1.0.5: version "1.0.5" @@ -484,11 +515,6 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - fs-minipass@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" @@ -623,6 +649,11 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" +httpolyglot@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/httpolyglot/-/httpolyglot-0.1.2.tgz#e4d347fe8984a62f467d4060df527f1851f6997b" + integrity sha1-5NNH/omEpi9GfUBg31J/GFH2mXs= + iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -691,7 +722,7 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" -is-buffer@^1.1.5: +is-buffer@^1.1.5, is-buffer@~1.1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -922,6 +953,15 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +md5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" + integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk= + dependencies: + charenc "~0.0.1" + crypt "~0.0.1" + is-buffer "~1.1.1" + micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -1151,7 +1191,7 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -once@^1.3.0, once@^1.4.0: +once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -1163,7 +1203,7 @@ os-homedir@^1.0.0: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= -os-tmpdir@^1.0.0: +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= @@ -1216,6 +1256,16 @@ path-key@^2.0.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +pem@^1.14.2: + version "1.14.2" + resolved "https://registry.yarnpkg.com/pem/-/pem-1.14.2.tgz#ab29350416bc3a532c30beeee0d541af897fb9ac" + integrity sha512-TOnPtq3ZFnCniOZ+rka4pk8UIze9xG1qI+wNE7EmkiR/cg+53uVvk5QbkWZ7M6RsuOxzz62FW1hlAobJr/lTOA== + dependencies: + es6-promisify "^6.0.0" + md5 "^2.2.1" + os-tmpdir "^1.0.1" + which "^1.3.1" + pify@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" @@ -1269,15 +1319,6 @@ readable-stream@^2.0.2, readable-stream@^2.0.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.1, readable-stream@^3.1.1: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" - integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - readdirp@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" @@ -1347,6 +1388,13 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-compare@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/safe-compare/-/safe-compare-1.1.4.tgz#5e0128538a82820e2e9250cd78e45da6786ba593" + integrity sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ== + dependencies: + buffer-alloc "^1.2.0" + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" @@ -1506,13 +1554,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string_decoder@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" - integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w== - dependencies: - safe-buffer "~5.1.0" - string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -1551,17 +1592,6 @@ supports-color@^5.2.0, supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -tar-stream@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.0.tgz#d1aaa3661f05b38b5acc9b7020efdca5179a2cc3" - integrity sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw== - dependencies: - bl "^3.0.0" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - tar@^4: version "4.4.8" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d" @@ -1694,12 +1724,12 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -which@^1.2.9: +which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==