Archived
1
0
This repository has been archived on 2024-09-09. You can view files and clone it, but cannot push or open issues or pull requests.
code-server/src/node/api/server.ts
2020-02-06 14:00:38 -06:00

165 lines
4.3 KiB
TypeScript

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<HttpResponse | undefined> {
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<true> {
if (!this.authenticated(request)) {
throw new Error("not authenticated")
}
await new Promise<ws>((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<ServerMessage> {
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<HttpResponse<LoginResponse>> {
// 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<HttpResponse<FilesResponse>> {
return {
content: {
files: [],
},
}
}
/**
* Return available applications.
*/
private async applications(): Promise<HttpResponse<ApplicationsResponse>> {
return {
content: {
applications: [Vscode],
},
}
}
}