|
6 | 6 | unlinkSync, |
7 | 7 | writeFileSync, |
8 | 8 | } from "node:fs"; |
| 9 | +import { createServer } from "node:http"; |
| 10 | +import type { AddressInfo } from "node:net"; |
9 | 11 | import { homedir } from "node:os"; |
10 | 12 | import { dirname, join } from "node:path"; |
11 | 13 | import { BASE_URL } from "@/const.js"; |
@@ -222,83 +224,85 @@ async function getAuthCode( |
222 | 224 | } |
223 | 225 | }; |
224 | 226 |
|
225 | | - const server = Bun.serve({ |
226 | | - port: 0, |
227 | | - hostname: "127.0.0.1", |
228 | | - fetch(req) { |
229 | | - const url = new URL(req.url); |
230 | | - if (url.pathname !== "/callback") { |
231 | | - return new Response("Not found", { status: 404 }); |
232 | | - } |
233 | | - |
234 | | - const code = url.searchParams.get("code"); |
235 | | - const error = url.searchParams.get("error"); |
236 | | - const returnedState = url.searchParams.get("state"); |
237 | | - |
238 | | - if (error) { |
239 | | - const desc = url.searchParams.get("error_description") || error; |
240 | | - finish(); |
241 | | - server.stop(); |
242 | | - reject(new Error(`SSO login failed: ${desc}`)); |
243 | | - return new Response(loginResultHtml(false, desc), { |
244 | | - headers: { "Content-Type": "text/html" }, |
245 | | - }); |
246 | | - } |
247 | | - |
248 | | - if (!code) { |
249 | | - finish(); |
250 | | - server.stop(); |
251 | | - reject(new Error("No authorization code received")); |
252 | | - return new Response( |
253 | | - loginResultHtml(false, "No authorization code received"), |
254 | | - { headers: { "Content-Type": "text/html" } }, |
255 | | - ); |
256 | | - } |
257 | | - |
258 | | - if (returnedState !== expectedState) { |
259 | | - finish(); |
260 | | - server.stop(); |
261 | | - reject(new Error("State mismatch (possible CSRF attack)")); |
262 | | - return new Response(loginResultHtml(false, "State mismatch"), { |
263 | | - headers: { "Content-Type": "text/html" }, |
264 | | - }); |
265 | | - } |
| 227 | + const server = createServer((req, res) => { |
| 228 | + const host = req.headers.host ?? "127.0.0.1"; |
| 229 | + const url = new URL(req.url ?? "/", `http://${host}`); |
266 | 230 |
|
| 231 | + if (url.pathname !== "/callback") { |
| 232 | + res.writeHead(404, { "Content-Type": "text/plain" }); |
| 233 | + res.end("Not found"); |
| 234 | + return; |
| 235 | + } |
| 236 | + |
| 237 | + const code = url.searchParams.get("code"); |
| 238 | + const error = url.searchParams.get("error"); |
| 239 | + const returnedState = url.searchParams.get("state"); |
| 240 | + |
| 241 | + const respond = (ok: boolean, msg?: string) => { |
| 242 | + res.writeHead(ok ? 200 : 400, { "Content-Type": "text/html" }); |
| 243 | + res.end(loginResultHtml(ok, msg)); |
| 244 | + }; |
| 245 | + |
| 246 | + if (error) { |
| 247 | + const desc = url.searchParams.get("error_description") || error; |
| 248 | + respond(false, desc); |
267 | 249 | finish(); |
268 | | - server.stop(); |
269 | | - resolve({ |
270 | | - code, |
271 | | - redirectUri: `http://127.0.0.1:${server.port}/callback`, |
272 | | - }); |
273 | | - |
274 | | - return new Response(loginResultHtml(true), { |
275 | | - headers: { "Content-Type": "text/html" }, |
276 | | - }); |
277 | | - }, |
278 | | - }); |
| 250 | + server.close(); |
| 251 | + reject(new Error(`SSO login failed: ${desc}`)); |
| 252 | + return; |
| 253 | + } |
279 | 254 |
|
280 | | - const redirectUri = `http://127.0.0.1:${server.port}/callback`; |
281 | | - const authorizeUrl = |
282 | | - `${SSO_BASE_URL}/authorize?` + |
283 | | - `response_type=code` + |
284 | | - `&client_id=${encodeURIComponent(CLIENT_ID)}` + |
285 | | - `&redirect_uri=${encodeURIComponent(redirectUri)}` + |
286 | | - `&scope=openid%20profile%20email%20offline_access` + |
287 | | - `&state=${expectedState}` + |
288 | | - `&nonce=${nonce}` + |
289 | | - `&code_challenge=${codeChallenge}` + |
290 | | - `&code_challenge_method=S256`; |
291 | | - |
292 | | - onReady(() => { |
293 | | - onLog("Opening browser for SSO login..."); |
294 | | - openBrowser(authorizeUrl); |
| 255 | + if (!code) { |
| 256 | + respond(false, "No authorization code received"); |
| 257 | + finish(); |
| 258 | + server.close(); |
| 259 | + reject(new Error("No authorization code received")); |
| 260 | + return; |
| 261 | + } |
| 262 | + |
| 263 | + if (returnedState !== expectedState) { |
| 264 | + respond(false, "State mismatch"); |
| 265 | + finish(); |
| 266 | + server.close(); |
| 267 | + reject(new Error("State mismatch (possible CSRF attack)")); |
| 268 | + return; |
| 269 | + } |
| 270 | + |
| 271 | + const { port } = server.address() as AddressInfo; |
| 272 | + respond(true); |
| 273 | + finish(); |
| 274 | + server.close(); |
| 275 | + resolve({ |
| 276 | + code, |
| 277 | + redirectUri: `http://127.0.0.1:${port}/callback`, |
| 278 | + }); |
295 | 279 | }); |
296 | 280 |
|
297 | | - timeoutHandle = setTimeout(() => { |
298 | | - timeoutHandle = null; |
299 | | - server.stop(); |
300 | | - reject(new Error("Login timed out after 120 seconds")); |
301 | | - }, 120_000); |
| 281 | + server.listen(0, "127.0.0.1", () => { |
| 282 | + const { port } = server.address() as AddressInfo; |
| 283 | + const redirectUri = `http://127.0.0.1:${port}/callback`; |
| 284 | + const authorizeUrl = |
| 285 | + `${SSO_BASE_URL}/authorize?` + |
| 286 | + `response_type=code` + |
| 287 | + `&client_id=${encodeURIComponent(CLIENT_ID)}` + |
| 288 | + `&redirect_uri=${encodeURIComponent(redirectUri)}` + |
| 289 | + `&scope=openid%20profile%20email%20offline_access` + |
| 290 | + `&state=${expectedState}` + |
| 291 | + `&nonce=${nonce}` + |
| 292 | + `&code_challenge=${codeChallenge}` + |
| 293 | + `&code_challenge_method=S256`; |
| 294 | + |
| 295 | + onReady(() => { |
| 296 | + onLog("Opening browser for SSO login..."); |
| 297 | + openBrowser(authorizeUrl); |
| 298 | + }); |
| 299 | + |
| 300 | + timeoutHandle = setTimeout(() => { |
| 301 | + timeoutHandle = null; |
| 302 | + server.close(); |
| 303 | + reject(new Error("Login timed out after 120 seconds")); |
| 304 | + }, 120_000); |
| 305 | + }); |
302 | 306 | }); |
303 | 307 | } |
304 | 308 |
|
|
0 commit comments