Skip to content

Commit

Permalink
feat: basic but generic http parser (#69)
Browse files Browse the repository at this point in the history
* slight refactor and basic test

* decent progress on request parse

* feat: very basic HTTP parsing

* fix typos, remove logs
  • Loading branch information
Autoparallel authored Aug 30, 2024
1 parent b3bd8da commit 5d6aac5
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 42 deletions.
12 changes: 7 additions & 5 deletions circuits.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,15 @@
10
]
},
"http_get_request": {
"file": "http/http_request/parser",
"get_request": {
"file": "http/parser/parser",
"template": "Parser",
"params": [
158
]
"params": [60]
},
"get_response": {
"file": "http/parser/parser",
"template": "Parser",
"params": [89]
"json_extract_value_string": {
"file": "main/value_string",
"template": "ExtractStringValue",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
pragma circom 2.1.9;

include "language.circom";
include "parser/language.circom";
include "../utils/array.circom";

template ParseMethod() {
signal input bytes[7];
/* TODO:
Notes --
- This is a pretty efficient way to simply check what the method used in a request is by checking
the first `DATA_LENGTH` number of bytes.
- Could probably change this to a template that checks if it is one of the given methods
so we don't check them all in one
*/
template YieldMethod(DATA_LENGTH) {
signal input bytes[DATA_LENGTH];
signal output MethodTag;

component RequestMethod = RequestMethod();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ template Syntax() {
// - ASCII char `"`
signal output QUOTE <== 34;
//-White_space--------------------------------------------------------------------------------//
// - ASCII pair: `\r\n`
signal output CLRF <== [13, 10]; // https://www.rfc-editor.org/rfc/rfc2616#section-2.2
// https://www.rfc-editor.org/rfc/rfc7230#section-3.5
// https://www.rfc-editor.org/rfc/rfc2616#section-2.2
// https://www.rfc-editor.org/rfc/rfc7230#section-3.5
// - ASCII char `\r` (carriage return)
signal output CR <== 13;
// - ASCII char `\n` (line feed)
signal output LF <== 10;
// - ASCII char: ` `
signal output SPACE <== 32;
//-Escape-------------------------------------------------------------------------------------//
Expand Down
73 changes: 73 additions & 0 deletions circuits/http/parser/machine.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
pragma circom 2.1.9;

include "language.circom";
include "../../utils/array.circom";

template StateUpdate() {
signal input parsing_start; // Bool flag for if we are in the start line
signal input parsing_header; // Flag + Counter for what header line we are in
signal input parsing_body;
signal input line_status; // Flag that counts up to 4 to read a double CLRF
signal input byte;

signal output next_parsing_start;
signal output next_parsing_header;
signal output next_parsing_body;
signal output next_line_status;

component Syntax = Syntax();

//---------------------------------------------------------------------------------//
// Check if what we just read is a CR / LF
component readCR = IsEqual();
readCR.in <== [byte, Syntax.CR];
component readLF = IsEqual();
readLF.in <== [byte, Syntax.LF];

signal notCRAndLF <== (1 - readCR.out) * (1 - readLF.out);
//---------------------------------------------------------------------------------//

//---------------------------------------------------------------------------------//
// Check if we had read previously CR / LF or multiple
component prevReadCR = IsEqual();
prevReadCR.in <== [line_status, 1];
component prevReadCRLF = IsEqual();
prevReadCRLF.in <== [line_status, 2];
component prevReadCRLFCR = IsEqual();
prevReadCRLFCR.in <== [line_status, 3];

signal readCRLF <== prevReadCR.out * readLF.out;
signal readCRLFCRLF <== prevReadCRLFCR.out * readLF.out;
//---------------------------------------------------------------------------------//

//---------------------------------------------------------------------------------//
// Take current state and CRLF info to update state
signal state[3] <== [parsing_start, parsing_header, parsing_body];
component stateChange = StateChange();
stateChange.readCRLF <== readCRLF;
stateChange.readCRLFCRLF <== readCRLFCRLF;
stateChange.state <== state;

component nextState = ArrayAdd(3);
nextState.lhs <== state;
nextState.rhs <== stateChange.out;
//---------------------------------------------------------------------------------//

next_parsing_start <== nextState.out[0];
next_parsing_header <== nextState.out[1];
next_parsing_body <== nextState.out[2];
next_line_status <== line_status + readCR.out + readCRLF + readCRLFCRLF - line_status * notCRAndLF;

}

template StateChange() {
signal input readCRLF;
signal input readCRLFCRLF;
signal input state[3];
signal output out[3];

signal disableParsingStart <== readCRLF * state[0];
signal disableParsingHeader <== readCRLFCRLF * state[1];

out <== [-disableParsingStart, disableParsingStart - disableParsingHeader, disableParsingHeader];
}
51 changes: 51 additions & 0 deletions circuits/http/parser/parser.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
pragma circom 2.1.9;

include "../../utils/bytes.circom";
include "machine.circom";


template Parser(DATA_BYTES) {
signal input data[DATA_BYTES];

signal output Method;

//--------------------------------------------------------------------------------------------//
//-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;

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;

// 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");

}
24 changes: 0 additions & 24 deletions circuits/http/parser_request/parser.circom

This file was deleted.

25 changes: 25 additions & 0 deletions circuits/test/http/interpreter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { circomkit, WitnessTester, generateDescription } from "../common";

describe("HTTP :: Interpreter", async () => {
describe("YieldMethod", async () => {
let circuit: WitnessTester<["bytes"], ["MethodTag"]>;

function generatePassCase(input: any, expected: any, depth: number, desc: string) {
const description = generateDescription(input);

it(`(valid) witness: ${description} ${desc}`, async () => {
circuit = await circomkit.WitnessTester(`YieldMethod`, {
file: "circuits/http/interpreter",
template: "YieldMethod",
params: [4],
});
console.log("#constraints:", await circuit.getConstraintCount());

await circuit.expectPass(input, expected);
});
}

// The string `"GET "`
generatePassCase({ bytes: [71, 69, 84, 32] }, { MethodTag: 1 }, 0, "");
});
});
6 changes: 2 additions & 4 deletions examples/http/get_request.http
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
GET /objectserver/restapi/alerts/status HTTP/1.1
GET /api HTTP/1.1
Accept: application/json
Authorization: Basic dGVzdHVzZXIwMTpuZXRjb29s
Host: localhost
Connection: keep-alive
Host: localhost
15 changes: 12 additions & 3 deletions src/bin/witness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ struct Args {
command: Command,

/// Output directory (will be created if it doesn't exist)
#[arg(global = true, short, long, default_value = ".")]
#[arg(global = true, long, default_value = ".")]
output_dir: PathBuf,

/// Output filename (will be created if it doesn't exist)
#[arg(global = true, short, long, default_value = "output.json")]
#[arg(global = true, long, default_value = "output.json")]
output_filename: String,
}

Expand Down Expand Up @@ -64,7 +64,16 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
(data, keys_map)
}
Command::Http { input_file } => {
let data = std::fs::read(input_file)?;
let mut data = std::fs::read(input_file)?;
let mut i = 0;
while i < data.len() {
if data[i] == 10 && (i == 0 || data[i - 1] != 13) {
data.insert(i, 13);
i += 2;
} else {
i += 1;
}
}
let keys_map = serde_json::Map::new();
(data, keys_map)
}
Expand Down

0 comments on commit 5d6aac5

Please sign in to comment.