34
loading...
This website collects cookies to deliver better user experience
const concat = (...chunks) => {
const zs = new Uint8Array(chunks.reduce((z, ys) => z + ys.byteLength, 0));
chunks.reduce((i, xs) => zs.set(xs, i) || i + xs.byteLength, 0);
return zs;
};
const chunks = [];
let n;
do {
const xs = new Uint8Array(1024);
n = await r.read(xs);
chunks.push(xs.subarray(0, n));
} while (n === 1024);
const request = concat(...chunks);
export const findIndexOfSequence = (xs, ys) => {
let i = xs.indexOf(ys[0]);
let z = false;
while (i >= 0 && i < xs.byteLength) {
let j = 0;
while (j < ys.byteLength) {
if (xs[j + i] !== ys[j]) break;
j++;
}
if (j === ys.byteLength) {
z = true;
break;
}
i++;
}
return z ? i : null;
};
// https://github.com/i-y-land/HTTP/blob/episode/03/library/utilities.js#L208
export const readLine = (xs) => xs.subarray(0, xs.indexOf(LF) + 1);
export const decodeRequest = (xs) => {
const headers = {};
let body, method, path;
const n = xs.byteLength;
let i = 0;
let seekedPassedHeader = false;
while (i < n) {
if (seekedPassedHeader) {
body = xs.subarray(i, n);
i = n;
continue;
}
const ys = readLine(xs.subarray(i, n));
if (i === 0) {
if (!findIndexOfSequence(ys, encode(" HTTP/"))) break;
[method, path] = decode(ys).split(" ");
} else if (
ys.byteLength === 2 &&
ys[0] === CR &&
ys[1] === LF &&
xs[i] === CR &&
xs[i + 1] === LF
) {
seekedPassedHeader = true;
} else if (ys.byteLength === 0) break;
else {
const [key, value] = decode(
ys.subarray(0, ys.indexOf(CR) || ys.indexOf(LF)),
).split(/(?<=^[A-Za-z-]+)\s*:\s*/);
headers[key.toLowerCase()] = value;
}
i += ys.byteLength;
}
return { body, headers, method, path };
};
// https://github.com/i-y-land/HTTP/blob/episode/03/library/utilities.js#L248
export const stringifyHeaders = (headers = {}) =>
Object.entries(headers)
.reduce(
(hs, [key, value]) => `${hs}\r\n${normalizeHeaderKey(key)}: ${value}`,
"",
);
export const encodeResponse = (response) =>
concat(
encode(
`HTTP/1.1 ${statusCodes[response.statusCode]}${
stringifyHeaders(response.headers)
}\r\n\r\n`,
),
response.body || new Uint8Array(0),
);
serve
function I've implemented previously....
serve(
Deno.listen({ port }),
(xs) => {
const request = decodeRequest(xs);
if (request.method === "GET" && request.path === "/") {
return encodeResponse({ statusCode: 204 })
}
}
).catch((e) => console.error(e));
...
if (request.method === "GET" && request.path === "/") {
const file = Deno.readFile(`${Deno.cwd()}/image.png`); // read the file
return encodeResponse({
body: file,
headers: {
"content-length": file.byteLength,
"content-type": "image/png"
},
statusCode: 200
});
}
const sourcePath =
(await Deno.permissions.query({ name: "env", variable: "SOURCE_PATH" }))
.state === "granted" && Deno.env.get("SOURCE_PATH") ||
`${Deno.cwd()}/library/assets_test`;
...
if (request.method === "GET") {
try {
const file = await Deno.readFile(sourcePath + request.path); // read the file
return encodeResponse({
body: file,
headers: {
"content-length": file.byteLength,
["content-type"]: mimeTypes[
request.path.match(/(?<extension>\.[a-z0-9]+$)/)?.groups?.extension
.toLowerCase()
].join(",") || "plain/text",
},
statusCode: 200
});
} catch (e) {
if (e instanceof Deno.errors.NotFound) { // if the file is not found
return encodeResponse({
body: new Uint8Array(0),
headers: {
["Content-Length"]: 0,
},
statusCode: 404,
});
}
throw e;
}
}
const targetPath =
(await Deno.permissions.query({ name: "env", variable: "TARGET_PATH" }))
.state === "granted" && Deno.env.get("TARGET_PATH") ||
`${Deno.cwd()}/`;
...
if (request.method === "GET") { ... }
else if (request.method === "POST") {
await Deno.writeFile(targetPath + request.path, request.body); // write the file
return encodeResponse({ statusCode: 204 });
}
curl
will make the request in two steps... The first request is 100 continue
before sending the file.serve
function from accepting a function that takes a Typed Array as an export const serve = async (listener, f) => {
for await (const connection of listener) {
await f(connection);
}
};
export const copy = async (r, w) => {
const xs = new Uint8Array(1024);
let n;
let i = 0;
do {
n = await r.read(xs);
await w.write(xs.subarray(0, n));
i += n;
} while (n === 1024);
return i;
};
...
let xs = new Uint8Array(1024);
const n = await Deno.read(r.rid, xs);
const request = xs.subarray(0, n);
const { fileName } = request.path.match(
/.*?\/(?<fileName>(?:[^%]|%[0-9A-Fa-f]{2})+\.[A-Za-z0-9]+?)$/,
)?.groups || {};
...
const file = await Deno.open(`${targetPath}/${fileName}`, {
create: true,
write: true,
});
if (request.headers.expect === "100-continue") {
// write the `100 Continue` response
await Deno.write(connection.rid, encodeResponse({ statusCode: 100 }));
const ys = new Uint8Array(1024);
const n = await Deno.read(connection.rid, ys); // read the follow-up
xs = ys.subarray(0, n);
}
const i = findIndexOfSequence(xs, CRLF); // find the beginning of the body
if (i > 0) {
await Deno.write(file.rid, xs.subarray(i + 4)); // write possible file chunk
if (xs.byteLength === 1024) {
await copy(connection, file); // copy subsequent chunks
}
}
await connection.write(
encodeResponse({ statusCode: 204 }), // terminate the exchange
);
...
HEAD
method.GET
-- not HEAD
-- I will copy the file to the connection....
try {
const { size } = await Deno.stat(`${sourcePath}/${fileName}`);
await connection.write(
encodeResponse({
headers: {
["Content-Type"]: mimeTypes[
fileName.match(/(?<extension>\.[a-z0-9]+$)/)?.groups?.extension
.toLowerCase()
].join(",") || "plain/text",
["Content-Length"]: size,
},
statusCode: 200,
}),
);
if (request.method === "GET") {
const file = await Deno.open(`${sourcePath}/${fileName}`);
await copy(file, connection);
}
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
Deno.write(
connection.rid,
encodeResponse({
headers: {
["Content-Length"]: 0,
},
statusCode: 404,
}),
);
}
throw e;
}
...
// https://github.com/i-y-land/HTTP/blob/episode/03/library/integration_test.js#L6
const withServer = (port, f) =>
async () => {
const p = await Deno.run({ // initialize the server
cmd: [
"deno",
"run",
"--allow-all",
`${Deno.cwd()}/cli.js`,
String(port),
],
env: { LOG_LEVEL: "ERROR", "NO_COLOR": "1" },
stdout: "null",
});
await new Promise((resolve) => setTimeout(resolve, 1000)); // wait to be sure
try {
await f(p); // call the test function passing the process
} finally {
Deno.close(p.rid);
}
};
// https://github.com/i-y-land/HTTP/blob/episode/03/library/integration_test.js#L58
[...]
.forEach(
({ headers = {}, method = "GET", path, title, f }) => {
Deno.test(
`Integration: ${title}`,
withServer(
8080,
async () => {
const response = await fetch(`http://localhost:8080${path}`, {
headers,
method,
});
await f(response);
},
),
);
},
);
receiveStaticFile
and sendStaticFile
....
if (method === "POST") {
return receiveStaticFile(?, { targetPath });
} else if (method === "GET" || method === "HEAD") {
return sendStaticFile(?, { sourcePath });
}
...
const request = decodeRequest(connection);
request.connection = connection;
...
if (method === "POST") {
return receiveStaticFile(request, { targetPath });
} else if (method === "GET" || method === "HEAD") {
return sendStaticFile(request, { sourcePath });
}
...
// https://github.com/i-y-land/HTTP/blob/episode/03/library/utilities.js#L11
export const factorizeBuffer = (r, mk = 1024, ml = 1024) => { ... }
peek
function specifically has a similar signature to read
, the difference is that it will move the cursorserve(
Deno.listen({ port }),
async (connection) => {
const r = factorizeBuffer(connection);
const xs = new Uint8Array(1024);
const reader = r.getReader();
await reader.peek(xs);
const [method] = decode(readLine(xs)).split(" ");
if (method !== "GET" && method !== "POST" && method !== "HEAD") {
return connection.write(
encodeResponse({ statusCode: 400 }),
);
}
if (method === "POST") {
return receiveStaticFile(r, { targetPath });
} else {
return sendStaticFile(r, { sourcePath });
}
}
)
receiveStaticFile
(https://github.com/i-y-land/HTTP/blob/episode/03/library/server.js#L15) and sendStaticFile
(https://github.com/i-y-land/HTTP/blob/episode/03/library/server.js#L71) functions, taking care of all