111 lines
4.5 KiB
JavaScript
111 lines
4.5 KiB
JavaScript
import http from "http";
|
|
import https from "https";
|
|
import { URL } from "url";
|
|
import querystring from "querystring";
|
|
import zlib from "zlib";
|
|
class fetch {
|
|
cookies = {};
|
|
constructor() { }
|
|
parseCookies(cookieHeader) {
|
|
if (!cookieHeader)
|
|
return;
|
|
cookieHeader.split(";").forEach(cookie => {
|
|
const [key, value] = cookie.split("=");
|
|
if (key && value)
|
|
this.cookies[key.trim()] = value.trim();
|
|
});
|
|
}
|
|
formatCookies() {
|
|
return Object.entries(this.cookies)
|
|
.map(([key, value]) => `${key}=${value}`)
|
|
.join("; ");
|
|
}
|
|
decompress(data, encoding) {
|
|
return encoding === "br" ? zlib.brotliDecompressSync(data) :
|
|
encoding === "gzip" ? zlib.gunzipSync(data) :
|
|
encoding === "deflate" ? zlib.inflateSync(data) :
|
|
data;
|
|
}
|
|
readData(res, mode) {
|
|
return new Promise((resolve, reject) => {
|
|
const chunks = [];
|
|
res
|
|
.on("data", chunk => chunks.push(chunk))
|
|
.on("end", () => {
|
|
try {
|
|
const data = this.decompress(Buffer.concat(chunks), res.headers["content-encoding"]);
|
|
resolve(mode === "json" ? JSON.parse(data.toString("utf8")) :
|
|
mode === "buffer" ? data :
|
|
mode === "arraybuffer" ? new Uint8Array(data).buffer :
|
|
data.toString("utf8"));
|
|
}
|
|
catch (err) {
|
|
reject(err);
|
|
}
|
|
})
|
|
.on("error", reject);
|
|
});
|
|
}
|
|
async fetch(urlString, options = {}, redirectCount = 0) {
|
|
options.followRedirects = options.followRedirects ?? true;
|
|
const { protocol, hostname, pathname, search, port } = new URL(urlString);
|
|
const body = options.method === "POST" && options.body
|
|
? options.headers?.["Content-Type"] === "application/json"
|
|
? JSON.stringify(options.body)
|
|
: querystring.stringify(options.body)
|
|
: null;
|
|
const requestOptions = {
|
|
hostname,
|
|
port: port || (protocol === "https:" ? 443 : 80),
|
|
path: pathname + search,
|
|
method: options.method || "GET",
|
|
headers: {
|
|
...options.headers,
|
|
...(body ? { "Content-Length": Buffer.byteLength(body) } : {}),
|
|
"Cookie": this.formatCookies(),
|
|
"Accept-Encoding": "br, gzip, deflate",
|
|
}
|
|
};
|
|
return new Promise((resolve, reject) => {
|
|
const requester = protocol === "https:" ? https : http;
|
|
const req = requester.request(requestOptions, res => {
|
|
this.parseCookies(res.headers["set-cookie"]?.join("; "));
|
|
if (options.followRedirects && res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
req.destroy();
|
|
if (redirectCount >= (options.maxRedirects || 5))
|
|
return reject(new Error("Max redirects exceeded"));
|
|
if (!res.headers.location)
|
|
return reject(new Error("Redirect location not provided"));
|
|
const nextUrl = new URL(res.headers.location, urlString);
|
|
return resolve(this.fetch(nextUrl.toString(), options, redirectCount + 1));
|
|
}
|
|
return resolve({
|
|
body: res,
|
|
headers: res.headers,
|
|
cookies: this.cookies,
|
|
text: () => this.readData(res, "text"),
|
|
json: () => this.readData(res, "json"),
|
|
buffer: () => this.readData(res, "buffer"),
|
|
arrayBuffer: () => this.readData(res, "arraybuffer"),
|
|
});
|
|
});
|
|
req.setTimeout(options.timeout || 5e3, () => {
|
|
req.destroy();
|
|
reject(new Error("Request timed out"));
|
|
});
|
|
req.on("error", reject);
|
|
if (options.signal) {
|
|
options.signal.addEventListener("abort", () => {
|
|
req.destroy(new Error("Request aborted"));
|
|
reject(new Error("Request aborted"));
|
|
});
|
|
}
|
|
if (body)
|
|
req.write(body);
|
|
req.end();
|
|
});
|
|
}
|
|
}
|
|
const inst = new fetch();
|
|
export default inst.fetch.bind(inst);
|