-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathperform-request.ts
More file actions
128 lines (117 loc) · 4.77 KB
/
perform-request.ts
File metadata and controls
128 lines (117 loc) · 4.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import type { DialedSocket } from "./types.ts";
export async function performRequest(conn: DialedSocket, url: URL, request: Request): Promise<Response> {
if (request.body) throw new Error(`TODO: Request#body`);
if (request.cache) throw new Error(`TODO: Request#cache`);
if (request.credentials) throw new Error(`TODO: Request#credentials`);
if (request.integrity) throw new Error(`TODO: Request#integrity`);
if (request.mode) throw new Error(`TODO: Request#mode`);
if (request.destination) throw new Error(`TODO: Request#destination`);
// TODO: default value is 'follow'
// if (request.redirect && request.redirect !== 'manual') throw new Error(`TODO: Request#redirect of ${request.redirect}`);
if (request.referrer) throw new Error(`TODO: Request#referrer`);
if (request.referrerPolicy) throw new Error(`TODO: Request#referrerPolicy`);
// if (request.signal) throw new Error(`TODO: Request#signal`);
const reqHeaders = new Headers(request.headers);
if (!reqHeaders.get("accept")) reqHeaders.set("accept", "*/*");
if (!reqHeaders.get("host")) {
if (url.protocol.endsWith("+unix:")) {
reqHeaders.set("host", "uds.localhost");
} else {
reqHeaders.set("host", url.host);
}
}
if (!reqHeaders.get("user-agent")) {
reqHeaders.set("user-agent", `Deno/${Deno.version.deno} socket_fetch/0`);
}
if (request.destination) {
reqHeaders.set("sec-fetch-dest", request.destination);
}
reqHeaders.set("connection", "close"); // TODO: connection reuse
const writer = conn.writable.getWriter();
await writer.write(new TextEncoder().encode(`${request.method} ${url.pathname}${url.search} HTTP/1.1\r\n`));
for (const header of reqHeaders) {
// TODO: surely values need to be sanitized
await writer.write(new TextEncoder().encode(`${header[0]}: ${header[1]}\r\n`));
}
await writer.write(new TextEncoder().encode(`\r\n`));
writer.releaseLock();
const headerLines = new Array<string>();
let leftovers = new Array<Uint8Array>();
let mode: "headers" | "body" | "chunks" = "headers";
try {
for await (const b of conn.readable) {
// console.log('---', b.length, new TextDecoder().decode(b));
let remaining = b.subarray(0);
while (mode === "headers" && remaining.includes(10)) {
const idx = remaining.indexOf(10);
const decoder = new TextDecoder();
let text = "";
for (const leftover of leftovers) {
text += decoder.decode(leftover, { stream: true });
}
leftovers.length = 0;
const line = remaining.subarray(0, idx);
text += decoder.decode(line).replace(/\r$/, "");
if (text == "") mode = "body";
else headerLines.push(text);
remaining = remaining.subarray(idx + 1);
}
if (remaining.length > 0) {
leftovers.push(remaining.slice(0));
}
}
} catch (thrown) {
if (thrown instanceof Deno.errors.UnexpectedEof) {
// Google sometimes doesn't send EOF. We'll just tolerate it
// This can be changed up once we process the received stream less greedily.
} else throw thrown;
}
if (headerLines.length == 0) throw new Error(`No HTTP response received`);
// TODO: the response body should really be a ReadableStream
const bodySize = leftovers.reduce((a, b) => b.byteLength + a, 0);
let body = new Uint8Array(bodySize);
let pos = 0;
for (const leftover of leftovers) {
body.set(leftover, pos);
pos += leftover.byteLength;
}
const statusLine = headerLines.shift()!.split(" ");
const responseHeaders = new Headers();
for (const line of headerLines) {
const colon = line.indexOf(":");
if (colon < 0) {
console.warn(`WARN: HTTP header ${JSON.stringify(line)} lacks a colon`);
} else {
responseHeaders.append(line.slice(0, colon).trim(), line.slice(colon + 1).trim());
}
}
if (responseHeaders.get("transfer-encoding")?.includes("chunked")) {
body = dechunk(body);
}
return new Response(body, {
headers: responseHeaders,
status: parseInt(statusLine[1]),
statusText: statusLine.slice(2).join(" "),
});
}
function dechunk(raw: Uint8Array) {
const chunks = new Array<Uint8Array>();
let inputPos = 0;
while (inputPos < raw.length) {
const nextNl = raw.indexOf(10, inputPos) + 1;
if (nextNl <= 0) throw new Error(`BUG`);
const headerSlice = raw.subarray(inputPos, nextNl);
const header = new TextDecoder().decode(headerSlice).trimEnd().split(';')[0];
const chunkSize = parseInt(header, 16);
chunks.push(raw.subarray(nextNl, nextNl + chunkSize));
inputPos = nextNl + chunkSize + 2;
}
const bodySize = chunks.reduce((a, b) => b.byteLength + a, 0);
const final = new Uint8Array(bodySize);
let finalPos = 0;
for (const chunk of chunks) {
final.set(chunk, finalPos);
finalPos += chunk.byteLength;
}
return final;
}