commit
5aded14b87
29
doc/FAQ.md
29
doc/FAQ.md
|
@ -65,6 +65,35 @@ only to HTTP requests.
|
||||||
You can use [Let's Encrypt](https://letsencrypt.org/) to get an SSL certificate
|
You can use [Let's Encrypt](https://letsencrypt.org/) to get an SSL certificate
|
||||||
for free.
|
for free.
|
||||||
|
|
||||||
|
## How do I securely access web services?
|
||||||
|
|
||||||
|
code-server is capable of proxying to any port using either a subdomain or a
|
||||||
|
subpath which means you can securely access these services using code-server's
|
||||||
|
built-in authentication.
|
||||||
|
|
||||||
|
### Sub-domains
|
||||||
|
|
||||||
|
You will need a DNS entry that points to your server for each port you want to
|
||||||
|
access. You can either set up a wildcard DNS entry for `*.<domain>` if your domain
|
||||||
|
name registrar supports it or you can create one for every port you want to
|
||||||
|
access (`3000.<domain>`, `8080.<domain>`, etc).
|
||||||
|
|
||||||
|
You should also set up TLS certificates for these subdomains, either using a
|
||||||
|
wildcard certificate for `*.<domain>` or individual certificates for each port.
|
||||||
|
|
||||||
|
Start code-server with the `--proxy-domain` flag set to your domain.
|
||||||
|
|
||||||
|
```
|
||||||
|
code-server --proxy-domain <domain>
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can browse to `<port>.<domain>`. Note that this uses the host header so
|
||||||
|
ensure your reverse proxy forwards that information if you are using one.
|
||||||
|
|
||||||
|
### Sub-paths
|
||||||
|
|
||||||
|
Just browse to `/proxy/<port>/`.
|
||||||
|
|
||||||
## x86 releases?
|
## x86 releases?
|
||||||
|
|
||||||
node has dropped support for x86 and so we decided to as well. See
|
node has dropped support for x86 and so we decided to as well. See
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/adm-zip": "^0.4.32",
|
"@types/adm-zip": "^0.4.32",
|
||||||
"@types/fs-extra": "^8.0.1",
|
"@types/fs-extra": "^8.0.1",
|
||||||
|
"@types/http-proxy": "^1.17.4",
|
||||||
"@types/mocha": "^5.2.7",
|
"@types/mocha": "^5.2.7",
|
||||||
"@types/node": "^12.12.7",
|
"@types/node": "^12.12.7",
|
||||||
"@types/parcel-bundler": "^1.12.1",
|
"@types/parcel-bundler": "^1.12.1",
|
||||||
|
@ -52,13 +53,14 @@
|
||||||
"@coder/logger": "1.1.11",
|
"@coder/logger": "1.1.11",
|
||||||
"adm-zip": "^0.4.14",
|
"adm-zip": "^0.4.14",
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^8.1.0",
|
||||||
|
"http-proxy": "^1.18.0",
|
||||||
"httpolyglot": "^0.1.2",
|
"httpolyglot": "^0.1.2",
|
||||||
"node-pty": "^0.9.0",
|
"node-pty": "^0.9.0",
|
||||||
"pem": "^1.14.2",
|
"pem": "^1.14.2",
|
||||||
"safe-compare": "^1.1.4",
|
"safe-compare": "^1.1.4",
|
||||||
"semver": "^7.1.3",
|
"semver": "^7.1.3",
|
||||||
"tar": "^6.0.1",
|
|
||||||
"ssh2": "^0.8.7",
|
"ssh2": "^0.8.7",
|
||||||
|
"tar": "^6.0.1",
|
||||||
"tar-fs": "^2.0.0",
|
"tar-fs": "^2.0.0",
|
||||||
"ws": "^7.2.0"
|
"ws": "^7.2.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
|
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
|
||||||
crossorigin="use-credentials"
|
crossorigin="use-credentials"
|
||||||
/>
|
/>
|
||||||
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.pnggg" />
|
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
|
||||||
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
|
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
|
||||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -43,7 +43,7 @@ export class ApiHttpProvider extends HttpProvider {
|
||||||
|
|
||||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||||
this.ensureAuthenticated(request)
|
this.ensureAuthenticated(request)
|
||||||
if (route.requestPath !== "/index.html") {
|
if (!this.isRoot(route)) {
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
throw new HttpError("Not found", HttpCode.NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ export class DashboardHttpProvider extends HttpProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||||
if (route.requestPath !== "/index.html") {
|
if (!this.isRoot(route)) {
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
throw new HttpError("Not found", HttpCode.NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ interface LoginPayload {
|
||||||
*/
|
*/
|
||||||
export class LoginHttpProvider extends HttpProvider {
|
export class LoginHttpProvider extends HttpProvider {
|
||||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||||
if (this.options.auth !== AuthType.Password || route.requestPath !== "/index.html") {
|
if (this.options.auth !== AuthType.Password || !this.isRoot(route)) {
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
throw new HttpError("Not found", HttpCode.NotFound)
|
||||||
}
|
}
|
||||||
switch (route.base) {
|
switch (route.base) {
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import * as http from "http"
|
||||||
|
import { HttpCode, HttpError } from "../../common/http"
|
||||||
|
import { HttpProvider, HttpResponse, Route, WsResponse } from "../http"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy HTTP provider.
|
||||||
|
*/
|
||||||
|
export class ProxyHttpProvider extends HttpProvider {
|
||||||
|
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||||
|
if (!this.authenticated(request)) {
|
||||||
|
if (this.isRoot(route)) {
|
||||||
|
return { redirect: "/login", query: { to: route.fullPath } }
|
||||||
|
}
|
||||||
|
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure there is a trailing slash so relative paths work correctly.
|
||||||
|
if (this.isRoot(route) && !route.fullPath.endsWith("/")) {
|
||||||
|
return {
|
||||||
|
redirect: `${route.fullPath}/`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = route.base.replace(/^\//, "")
|
||||||
|
return {
|
||||||
|
proxy: {
|
||||||
|
base: `${this.options.base}/${port}`,
|
||||||
|
port,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handleWebSocket(route: Route, request: http.IncomingMessage): Promise<WsResponse> {
|
||||||
|
this.ensureAuthenticated(request)
|
||||||
|
const port = route.base.replace(/^\//, "")
|
||||||
|
return {
|
||||||
|
proxy: {
|
||||||
|
base: `${this.options.base}/${port}`,
|
||||||
|
port,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -61,7 +61,7 @@ export class UpdateHttpProvider extends HttpProvider {
|
||||||
this.ensureAuthenticated(request)
|
this.ensureAuthenticated(request)
|
||||||
this.ensureMethod(request)
|
this.ensureMethod(request)
|
||||||
|
|
||||||
if (route.requestPath !== "/index.html") {
|
if (!this.isRoot(route)) {
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
throw new HttpError("Not found", HttpCode.NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -126,7 +126,7 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||||
|
|
||||||
switch (route.base) {
|
switch (route.base) {
|
||||||
case "/":
|
case "/":
|
||||||
if (route.requestPath !== "/index.html") {
|
if (!this.isRoot(route)) {
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
throw new HttpError("Not found", HttpCode.NotFound)
|
||||||
} else if (!this.authenticated(request)) {
|
} else if (!this.authenticated(request)) {
|
||||||
return { redirect: "/login", query: { to: this.options.base } }
|
return { redirect: "/login", query: { to: this.options.base } }
|
||||||
|
|
|
@ -39,6 +39,7 @@ export interface Args extends VsArgs {
|
||||||
readonly "install-extension"?: string[]
|
readonly "install-extension"?: string[]
|
||||||
readonly "show-versions"?: boolean
|
readonly "show-versions"?: boolean
|
||||||
readonly "uninstall-extension"?: string[]
|
readonly "uninstall-extension"?: string[]
|
||||||
|
readonly "proxy-domain"?: string[]
|
||||||
readonly locale?: string
|
readonly locale?: string
|
||||||
readonly _: string[]
|
readonly _: string[]
|
||||||
}
|
}
|
||||||
|
@ -111,6 +112,7 @@ const options: Options<Required<Args>> = {
|
||||||
"install-extension": { type: "string[]", description: "Install or update a VS Code extension by id or vsix." },
|
"install-extension": { type: "string[]", description: "Install or update a VS Code extension by id or vsix." },
|
||||||
"uninstall-extension": { type: "string[]", description: "Uninstall a VS Code extension by id." },
|
"uninstall-extension": { type: "string[]", description: "Uninstall a VS Code extension by id." },
|
||||||
"show-versions": { type: "boolean", description: "Show VS Code extension versions." },
|
"show-versions": { type: "boolean", description: "Show VS Code extension versions." },
|
||||||
|
"proxy-domain": { type: "string[]", description: "Domain used for proxying ports." },
|
||||||
|
|
||||||
locale: { type: "string" },
|
locale: { type: "string" },
|
||||||
log: { type: LogLevel },
|
log: { type: LogLevel },
|
||||||
|
|
|
@ -5,11 +5,12 @@ import { CliMessage } from "../../lib/vscode/src/vs/server/ipc"
|
||||||
import { ApiHttpProvider } from "./app/api"
|
import { ApiHttpProvider } from "./app/api"
|
||||||
import { DashboardHttpProvider } from "./app/dashboard"
|
import { DashboardHttpProvider } from "./app/dashboard"
|
||||||
import { LoginHttpProvider } from "./app/login"
|
import { LoginHttpProvider } from "./app/login"
|
||||||
|
import { ProxyHttpProvider } from "./app/proxy"
|
||||||
import { StaticHttpProvider } from "./app/static"
|
import { StaticHttpProvider } from "./app/static"
|
||||||
import { UpdateHttpProvider } from "./app/update"
|
import { UpdateHttpProvider } from "./app/update"
|
||||||
import { VscodeHttpProvider } from "./app/vscode"
|
import { VscodeHttpProvider } from "./app/vscode"
|
||||||
import { Args, optionDescriptions, parse } from "./cli"
|
import { Args, optionDescriptions, parse } from "./cli"
|
||||||
import { AuthType, HttpServer } from "./http"
|
import { AuthType, HttpServer, HttpServerOptions } from "./http"
|
||||||
import { SshProvider } from "./ssh/server"
|
import { SshProvider } from "./ssh/server"
|
||||||
import { generateCertificate, generatePassword, generateSshHostKey, hash, open } from "./util"
|
import { generateCertificate, generatePassword, generateSshHostKey, hash, open } from "./util"
|
||||||
import { ipcMain, wrap } from "./wrapper"
|
import { ipcMain, wrap } from "./wrapper"
|
||||||
|
@ -36,42 +37,31 @@ const main = async (args: Args): Promise<void> => {
|
||||||
const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword()))
|
const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword()))
|
||||||
|
|
||||||
// Spawn the main HTTP server.
|
// Spawn the main HTTP server.
|
||||||
const options = {
|
const options: HttpServerOptions = {
|
||||||
auth,
|
auth,
|
||||||
cert: args.cert ? args.cert.value : undefined,
|
|
||||||
certKey: args["cert-key"],
|
|
||||||
sshHostKey: args["ssh-host-key"],
|
|
||||||
commit,
|
commit,
|
||||||
host: args.host || (args.auth === AuthType.Password && typeof args.cert !== "undefined" ? "0.0.0.0" : "localhost"),
|
host: args.host || (args.auth === AuthType.Password && typeof args.cert !== "undefined" ? "0.0.0.0" : "localhost"),
|
||||||
password: originalPassword ? hash(originalPassword) : undefined,
|
password: originalPassword ? hash(originalPassword) : undefined,
|
||||||
port: typeof args.port !== "undefined" ? args.port : process.env.PORT ? parseInt(process.env.PORT, 10) : 8080,
|
port: typeof args.port !== "undefined" ? args.port : process.env.PORT ? parseInt(process.env.PORT, 10) : 8080,
|
||||||
|
proxyDomains: args["proxy-domain"],
|
||||||
socket: args.socket,
|
socket: args.socket,
|
||||||
|
...(args.cert && !args.cert.value
|
||||||
|
? await generateCertificate()
|
||||||
|
: {
|
||||||
|
cert: args.cert && args.cert.value,
|
||||||
|
certKey: args["cert-key"],
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.cert && args.cert) {
|
if (options.cert && !options.certKey) {
|
||||||
const { cert, certKey } = await generateCertificate()
|
|
||||||
options.cert = cert
|
|
||||||
options.certKey = certKey
|
|
||||||
} else if (args.cert && !args["cert-key"]) {
|
|
||||||
throw new Error("--cert-key is missing")
|
throw new Error("--cert-key is missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!args["disable-ssh"]) {
|
|
||||||
if (!options.sshHostKey && typeof options.sshHostKey !== "undefined") {
|
|
||||||
throw new Error("--ssh-host-key cannot be blank")
|
|
||||||
} else if (!options.sshHostKey) {
|
|
||||||
try {
|
|
||||||
options.sshHostKey = await generateSshHostKey()
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Unable to start SSH server", field("error", error.message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const httpServer = new HttpServer(options)
|
const httpServer = new HttpServer(options)
|
||||||
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
|
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
|
||||||
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
|
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
|
||||||
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"])
|
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"])
|
||||||
|
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
|
||||||
httpServer.registerHttpProvider("/login", LoginHttpProvider)
|
httpServer.registerHttpProvider("/login", LoginHttpProvider)
|
||||||
httpServer.registerHttpProvider("/static", StaticHttpProvider)
|
httpServer.registerHttpProvider("/static", StaticHttpProvider)
|
||||||
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
|
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
|
||||||
|
@ -84,7 +74,7 @@ const main = async (args: Args): Promise<void> => {
|
||||||
|
|
||||||
if (auth === AuthType.Password && !process.env.PASSWORD) {
|
if (auth === AuthType.Password && !process.env.PASSWORD) {
|
||||||
logger.info(` - Password is ${originalPassword}`)
|
logger.info(` - Password is ${originalPassword}`)
|
||||||
logger.info(" - To use your own password, set the PASSWORD environment variable")
|
logger.info(" - To use your own password set the PASSWORD environment variable")
|
||||||
if (!args.auth) {
|
if (!args.auth) {
|
||||||
logger.info(" - To disable use `--auth none`")
|
logger.info(" - To disable use `--auth none`")
|
||||||
}
|
}
|
||||||
|
@ -96,7 +86,7 @@ const main = async (args: Args): Promise<void> => {
|
||||||
|
|
||||||
if (httpServer.protocol === "https") {
|
if (httpServer.protocol === "https") {
|
||||||
logger.info(
|
logger.info(
|
||||||
typeof args.cert === "string"
|
args.cert && args.cert.value
|
||||||
? ` - Using provided certificate and key for HTTPS`
|
? ` - Using provided certificate and key for HTTPS`
|
||||||
: ` - Using generated certificate and key for HTTPS`,
|
: ` - Using generated certificate and key for HTTPS`,
|
||||||
)
|
)
|
||||||
|
@ -104,11 +94,25 @@ const main = async (args: Args): Promise<void> => {
|
||||||
logger.info(" - Not serving HTTPS")
|
logger.info(" - Not serving HTTPS")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (httpServer.proxyDomains.size > 0) {
|
||||||
|
logger.info(` - Proxying the following domain${httpServer.proxyDomains.size === 1 ? "" : "s"}:`)
|
||||||
|
httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`)
|
logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`)
|
||||||
|
|
||||||
|
let sshHostKey = args["ssh-host-key"]
|
||||||
|
if (!args["disable-ssh"] && !sshHostKey) {
|
||||||
|
try {
|
||||||
|
sshHostKey = await generateSshHostKey()
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Unable to start SSH server", field("error", error.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let sshPort: number | undefined
|
let sshPort: number | undefined
|
||||||
if (!args["disable-ssh"] && options.sshHostKey) {
|
if (!args["disable-ssh"] && sshHostKey) {
|
||||||
const sshProvider = httpServer.registerHttpProvider("/ssh", SshProvider, options.sshHostKey as string)
|
const sshProvider = httpServer.registerHttpProvider("/ssh", SshProvider, sshHostKey)
|
||||||
try {
|
try {
|
||||||
sshPort = await sshProvider.listen()
|
sshPort = await sshProvider.listen()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -118,6 +122,7 @@ const main = async (args: Args): Promise<void> => {
|
||||||
|
|
||||||
if (typeof sshPort !== "undefined") {
|
if (typeof sshPort !== "undefined") {
|
||||||
logger.info(`SSH server listening on localhost:${sshPort}`)
|
logger.info(`SSH server listening on localhost:${sshPort}`)
|
||||||
|
logger.info(" - To disable use `--disable-ssh`")
|
||||||
} else {
|
} else {
|
||||||
logger.info("SSH server disabled")
|
logger.info("SSH server disabled")
|
||||||
}
|
}
|
||||||
|
|
222
src/node/http.ts
222
src/node/http.ts
|
@ -1,6 +1,7 @@
|
||||||
import { field, logger } from "@coder/logger"
|
import { field, logger } from "@coder/logger"
|
||||||
import * as fs from "fs-extra"
|
import * as fs from "fs-extra"
|
||||||
import * as http from "http"
|
import * as http from "http"
|
||||||
|
import proxy from "http-proxy"
|
||||||
import * as httpolyglot from "httpolyglot"
|
import * as httpolyglot from "httpolyglot"
|
||||||
import * as https from "https"
|
import * as https from "https"
|
||||||
import * as net from "net"
|
import * as net from "net"
|
||||||
|
@ -18,6 +19,10 @@ import { getMediaMime, xdgLocalDir } from "./util"
|
||||||
export type Cookies = { [key: string]: string[] | undefined }
|
export type Cookies = { [key: string]: string[] | undefined }
|
||||||
export type PostData = { [key: string]: string | string[] | undefined }
|
export type PostData = { [key: string]: string | string[] | undefined }
|
||||||
|
|
||||||
|
interface ProxyRequest extends http.IncomingMessage {
|
||||||
|
base?: string
|
||||||
|
}
|
||||||
|
|
||||||
interface AuthPayload extends Cookies {
|
interface AuthPayload extends Cookies {
|
||||||
key?: string[]
|
key?: string[]
|
||||||
}
|
}
|
||||||
|
@ -29,6 +34,17 @@ export enum AuthType {
|
||||||
|
|
||||||
export type Query = { [key: string]: string | string[] | undefined }
|
export type Query = { [key: string]: string | string[] | undefined }
|
||||||
|
|
||||||
|
export interface ProxyOptions {
|
||||||
|
/**
|
||||||
|
* A base path to strip from from the request before proxying if necessary.
|
||||||
|
*/
|
||||||
|
base?: string
|
||||||
|
/**
|
||||||
|
* The port to proxy.
|
||||||
|
*/
|
||||||
|
port: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface HttpResponse<T = string | Buffer | object> {
|
export interface HttpResponse<T = string | Buffer | object> {
|
||||||
/*
|
/*
|
||||||
* Whether to set cache-control headers for this response.
|
* Whether to set cache-control headers for this response.
|
||||||
|
@ -77,6 +93,17 @@ export interface HttpResponse<T = string | Buffer | object> {
|
||||||
* `undefined` to remove a query variable.
|
* `undefined` to remove a query variable.
|
||||||
*/
|
*/
|
||||||
query?: Query
|
query?: Query
|
||||||
|
/**
|
||||||
|
* Indicates the request should be proxied.
|
||||||
|
*/
|
||||||
|
proxy?: ProxyOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WsResponse {
|
||||||
|
/**
|
||||||
|
* Indicates the web socket should be proxied.
|
||||||
|
*/
|
||||||
|
proxy?: ProxyOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,14 +127,31 @@ export interface HttpServerOptions {
|
||||||
readonly host?: string
|
readonly host?: string
|
||||||
readonly password?: string
|
readonly password?: string
|
||||||
readonly port?: number
|
readonly port?: number
|
||||||
|
readonly proxyDomains?: string[]
|
||||||
readonly socket?: string
|
readonly socket?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Route {
|
export interface Route {
|
||||||
|
/**
|
||||||
|
* Base path part (in /test/path it would be "/test").
|
||||||
|
*/
|
||||||
base: string
|
base: string
|
||||||
|
/**
|
||||||
|
* Remaining part of the route (in /test/path it would be "/path"). It can be
|
||||||
|
* blank.
|
||||||
|
*/
|
||||||
requestPath: string
|
requestPath: string
|
||||||
|
/**
|
||||||
|
* Query variables included in the request.
|
||||||
|
*/
|
||||||
query: querystring.ParsedUrlQuery
|
query: querystring.ParsedUrlQuery
|
||||||
|
/**
|
||||||
|
* Normalized version of `originalPath`.
|
||||||
|
*/
|
||||||
fullPath: string
|
fullPath: string
|
||||||
|
/**
|
||||||
|
* Original path of the request without any modifications.
|
||||||
|
*/
|
||||||
originalPath: string
|
originalPath: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +180,9 @@ export abstract class HttpProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle web sockets on the registered endpoint.
|
* Handle web sockets on the registered endpoint. Normally the provider
|
||||||
|
* handles the request itself but it can return a response when necessary. The
|
||||||
|
* default is to throw a 404.
|
||||||
*/
|
*/
|
||||||
public handleWebSocket(
|
public handleWebSocket(
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
@ -145,7 +191,7 @@ export abstract class HttpProvider {
|
||||||
_socket: net.Socket,
|
_socket: net.Socket,
|
||||||
_head: Buffer,
|
_head: Buffer,
|
||||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||||
): Promise<void> {
|
): Promise<WsResponse | void> {
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
throw new HttpError("Not found", HttpCode.NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,7 +310,7 @@ export abstract class HttpProvider {
|
||||||
* Return the provided password value if the payload contains the right
|
* Return the provided password value if the payload contains the right
|
||||||
* password otherwise return false. If no payload is specified use cookies.
|
* password otherwise return false. If no payload is specified use cookies.
|
||||||
*/
|
*/
|
||||||
protected authenticated(request: http.IncomingMessage, payload?: AuthPayload): string | boolean {
|
public authenticated(request: http.IncomingMessage, payload?: AuthPayload): string | boolean {
|
||||||
switch (this.options.auth) {
|
switch (this.options.auth) {
|
||||||
case AuthType.None:
|
case AuthType.None:
|
||||||
return true
|
return true
|
||||||
|
@ -335,6 +381,14 @@ export abstract class HttpProvider {
|
||||||
}
|
}
|
||||||
return cookies as T
|
return cookies as T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the route is for the root page. For example /base, /base/,
|
||||||
|
* or /base/index.html but not /base/path or /base/file.js.
|
||||||
|
*/
|
||||||
|
protected isRoot(route: Route): boolean {
|
||||||
|
return !route.requestPath || route.requestPath === "/index.html"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -407,7 +461,18 @@ export class HttpServer {
|
||||||
private readonly heart: Heart
|
private readonly heart: Heart
|
||||||
private readonly socketProvider = new SocketProxyProvider()
|
private readonly socketProvider = new SocketProxyProvider()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy domains are stored here without the leading `*.`
|
||||||
|
*/
|
||||||
|
public readonly proxyDomains: Set<string>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the actual proxying functionality.
|
||||||
|
*/
|
||||||
|
private readonly proxy = proxy.createProxyServer({})
|
||||||
|
|
||||||
public constructor(private readonly options: HttpServerOptions) {
|
public constructor(private readonly options: HttpServerOptions) {
|
||||||
|
this.proxyDomains = new Set((options.proxyDomains || []).map((d) => d.replace(/^\*\./, "")))
|
||||||
this.heart = new Heart(path.join(xdgLocalDir, "heartbeat"), async () => {
|
this.heart = new Heart(path.join(xdgLocalDir, "heartbeat"), async () => {
|
||||||
const connections = await this.getConnections()
|
const connections = await this.getConnections()
|
||||||
logger.trace(`${connections} active connection${plural(connections)}`)
|
logger.trace(`${connections} active connection${plural(connections)}`)
|
||||||
|
@ -425,6 +490,16 @@ export class HttpServer {
|
||||||
} else {
|
} else {
|
||||||
this.server = http.createServer(this.onRequest)
|
this.server = http.createServer(this.onRequest)
|
||||||
}
|
}
|
||||||
|
this.proxy.on("error", (error, _request, response) => {
|
||||||
|
response.writeHead(HttpCode.ServerError)
|
||||||
|
response.end(error.message)
|
||||||
|
})
|
||||||
|
// Intercept the response to rewrite absolute redirects against the base path.
|
||||||
|
this.proxy.on("proxyRes", (response, request: ProxyRequest) => {
|
||||||
|
if (response.headers.location && response.headers.location.startsWith("/") && request.base) {
|
||||||
|
response.headers.location = request.base + response.headers.location
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
|
@ -515,6 +590,9 @@ export class HttpServer {
|
||||||
this.heart.beat()
|
this.heart.beat()
|
||||||
const route = this.parseUrl(request)
|
const route = this.parseUrl(request)
|
||||||
const write = (payload: HttpResponse): void => {
|
const write = (payload: HttpResponse): void => {
|
||||||
|
const host = request.headers.host || ""
|
||||||
|
const idx = host.indexOf(":")
|
||||||
|
const domain = idx !== -1 ? host.substring(0, idx) : host
|
||||||
response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, {
|
response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, {
|
||||||
"Content-Type": payload.mime || getMediaMime(payload.filePath),
|
"Content-Type": payload.mime || getMediaMime(payload.filePath),
|
||||||
...(payload.redirect ? { Location: this.constructRedirect(request, route, payload as RedirectResponse) } : {}),
|
...(payload.redirect ? { Location: this.constructRedirect(request, route, payload as RedirectResponse) } : {}),
|
||||||
|
@ -525,9 +603,12 @@ export class HttpServer {
|
||||||
"Set-Cookie": [
|
"Set-Cookie": [
|
||||||
`${payload.cookie.key}=${payload.cookie.value}`,
|
`${payload.cookie.key}=${payload.cookie.value}`,
|
||||||
`Path=${normalize(payload.cookie.path || "/", true)}`,
|
`Path=${normalize(payload.cookie.path || "/", true)}`,
|
||||||
|
domain ? `Domain=${this.getCookieDomain(domain)}` : undefined,
|
||||||
// "HttpOnly",
|
// "HttpOnly",
|
||||||
"SameSite=strict",
|
"SameSite=lax",
|
||||||
].join(";"),
|
]
|
||||||
|
.filter((l) => !!l)
|
||||||
|
.join(";"),
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...payload.headers,
|
...payload.headers,
|
||||||
|
@ -547,20 +628,27 @@ export class HttpServer {
|
||||||
response.end()
|
response.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = this.maybeRedirect(request, route) || (await route.provider.handleRequest(route, request))
|
const payload =
|
||||||
if (!payload) {
|
this.maybeRedirect(request, route) ||
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
(route.provider.authenticated(request) && this.maybeProxy(request)) ||
|
||||||
|
(await route.provider.handleRequest(route, request))
|
||||||
|
if (payload.proxy) {
|
||||||
|
this.doProxy(route, request, response, payload.proxy)
|
||||||
|
} else {
|
||||||
|
write(payload)
|
||||||
}
|
}
|
||||||
write(payload)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let e = error
|
let e = error
|
||||||
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
||||||
e = new HttpError("Not found", HttpCode.NotFound)
|
e = new HttpError("Not found", HttpCode.NotFound)
|
||||||
}
|
}
|
||||||
logger.debug("Request error", field("url", request.url))
|
|
||||||
logger.debug(error.stack)
|
|
||||||
const code = typeof e.code === "number" ? e.code : HttpCode.ServerError
|
const code = typeof e.code === "number" ? e.code : HttpCode.ServerError
|
||||||
|
logger.debug("Request error", field("url", request.url), field("code", code))
|
||||||
|
if (code >= HttpCode.ServerError) {
|
||||||
|
logger.error(error.stack)
|
||||||
|
}
|
||||||
const payload = await route.provider.getErrorRoot(route, code, code, e.message)
|
const payload = await route.provider.getErrorRoot(route, code, code, e.message)
|
||||||
write({
|
write({
|
||||||
code,
|
code,
|
||||||
|
@ -625,7 +713,14 @@ export class HttpServer {
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
throw new HttpError("Not found", HttpCode.NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
await route.provider.handleWebSocket(route, request, await this.socketProvider.createProxy(socket), head)
|
// The socket proxy is so we can pass them to child processes (TLS sockets
|
||||||
|
// can't be transferred so we need an in-between).
|
||||||
|
const socketProxy = await this.socketProvider.createProxy(socket)
|
||||||
|
const payload =
|
||||||
|
this.maybeProxy(request) || (await route.provider.handleWebSocket(route, request, socketProxy, head))
|
||||||
|
if (payload && payload.proxy) {
|
||||||
|
this.doProxy(route, request, { socket: socketProxy, head }, payload.proxy)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
socket.destroy(error)
|
socket.destroy(error)
|
||||||
logger.warn(`discarding socket connection: ${error.message}`)
|
logger.warn(`discarding socket connection: ${error.message}`)
|
||||||
|
@ -647,7 +742,6 @@ export class HttpServer {
|
||||||
// Happens if it's a plain `domain.com`.
|
// Happens if it's a plain `domain.com`.
|
||||||
base = "/"
|
base = "/"
|
||||||
}
|
}
|
||||||
requestPath = requestPath || "/index.html"
|
|
||||||
return { base, requestPath }
|
return { base, requestPath }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -670,4 +764,106 @@ export class HttpServer {
|
||||||
}
|
}
|
||||||
return { base, fullPath, requestPath, query: parsedUrl.query, provider, originalPath }
|
return { base, fullPath, requestPath, query: parsedUrl.query, provider, originalPath }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy a request to the target.
|
||||||
|
*/
|
||||||
|
private doProxy(
|
||||||
|
route: Route,
|
||||||
|
request: http.IncomingMessage,
|
||||||
|
response: http.ServerResponse,
|
||||||
|
options: ProxyOptions,
|
||||||
|
): void
|
||||||
|
/**
|
||||||
|
* Proxy a web socket to the target.
|
||||||
|
*/
|
||||||
|
private doProxy(
|
||||||
|
route: Route,
|
||||||
|
request: http.IncomingMessage,
|
||||||
|
response: { socket: net.Socket; head: Buffer },
|
||||||
|
options: ProxyOptions,
|
||||||
|
): void
|
||||||
|
/**
|
||||||
|
* Proxy a request or web socket to the target.
|
||||||
|
*/
|
||||||
|
private doProxy(
|
||||||
|
route: Route,
|
||||||
|
request: http.IncomingMessage,
|
||||||
|
response: http.ServerResponse | { socket: net.Socket; head: Buffer },
|
||||||
|
options: ProxyOptions,
|
||||||
|
): void {
|
||||||
|
const port = parseInt(options.port, 10)
|
||||||
|
if (isNaN(port)) {
|
||||||
|
throw new HttpError(`"${options.port}" is not a valid number`, HttpCode.BadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// REVIEW: Absolute redirects need to be based on the subpath but I'm not
|
||||||
|
// sure how best to get this information to the `proxyRes` event handler.
|
||||||
|
// For now I'm sticking it on the request object which is passed through to
|
||||||
|
// the event.
|
||||||
|
;(request as ProxyRequest).base = options.base
|
||||||
|
|
||||||
|
const isHttp = response instanceof http.ServerResponse
|
||||||
|
const path = options.base ? route.fullPath.replace(options.base, "") : route.fullPath
|
||||||
|
const proxyOptions: proxy.ServerOptions = {
|
||||||
|
changeOrigin: true,
|
||||||
|
ignorePath: true,
|
||||||
|
target: `${isHttp ? "http" : "ws"}://127.0.0.1:${port}${path}${
|
||||||
|
Object.keys(route.query).length > 0 ? `?${querystring.stringify(route.query)}` : ""
|
||||||
|
}`,
|
||||||
|
ws: !isHttp,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response instanceof http.ServerResponse) {
|
||||||
|
this.proxy.web(request, response, proxyOptions)
|
||||||
|
} else {
|
||||||
|
this.proxy.ws(request, response.socket, response.head, proxyOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the domain that should be used for setting a cookie. This will allow
|
||||||
|
* the user to authenticate only once. This will return the highest level
|
||||||
|
* domain (e.g. `coder.com` over `test.coder.com` if both are specified).
|
||||||
|
*/
|
||||||
|
private getCookieDomain(host: string): string {
|
||||||
|
let current: string | undefined
|
||||||
|
this.proxyDomains.forEach((domain) => {
|
||||||
|
if (host.endsWith(domain) && (!current || domain.length < current.length)) {
|
||||||
|
current = domain
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Setting the domain to localhost doesn't seem to work for subdomains (for
|
||||||
|
// example dev.localhost).
|
||||||
|
return current && current !== "localhost" ? current : host
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a response if the request should be proxied. Anything that ends in a
|
||||||
|
* proxy domain and has a *single* subdomain should be proxied. Anything else
|
||||||
|
* should return `undefined` and will be handled as normal.
|
||||||
|
*
|
||||||
|
* For example if `coder.com` is specified `8080.coder.com` will be proxied
|
||||||
|
* but `8080.test.coder.com` and `test.8080.coder.com` will not.
|
||||||
|
*/
|
||||||
|
public maybeProxy(request: http.IncomingMessage): HttpResponse | undefined {
|
||||||
|
// Split into parts.
|
||||||
|
const host = request.headers.host || ""
|
||||||
|
const idx = host.indexOf(":")
|
||||||
|
const domain = idx !== -1 ? host.substring(0, idx) : host
|
||||||
|
const parts = domain.split(".")
|
||||||
|
|
||||||
|
// There must be an exact match.
|
||||||
|
const port = parts.shift()
|
||||||
|
const proxyDomain = parts.join(".")
|
||||||
|
if (!port || !this.proxyDomains.has(proxyDomain)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
proxy: {
|
||||||
|
port,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,6 +117,7 @@ describe("cli", () => {
|
||||||
assert.throws(() => parse(["--auth=", "--log=debug"]), /--auth requires a value/)
|
assert.throws(() => parse(["--auth=", "--log=debug"]), /--auth requires a value/)
|
||||||
assert.throws(() => parse(["--auth", "--log"]), /--auth requires a value/)
|
assert.throws(() => parse(["--auth", "--log"]), /--auth requires a value/)
|
||||||
assert.throws(() => parse(["--auth", "--invalid"]), /--auth requires a value/)
|
assert.throws(() => parse(["--auth", "--invalid"]), /--auth requires a value/)
|
||||||
|
assert.throws(() => parse(["--ssh-host-key"]), /--ssh-host-key requires a value/)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should error if value is invalid", () => {
|
it("should error if value is invalid", () => {
|
||||||
|
@ -160,4 +161,19 @@ describe("cli", () => {
|
||||||
auth: "none",
|
auth: "none",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should support repeatable flags", () => {
|
||||||
|
assert.deepEqual(parse(["--proxy-domain", "*.coder.com"]), {
|
||||||
|
_: [],
|
||||||
|
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
||||||
|
"user-data-dir": xdgLocalDir,
|
||||||
|
"proxy-domain": ["*.coder.com"],
|
||||||
|
})
|
||||||
|
assert.deepEqual(parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "test.com"]), {
|
||||||
|
_: [],
|
||||||
|
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
||||||
|
"user-data-dir": xdgLocalDir,
|
||||||
|
"proxy-domain": ["*.coder.com", "test.com"],
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
35
yarn.lock
35
yarn.lock
|
@ -871,6 +871,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/http-proxy@^1.17.4":
|
||||||
|
version "1.17.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b"
|
||||||
|
integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/json-schema@^7.0.3":
|
"@types/json-schema@^7.0.3":
|
||||||
version "7.0.4"
|
version "7.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
|
||||||
|
@ -2240,7 +2247,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms "2.0.0"
|
ms "2.0.0"
|
||||||
|
|
||||||
debug@3.2.6:
|
debug@3.2.6, debug@^3.0.0:
|
||||||
version "3.2.6"
|
version "3.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
|
||||||
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
|
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
|
||||||
|
@ -2745,6 +2752,11 @@ etag@~1.8.1:
|
||||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||||
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
|
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
|
||||||
|
|
||||||
|
eventemitter3@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb"
|
||||||
|
integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==
|
||||||
|
|
||||||
events@^3.0.0:
|
events@^3.0.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59"
|
resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59"
|
||||||
|
@ -2980,6 +2992,13 @@ flatted@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08"
|
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08"
|
||||||
integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==
|
integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==
|
||||||
|
|
||||||
|
follow-redirects@^1.0.0:
|
||||||
|
version "1.10.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.10.0.tgz#01f5263aee921c6a54fb91667f08f4155ce169eb"
|
||||||
|
integrity sha512-4eyLK6s6lH32nOvLLwlIOnr9zrL8Sm+OvW4pVTJNoXeGzYIkHVf+pADQi+OJ0E67hiuSLezPVPyBcIZO50TmmQ==
|
||||||
|
dependencies:
|
||||||
|
debug "^3.0.0"
|
||||||
|
|
||||||
for-in@^1.0.2:
|
for-in@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
|
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
|
||||||
|
@ -3403,6 +3422,15 @@ http-errors@~1.7.2:
|
||||||
statuses ">= 1.5.0 < 2"
|
statuses ">= 1.5.0 < 2"
|
||||||
toidentifier "1.0.0"
|
toidentifier "1.0.0"
|
||||||
|
|
||||||
|
http-proxy@^1.18.0:
|
||||||
|
version "1.18.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a"
|
||||||
|
integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==
|
||||||
|
dependencies:
|
||||||
|
eventemitter3 "^4.0.0"
|
||||||
|
follow-redirects "^1.0.0"
|
||||||
|
requires-port "^1.0.0"
|
||||||
|
|
||||||
http-signature@~1.2.0:
|
http-signature@~1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
|
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
|
||||||
|
@ -5894,6 +5922,11 @@ require-main-filename@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
|
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
|
||||||
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
|
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
|
||||||
|
|
||||||
|
requires-port@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||||
|
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
||||||
|
|
||||||
resolve-from@^3.0.0:
|
resolve-from@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
|
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
|
||||||
|
|
Loading…
Reference in New Issue