import { field, logger } from "@coder/logger" import * as http from "http" import * as net from "net" import * as ws from "ws" import { Application, ApplicationsResponse, ClientMessage, FilesResponse, LoginRequest, LoginResponse, ServerMessage, } from "../../common/api" import { ApiEndpoint, HttpCode } from "../../common/http" import { normalize } from "../../common/util" import { HttpProvider, HttpProviderOptions, HttpResponse, HttpServer, Route } from "../http" import { hash } from "../util" export const Vscode: Application = { name: "VS Code", path: "/", embedPath: "./vscode-embed", } /** * API HTTP provider. */ export class ApiHttpProvider extends HttpProvider { private readonly ws = new ws.Server({ noServer: true }) public constructor(options: HttpProviderOptions, private readonly server: HttpServer) { super(options) } public async handleRequest(route: Route, request: http.IncomingMessage): Promise { switch (route.base) { case ApiEndpoint.login: if (request.method === "POST") { return this.login(request) } break } if (!this.authenticated(request)) { return { code: HttpCode.Unauthorized } } switch (route.base) { case ApiEndpoint.applications: return this.applications() case ApiEndpoint.files: return this.files() } return undefined } public async handleWebSocket( _route: Route, request: http.IncomingMessage, socket: net.Socket, head: Buffer ): Promise { if (!this.authenticated(request)) { throw new Error("not authenticated") } await new Promise((resolve) => { this.ws.handleUpgrade(request, socket, head, (ws) => { const send = (event: ServerMessage): void => { ws.send(JSON.stringify(event)) } ws.on("message", (data) => { logger.trace("got message", field("message", data)) try { const message: ClientMessage = JSON.parse(data.toString()) this.getMessageResponse(message.event).then(send) } catch (error) { logger.error(error.message, field("message", data)) } }) resolve() }) }) return true } private async getMessageResponse(event: "health"): Promise { switch (event) { case "health": return { event, connections: await this.server.getConnections() } default: throw new Error("unexpected message") } } /** * Return OK and a cookie if the user is authenticated otherwise return * unauthorized. */ private async login(request: http.IncomingMessage): Promise> { // Already authenticated via cookies? const providedPassword = this.authenticated(request) if (providedPassword) { return { code: HttpCode.Ok } } const data = await this.getData(request) const payload: LoginRequest = data ? JSON.parse(data) : {} const password = this.authenticated(request, { key: typeof payload.password === "string" ? [hash(payload.password)] : undefined, }) if (password) { return { content: { success: true, // TEMP: Auto-load VS Code. app: Vscode, }, cookie: typeof password === "string" ? { key: "key", value: password, path: normalize(payload.basePath), } : undefined, } } // Only log if it was an actual login attempt. if (payload && payload.password) { console.error( "Failed login attempt", JSON.stringify({ xForwardedFor: request.headers["x-forwarded-for"], remoteAddress: request.connection.remoteAddress, userAgent: request.headers["user-agent"], timestamp: Math.floor(new Date().getTime() / 1000), }) ) } return { code: HttpCode.Unauthorized } } /** * Return files at the requested directory. */ private async files(): Promise> { return { content: { files: [], }, } } /** * Return available applications. */ private async applications(): Promise> { return { content: { applications: [Vscode], }, } } }