Skip to content

Instantly share code, notes, and snippets.

@mrchaofan
Created May 21, 2024 20:17
Show Gist options
  • Select an option

  • Save mrchaofan/989109b82706f5b87a435604ada9a333 to your computer and use it in GitHub Desktop.

Select an option

Save mrchaofan/989109b82706f5b87a435604ada9a333 to your computer and use it in GitHub Desktop.
electron-fix
const { app, net, session, BrowserWindow } = require("electron");
const { patchSessionFetch, patchSessionProtocol } = require("./patch");
app.whenReady().then(async () => {
patchSessionFetch(session.defaultSession);
patchSessionProtocol(session.defaultSession);
session.defaultSession.protocol.handle("https", async (req) => {
req.headers.set("referer", "https://docs.qq.com");
return await session.defaultSession.fetch(req, {
bypassCustomProtocolHandlers: true,
redirect: "manual",
});
});
const window = new BrowserWindow({
width: 1200,
height: 900,
});
window.loadURL("https://docs.qq.com/desktop");
});
{
"name": "fxxk",
"version": "1.0.0",
"main": "main.js",
"license": "MIT",
"dependencies": {
"@types/node": "^20.12.12",
"electron": "25.8.1"
}
}
const { net } = require("electron");
const { Readable } = require("stream");
const netRequest = net.request;
function patch(obj, method, impl) {
const ret = {
_obj: obj,
_method: method,
_impl: obj[method],
};
obj[method] = impl;
return ret;
}
function unpatch(obj) {
if (obj._obj) {
obj._obj[obj._method] = obj._impl;
delete obj._obj;
delete obj._method;
delete obj._impl;
}
}
/**
*
* @param {Electron.Session} session
* @returns {() => void}
*/
function patchSessionFetch(session) {
const sessionFetch = session.fetch;
const obj = patch(session, "fetch", function fetch(...args) {
return new Promise((resolve, reject) => {
let loader;
let handleRedirect = false;
const options = args.find(
(arg) => Object.prototype.toString.call(arg) === "[object Object]"
);
if (options != null && options.redirect === "manual") {
handleRedirect = true;
}
const obj = patch(net, "request", (...args) => {
loader = netRequest(...args);
if (handleRedirect) {
loader.once("redirect", (status, _, url) => {
resolve(Response.redirect(url, status));
});
}
return loader;
});
let promiseRes;
try {
promiseRes = sessionFetch.call(this, ...args);
} catch (er) {
reject(er);
} finally {
unpatch(obj);
}
promiseRes.then(resolve, reject);
});
});
return () => {
unpatch(obj);
};
}
const ERR_FAILED = -2;
const ERR_UNEXPECTED = -9;
const isBuiltInScheme = (scheme) => ["http", "https", "file"].includes(scheme);
function makeStreamFromPipe(pipe) {
const buf = new Uint8Array(1024 * 1024 /* 1 MB */);
return new ReadableStream({
async pull(controller) {
try {
const rv = await pipe.read(buf);
if (rv > 0) {
controller.enqueue(buf.slice(0, rv));
} else {
controller.close();
}
} catch (e) {
controller.error(e);
}
},
});
}
function makeStreamFromFileInfo({ filePath, offset = 0, length = -1 }) {
return Readable.toWeb(
createReadStream(filePath, {
start: offset,
end: length >= 0 ? offset + length : undefined,
})
);
}
function convertToRequestBody(session, uploadData) {
if (!uploadData) return null;
// Optimization: skip creating a stream if the request is just a single buffer.
if (uploadData.length === 1 && uploadData[0].type === "rawData")
return uploadData[0].bytes;
const chunks = [...uploadData];
let current = null;
return new ReadableStream({
async pull(controller) {
if (current) {
const { done, value } = await current.read();
// (done => value === undefined) as per WHATWG spec
if (done) {
current = null;
return this.pull(controller);
} else {
controller.enqueue(value);
}
} else {
if (!chunks.length) {
return controller.close();
}
const chunk = chunks.shift();
if (chunk.type === "rawData") {
controller.enqueue(chunk.bytes);
} else if (chunk.type === "file") {
current = makeStreamFromFileInfo(chunk).getReader();
return this.pull(controller);
} else if (chunk.type === "stream") {
current = makeStreamFromPipe(chunk.body).getReader();
return this.pull(controller);
} else if (chunk.type === "blob") {
// Note that even though `getBlobData()` is a `Session` API, it doesn't
// actually use the `Session` context. Its implementation solely relies
// on global variables which allows us to implement this feature without
// knowledge of the `Session` associated with the current request by
// always pulling `Blob` data out of the default `Session`.
controller.enqueue(await session.getBlobData(chunk.blobUUID));
} else {
throw new Error(`Unknown upload data chunk type: ${chunk.type}`);
}
}
},
});
}
function validateResponse(res) {
if (!res || typeof res !== "object") return false;
if (res.type === "error") return true;
const exists = (key) => Object.hasOwn(res, key);
if (exists("status") && typeof res.status !== "number") return false;
if (exists("statusText") && typeof res.statusText !== "string") return false;
if (exists("headers") && typeof res.headers !== "object") return false;
if (exists("body")) {
if (typeof res.body !== "object") return false;
if (res.body !== null && !res.body) return false;
}
return true;
}
/**
*
* @param {Electron.Session} session
* @returns {() => void}
*/
function patchSessionProtocol(session) {
const obj = patch(
session.protocol,
"handle",
function handle(scheme, handler) {
const register = isBuiltInScheme(scheme)
? this.interceptProtocol
: this.registerProtocol;
const success = register.call(this, scheme, async (preq, cb) => {
try {
const body = convertToRequestBody(session, preq.uploadData);
const headers = new Headers(preq.headers);
if (headers.get("origin") === "null") {
headers.delete("origin");
}
const req = new Request(preq.url, {
headers,
method: preq.method,
referrer: preq.referrer,
body,
duplex: body instanceof ReadableStream ? "half" : undefined,
});
const res = await handler(req);
if (!validateResponse(res)) {
return cb({ error: ERR_UNEXPECTED });
} else if (res.type === "error") {
cb({ error: ERR_FAILED });
} else {
cb({
data: res.body ? Readable.fromWeb(res.body) : null,
headers: res.headers ? Object.fromEntries(res.headers) : {},
statusCode: res.status,
statusText: res.statusText,
mimeType: res.__original_resp?._responseHead?.mimeType,
});
}
} catch (e) {
console.error(e);
cb({ error: ERR_UNEXPECTED });
}
});
if (!success) throw new Error(`Failed to register protocol: ${scheme}`);
}
);
return () => {
unpatch(obj);
};
}
exports.patch = patch;
exports.unpatch = unpatch;
exports.patchSessionFetch = patchSessionFetch;
exports.patchSessionProtocol = patchSessionProtocol;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment