Skip to content

Commit f763319

Browse files
authored
Merge pull request #2160 from cdr/github-auth
Fix GitHub auth
2 parents c7baf5d + 5f7f7f1 commit f763319

File tree

4 files changed

+121
-4
lines changed

4 files changed

+121
-4
lines changed

lib/vscode/extensions/github-authentication/src/githubServer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Logger from './common/logger';
1313
const localize = nls.loadMessageBundle();
1414

1515
export const NETWORK_ERROR = 'network error';
16-
const AUTH_RELAY_SERVER = 'vscode-auth.github.com';
16+
const AUTH_RELAY_SERVER = 'auth.code-server.dev';
1717

1818
class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
1919
public handleUri(uri: vscode.Uri) {

src/common/util.ts

+11
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,14 @@ export const arrayify = <T>(value?: T | T[]): T[] => {
101101
}
102102
return [value]
103103
}
104+
105+
/**
106+
* Get the first string. If there's no string return undefined.
107+
*/
108+
export const getFirstString = (value: string | string[] | object | undefined): string | undefined => {
109+
if (Array.isArray(value)) {
110+
return value[0]
111+
}
112+
113+
return typeof value === "string" ? value : undefined
114+
}

src/node/routes/static.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Readable } from "stream"
66
import * as tarFs from "tar-fs"
77
import * as zlib from "zlib"
88
import { HttpCode, HttpError } from "../../common/http"
9+
import { getFirstString } from "../../common/util"
910
import { rootPath } from "../constants"
1011
import { authenticated, ensureAuthenticated, replaceTemplates } from "../http"
1112
import { getMediaMime, pathToFsPath } from "../util"
@@ -15,8 +16,8 @@ export const router = Router()
1516
// The commit is for caching.
1617
router.get("/(:commit)(/*)?", async (req, res) => {
1718
// Used by VS Code to load extensions into the web worker.
18-
const tar = Array.isArray(req.query.tar) ? req.query.tar[0] : req.query.tar
19-
if (typeof tar === "string") {
19+
const tar = getFirstString(req.query.tar)
20+
if (tar) {
2021
ensureAuthenticated(req)
2122
let stream: Readable = tarFs.pack(pathToFsPath(tar))
2223
if (req.headers["accept-encoding"] && req.headers["accept-encoding"].includes("gzip")) {

src/node/routes/vscode.ts

+106-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import * as crypto from "crypto"
2-
import { Router } from "express"
2+
import { Request, Router } from "express"
33
import { promises as fs } from "fs"
44
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"
59
import { commit, rootPath, version } from "../constants"
610
import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http"
711
import { getMediaMime, pathToFsPath } from "../util"
@@ -86,6 +90,107 @@ router.get("/webview/*", ensureAuthenticated, async (req, res) => {
8690
)
8791
})
8892

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+
89194
export const wsRouter = WsRouter()
90195

91196
wsRouter.ws("/", ensureAuthenticated, async (req) => {

0 commit comments

Comments
 (0)