Skip to content

Commit

Permalink
feat: http body extractor (#73)
Browse files Browse the repository at this point in the history
* add body extractor circuit

* add tests

* add failure test

* support request data extraction
  • Loading branch information
lonerapier authored Sep 3, 2024
1 parent cc47938 commit a0ee612
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 6 deletions.
71 changes: 71 additions & 0 deletions circuits/http/extractor.circom
Original file line number Diff line number Diff line change
@@ -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<DATA_BYTES ; i++) {
isZeroMask[i] <== IsZero()(dataMask[i]);
isPrevStartingIndex[i] <== IsZero()(valueStartingIndex[i-1]);
valueStartingIndex[i] <== valueStartingIndex[i-1] + i * (1-isZeroMask[i]) * isPrevStartingIndex[i];
}

response <== SelectSubArray(DATA_BYTES, maxContentLength)(dataMask, valueStartingIndex[DATA_BYTES-1]+1, DATA_BYTES - valueStartingIndex[DATA_BYTES-1]);
}
52 changes: 52 additions & 0 deletions circuits/test/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,56 @@ export function readJSONInputFile(filename: string, key: any[]): [number[], numb
}

return [input, keyUnicode, output];
}

function toByte(data: string): number[] {
const byteArray = [];
for (let i = 0; i < data.length; i++) {
byteArray.push(data.charCodeAt(i));
}
return byteArray
}

export function readHTTPInputFile(filename: string) {
const filePath = join(__dirname, "..", "..", "..", "examples", "http", filename);
let input: number[] = [];

let data = readFileSync(filePath, 'utf-8');

input = toByte(data);

// Split headers and body
const [headerSection, bodySection] = data.split('\r\n\r\n');

// Function to parse headers into a dictionary
function parseHeaders(headerLines: string[]) {
const headers: { [id: string]: string } = {};

headerLines.forEach(line => {
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),
};
}
53 changes: 53 additions & 0 deletions circuits/test/http/extractor.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
4 changes: 2 additions & 2 deletions examples/http/get_request.http
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
GET /api HTTP/1.1
Accept: application/json
GET /api HTTP/1.1
Accept: application/json
Host: localhost
8 changes: 4 additions & 4 deletions examples/http/get_response.http
Original file line number Diff line number Diff line change
@@ -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"}
6 changes: 6 additions & 0 deletions examples/http/post_request.http
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit a0ee612

Please sign in to comment.