From 847167522b6306d7f4d68d4626b3dca808b94fcb Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 1 Oct 2020 17:25:20 -0500 Subject: [PATCH] wip api --- plugin.d.ts | 298 +++++++++++++++++++++++++++++++++++++++++++++ src/node/http.ts | 1 + src/node/plugin.ts | 11 +- tsconfig.json | 7 +- 4 files changed, 306 insertions(+), 11 deletions(-) create mode 100644 plugin.d.ts diff --git a/plugin.d.ts b/plugin.d.ts new file mode 100644 index 00000000..63bbedab --- /dev/null +++ b/plugin.d.ts @@ -0,0 +1,298 @@ +import * as http from "http" +import * as net from "net" +import * as querystring from "querystring" +import { Readable } from "stream" + +export type Cookies = { [key: string]: string[] | undefined } + +export type Query = { [key: string]: string | string[] | undefined } + +export interface ProxyOptions { + /** + * A path to strip from from the beginning of the request before proxying + */ + strip?: string + /** + * A path to add to the beginning of the request before proxying. + */ + prepend?: string + /** + * The port to proxy. + */ + port: string +} + +export interface HttpResponse { + /* + * Whether to set cache-control headers for this response. + */ + cache?: boolean + /** + * If the code cannot be determined automatically set it here. The + * defaults are 302 for redirects and 200 for successful requests. For errors + * you should throw an HttpError and include the code there. If you + * use Error it will default to 404 for ENOENT and EISDIR and 500 otherwise. + */ + code?: number + /** + * Content to write in the response. Mutually exclusive with stream. + */ + content?: T + /** + * Cookie to write with the response. + * NOTE: Cookie paths must be absolute. The default is /. + */ + cookie?: { key: string; value: string; path?: string } + /** + * Used to automatically determine the appropriate mime type. + */ + filePath?: string + /** + * Additional headers to include. + */ + headers?: http.OutgoingHttpHeaders + /** + * If the mime type cannot be determined automatically set it here. + */ + mime?: string + /** + * Redirect to this path. This is constructed against the site base (not the + * provider's base). + */ + redirect?: string + /** + * Stream this to the response. Mutually exclusive with content. + */ + stream?: Readable + /** + * Query variables to add in addition to current ones when redirecting. Use + * `undefined` to remove a query variable. + */ + query?: Query + /** + * Indicates the request should be proxied. + */ + proxy?: ProxyOptions +} + +/** + * Use when you need to run search and replace on a file's content before + * sending it. + */ +export interface HttpStringFileResponse extends HttpResponse { + content: string + filePath: string +} + +export interface WsResponse { + /** + * Indicates the web socket should be proxied. + */ + proxy?: ProxyOptions +} + +export interface Route { + /** + * Provider base path part (for /provider/base/path it would be /provider). + */ + providerBase: string + /** + * Base path part (for /provider/base/path it would be /base). + */ + base: string + /** + * Remaining part of the route after factoring out the base and provider base + * (for /provider/base/path it would be /path). It can be blank. + */ + requestPath: string + /** + * Query variables included in the request. + */ + query: querystring.ParsedUrlQuery + /** + * Normalized version of `originalPath`. + */ + fullPath: string + /** + * Original path of the request without any modifications. + */ + originalPath: string +} + +export interface HttpProviderOptions { + readonly auth: AuthType + readonly commit: string + readonly password?: string +} + +export enum AuthType { + Password = "password", + None = "none", +} + +export interface HttpProvider0 { + new (options: HttpProviderOptions): T +} + +export interface HttpProvider1 { + new (options: HttpProviderOptions, a1: A1): T +} + +export interface HttpProvider2 { + new (options: HttpProviderOptions, a1: A1, a2: A2): T +} + +export interface HttpProvider3 { + new (options: HttpProviderOptions, a1: A1, a2: A2, a3: A3): T +} + +/** + * Provides HTTP responses. This abstract class provides some helpers for + * interpreting, creating, and authenticating responses. + */ +export class HttpProvider { + protected readonly rootPath: string + + public dispose(): Promise + + /** + * 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( + route: Route, + request: http.IncomingMessage, + socket: net.Socket, + head: Buffer, + ): Promise + + /** + * Handle requests to the registered endpoint. + */ + public handleRequest(route: Route, request: http.IncomingMessage): Promise + + /** + * Get the base relative to the provided route. For each slash we need to go + * up a directory. For example: + * / => ./ + * /foo => ./ + * /foo/ => ./../ + * /foo/bar => ./../ + * /foo/bar/ => ./../../ + */ + public base(route: Route): string + + /** + * Get error response. + */ + public getErrorRoot(route: Route, title: string, header: string, body: string): Promise + + /** + * Replace common templates strings. + */ + public replaceTemplates(route: Route, response: HttpStringFileResponse, sessionId?: string): HttpStringFileResponse + public replaceTemplates( + route: Route, + response: HttpStringFileResponse, + options: T, + ): HttpStringFileResponse + public replaceTemplates( + route: Route, + response: HttpStringFileResponse, + sessionIdOrOptions?: string | object, + ): HttpStringFileResponse + + protected get isDev(): boolean + + /** + * Get a file resource. + */ + protected getResource(...parts: string[]): Promise + + /** + * Get a file resource as a string. + */ + protected getUtf8Resource(...parts: string[]): Promise + + /** + * Helper to error on invalid methods (default GET). + */ + protected ensureMethod(request: http.IncomingMessage, method?: string | string[]): void + + /** + * Helper to error if not authorized. + */ + protected ensureAuthenticated(request: http.IncomingMessage): void + + /** + * Use the first query value or the default if there isn't one. + */ + protected queryOrDefault(value: string | string[] | undefined, def: string): string + + /** + * Return the provided password value if the payload contains the right + * password otherwise return false. If no payload is specified use cookies. + */ + public authenticated(request: http.IncomingMessage): string | boolean + + /** + * Parse POST data. + */ + protected getData(request: http.IncomingMessage): Promise + + /** + * Parse cookies. + */ + protected parseCookies(request: http.IncomingMessage): 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 +} + +export interface HttpServer { + /** + * Register a provider for a top-level endpoint. + */ + registerHttpProvider(endpoint: string | string[], provider: HttpProvider0): T + registerHttpProvider( + endpoint: string | string[], + provider: HttpProvider1, + a1: A1, + ): T + registerHttpProvider( + endpoint: string | string[], + provider: HttpProvider2, + a1: A1, + a2: A2, + ): T + registerHttpProvider( + endpoint: string | string[], + provider: HttpProvider3, + a1: A1, + a2: A2, + a3: A3, + ): T + // eslint-disable-next-line @typescript-eslint/no-explicit-any + registerHttpProvider(endpoint: string | string[], provider: any, ...args: any[]): void +} + +/** + * Command line arguments after merging with the defaults. + */ +export interface Args { + "user-data-dir"?: string + _: string[] +} + +export type Activate = (server: HttpServer, args: Args) => void + +/** + * Plugins must implement this interface. + */ +export interface Plugin { + activate: Activate +} diff --git a/src/node/http.ts b/src/node/http.ts index a8abb94b..7476d125 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -15,6 +15,7 @@ import { HttpCode, HttpError } from "../common/http" import { arrayify, normalize, Options, plural, split, trimSlashes } from "../common/util" import { SocketProxyProvider } from "./socket" import { getMediaMime, paths } from "./util" +// import * as api from "../../plugin" export type Cookies = { [key: string]: string[] | undefined } export type PostData = { [key: string]: string | string[] | undefined } diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 29986219..01c9dfa2 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -2,21 +2,13 @@ import { field, logger } from "@coder/logger" import * as fs from "fs" import * as path from "path" import * as util from "util" +import { Plugin } from "../../plugin" import { Args } from "./cli" import { HttpServer } from "./http" import { paths } from "./util" /* eslint-disable @typescript-eslint/no-var-requires */ -export type Activate = (httpServer: HttpServer, args: Args) => void - -/** - * Plugins must implement this interface. - */ -export interface Plugin { - activate: Activate -} - /** * Intercept imports so we can inject code-server when the plugin tries to * import it. @@ -54,6 +46,7 @@ const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args */ const _loadPlugins = async (pluginDir: string, httpServer: HttpServer, args: Args): Promise => { try { + logger.debug("Loading plugins", field("dir", pluginDir)) const files = await util.promisify(fs.readdir)(pluginDir, { withFileTypes: true, }) diff --git a/tsconfig.json b/tsconfig.json index ac3a1df5..17ee2935 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,6 @@ "noUnusedLocals": true, "forceConsistentCasingInFileNames": true, "outDir": "./out", - "declaration": true, "experimentalDecorators": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, @@ -16,7 +15,11 @@ "tsBuildInfoFile": "./.cache/tsbuildinfo", "incremental": true, "rootDir": "./src", - "typeRoots": ["./node_modules/@types", "./typings"] + "typeRoots": ["./node_modules/@types", "./typings"], + "baseUrl": ".", + "paths": { + "coder-server": ["src/code-server.d.ts"] + } }, "include": ["./src/**/*.ts"] } -- GitLab