Skip to content

Commit

Permalink
Relaxed cookie support, better error handling
Browse files Browse the repository at this point in the history
Mention #2
  • Loading branch information
kirill-konshin committed Nov 5, 2024
1 parent 6ed70f9 commit 781328b
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 90 deletions.
9 changes: 2 additions & 7 deletions demo/src-electron/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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
Expand All @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions demo/src/app/test/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
4 changes: 2 additions & 2 deletions lib/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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"
},
Expand Down
154 changes: 79 additions & 75 deletions lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IncomingMessage> {
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);
Expand Down Expand Up @@ -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 }));

Expand All @@ -160,8 +164,6 @@ export function createHandler({

const preparePromise = app.prepare();

let socket;

protocol.registerSchemesAsPrivileged([
{
scheme: 'http',
Expand All @@ -173,69 +175,69 @@ 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}
*/
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);

handler(req, res, url);

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);
Expand All @@ -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();
};
}

Expand Down
2 changes: 1 addition & 1 deletion lib/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 781328b

Please sign in to comment.