|
1 | 1 | import * as crypto from "crypto"
|
2 |
| -import { Router } from "express" |
| 2 | +import { Request, Router } from "express" |
3 | 3 | import { promises as fs } from "fs"
|
4 | 4 | import * as path from "path"
|
| 5 | +import qs from "qs" |
| 6 | +import { Emitter } from "../../common/emitter" |
| 7 | +import { HttpCode, HttpError } from "../../common/http" |
| 8 | +import { getFirstString } from "../../common/util" |
5 | 9 | import { commit, rootPath, version } from "../constants"
|
6 | 10 | import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http"
|
7 | 11 | import { getMediaMime, pathToFsPath } from "../util"
|
@@ -86,6 +90,107 @@ router.get("/webview/*", ensureAuthenticated, async (req, res) => {
|
86 | 90 | )
|
87 | 91 | })
|
88 | 92 |
|
| 93 | +interface Callback { |
| 94 | + uri: { |
| 95 | + scheme: string |
| 96 | + authority?: string |
| 97 | + path?: string |
| 98 | + query?: string |
| 99 | + fragment?: string |
| 100 | + } |
| 101 | + timeout: NodeJS.Timeout |
| 102 | +} |
| 103 | + |
| 104 | +const callbacks = new Map<string, Callback>() |
| 105 | +const callbackEmitter = new Emitter<{ id: string; callback: Callback }>() |
| 106 | + |
| 107 | +/** |
| 108 | + * Get vscode-requestId from the query and throw if it's missing or invalid. |
| 109 | + */ |
| 110 | +const getRequestId = (req: Request): string => { |
| 111 | + if (!req.query["vscode-requestId"]) { |
| 112 | + throw new HttpError("vscode-requestId is missing", HttpCode.BadRequest) |
| 113 | + } |
| 114 | + |
| 115 | + if (typeof req.query["vscode-requestId"] !== "string") { |
| 116 | + throw new HttpError("vscode-requestId is not a string", HttpCode.BadRequest) |
| 117 | + } |
| 118 | + |
| 119 | + return req.query["vscode-requestId"] |
| 120 | +} |
| 121 | + |
| 122 | +// Matches VS Code's fetch timeout. |
| 123 | +const fetchTimeout = 5 * 60 * 1000 |
| 124 | + |
| 125 | +// The callback endpoints are used during authentication. A URI is stored on |
| 126 | +// /callback and then fetched later on /fetch-callback. |
| 127 | +// See ../../../lib/vscode/resources/web/code-web.js |
| 128 | +router.get("/callback", ensureAuthenticated, async (req, res) => { |
| 129 | + const uriKeys = [ |
| 130 | + "vscode-requestId", |
| 131 | + "vscode-scheme", |
| 132 | + "vscode-authority", |
| 133 | + "vscode-path", |
| 134 | + "vscode-query", |
| 135 | + "vscode-fragment", |
| 136 | + ] |
| 137 | + |
| 138 | + const id = getRequestId(req) |
| 139 | + |
| 140 | + // Move any query variables that aren't URI keys into the URI's query |
| 141 | + // (importantly, this will include the code for oauth). |
| 142 | + const query: qs.ParsedQs = {} |
| 143 | + for (const key in req.query) { |
| 144 | + if (!uriKeys.includes(key)) { |
| 145 | + query[key] = req.query[key] |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + const callback = { |
| 150 | + uri: { |
| 151 | + scheme: getFirstString(req.query["vscode-scheme"]) || "code-oss", |
| 152 | + authority: getFirstString(req.query["vscode-authority"]), |
| 153 | + path: getFirstString(req.query["vscode-path"]), |
| 154 | + query: (getFirstString(req.query.query) || "") + "&" + qs.stringify(query), |
| 155 | + fragment: getFirstString(req.query["vscode-fragment"]), |
| 156 | + }, |
| 157 | + // Make sure the map doesn't leak if nothing fetches this URI. |
| 158 | + timeout: setTimeout(() => callbacks.delete(id), fetchTimeout), |
| 159 | + } |
| 160 | + |
| 161 | + callbacks.set(id, callback) |
| 162 | + callbackEmitter.emit({ id, callback }) |
| 163 | + |
| 164 | + res.sendFile(path.join(rootPath, "lib/vscode/resources/web/callback.html")) |
| 165 | +}) |
| 166 | + |
| 167 | +router.get("/fetch-callback", ensureAuthenticated, async (req, res) => { |
| 168 | + const id = getRequestId(req) |
| 169 | + |
| 170 | + const send = (callback: Callback) => { |
| 171 | + clearTimeout(callback.timeout) |
| 172 | + callbacks.delete(id) |
| 173 | + res.json(callback.uri) |
| 174 | + } |
| 175 | + |
| 176 | + const callback = callbacks.get(id) |
| 177 | + if (callback) { |
| 178 | + return send(callback) |
| 179 | + } |
| 180 | + |
| 181 | + // VS Code will try again if the route returns no content but it seems more |
| 182 | + // efficient to just wait on this request for as long as possible? |
| 183 | + const handler = callbackEmitter.event(({ id: emitId, callback }) => { |
| 184 | + if (id === emitId) { |
| 185 | + handler.dispose() |
| 186 | + send(callback) |
| 187 | + } |
| 188 | + }) |
| 189 | + |
| 190 | + // If the client closes the connection. |
| 191 | + req.on("close", () => handler.dispose()) |
| 192 | +}) |
| 193 | + |
89 | 194 | export const wsRouter = WsRouter()
|
90 | 195 |
|
91 | 196 | wsRouter.ws("/", ensureAuthenticated, async (req) => {
|
|
0 commit comments