mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-06 01:20:41 +00:00
support for request bodies
This commit is contained in:
@@ -33,188 +33,211 @@ function parseHTTPHead(head) {
|
||||
// TODO optional redirect handling
|
||||
|
||||
export function pFetch(...args) {
|
||||
return new Promise((res, rej) => {
|
||||
const reqObj = new Request(...args);
|
||||
const parsedURL = new URL(reqObj.url);
|
||||
let headers = new Headers(reqObj.headers); // Make a headers object we can modify
|
||||
return new Promise(async (res, rej) => {
|
||||
try {
|
||||
const reqObj = new Request(...args);
|
||||
const parsedURL = new URL(reqObj.url);
|
||||
let headers = new Headers(reqObj.headers); // Make a headers object we can modify
|
||||
|
||||
// Socket creation: regular for HTTP, TLS for https
|
||||
let socket;
|
||||
if (parsedURL.protocol === "http:") {
|
||||
socket = new puter.net.Socket(
|
||||
parsedURL.hostname,
|
||||
parsedURL.port || 80,
|
||||
);
|
||||
} else if (parsedURL.protocol === "https:") {
|
||||
socket = new puter.net.tls.TLSSocket(
|
||||
parsedURL.hostname,
|
||||
parsedURL.port || 443,
|
||||
);
|
||||
} else {
|
||||
rej(
|
||||
`Failed to fetch. URL scheme "${parsedURL.protocol}" is not supported.`,
|
||||
);
|
||||
}
|
||||
// Socket creation: regular for HTTP, TLS for https
|
||||
let socket;
|
||||
if (parsedURL.protocol === "http:") {
|
||||
socket = new puter.net.Socket(
|
||||
parsedURL.hostname,
|
||||
parsedURL.port || 80,
|
||||
);
|
||||
} else if (parsedURL.protocol === "https:") {
|
||||
socket = new puter.net.tls.TLSSocket(
|
||||
parsedURL.hostname,
|
||||
parsedURL.port || 443,
|
||||
);
|
||||
} else {
|
||||
rej(
|
||||
`Failed to fetch. URL scheme "${parsedURL.protocol}" is not supported.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Sending default UA
|
||||
if (!headers.get("user-agent")) {
|
||||
headers.set("user-agent", navigator.userAgent);
|
||||
}
|
||||
// Sending default UA
|
||||
if (!headers.get("user-agent")) {
|
||||
headers.set("user-agent", navigator.userAgent);
|
||||
}
|
||||
|
||||
let reqHead = `${reqObj.method} ${parsedURL.pathname}${parsedURL.search} HTTP/1.1\r\nHost: ${parsedURL.host}\r\nConnection: close\r\n`;
|
||||
for (const [key, value] of headers) {
|
||||
reqHead += `${key}: ${value}\r\n`;
|
||||
}
|
||||
reqHead += "\r\n";
|
||||
let reqHead = `${reqObj.method} ${parsedURL.pathname}${parsedURL.search} HTTP/1.1\r\nHost: ${parsedURL.host}\r\nConnection: close\r\n`;
|
||||
for (const [key, value] of headers) {
|
||||
reqHead += `${key}: ${value}\r\n`;
|
||||
}
|
||||
let requestBody;
|
||||
if (reqObj.body) {
|
||||
requestBody = new Uint8Array(await reqObj.arrayBuffer());
|
||||
// If we have a body, we need to set the content length
|
||||
if (!headers.has("content-length")) {
|
||||
headers.set("content-length", requestBody.length);
|
||||
} else if (
|
||||
headers.get("content-length") !== String(requestBody.length)
|
||||
) {
|
||||
return rej(
|
||||
"Content-Length header does not match the body length. Please check your request.",
|
||||
);
|
||||
}
|
||||
reqHead += `Content-Length: ${requestBody.length}\r\n`;
|
||||
}
|
||||
|
||||
socket.on("open", () => {
|
||||
socket.write(reqHead);
|
||||
});
|
||||
const decoder = new TextDecoder();
|
||||
let responseHead = "";
|
||||
let dataOffset = -1;
|
||||
const fullDataParts = [];
|
||||
let responseReturned = false;
|
||||
let contentLength = -1;
|
||||
let ingestedContent = 0;
|
||||
let chunkedTransfer = false;
|
||||
let currentChunkLeft = -1;
|
||||
let buffer = new Uint8Array(0);
|
||||
reqHead += "\r\n";
|
||||
|
||||
const outStream = new ReadableStream({
|
||||
start(controller) {
|
||||
// This is annoyingly long
|
||||
function parseIncomingChunk(data) {
|
||||
// append new data to our rolling buffer
|
||||
const tmp = new Uint8Array(buffer.length + data.length);
|
||||
tmp.set(buffer, 0);
|
||||
tmp.set(data, buffer.length);
|
||||
buffer = tmp;
|
||||
socket.on("open", async () => {
|
||||
socket.write(reqHead); // Send headers
|
||||
if (requestBody) {
|
||||
console.log("Sending body", requestBody);
|
||||
socket.write(requestBody); // Send body if present
|
||||
}
|
||||
});
|
||||
const decoder = new TextDecoder();
|
||||
let responseHead = "";
|
||||
let dataOffset = -1;
|
||||
const fullDataParts = [];
|
||||
let responseReturned = false;
|
||||
let contentLength = -1;
|
||||
let ingestedContent = 0;
|
||||
let chunkedTransfer = false;
|
||||
let currentChunkLeft = -1;
|
||||
let buffer = new Uint8Array(0);
|
||||
|
||||
// pull out as many complete chunks (or headers) as we can
|
||||
while (true) {
|
||||
if (currentChunkLeft > 0) {
|
||||
// we’re in the middle of reading a chunk body
|
||||
// need size + 2 bytes (for trailing \r\n)
|
||||
if (buffer.length >= currentChunkLeft + 2) {
|
||||
// full body + CRLF available
|
||||
const chunk = buffer.slice(0, currentChunkLeft);
|
||||
controller.enqueue(chunk);
|
||||
const outStream = new ReadableStream({
|
||||
start(controller) {
|
||||
// This is annoyingly long
|
||||
function parseIncomingChunk(data) {
|
||||
// append new data to our rolling buffer
|
||||
const tmp = new Uint8Array(buffer.length + data.length);
|
||||
tmp.set(buffer, 0);
|
||||
tmp.set(data, buffer.length);
|
||||
buffer = tmp;
|
||||
|
||||
// strip body + CRLF and reset for next header
|
||||
buffer = buffer.slice(currentChunkLeft + 2);
|
||||
currentChunkLeft = 0;
|
||||
// pull out as many complete chunks (or headers) as we can
|
||||
while (true) {
|
||||
if (currentChunkLeft > 0) {
|
||||
// we’re in the middle of reading a chunk body
|
||||
// need size + 2 bytes (for trailing \r\n)
|
||||
if (buffer.length >= currentChunkLeft + 2) {
|
||||
// full body + CRLF available
|
||||
const chunk = buffer.slice(0, currentChunkLeft);
|
||||
controller.enqueue(chunk);
|
||||
|
||||
// strip body + CRLF and reset for next header
|
||||
buffer = buffer.slice(currentChunkLeft + 2);
|
||||
currentChunkLeft = 0;
|
||||
} else {
|
||||
// only a partial body available
|
||||
controller.enqueue(buffer);
|
||||
currentChunkLeft -= buffer.length;
|
||||
buffer = new Uint8Array(0);
|
||||
break; // wait for more data
|
||||
}
|
||||
} else {
|
||||
// only a partial body available
|
||||
controller.enqueue(buffer);
|
||||
currentChunkLeft -= buffer.length;
|
||||
buffer = new Uint8Array(0);
|
||||
break; // wait for more data
|
||||
}
|
||||
} else {
|
||||
// we need to parse the next size line
|
||||
// find the first "\r\n"
|
||||
let idx = -1;
|
||||
for (let i = 0; i + 1 < buffer.length; i++) {
|
||||
if (
|
||||
buffer[i] === 0x0d &&
|
||||
buffer[i + 1] === 0x0a
|
||||
) {
|
||||
idx = i;
|
||||
// we need to parse the next size line
|
||||
// find the first "\r\n"
|
||||
let idx = -1;
|
||||
for (let i = 0; i + 1 < buffer.length; i++) {
|
||||
if (
|
||||
buffer[i] === 0x0d &&
|
||||
buffer[i + 1] === 0x0a
|
||||
) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (idx < 0) {
|
||||
// we don’t yet have a full size line
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (idx < 0) {
|
||||
// we don’t yet have a full size line
|
||||
break;
|
||||
}
|
||||
|
||||
// decode just the size line as ASCII hex
|
||||
const sizeText = decoder
|
||||
.decode(buffer.slice(0, idx))
|
||||
.trim();
|
||||
currentChunkLeft = parseInt(sizeText, 16);
|
||||
if (isNaN(currentChunkLeft)) {
|
||||
controller.error(
|
||||
"Invalid chunk length from server",
|
||||
);
|
||||
}
|
||||
// strip off the size line + CRLF
|
||||
buffer = buffer.slice(idx + 2);
|
||||
// decode just the size line as ASCII hex
|
||||
const sizeText = decoder
|
||||
.decode(buffer.slice(0, idx))
|
||||
.trim();
|
||||
currentChunkLeft = parseInt(sizeText, 16);
|
||||
if (isNaN(currentChunkLeft)) {
|
||||
controller.error(
|
||||
"Invalid chunk length from server",
|
||||
);
|
||||
}
|
||||
// strip off the size line + CRLF
|
||||
buffer = buffer.slice(idx + 2);
|
||||
|
||||
// zero-length => end of stream
|
||||
if (currentChunkLeft === 0) {
|
||||
// zero-length => end of stream
|
||||
if (currentChunkLeft === 0) {
|
||||
responseReturned = true;
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
socket.on("data", (data) => {
|
||||
// Dataoffset is set to another value once head is returned, its safe to assume all remaining data is body
|
||||
if (dataOffset !== -1 && !chunkedTransfer) {
|
||||
controller.enqueue(data);
|
||||
ingestedContent += data.length;
|
||||
}
|
||||
|
||||
// We dont have the full responseHead yet
|
||||
if (dataOffset === -1) {
|
||||
fullDataParts.push(data);
|
||||
responseHead += decoder.decode(data, { stream: true });
|
||||
}
|
||||
if (chunkedTransfer) {
|
||||
parseIncomingChunk(data);
|
||||
}
|
||||
|
||||
// See if we have the HEAD of an HTTP/1.1 yet
|
||||
if (responseHead.indexOf("\r\n\r\n") !== -1) {
|
||||
dataOffset = responseHead.indexOf("\r\n\r\n");
|
||||
responseHead = responseHead.slice(0, dataOffset);
|
||||
const parsedHead = parseHTTPHead(responseHead);
|
||||
contentLength = Number(
|
||||
parsedHead.headers.get("content-length"),
|
||||
);
|
||||
chunkedTransfer =
|
||||
parsedHead.headers.get("transfer-encoding") ===
|
||||
"chunked";
|
||||
// Return initial response object
|
||||
res(new Response(outStream, parsedHead));
|
||||
|
||||
const residualBody = mergeUint8Arrays(
|
||||
...fullDataParts,
|
||||
).slice(dataOffset + 4);
|
||||
if (!chunkedTransfer) {
|
||||
// Add any content we have but isn't part of the head into the body stream
|
||||
ingestedContent += residualBody.length;
|
||||
controller.enqueue(residualBody);
|
||||
} else {
|
||||
parseIncomingChunk(residualBody);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
contentLength !== -1 &&
|
||||
ingestedContent === contentLength &&
|
||||
!chunkedTransfer
|
||||
) {
|
||||
// Work around for the close bug for compliant HTTP/1.1 servers
|
||||
if (!responseReturned) {
|
||||
responseReturned = true;
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
socket.on("data", (data) => {
|
||||
// Dataoffset is set to another value once head is returned, its safe to assume all remaining data is body
|
||||
if (dataOffset !== -1 && !chunkedTransfer) {
|
||||
controller.enqueue(data);
|
||||
ingestedContent += data.length;
|
||||
}
|
||||
|
||||
// We dont have the full responseHead yet
|
||||
if (dataOffset === -1) {
|
||||
fullDataParts.push(data);
|
||||
responseHead += decoder.decode(data, { stream: true });
|
||||
}
|
||||
if (chunkedTransfer) {
|
||||
parseIncomingChunk(data);
|
||||
}
|
||||
|
||||
// See if we have the HEAD of an HTTP/1.1 yet
|
||||
if (responseHead.indexOf("\r\n\r\n") !== -1) {
|
||||
dataOffset = responseHead.indexOf("\r\n\r\n");
|
||||
responseHead = responseHead.slice(0, dataOffset);
|
||||
const parsedHead = parseHTTPHead(responseHead);
|
||||
contentLength = Number(
|
||||
parsedHead.headers.get("content-length"),
|
||||
);
|
||||
chunkedTransfer =
|
||||
parsedHead.headers.get("transfer-encoding") ===
|
||||
"chunked";
|
||||
// Return initial response object
|
||||
res(new Response(outStream, parsedHead));
|
||||
|
||||
const residualBody = mergeUint8Arrays(
|
||||
...fullDataParts,
|
||||
).slice(dataOffset + 4);
|
||||
if (!chunkedTransfer) {
|
||||
// Add any content we have but isn't part of the head into the body stream
|
||||
ingestedContent += residualBody.length;
|
||||
controller.enqueue(residualBody);
|
||||
} else {
|
||||
parseIncomingChunk(residualBody);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
contentLength !== -1 &&
|
||||
ingestedContent === contentLength &&
|
||||
!chunkedTransfer
|
||||
) {
|
||||
// Work around for the close bug for compliant HTTP/1.1 servers
|
||||
});
|
||||
socket.on("close", () => {
|
||||
if (!responseReturned) {
|
||||
responseReturned = true;
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
socket.on("close", () => {
|
||||
if (!responseReturned) {
|
||||
responseReturned = true;
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
socket.on("error", (reason) => {
|
||||
rej("Socket errored with the following reason: " + reason);
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
socket.on("error", (reason) => {
|
||||
rej("Socket errored with the following reason: " + reason);
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
rej(e);
|
||||
}});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user