Skip to content

Commit

Permalink
add handle code
Browse files Browse the repository at this point in the history
  • Loading branch information
pilcrowonpaper committed Jan 20, 2023
1 parent b21113e commit c77ff0f
Showing 1 changed file with 176 additions and 124 deletions.
300 changes: 176 additions & 124 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -1,132 +1,184 @@
---
type Part = {
contentDispositionHeader: string;
contentTypeHeader: string;
part: number[];
};
type Input =
| {
name: string;
data: Uint8Array;
}
| {
type: string;
filename: string;
name: string;
data: Uint8Array;
};
enum ParsingState {
INIT,
READING_HEADERS,
READING_DATA,
READING_PART_SEPARATOR
}
import type { AstroGlobal } from "astro";
const parse = (
multipartBodyBuffer: Uint8Array,
boundary: string
): Input[] => {
let lastline = "";
let contentDispositionHeader = "";
let contentTypeHeader = "";
let state: ParsingState = ParsingState.INIT;
let buffer: number[] = [];
const allParts: Input[] = [];
type ExtractResolveResponse<Handle extends (...args: any) => any> = Extract<
Awaited<ReturnType<Handle>>,
InstanceType<any>
>;
let currentPartHeaders: string[] = [];
type ExtractRejectResponse<Handle extends (...args: any) => any> = Extract<
Awaited<ReturnType<Handle>>,
InstanceType<any>
>;
for (let i = 0; i < multipartBodyBuffer.length; i++) {
const oneByte: number = multipartBodyBuffer[i];
const prevByte: number | null = i > 0 ? multipartBodyBuffer[i - 1] : null;
// 0x0a => \n
// 0x0d => \r
const newLineDetected: boolean = oneByte === 0x0a && prevByte === 0x0d;
const newLineChar: boolean = oneByte === 0x0a || oneByte === 0x0d;
if (!newLineChar) lastline += String.fromCharCode(oneByte);
if (ParsingState.INIT === state && newLineDetected) {
// searching for boundary
if ("--" + boundary === lastline) {
state = ParsingState.READING_HEADERS; // found boundary. start reading headers
}
lastline = "";
} else if (ParsingState.READING_HEADERS === state && newLineDetected) {
// parsing headers. Headers are separated by an empty line from the content. Stop reading headers when the line is empty
if (lastline.length) {
currentPartHeaders.push(lastline);
} else {
// found empty line. search for the headers we want and set the values
for (const h of currentPartHeaders) {
if (h.toLowerCase().startsWith("content-disposition:")) {
contentDispositionHeader = h;
} else if (h.toLowerCase().startsWith("content-type:")) {
contentTypeHeader = h;
}
}
state = ParsingState.READING_DATA;
buffer = [];
}
lastline = "";
} else if (ParsingState.READING_DATA === state) {
// parsing data
if (lastline.length > boundary.length + 4) {
lastline = ""; // mem save
}
if ("--" + boundary === lastline) {
const j = buffer.length - lastline.length;
const part = buffer.slice(0, j - 1);
allParts.push(
process({ contentDispositionHeader, contentTypeHeader, part })
);
buffer = [];
currentPartHeaders = [];
lastline = "";
state = ParsingState.READING_PART_SEPARATOR;
contentDispositionHeader = "";
contentTypeHeader = "";
} else {
buffer.push(oneByte);
}
if (newLineDetected) {
lastline = "";
}
} else if (ParsingState.READING_PART_SEPARATOR === state) {
if (newLineDetected) {
state = ParsingState.READING_HEADERS;
}
}
}
return allParts;
};
type ExtractRedirectResponse<Handle extends (...args: any) => any> = Extract<
Awaited<ReturnType<Handle>>,
InstanceType<any>
>;
const process = (part: Part): Input => {
// will transform this object:
// { header: 'Content-Disposition: form-data; name="uploads[]"; filename="A.txt"',
// info: 'Content-Type: text/plain',
// part: 'AAAABBBB' }
// into this one:
// { filename: 'A.txt', type: 'text/plain', data: <Buffer 41 41 41 41 42 42 42 42> }
const header = part.contentDispositionHeader.split(";");
const filenameData = header[2];
if (filenameData) {
const getFileName = () => {
return JSON.parse(filenameData.split("=")[1].trim()) as string;
};
const contentType = part.contentTypeHeader.split(":")[1].trim();
return {
type: contentType,
name: header[1].split("=")[1].replace(/"/g, ""),
data: new Uint8Array(part.part),
filename: getFileName()
};
}
return {
name: header[1].split("=")[1].replace(/"/g, ""),
data: new Uint8Array(part.part)
};
export const handleFormSubmission = async <
Handle extends (
formData: FormData
) => Promise<InstanceType<any> | InstanceType<any> | InstanceType<any>>
>(
{
request,
response,
}: {
request: Request;
response: AstroGlobal["response"];
},
handle: Handle,
enabledCSRFProtection = true
): Promise<any> => {
const clonedRequest = request.clone();
const contentType = clonedRequest.headers.get("content-type") ?? "";
const isMultipartForm = contentType.includes("multipart/form-data");
const isUrlEncodedForm = contentType.includes(
"application/x-www-form-urlencoded"
);
const requestOrigin = clonedRequest.headers.get("origin");
const url = new URL(clonedRequest.url);
const isValidRequest = enabledCSRFProtection
? requestOrigin === url.origin
: true;
const isValidContentType = isMultipartForm || isUrlEncodedForm;
if (clonedRequest.method !== "POST" || !isValidContentType || !isValidRequest)
return {
type: "ignore",
response: null,
body: null,
inputValues: {},
error: null,
redirectLocation: null,
};
const formData = new FormData();
const acceptHeader = clonedRequest.headers.get("accept");
if (isMultipartForm) {
const boundary = contentType.replace("multipart/form-data; boundary=", "");
const bodyArrayBuffer = await clonedRequest.arrayBuffer();
const parts: any[] = [];
parts.forEach((value) => {
const isFile = "type" in value;
if (isFile) {
formData.append(value.name!, new Blob([value.data]), value.filename!);
} else {
const textDecoder = new TextDecoder();
formData.append(value.name!, textDecoder.decode(value.data));
}
});
} else if (isUrlEncodedForm) {
const requestBodyFormData = await clonedRequest.formData();
requestBodyFormData.forEach((value, key) => {
formData.append(key, value);
});
} else {
throw new Error("Unexpected value");
}
const inputValues = Object.fromEntries(
[...formData.entries()].filter(
(val): val is [string, string] => typeof val[1] === "string"
)
);
const result = (await handle(formData)) as Awaited<ReturnType<Handle>>;
if ("") {
const type = "rejected";
const body = null;
const redirectLocation = null;
const error = result.data;
const status = result.status;
if (acceptHeader === "application/json") {
return {
type,
body,
response: new Response(
JSON.stringify({
type,
body,
error,
redirect_location: null,
} satisfies any),
{
status,
}
),
inputValues,
error,
redirectLocation,
};
}
response.status = status;
return {
type,
body,
response: null,
inputValues,
error,
redirectLocation,
};
}
if ("") {
const type = "redirect";
const body = null;
const error = null;
const redirectLocation = result.location;
const redirectResponse =
acceptHeader === "application/json"
? new Response(
JSON.stringify({
type,
body,
error,
redirect_location: redirectLocation,
}),
{
status: result.status,
}
)
: new Response(null, {
status: result.status,
headers: {
location: redirectLocation,
},
});
return {
type,
body,
response: redirectResponse,
inputValues,
error,
redirectLocation,
} as any;
}
const body = result;
const type = "resolved";
const redirectLocation = null;
const error = null;
if (acceptHeader === "application/json") {
return {
type,
body,
response: new Response(
JSON.stringify({
type,
body,
error,
redirect_location: null,
})
),
inputValues,
error,
redirectLocation,
} as any;
}
return {
type,
body,
response: null,
inputValues,
error,
redirectLocation,
};
};
const clonedRequest = Astro.request.clone();
Expand Down

0 comments on commit c77ff0f

Please sign in to comment.