From 781328b09adf24dffe5bdabc8f8b072f17f422e4 Mon Sep 17 00:00:00 2001 From: Kirill Konshin Date: Mon, 4 Nov 2024 19:36:14 -0800 Subject: [PATCH] Relaxed cookie support, better error handling Mention #2 --- demo/src-electron/index.ts | 9 +-- demo/src/app/test/route.ts | 5 ++ lib/package.json | 4 +- lib/src/index.ts | 154 +++++++++++++++++++------------------ lib/tsconfig.json | 2 +- yarn.lock | 10 +-- 6 files changed, 94 insertions(+), 90 deletions(-) diff --git a/demo/src-electron/index.ts b/demo/src-electron/index.ts index a6865c2..6b2d771 100644 --- a/demo/src-electron/index.ts +++ b/demo/src-electron/index.ts @@ -16,11 +16,6 @@ process.env['ELECTRON_ENABLE_LOGGING'] = 'true'; process.on('SIGTERM', () => process.exit(0)); process.on('SIGINT', () => process.exit(0)); -const openDevTools = () => { - mainWindow.setBounds({ width: 2000 }); - mainWindow.webContents.openDevTools(); -}; - // Next.js handler const standaloneDir = path.join(appPath, '.next', 'standalone', 'demo'); @@ -36,7 +31,7 @@ const { createInterceptor } = createHandler({ const createWindow = async () => { mainWindow = new BrowserWindow({ - width: 1000, + width: 1600, height: 800, webPreferences: { contextIsolation: true, // protect against prototype pollution @@ -53,7 +48,7 @@ const createWindow = async () => { // Next.js handler - mainWindow.once('ready-to-show', () => openDevTools()); + mainWindow.once('ready-to-show', () => mainWindow.webContents.openDevTools()); mainWindow.on('closed', () => { mainWindow = null; diff --git a/demo/src/app/test/route.ts b/demo/src/app/test/route.ts index 1a8e2d3..04b1731 100644 --- a/demo/src/app/test/route.ts +++ b/demo/src/app/test/route.ts @@ -17,5 +17,10 @@ export async function POST(req: NextRequest) { maxAge: 60 * 60, // 1 hour }); + res.cookies.set('sidebar:state', Date.now().toString(), { + path: '/', + maxAge: 60 * 60, // 1 hour + }); + return res; } diff --git a/lib/package.json b/lib/package.json index 19d5027..cc6a89b 100644 --- a/lib/package.json +++ b/lib/package.json @@ -1,6 +1,6 @@ { "name": "next-electron-rsc", - "version": "0.2.0", + "version": "0.2.1", "description": "Next.js + Electron + React Server Components", "main": "build/index.js", "main:src": "build/index.tsx", @@ -15,7 +15,7 @@ }, "license": "MIT", "dependencies": { - "cookie": "^1.0.1", + "cookie": "0.6.0", "resolve": "^1.22.8", "set-cookie-parser": "^2.7.1" }, diff --git a/lib/src/index.ts b/lib/src/index.ts index d0e7f1b..4ba7760 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -4,59 +4,64 @@ import type NextNodeServer from 'next/dist/server/next-server'; import { IncomingMessage, ServerResponse } from 'node:http'; import { Socket } from 'node:net'; +import { parse } from 'node:url'; +import path from 'node:path'; +import fs from 'node:fs'; +import assert from 'node:assert'; + import resolve from 'resolve'; -import { parse } from 'url'; -import path from 'path'; -import fs from 'fs'; import { parse as parseCookie, splitCookiesString } from 'set-cookie-parser'; import { serialize as serializeCookie } from 'cookie'; -import assert = require('node:assert'); async function createRequest({ socket, - origReq, + request, session, }: { socket: Socket; - origReq: Request; + request: Request; session: Session; }): Promise { const req = new IncomingMessage(socket); - const url = new URL(origReq.url); + const url = new URL(request.url); // Normal Next.js URL does not contain schema and host/port, otherwise endless loops due to butchering of schema by normalizeRepeatedSlashes in resolve-routes req.url = url.pathname + (url.search || ''); - req.method = origReq.method; + req.method = request.method; - origReq.headers.forEach((value, key) => { + request.headers.forEach((value, key) => { req.headers[key] = value; }); - // @see https://github.com/electron/electron/issues/39525#issue-1852825052 - const cookies = await session.cookies.get({ - url: origReq.url, - // domain: url.hostname, - // path: url.pathname, - // `secure: true` Cookies should not be sent via http - // secure: url.protocol === 'http:' ? false : undefined, - // theoretically not possible to implement sameSite because we don't know the url - // of the website that is requesting the resource - }); + try { + // @see https://github.com/electron/electron/issues/39525#issue-1852825052 + const cookies = await session.cookies.get({ + url: request.url, + // domain: url.hostname, + // path: url.pathname, + // `secure: true` Cookies should not be sent via http + // secure: url.protocol === 'http:' ? false : undefined, + // theoretically not possible to implement sameSite because we don't know the url + // of the website that is requesting the resource + }); - if (cookies.length) { - const cookiesHeader = []; + if (cookies.length) { + const cookiesHeader = []; - for (const cookie of cookies) { - const { name, value, ...options } = cookie; - cookiesHeader.push(serializeCookie(name, value)); // ...(options as any)? - } + for (const cookie of cookies) { + const { name, value, ...options } = cookie; + cookiesHeader.push(serializeCookie(name, value)); // ...(options as any)? + } - req.headers.cookie = cookiesHeader.join('; '); + req.headers.cookie = cookiesHeader.join('; '); + } + } catch (e) { + throw new Error('Failed to parse cookies', { cause: e }); } - if (origReq.body) { - req.push(Buffer.from(await origReq.arrayBuffer())); + if (request.body) { + req.push(Buffer.from(await request.arrayBuffer())); } req.push(null); @@ -141,9 +146,8 @@ export function createHandler({ debug?: boolean; }) { assert(standaloneDir, 'standaloneDir is required'); - assert(protocol, 'protocol is required'); - assert(fs.existsSync(standaloneDir), 'standaloneDir does not exist'); + assert(protocol, 'protocol is required'); const next = require(resolve.sync('next', { basedir: standaloneDir })); @@ -160,8 +164,6 @@ export function createHandler({ const preparePromise = app.prepare(); - let socket; - protocol.registerSchemesAsPrivileged([ { scheme: 'http', @@ -173,10 +175,6 @@ export function createHandler({ }, ]); - //TODO Return function to close socket - process.on('SIGTERM', () => socket.end()); - process.on('SIGINT', () => socket.end()); - /** * @param {import('electron').Session} session * @returns {() => void} @@ -184,20 +182,20 @@ export function createHandler({ function createInterceptor({ session }: { session: Session }) { assert(session, 'Session is required'); - socket = new Socket(); + const socket = new Socket(); + + const closeSocket = () => socket.end(); + + process.on('SIGTERM', () => closeSocket); + process.on('SIGINT', () => closeSocket); protocol.handle('http', async (request) => { try { - if (!request.url.startsWith(localhostUrl)) { - if (debug) console.log('[NEXT] External HTTP not supported', request.url); - throw new Error('External HTTP not supported, use HTTPS'); - } - - if (!socket) throw new Error('Socket is not initialized, check if createInterceptor was called'); + assert(request.url.startsWith(localhostUrl), 'External HTTP not supported, use HTTPS'); await preparePromise; - const req = await createRequest({ socket, origReq: request, session }); + const req = await createRequest({ socket, request, session }); const res = new ReadableServerResponse(req); const url = parse(req.url, true); @@ -205,37 +203,41 @@ export function createHandler({ const response = await res.getResponse(); - // @see https://github.com/electron/electron/issues/30717 - // @see https://github.com/electron/electron/issues/39525 - const cookies = parseCookie( - response.headers.getSetCookie().reduce((r, c) => { - // @see https://github.com/nfriedly/set-cookie-parser?tab=readme-ov-file#usage-in-react-native-and-with-some-other-fetch-implementations - return [...r, ...splitCookiesString(c)]; - }, []), - ); - - for (const cookie of cookies) { - const expires = cookie.expires - ? cookie.expires.getTime() - : cookie.maxAge - ? Date.now() + cookie.maxAge * 1000 - : undefined; + try { + // @see https://github.com/electron/electron/issues/30717 + // @see https://github.com/electron/electron/issues/39525 + const cookies = parseCookie( + response.headers.getSetCookie().reduce((r, c) => { + // @see https://github.com/nfriedly/set-cookie-parser?tab=readme-ov-file#usage-in-react-native-and-with-some-other-fetch-implementations + return [...r, ...splitCookiesString(c)]; + }, []), + ); - if (expires < Date.now()) { - await session.cookies.remove(request.url, cookie.name); - continue; + for (const cookie of cookies) { + const expires = cookie.expires + ? cookie.expires.getTime() + : cookie.maxAge + ? Date.now() + cookie.maxAge * 1000 + : undefined; + + if (expires < Date.now()) { + await session.cookies.remove(request.url, cookie.name); + continue; + } + + await session.cookies.set({ + name: cookie.name, + value: cookie.value, + path: cookie.path, + domain: cookie.domain, + secure: cookie.secure, + httpOnly: cookie.httpOnly, + url: request.url, + expirationDate: expires, + } as any); } - - await session.cookies.set({ - name: cookie.name, - value: cookie.value, - path: cookie.path, - domain: cookie.domain, - secure: cookie.secure, - httpOnly: cookie.httpOnly, - url: request.url, - expirationDate: expires, - } as any); + } catch (e) { + throw new Error('Failed to set cookies', { cause: e }); } if (debug) console.log('[NEXT] Handler', request.url, response.status); @@ -246,9 +248,11 @@ export function createHandler({ } }); - return () => { + return function stopIntercept() { protocol.unhandle('http'); - socket.end(); + process.off('SIGTERM', () => closeSocket); + process.off('SIGINT', () => closeSocket); + closeSocket(); }; } diff --git a/lib/tsconfig.json b/lib/tsconfig.json index 58df350..90216d8 100644 --- a/lib/tsconfig.json +++ b/lib/tsconfig.json @@ -6,7 +6,7 @@ "esModuleInterop": true, "experimentalDecorators": true, "forceConsistentCasingInFileNames": true, - "lib": ["es2021", "dom", "ESNext.Promise"], + "lib": ["es2021", "es2022", "dom", "ESNext.Promise"], "jsx": "react", "moduleResolution": "node", "target": "es6", diff --git a/yarn.lock b/yarn.lock index 1759559..4e18d78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1933,10 +1933,10 @@ __metadata: languageName: node linkType: hard -"cookie@npm:^1.0.1": - version: 1.0.1 - resolution: "cookie@npm:1.0.1" - checksum: 10/4b24d4fad5ba94ab76d74a8fc33ae1dcdb5dc02013e03e9577b26f019d9dfe396ffb9b3711ba1726bcfa1b93c33d117db0f31e187838aed7753dee1abc691688 +"cookie@npm:0.6.0": + version: 0.6.0 + resolution: "cookie@npm:0.6.0" + checksum: 10/c1f8f2ea7d443b9331680598b0ae4e6af18a618c37606d1bbdc75bec8361cce09fe93e727059a673f2ba24467131a9fb5a4eec76bb1b149c1b3e1ccb268dc583 languageName: node linkType: hard @@ -4959,7 +4959,7 @@ __metadata: "@types/react-dom": "npm:^18.3.1" "@types/resolve": "npm:^1.20.6" "@types/set-cookie-parser": "npm:^2" - cookie: "npm:^1.0.1" + cookie: "npm:0.6.0" electron: "npm:^33.0.2" next: "npm:^15.0.2" resolve: "npm:^1.22.8"