import { logger } from "@coder/logger" import * as http from "http" import * as querystring from "querystring" import { Application } from "../../common/api" import { HttpCode, HttpError } from "../../common/http" import { Options } from "../../common/util" import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" import { ApiHttpProvider } from "./api" import { UpdateHttpProvider } from "./update" /** * Top-level and fallback HTTP provider. */ export class MainHttpProvider extends HttpProvider { public constructor( options: HttpProviderOptions, private readonly api: ApiHttpProvider, private readonly update: UpdateHttpProvider, ) { super(options) } public async handleRequest(route: Route, request: http.IncomingMessage): Promise { switch (route.base) { case "/static": { this.ensureMethod(request) const response = await this.getReplacedResource(route) if (!this.isDev) { response.cache = true } return response } case "/delete": { this.ensureMethod(request, "POST") const data = await this.getData(request) const p = data ? querystring.parse(data) : {} this.api.deleteSession(p.sessionId as string) return { redirect: "/" } } case "/": { this.ensureMethod(request) if (route.requestPath !== "/index.html") { throw new HttpError("Not found", HttpCode.NotFound) } else if (!this.authenticated(request)) { return { redirect: "/login" } } return this.getRoot(route) } } // Run an existing app, but if it doesn't exist go ahead and start it. let app = this.api.getRunningApplication(route.base) let sessionId = app && app.sessionId if (!app) { app = (await this.api.installedApplications()).find((a) => a.path === route.base) if (app) { sessionId = await this.api.createSession(app) } } if (sessionId) { return this.getAppRoot( route, { sessionId, base: this.base(route), logLevel: logger.level, }, (app && app.name) || "", ) } return this.getErrorRoot(route, "404", "404", "Application not found") } /** * Return a resource with variables replaced where necessary. */ protected async getReplacedResource(route: Route): Promise { if (route.requestPath.endsWith("/manifest.json")) { const response = await this.getUtf8Resource(this.rootPath, route.requestPath) response.content = response.content .replace(/{{BASE}}/g, this.base(route)) .replace(/{{COMMIT}}/g, this.options.commit) return response } return this.getResource(this.rootPath, route.requestPath) } public async getRoot(route: Route): Promise { const running = await this.api.running() const apps = await this.api.installedApplications() const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/home.html") response.content = response.content .replace(/{{COMMIT}}/g, this.options.commit) .replace(/{{BASE}}/g, this.base(route)) .replace(/{{UPDATE:NAME}}/, await this.getUpdate()) .replace(/{{APP_LIST:RUNNING}}/, this.getAppRows(running.applications)) .replace( /{{APP_LIST:EDITORS}}/, this.getAppRows(apps.filter((app) => app.categories && app.categories.includes("Editor"))), ) .replace( /{{APP_LIST:OTHER}}/, this.getAppRows(apps.filter((app) => !app.categories || !app.categories.includes("Editor"))), ) return response } public async getAppRoot(route: Route, options: Options, name: string): Promise { const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/app.html") response.content = response.content .replace(/{{COMMIT}}/g, this.options.commit) .replace(/{{BASE}}/g, this.base(route)) .replace(/{{APP_NAME}}/, name) .replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`) return response } public async handleWebSocket(): Promise { return undefined } private getAppRows(apps: ReadonlyArray): string { return apps.length > 0 ? apps.map((app) => this.getAppRow(app)).join("\n") : `
None
` } private getAppRow(app: Application): string { return `
${ app.icon ? `` : `
` }
${app.name}
${ app.sessionId ? `
` : "" }
` } private async getUpdate(): Promise { if (!this.update.enabled) { return `
Updates are disabled
` } const humanize = (time: number): string => { const d = new Date(time) const pad = (t: number): string => (t < 10 ? "0" : "") + t return ( `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` + ` ${pad(d.getHours())}:${pad(d.getMinutes())}` ) } const update = await this.update.getUpdate() if (this.update.isLatestVersion(update)) { return `
${update.version}
Up to date
${humanize(update.checked)} Check now
Current: ${this.update.currentVersion}
` } return `
${update.version}
Out of date
${humanize(update.checked)} Check now
Current: ${this.update.currentVersion}
` } }