From a0ee6122dc1b11e0f6dcaa46b7cd00dd8492e978 Mon Sep 17 00:00:00 2001 From: Sambhav Date: Wed, 4 Sep 2024 02:35:16 +0530 Subject: [PATCH] feat: http body extractor (#73) * add body extractor circuit * add tests * add failure test * support request data extraction --- circuits/http/extractor.circom | 71 ++++++++++++++++++++++++++++ circuits/test/common/index.ts | 52 ++++++++++++++++++++ circuits/test/http/extractor.test.ts | 53 +++++++++++++++++++++ examples/http/get_request.http | 4 +- examples/http/get_response.http | 8 ++-- examples/http/post_request.http | 6 +++ 6 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 circuits/http/extractor.circom create mode 100644 circuits/test/http/extractor.test.ts create mode 100644 examples/http/post_request.http diff --git a/circuits/http/extractor.circom b/circuits/http/extractor.circom new file mode 100644 index 0000000..d7106f5 --- /dev/null +++ b/circuits/http/extractor.circom @@ -0,0 +1,71 @@ +pragma circom 2.1.9; + +include "../utils/bytes.circom"; +include "parser/machine.circom"; +include "@zk-email/circuits/utils/array.circom"; + +// TODO: +// - handle CRLF in response data + +template ExtractResponse(DATA_BYTES, maxContentLength) { + signal input data[DATA_BYTES]; + signal output response[maxContentLength]; + + //--------------------------------------------------------------------------------------------// + //-CONSTRAINTS--------------------------------------------------------------------------------// + //--------------------------------------------------------------------------------------------// + component dataASCII = ASCII(DATA_BYTES); + dataASCII.in <== data; + //--------------------------------------------------------------------------------------------// + + // Initialze the parser + component State[DATA_BYTES]; + State[0] = StateUpdate(); + State[0].byte <== data[0]; + State[0].parsing_start <== 1; + State[0].parsing_header <== 0; + State[0].parsing_body <== 0; + State[0].line_status <== 0; + + signal dataMask[DATA_BYTES]; + dataMask[0] <== 0; + + for(var data_idx = 1; data_idx < DATA_BYTES; data_idx++) { + State[data_idx] = StateUpdate(); + State[data_idx].byte <== data[data_idx]; + State[data_idx].parsing_start <== State[data_idx - 1].next_parsing_start; + State[data_idx].parsing_header <== State[data_idx - 1].next_parsing_header; + State[data_idx].parsing_body <== State[data_idx - 1].next_parsing_body; + State[data_idx].line_status <== State[data_idx - 1].next_line_status; + + // apply body mask to data + dataMask[data_idx] <== data[data_idx] * State[data_idx].next_parsing_body; + + // Debugging + log("State[", data_idx, "].parsing_start ", "= ", State[data_idx].parsing_start); + log("State[", data_idx, "].parsing_header", "= ", State[data_idx].parsing_header); + log("State[", data_idx, "].parsing_body ", "= ", State[data_idx].parsing_body); + log("State[", data_idx, "].line_status ", "= ", State[data_idx].line_status); + log("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); + } + + // Debugging + log("State[", DATA_BYTES, "].parsing_start ", "= ", State[DATA_BYTES-1].next_parsing_start); + log("State[", DATA_BYTES, "].parsing_header", "= ", State[DATA_BYTES-1].next_parsing_header); + log("State[", DATA_BYTES, "].parsing_body ", "= ", State[DATA_BYTES-1].next_parsing_body); + log("State[", DATA_BYTES, "].line_status ", "= ", State[DATA_BYTES-1].next_line_status); + log("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); + + signal valueStartingIndex[DATA_BYTES]; + signal isZeroMask[DATA_BYTES]; + signal isPrevStartingIndex[DATA_BYTES]; + valueStartingIndex[0] <== 0; + isZeroMask[0] <== IsZero()(dataMask[0]); + for (var i=1 ; i { + const [key, value] = line.split(/:\s(.+)/); + headers[key] = value ? value : ''; + }); + + return headers; + } + + // Parse the headers + const headerLines = headerSection.split('\r\n'); + const initialLine = headerLines[0].split(' '); + const headers = parseHeaders(headerLines.slice(1)); + + // Parse the body, if JSON response + let responseBody = {}; + if (headers["Content-Type"] == "application/json") { + responseBody = JSON.parse(bodySection); + } + + // Combine headers and body into an object + return { + input: input, + initialLine: initialLine, + headers: headers, + body: responseBody, + bodyBytes: toByte(bodySection), + }; } \ No newline at end of file diff --git a/circuits/test/http/extractor.test.ts b/circuits/test/http/extractor.test.ts new file mode 100644 index 0000000..6c94490 --- /dev/null +++ b/circuits/test/http/extractor.test.ts @@ -0,0 +1,53 @@ +import { circomkit, WitnessTester, generateDescription, readHTTPInputFile } from "../common"; + +describe("HTTP :: Extractor", async () => { + let circuit: WitnessTester<["data"], ["response"]>; + + + function generatePassCase(input: number[], expected: any, desc: string) { + const description = generateDescription(input); + + it(`(valid) witness: ${description} ${desc}`, async () => { + circuit = await circomkit.WitnessTester(`ExtractResponseData`, { + file: "circuits/http/extractor", + template: "ExtractResponse", + params: [input.length, expected.length], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + + await circuit.expectPass({ data: input }, { response: expected }); + }); + } + + describe("response", async () => { + + let parsedHttp = readHTTPInputFile("get_response.http"); + + generatePassCase(parsedHttp.input, parsedHttp.bodyBytes, ""); + + let output2 = parsedHttp.bodyBytes.slice(0); + output2.push(0, 0, 0, 0); + generatePassCase(parsedHttp.input, output2, "output length more than actual length"); + + let output3 = parsedHttp.bodyBytes.slice(0); + output3.pop(); + // output3.pop(); // TODO: fails due to shift subarray bug + generatePassCase(parsedHttp.input, output3, "output length less than actual length"); + }); + + describe("request", async () => { + let parsedHttp = readHTTPInputFile("post_request.http"); + + generatePassCase(parsedHttp.input, parsedHttp.bodyBytes, ""); + + let output2 = parsedHttp.bodyBytes.slice(0); + output2.push(0, 0, 0, 0, 0, 0); + generatePassCase(parsedHttp.input, output2, "output length more than actual length"); + + console.log(parsedHttp.bodyBytes.length); + let output3 = parsedHttp.bodyBytes.slice(0); + output3.pop(); + output3.pop(); + generatePassCase(parsedHttp.input, output3, "output length less than actual length"); + }); +}); \ No newline at end of file diff --git a/examples/http/get_request.http b/examples/http/get_request.http index 89257e9..388d2d2 100644 --- a/examples/http/get_request.http +++ b/examples/http/get_request.http @@ -1,3 +1,3 @@ -GET /api HTTP/1.1 -Accept: application/json +GET /api HTTP/1.1 +Accept: application/json Host: localhost \ No newline at end of file diff --git a/examples/http/get_response.http b/examples/http/get_response.http index ac11942..e505ef8 100644 --- a/examples/http/get_response.http +++ b/examples/http/get_response.http @@ -1,5 +1,5 @@ -HTTP/1.1 200 OK -Content-Type: application/json -Content-Length: 19 - +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 19 + {"success":"true"} \ No newline at end of file diff --git a/examples/http/post_request.http b/examples/http/post_request.http new file mode 100644 index 0000000..415a117 --- /dev/null +++ b/examples/http/post_request.http @@ -0,0 +1,6 @@ +POST /contact_form.php HTTP/1.1 +Host: developer.mozilla.org +Content-Length: 64 +Content-Type: application/x-www-form-urlencoded + +name=Joe%20User&request=Send%20me%20one%20of%20your%20catalogue \ No newline at end of file