From c55032d853f6a13945d992afec409f9963558341 Mon Sep 17 00:00:00 2001 From: th4s Date: Fri, 18 Aug 2023 15:12:22 +0200 Subject: [PATCH 01/15] Add ideas for redacting --- tlsn/examples/twitter_dm.rs | 2 +- tlsn/tests-integration/tests/test.rs | 2 +- tlsn/tlsn-core/src/lib.rs | 1 + tlsn/tlsn-core/src/redact/http.rs | 1 + tlsn/tlsn-core/src/redact/json.rs | 1 + tlsn/tlsn-core/src/redact/mod.rs | 33 ++++++++++++++++++++++++++++ tlsn/tlsn-core/src/transcript.rs | 10 +++++++++ tlsn/tlsn-prover/src/lib.rs | 19 +++++++++++++--- 8 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 tlsn/tlsn-core/src/redact/http.rs create mode 100644 tlsn/tlsn-core/src/redact/json.rs create mode 100644 tlsn/tlsn-core/src/redact/mod.rs diff --git a/tlsn/examples/twitter_dm.rs b/tlsn/examples/twitter_dm.rs index 297fd2b4ad..2cf17208eb 100644 --- a/tlsn/examples/twitter_dm.rs +++ b/tlsn/examples/twitter_dm.rs @@ -270,7 +270,7 @@ async fn main() { prover.add_commitment_recv(0..recv_len as u32).unwrap(); // Finalize, returning the notarized session - let notarized_session = prover.finalize().await.unwrap(); + let notarized_session = prover.finalize(None).await.unwrap(); debug!("Notarization complete!"); diff --git a/tlsn/tests-integration/tests/test.rs b/tlsn/tests-integration/tests/test.rs index 9a34927066..89ed94e247 100644 --- a/tlsn/tests-integration/tests/test.rs +++ b/tlsn/tests-integration/tests/test.rs @@ -84,7 +84,7 @@ async fn prover(notary_socke prover.add_commitment_sent(0..sent_len as u32).unwrap(); prover.add_commitment_recv(0..recv_len as u32).unwrap(); - _ = prover.finalize().await.unwrap(); + _ = prover.finalize(None).await.unwrap(); } #[instrument(skip(socket))] diff --git a/tlsn/tlsn-core/src/lib.rs b/tlsn/tlsn-core/src/lib.rs index 30e6766d28..2190970b59 100644 --- a/tlsn/tlsn-core/src/lib.rs +++ b/tlsn/tlsn-core/src/lib.rs @@ -13,6 +13,7 @@ mod handshake_summary; pub(crate) mod inclusion_proof; pub mod merkle; pub mod msg; +pub mod redact; mod session; pub mod signature; pub mod substrings; diff --git a/tlsn/tlsn-core/src/redact/http.rs b/tlsn/tlsn-core/src/redact/http.rs new file mode 100644 index 0000000000..b483915f27 --- /dev/null +++ b/tlsn/tlsn-core/src/redact/http.rs @@ -0,0 +1 @@ +//! Some support for redacting http using e.g. pest diff --git a/tlsn/tlsn-core/src/redact/json.rs b/tlsn/tlsn-core/src/redact/json.rs new file mode 100644 index 0000000000..a325c9ae8b --- /dev/null +++ b/tlsn/tlsn-core/src/redact/json.rs @@ -0,0 +1 @@ +//! Some support for redacting json using e.g. pest diff --git a/tlsn/tlsn-core/src/redact/mod.rs b/tlsn/tlsn-core/src/redact/mod.rs new file mode 100644 index 0000000000..37add22b2d --- /dev/null +++ b/tlsn/tlsn-core/src/redact/mod.rs @@ -0,0 +1,33 @@ +//! This module provides tooling for redaction of sensitive information in the notarized +//! transcripts before sending them to the verifier. + +pub mod http; +pub mod json; + +/// A trait which allows users to redact bytes from the transcript +/// +/// After the traffic of a TLS session has been notarized, it is possible that the user wants to +/// redact certain information before sending it to the verifier. This trait allows arbitrary +/// redaction of bytes. +/// +/// The user has to make sure that redacted bytes have not been committed to. +pub trait Redact { + /// Redact the sent http request + fn redact_sent_headers(&mut self, headers: &mut [u8]); + ///Redact the body of the http request + fn redact_sent_body(&mut self, body: &mut [u8]); + /// Redact the http response + fn redact_received_headers(&mut self, headers: &mut [u8]); + ///Redact the body of the http response + fn redact_received_body(&mut self, body: &mut [u8]); +} + +/// A redaction implementation which does not redact anything +pub struct Identity; + +impl Redact for Identity { + fn redact_sent_headers(&mut self, _headers: &mut [u8]) {} + fn redact_sent_body(&mut self, _body: &mut [u8]) {} + fn redact_received_headers(&mut self, _headers: &mut [u8]) {} + fn redact_received_body(&mut self, _body: &mut [u8]) {} +} diff --git a/tlsn/tlsn-core/src/transcript.rs b/tlsn/tlsn-core/src/transcript.rs index 3e0a977d28..87fded1a9c 100644 --- a/tlsn/tlsn-core/src/transcript.rs +++ b/tlsn/tlsn-core/src/transcript.rs @@ -64,6 +64,16 @@ impl Transcript { pub fn data(&self) -> &[u8] { &self.data } + + /// Returns a mutable reference to data + pub fn data_mut(&mut self) -> &mut [u8] { + &mut self.data + } + + /// Returns a mutable reference to the bytes belonging to the http body + pub fn body_mut(&mut self) -> &mut [u8] { + todo!() + } } /// Authenticated slice of [Transcript]. The [Direction] should be infered from some outer context. diff --git a/tlsn/tlsn-prover/src/lib.rs b/tlsn/tlsn-prover/src/lib.rs index adab92bd68..d45e661cf2 100644 --- a/tlsn/tlsn-prover/src/lib.rs +++ b/tlsn/tlsn-prover/src/lib.rs @@ -36,6 +36,7 @@ use tlsn_core::{ commitment::Blake3, merkle::MerkleTree, msg::{SignedSessionHeader, TlsnMessage}, + redact::{Identity, Redact}, transcript::Transcript, Direction, NotarizedSession, SessionData, SubstringsCommitment, SubstringsCommitmentSet, }; @@ -313,7 +314,10 @@ where /// Finalize the notarization returning a [`NotarizedSession`] #[cfg_attr(feature = "tracing", instrument(level = "info", skip(self), err))] - pub async fn finalize(self) -> Result { + pub async fn finalize( + self, + redactor: Option>, + ) -> Result { let Notarize { notary_mux: mut mux, mut vm, @@ -322,8 +326,8 @@ where start_time, handshake_decommitment, server_public_key, - transcript_tx, - transcript_rx, + mut transcript_tx, + mut transcript_rx, commitments, substring_commitments, } = self.state; @@ -369,6 +373,15 @@ where let commitments = SubstringsCommitmentSet::new(substring_commitments); + // Redact parts of the transcript + let mut redactor = redactor.unwrap_or_else(|| Box::new(Identity)); + + redactor.redact_sent_headers(transcript_tx.data_mut()); + redactor.redact_sent_body(transcript_tx.body_mut()); + + redactor.redact_received_headers(transcript_rx.data_mut()); + redactor.redact_received_body(transcript_rx.body_mut()); + let data = SessionData::new( handshake_decommitment, transcript_tx, From 766224d56aca2937c24a9cfd88accc5dc3d8ed1b Mon Sep 17 00:00:00 2001 From: th4s Date: Sun, 20 Aug 2023 11:54:43 +0200 Subject: [PATCH 02/15] Do not assume http format for redaction --- tlsn/tlsn-core/src/redact/http.rs | 2 +- tlsn/tlsn-core/src/redact/mod.rs | 22 ++++------------------ tlsn/tlsn-core/src/transcript.rs | 5 ----- tlsn/tlsn-prover/src/lib.rs | 13 +++++-------- 4 files changed, 10 insertions(+), 32 deletions(-) diff --git a/tlsn/tlsn-core/src/redact/http.rs b/tlsn/tlsn-core/src/redact/http.rs index b483915f27..aa502325cf 100644 --- a/tlsn/tlsn-core/src/redact/http.rs +++ b/tlsn/tlsn-core/src/redact/http.rs @@ -1 +1 @@ -//! Some support for redacting http using e.g. pest +//! Some support for redacting http using httparse diff --git a/tlsn/tlsn-core/src/redact/mod.rs b/tlsn/tlsn-core/src/redact/mod.rs index 37add22b2d..18c7b054fe 100644 --- a/tlsn/tlsn-core/src/redact/mod.rs +++ b/tlsn/tlsn-core/src/redact/mod.rs @@ -12,22 +12,8 @@ pub mod json; /// /// The user has to make sure that redacted bytes have not been committed to. pub trait Redact { - /// Redact the sent http request - fn redact_sent_headers(&mut self, headers: &mut [u8]); - ///Redact the body of the http request - fn redact_sent_body(&mut self, body: &mut [u8]); - /// Redact the http response - fn redact_received_headers(&mut self, headers: &mut [u8]); - ///Redact the body of the http response - fn redact_received_body(&mut self, body: &mut [u8]); -} - -/// A redaction implementation which does not redact anything -pub struct Identity; - -impl Redact for Identity { - fn redact_sent_headers(&mut self, _headers: &mut [u8]) {} - fn redact_sent_body(&mut self, _body: &mut [u8]) {} - fn redact_received_headers(&mut self, _headers: &mut [u8]) {} - fn redact_received_body(&mut self, _body: &mut [u8]) {} + /// Redact bytes of the request + fn redact_request(&mut self, request: &mut [u8]); + /// Redact bytes of the response + fn redact_response(&mut self, response: &mut [u8]); } diff --git a/tlsn/tlsn-core/src/transcript.rs b/tlsn/tlsn-core/src/transcript.rs index 87fded1a9c..c25887f236 100644 --- a/tlsn/tlsn-core/src/transcript.rs +++ b/tlsn/tlsn-core/src/transcript.rs @@ -69,11 +69,6 @@ impl Transcript { pub fn data_mut(&mut self) -> &mut [u8] { &mut self.data } - - /// Returns a mutable reference to the bytes belonging to the http body - pub fn body_mut(&mut self) -> &mut [u8] { - todo!() - } } /// Authenticated slice of [Transcript]. The [Direction] should be infered from some outer context. diff --git a/tlsn/tlsn-prover/src/lib.rs b/tlsn/tlsn-prover/src/lib.rs index d45e661cf2..6cef8aac9e 100644 --- a/tlsn/tlsn-prover/src/lib.rs +++ b/tlsn/tlsn-prover/src/lib.rs @@ -36,7 +36,7 @@ use tlsn_core::{ commitment::Blake3, merkle::MerkleTree, msg::{SignedSessionHeader, TlsnMessage}, - redact::{Identity, Redact}, + redact::Redact, transcript::Transcript, Direction, NotarizedSession, SessionData, SubstringsCommitment, SubstringsCommitmentSet, }; @@ -374,13 +374,10 @@ where let commitments = SubstringsCommitmentSet::new(substring_commitments); // Redact parts of the transcript - let mut redactor = redactor.unwrap_or_else(|| Box::new(Identity)); - - redactor.redact_sent_headers(transcript_tx.data_mut()); - redactor.redact_sent_body(transcript_tx.body_mut()); - - redactor.redact_received_headers(transcript_rx.data_mut()); - redactor.redact_received_body(transcript_rx.body_mut()); + if let Some(mut redactor) = redactor { + redactor.redact_request(transcript_tx.data_mut()); + redactor.redact_response(transcript_rx.data_mut()); + } let data = SessionData::new( handshake_decommitment, From 0208859a7f339165b0d7282527ba4cb20bc82f0e Mon Sep 17 00:00:00 2001 From: th4s Date: Sun, 20 Aug 2023 13:53:16 +0200 Subject: [PATCH 03/15] Added SpanCommit trait --- tlsn/tlsn-core/src/lib.rs | 2 +- tlsn/tlsn-core/src/redact/mod.rs | 19 ------------ tlsn/tlsn-core/src/{redact => span}/http.rs | 0 tlsn/tlsn-core/src/{redact => span}/json.rs | 0 tlsn/tlsn-core/src/span/mod.rs | 21 +++++++++++++ tlsn/tlsn-core/src/transcript.rs | 5 --- tlsn/tlsn-prover/src/lib.rs | 34 ++++++++------------- 7 files changed, 35 insertions(+), 46 deletions(-) delete mode 100644 tlsn/tlsn-core/src/redact/mod.rs rename tlsn/tlsn-core/src/{redact => span}/http.rs (100%) rename tlsn/tlsn-core/src/{redact => span}/json.rs (100%) create mode 100644 tlsn/tlsn-core/src/span/mod.rs diff --git a/tlsn/tlsn-core/src/lib.rs b/tlsn/tlsn-core/src/lib.rs index 2190970b59..9537279ee4 100644 --- a/tlsn/tlsn-core/src/lib.rs +++ b/tlsn/tlsn-core/src/lib.rs @@ -13,9 +13,9 @@ mod handshake_summary; pub(crate) mod inclusion_proof; pub mod merkle; pub mod msg; -pub mod redact; mod session; pub mod signature; +pub mod span; pub mod substrings; pub mod transcript; mod utils; diff --git a/tlsn/tlsn-core/src/redact/mod.rs b/tlsn/tlsn-core/src/redact/mod.rs deleted file mode 100644 index 18c7b054fe..0000000000 --- a/tlsn/tlsn-core/src/redact/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! This module provides tooling for redaction of sensitive information in the notarized -//! transcripts before sending them to the verifier. - -pub mod http; -pub mod json; - -/// A trait which allows users to redact bytes from the transcript -/// -/// After the traffic of a TLS session has been notarized, it is possible that the user wants to -/// redact certain information before sending it to the verifier. This trait allows arbitrary -/// redaction of bytes. -/// -/// The user has to make sure that redacted bytes have not been committed to. -pub trait Redact { - /// Redact bytes of the request - fn redact_request(&mut self, request: &mut [u8]); - /// Redact bytes of the response - fn redact_response(&mut self, response: &mut [u8]); -} diff --git a/tlsn/tlsn-core/src/redact/http.rs b/tlsn/tlsn-core/src/span/http.rs similarity index 100% rename from tlsn/tlsn-core/src/redact/http.rs rename to tlsn/tlsn-core/src/span/http.rs diff --git a/tlsn/tlsn-core/src/redact/json.rs b/tlsn/tlsn-core/src/span/json.rs similarity index 100% rename from tlsn/tlsn-core/src/redact/json.rs rename to tlsn/tlsn-core/src/span/json.rs diff --git a/tlsn/tlsn-core/src/span/mod.rs b/tlsn/tlsn-core/src/span/mod.rs new file mode 100644 index 0000000000..d5cf1e5bbf --- /dev/null +++ b/tlsn/tlsn-core/src/span/mod.rs @@ -0,0 +1,21 @@ +//! This module provides tooling to create spanning information for the [transcripts](crate::transcript::Transcript). +//! +//! When creating a [NotarizedSession](crate::NotarizedSession), the +//! [SessionData](crate::SessionData) inside contains the plaintext of the request and response. +//! The prover can decide to only commit to a subset of these bytes in order to withhold content +//! from the verifier. Consumers of this crate can implement the [SpanCommit] trait to come up with +//! their own approach for identifying the byte ranges which shall be committed to. + +use std::ops::Range; + +pub mod http; +pub mod json; + +/// A trait for identifying byte ranges in the request and response for which commitments will be +/// created +pub trait SpanCommit { + /// Identify byte ranges in the request to commit to + fn span_request(&mut self, request: &[u8]) -> Vec>; + /// Identify byte ranges in the response to commit to + fn span_response(&mut self, response: &[u8]) -> Vec>; +} diff --git a/tlsn/tlsn-core/src/transcript.rs b/tlsn/tlsn-core/src/transcript.rs index c25887f236..3e0a977d28 100644 --- a/tlsn/tlsn-core/src/transcript.rs +++ b/tlsn/tlsn-core/src/transcript.rs @@ -64,11 +64,6 @@ impl Transcript { pub fn data(&self) -> &[u8] { &self.data } - - /// Returns a mutable reference to data - pub fn data_mut(&mut self) -> &mut [u8] { - &mut self.data - } } /// Authenticated slice of [Transcript]. The [Direction] should be infered from some outer context. diff --git a/tlsn/tlsn-prover/src/lib.rs b/tlsn/tlsn-prover/src/lib.rs index 6cef8aac9e..006d1b2273 100644 --- a/tlsn/tlsn-prover/src/lib.rs +++ b/tlsn/tlsn-prover/src/lib.rs @@ -36,7 +36,7 @@ use tlsn_core::{ commitment::Blake3, merkle::MerkleTree, msg::{SignedSessionHeader, TlsnMessage}, - redact::Redact, + span::SpanCommit, transcript::Transcript, Direction, NotarizedSession, SessionData, SubstringsCommitment, SubstringsCommitmentSet, }; @@ -261,16 +261,6 @@ where &self.state.transcript_rx } - /// Add a commitment to the sent requests - pub fn add_commitment_sent(&mut self, range: Range) -> Result<(), ProverError> { - self.add_commitment(range, Direction::Sent) - } - - /// Add a commitment to the received responses - pub fn add_commitment_recv(&mut self, range: Range) -> Result<(), ProverError> { - self.add_commitment(range, Direction::Received) - } - #[cfg_attr( feature = "tracing", instrument(level = "debug", skip(self, range), err) @@ -315,9 +305,17 @@ where /// Finalize the notarization returning a [`NotarizedSession`] #[cfg_attr(feature = "tracing", instrument(level = "info", skip(self), err))] pub async fn finalize( - self, - redactor: Option>, + mut self, + mut spanner: Box, ) -> Result { + // Add commitments identified by the spanner + for range in spanner.span_request(self.state.transcript_tx.data()) { + self.add_commitment(range, Direction::Sent)?; + } + for range in spanner.span_response(self.state.transcript_rx.data()) { + self.add_commitment(range, Direction::Received)?; + } + let Notarize { notary_mux: mut mux, mut vm, @@ -326,8 +324,8 @@ where start_time, handshake_decommitment, server_public_key, - mut transcript_tx, - mut transcript_rx, + transcript_tx, + transcript_rx, commitments, substring_commitments, } = self.state; @@ -373,12 +371,6 @@ where let commitments = SubstringsCommitmentSet::new(substring_commitments); - // Redact parts of the transcript - if let Some(mut redactor) = redactor { - redactor.redact_request(transcript_tx.data_mut()); - redactor.redact_response(transcript_rx.data_mut()); - } - let data = SessionData::new( handshake_decommitment, transcript_tx, From 6828734dc5cb7fe39719b64f50492f7ea4a7ad87 Mon Sep 17 00:00:00 2001 From: th4s Date: Sun, 20 Aug 2023 15:46:15 +0200 Subject: [PATCH 04/15] WIP: Implement spanning for http --- tlsn/tlsn-core/Cargo.toml | 1 + tlsn/tlsn-core/src/span/http.rs | 58 +++++++++++++++++++++++++++++++++ tlsn/tlsn-core/src/span/mod.rs | 8 +++++ 3 files changed, 67 insertions(+) diff --git a/tlsn/tlsn-core/Cargo.toml b/tlsn/tlsn-core/Cargo.toml index 64aacec495..2bad9a3bba 100644 --- a/tlsn/tlsn-core/Cargo.toml +++ b/tlsn/tlsn-core/Cargo.toml @@ -30,6 +30,7 @@ rs_merkle.workspace = true rstest = { workspace = true, optional = true} hex = { workspace = true, optional = true} tracing = { workspace = true, optional = true } +httparse = "1" [dev-dependencies] rstest.workspace = true diff --git a/tlsn/tlsn-core/src/span/http.rs b/tlsn/tlsn-core/src/span/http.rs index aa502325cf..1773f693b5 100644 --- a/tlsn/tlsn-core/src/span/http.rs +++ b/tlsn/tlsn-core/src/span/http.rs @@ -1 +1,59 @@ //! Some support for redacting http using httparse + +use super::SpanError; +use httparse::{Header, Request, Response, Status}; +use std::ops::Range; + +/// A Spanner for HTTP +pub struct HttpSpanner<'a, 'b> { + headers: Vec>, + body_start: usize, + request: Option>, + response: Option>, +} + +impl<'a, 'b> HttpSpanner<'a, 'b> { + /// Create a new HttpSpanner + pub fn new(len: usize) -> Self { + HttpSpanner { + headers: vec![httparse::EMPTY_HEADER; len], + body_start: 0, + request: None, + response: None, + } + } +} + +impl<'a, 'b> HttpSpanner<'a, 'b> { + /// Parse a http request + pub fn parse_request(&'a mut self, bytes: &'b [u8]) -> Result<(), SpanError> { + let mut request = Request::new(&mut self.headers[..]); + match request.parse(bytes) { + Ok(Status::Complete(body_start)) => { + self.body_start = body_start; + self.request = Some(request); + Ok(()) + } + Ok(Status::Partial) => Err(SpanError::ParseError), + Err(_) => Err(SpanError::ParseError), + } + } + + /// Return the byte offset where the body starts + pub fn body_start(&self) -> usize { + self.body_start + } + + /// Return the byte range matching the value belonging to the specified header key + pub fn header_span(&self, key: &str, bytes: &[u8]) -> Option> { + let request = self.request.as_ref()?; + let header = request.headers.iter().find(|h| h.name == key)?; + Some(get_ptr_range(bytes, header.value)) + } +} + +fn get_ptr_range(whole: &[u8], part: &[u8]) -> Range { + let start = part.as_ptr() as u32 - whole.as_ptr() as u32; + let end = start + part.len() as u32; + Range { start, end } +} diff --git a/tlsn/tlsn-core/src/span/mod.rs b/tlsn/tlsn-core/src/span/mod.rs index d5cf1e5bbf..d504bf41d3 100644 --- a/tlsn/tlsn-core/src/span/mod.rs +++ b/tlsn/tlsn-core/src/span/mod.rs @@ -19,3 +19,11 @@ pub trait SpanCommit { /// Identify byte ranges in the response to commit to fn span_response(&mut self, response: &[u8]) -> Vec>; } + +/// An error that can occur during span creation +#[derive(Debug, thiserror::Error)] +pub enum SpanError { + /// The request or response could not be parsed + #[error("Error during parsing")] + ParseError, +} From 4fa3223bdc4c9c6210854bb6a27ffa6df60844ef Mon Sep 17 00:00:00 2001 From: th4s Date: Mon, 21 Aug 2023 17:06:42 +0200 Subject: [PATCH 05/15] Added HttpSpanner functionality and tests --- tlsn/tlsn-core/src/lib.rs | 1 - tlsn/tlsn-core/src/span/http.rs | 163 ++++++++++++++++++++++++++++---- 2 files changed, 146 insertions(+), 18 deletions(-) diff --git a/tlsn/tlsn-core/src/lib.rs b/tlsn/tlsn-core/src/lib.rs index 9537279ee4..e541e6621d 100644 --- a/tlsn/tlsn-core/src/lib.rs +++ b/tlsn/tlsn-core/src/lib.rs @@ -2,7 +2,6 @@ #![deny(missing_docs, unreachable_pub, unused_must_use)] #![deny(clippy::all)] -#![forbid(unsafe_code)] pub mod commitment; mod error; diff --git a/tlsn/tlsn-core/src/span/http.rs b/tlsn/tlsn-core/src/span/http.rs index 1773f693b5..8749542db4 100644 --- a/tlsn/tlsn-core/src/span/http.rs +++ b/tlsn/tlsn-core/src/span/http.rs @@ -2,35 +2,45 @@ use super::SpanError; use httparse::{Header, Request, Response, Status}; -use std::ops::Range; +use std::{ops::Range, panic}; /// A Spanner for HTTP pub struct HttpSpanner<'a, 'b> { - headers: Vec>, - body_start: usize, + body_start_request: Option, + body_start_response: Option, request: Option>, response: Option>, } impl<'a, 'b> HttpSpanner<'a, 'b> { /// Create a new HttpSpanner - pub fn new(len: usize) -> Self { + pub fn new() -> Self { HttpSpanner { - headers: vec![httparse::EMPTY_HEADER; len], - body_start: 0, + body_start_request: None, + body_start_response: None, request: None, response: None, } } } +impl<'a, 'b> Default for HttpSpanner<'a, 'b> { + fn default() -> Self { + HttpSpanner::new() + } +} + impl<'a, 'b> HttpSpanner<'a, 'b> { /// Parse a http request - pub fn parse_request(&'a mut self, bytes: &'b [u8]) -> Result<(), SpanError> { - let mut request = Request::new(&mut self.headers[..]); + pub fn parse_request( + &mut self, + headers: &'a mut [Header<'b>], + bytes: &'b [u8], + ) -> Result<(), SpanError> { + let mut request = Request::new(headers); match request.parse(bytes) { Ok(Status::Complete(body_start)) => { - self.body_start = body_start; + self.body_start_request = Some(body_start); self.request = Some(request); Ok(()) } @@ -39,21 +49,140 @@ impl<'a, 'b> HttpSpanner<'a, 'b> { } } - /// Return the byte offset where the body starts - pub fn body_start(&self) -> usize { - self.body_start + /// Parse a http response + pub fn parse_response( + &mut self, + headers: &'a mut [Header<'b>], + bytes: &'b [u8], + ) -> Result<(), SpanError> { + let mut response = Response::new(headers); + match response.parse(bytes) { + Ok(Status::Complete(body_start)) => { + self.body_start_response = Some(body_start); + self.response = Some(response); + Ok(()) + } + Ok(Status::Partial) => Err(SpanError::ParseError), + Err(_) => Err(SpanError::ParseError), + } + } + + /// Return the byte offset where the request body starts + pub fn body_start_request(&self) -> Option { + self.body_start_request } - /// Return the byte range matching the value belonging to the specified header key - pub fn header_span(&self, key: &str, bytes: &[u8]) -> Option> { + /// Return the byte offset where the response body starts + pub fn body_start_response(&self) -> Option { + self.body_start_response + } + + /// Return the byte range matching the value belonging to the specified header key in the + /// request + pub fn header_value_span_request(&self, key: &str, bytes: &[u8]) -> Option> { let request = self.request.as_ref()?; let header = request.headers.iter().find(|h| h.name == key)?; Some(get_ptr_range(bytes, header.value)) } + + /// Return the byte range matching the value belonging to the specified header key in the + /// response + pub fn header_value_span_response(&self, key: &str, bytes: &[u8]) -> Option> { + let request = self.response.as_ref()?; + let header = request.headers.iter().find(|h| h.name == key)?; + Some(get_ptr_range(bytes, header.value)) + } } -fn get_ptr_range(whole: &[u8], part: &[u8]) -> Range { - let start = part.as_ptr() as u32 - whole.as_ptr() as u32; - let end = start + part.len() as u32; +fn get_ptr_range(whole: &[u8], part: &[u8]) -> Range { + if part.as_ptr() < whole.as_ptr() { + panic!("part is not a part of whole"); + } + let start = unsafe { part.as_ptr().offset_from(whole.as_ptr()) as usize }; + let end = start + part.len(); Range { start, end } } + +#[cfg(test)] +mod tests { + use super::*; + + const TEST_REQUEST: &[u8] = b"\ + GET /home.html HTTP/1.1\n\ + Host: developer.mozilla.org\n\ + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0\n\ + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.\n\ + Accept-Language: en-US,en;q=0.\n\ + Accept-Encoding: gzip, deflate, b\n\ + Referer: https://developer.mozilla.org/testpage.htm\n\ + Connection: keep-alive\n\ + Cache-Control: max-age=0\n\n\ + Hello World!"; + + const TEST_RESPONSE: &[u8] = b"\ + HTTP/1.1 200 OK\n\ + Date: Mon, 27 Jul 2009 12:28:53 GMT\n\ + Server: Apache/2.2.14 (Win32)\n\ + Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT\n\ + Content-Length: 88\n\ + Content-Type: text/html\n\ + Connection: Closed\n\n\ + \n\ + \n\ +

Hello, World!

\n\ + \n\ + "; + + #[test] + fn test_parse_request() { + let mut spanner = HttpSpanner::new(); + let mut headers = vec![httparse::EMPTY_HEADER; 8]; + spanner.parse_request(&mut headers, TEST_REQUEST).unwrap(); + assert_eq!( + &TEST_REQUEST[spanner.body_start_request().unwrap()..], + b"Hello World!" + ); + } + + #[test] + fn test_header_value_span_request() { + let mut spanner = HttpSpanner::new(); + let mut headers = vec![httparse::EMPTY_HEADER; 8]; + spanner.parse_request(&mut headers, TEST_REQUEST).unwrap(); + assert_eq!( + &TEST_REQUEST[spanner + .header_value_span_request("Host", TEST_REQUEST) + .unwrap()], + b"developer.mozilla.org" + ); + } + + #[test] + fn test_parse_response() { + let mut spanner = HttpSpanner::new(); + let mut headers = vec![httparse::EMPTY_HEADER; 6]; + spanner.parse_response(&mut headers, TEST_RESPONSE).unwrap(); + assert_eq!( + &TEST_RESPONSE[spanner.body_start_response().unwrap()..], + b"\ + \n\ + \n\ +

Hello, World!

\n\ + \n\ + " + ); + } + + #[test] + fn test_header_value_span_response() { + let mut spanner = HttpSpanner::new(); + let mut headers = vec![httparse::EMPTY_HEADER; 6]; + spanner.parse_response(&mut headers, TEST_RESPONSE).unwrap(); + assert_eq!( + &TEST_RESPONSE[spanner + .header_value_span_response("Content-Type", TEST_RESPONSE) + .unwrap()], + b"text/html" + ); + } +} From 3074bc15fa59eb79955be3a4d5d5f5516dd6f48f Mon Sep 17 00:00:00 2001 From: th4s Date: Mon, 21 Aug 2023 22:58:39 +0200 Subject: [PATCH 06/15] WIP: Modifying twitter example and improving API... --- tlsn/examples/Cargo.toml | 1 + tlsn/examples/twitter_dm.rs | 32 ++++++++++++++++++++++++++++++++ tlsn/tlsn-core/src/span/mod.rs | 34 ++++++++++++++++++++++++++++++++-- 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/tlsn/examples/Cargo.toml b/tlsn/examples/Cargo.toml index ff8f3a8e9c..2c7e572b97 100644 --- a/tlsn/examples/Cargo.toml +++ b/tlsn/examples/Cargo.toml @@ -39,6 +39,7 @@ rustls = { version = "0.21" } rustls-pemfile = { version = "1.0.2" } tokio-rustls = { version = "0.24.1" } dotenv = "0.15.0" +httparse = "1" [[example]] name = "twitter_dm" diff --git a/tlsn/examples/twitter_dm.rs b/tlsn/examples/twitter_dm.rs index 2cf17208eb..4b1ff35025 100644 --- a/tlsn/examples/twitter_dm.rs +++ b/tlsn/examples/twitter_dm.rs @@ -1,6 +1,7 @@ /// This prover implementation talks to the notary server implemented in https://github.com/tlsnotary/notary-server, instead of the simple_notary.rs in this example directory use eyre::Result; use futures::AsyncWriteExt; +use httparse::EMPTY_HEADER; use hyper::{body::to_bytes, client::conn::Parts, Body, Request, StatusCode}; use rustls::{Certificate, ClientConfig, RootCertStore}; use serde::{Deserialize, Serialize}; @@ -12,6 +13,7 @@ use std::{ ops::Range, sync::Arc, }; +use tlsn_core::span::{http::HttpSpanner, SpanCommit}; use tokio::{fs::File, io::AsyncWriteExt as _}; use tokio_rustls::TlsConnector; use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt}; @@ -322,3 +324,33 @@ async fn read_pem_file(file_path: &str) -> Result> { let key_file = File::open(file_path).await?.into_std().await; Ok(BufReader::new(key_file)) } + +struct TwitterSpanner<'a, 'b> { + http: HttpSpanner<'a, 'b>, +} + +impl<'a, 'b> SpanCommit for TwitterSpanner<'a, 'b> { + fn span_request(&mut self, request: &[u8]) -> Vec> { + let mut headers = vec![EMPTY_HEADER; 12]; + self.http.parse_request(&mut headers, request); + + let cookie = self + .http + .header_value_span_request("Cookie", request) + .unwrap(); + let authorization = self + .http + .header_value_span_request("Authorization", request) + .unwrap(); + let csrf = self + .http + .header_value_span_request("X-Csrf-Token", request) + .unwrap(); + + vec![cookie, authorization, csrf] + } + + fn span_response(&mut self, response: &[u8]) -> Vec> { + vec![0..response.len()] + } +} diff --git a/tlsn/tlsn-core/src/span/mod.rs b/tlsn/tlsn-core/src/span/mod.rs index d504bf41d3..30d1c81a96 100644 --- a/tlsn/tlsn-core/src/span/mod.rs +++ b/tlsn/tlsn-core/src/span/mod.rs @@ -15,9 +15,37 @@ pub mod json; /// created pub trait SpanCommit { /// Identify byte ranges in the request to commit to - fn span_request(&mut self, request: &[u8]) -> Vec>; + fn span_request(&mut self, request: &[u8]) -> Vec>; /// Identify byte ranges in the response to commit to - fn span_response(&mut self, response: &[u8]) -> Vec>; + fn span_response(&mut self, response: &[u8]) -> Vec>; +} + +pub fn invert_ranges( + ranges: Vec>, + len: usize, +) -> Result>, SpanError> { + for range in ranges.iter() { + // Check that there is no invalid or empty range + if range.start >= range.end { + return Err(SpanError::InvalidRange); + } + + // Check that ranges are not out of bounds + if range.start >= len || range.end > len { + return Err(SpanError::InvalidRange); + } + + // Check that ranges are not overlapping + if ranges + .iter() + .any(|r| r.start < range.end && r.end > range.start) + { + return Err(SpanError::InvalidRange); + } + } + + // Now invert ranges + let mut inverted = (0..len).collect::>(); } /// An error that can occur during span creation @@ -26,4 +54,6 @@ pub enum SpanError { /// The request or response could not be parsed #[error("Error during parsing")] ParseError, + #[error("Found invalid ranges")] + InvalidRange, } From 26361d4d2331b83d0bb39e3dbfb0e3fb54358006 Mon Sep 17 00:00:00 2001 From: th4s Date: Tue, 22 Aug 2023 16:17:36 +0200 Subject: [PATCH 07/15] Adapted examples and added `TotalSpanner` - completed `fn invert_ranges` --- tlsn/examples/twitter_dm.rs | 90 ++++++---------------------- tlsn/tests-integration/Cargo.toml | 1 + tlsn/tests-integration/tests/test.rs | 11 +--- tlsn/tlsn-core/src/span/mod.rs | 47 +++++++++++++-- tlsn/tlsn-prover/src/error.rs | 2 + tlsn/tlsn-prover/src/lib.rs | 8 +-- 6 files changed, 72 insertions(+), 87 deletions(-) diff --git a/tlsn/examples/twitter_dm.rs b/tlsn/examples/twitter_dm.rs index 4b1ff35025..2e30b14fab 100644 --- a/tlsn/examples/twitter_dm.rs +++ b/tlsn/examples/twitter_dm.rs @@ -13,7 +13,7 @@ use std::{ ops::Range, sync::Arc, }; -use tlsn_core::span::{http::HttpSpanner, SpanCommit}; +use tlsn_core::span::{http::HttpSpanner, invert_ranges, SpanCommit, SpanError}; use tokio::{fs::File, io::AsyncWriteExt as _}; use tokio_rustls::TlsConnector; use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt}; @@ -119,7 +119,7 @@ async fn main() { let request = Request::builder() .uri(format!("https://{NOTARY_DOMAIN}:{NOTARY_PORT}/session")) .method("POST") - .header("Host", NOTARY_DOMAIN.clone()) + .header("Host", NOTARY_DOMAIN) // Need to specify application/json for axum to parse it as json .header("Content-Type", "application/json") .body(Body::from(payload)) @@ -250,29 +250,9 @@ async fn main() { client_socket.close().await.unwrap(); // The Prover task should be done now, so we can grab it. - let mut prover = prover_task.await.unwrap().unwrap(); - - // Identify the ranges in the transcript that contain secrets - let (public_ranges, private_ranges) = find_ranges( - prover.sent_transcript().data(), - &[ - access_token.as_bytes(), - auth_token.as_bytes(), - csrf_token.as_bytes(), - ], - ); - - // Commit to the outbound transcript, isolating the data that contain secrets - for range in public_ranges.iter().chain(private_ranges.iter()) { - prover.add_commitment_sent(range.clone()).unwrap(); - } + let prover = prover_task.await.unwrap().unwrap(); - // Commit to the full received transcript in one shot, as we don't need to redact anything - let recv_len = prover.recv_transcript().data().len(); - prover.add_commitment_recv(0..recv_len as u32).unwrap(); - - // Finalize, returning the notarized session - let notarized_session = prover.finalize(None).await.unwrap(); + let notarized_session = prover.finalize(Box::new(TwitterSpanner)).await.unwrap(); debug!("Notarization complete!"); @@ -287,70 +267,38 @@ async fn main() { .unwrap(); } -/// Find the ranges of the public and private parts of a sequence. -/// -/// Returns a tuple of `(public, private)` ranges. -fn find_ranges(seq: &[u8], sub_seq: &[&[u8]]) -> (Vec>, Vec>) { - let mut private_ranges = Vec::new(); - for s in sub_seq { - for (idx, w) in seq.windows(s.len()).enumerate() { - if w == *s { - private_ranges.push(idx as u32..(idx + w.len()) as u32); - } - } - } - - let mut sorted_ranges = private_ranges.clone(); - sorted_ranges.sort_by_key(|r| r.start); - - let mut public_ranges = Vec::new(); - let mut last_end = 0; - for r in sorted_ranges { - if r.start > last_end { - public_ranges.push(last_end..r.start); - } - last_end = r.end; - } - - if last_end < seq.len() as u32 { - public_ranges.push(last_end..seq.len() as u32); - } - - (public_ranges, private_ranges) -} - /// Read a PEM-formatted file and return its buffer reader async fn read_pem_file(file_path: &str) -> Result> { let key_file = File::open(file_path).await?.into_std().await; Ok(BufReader::new(key_file)) } -struct TwitterSpanner<'a, 'b> { - http: HttpSpanner<'a, 'b>, -} +struct TwitterSpanner; -impl<'a, 'b> SpanCommit for TwitterSpanner<'a, 'b> { - fn span_request(&mut self, request: &[u8]) -> Vec> { +impl SpanCommit for TwitterSpanner { + fn span_request(&mut self, request: &[u8]) -> Result>, SpanError> { let mut headers = vec![EMPTY_HEADER; 12]; - self.http.parse_request(&mut headers, request); + let mut http_spanner = HttpSpanner::new(); + + http_spanner.parse_request(&mut headers, request).unwrap(); - let cookie = self - .http + let cookie = http_spanner .header_value_span_request("Cookie", request) .unwrap(); - let authorization = self - .http + let authorization = http_spanner .header_value_span_request("Authorization", request) .unwrap(); - let csrf = self - .http + let csrf = http_spanner .header_value_span_request("X-Csrf-Token", request) .unwrap(); - vec![cookie, authorization, csrf] + invert_ranges(vec![cookie, authorization, csrf], request.len()) } - fn span_response(&mut self, response: &[u8]) -> Vec> { - vec![0..response.len()] + fn span_response(&mut self, response: &[u8]) -> Result>, SpanError> { + Ok(vec![Range { + start: 0, + end: response.len(), + }]) } } diff --git a/tlsn/tests-integration/Cargo.toml b/tlsn/tests-integration/Cargo.toml index 2c6f63d81c..2444fd9813 100644 --- a/tlsn/tests-integration/Cargo.toml +++ b/tlsn/tests-integration/Cargo.toml @@ -13,6 +13,7 @@ publish = false tlsn-tls-core.workspace = true tlsn-prover.workspace = true tlsn-notary.workspace = true +tlsn-core.workspace = true tls-server-fixture.workspace = true p256 = { workspace = true, features = ["ecdsa"] } diff --git a/tlsn/tests-integration/tests/test.rs b/tlsn/tests-integration/tests/test.rs index 89ed94e247..b79365a20e 100644 --- a/tlsn/tests-integration/tests/test.rs +++ b/tlsn/tests-integration/tests/test.rs @@ -1,6 +1,7 @@ use futures::AsyncWriteExt; use hyper::{body::to_bytes, Body, Request, StatusCode}; use tls_server_fixture::{bind_test_server_hyper, CA_CERT_DER, SERVER_DOMAIN}; +use tlsn_core::span::TotalSpanner; use tlsn_notary::{bind_notary, NotaryConfig}; use tlsn_prover::{bind_prover, ProverConfig}; use tokio::io::{AsyncRead, AsyncWrite}; @@ -76,15 +77,9 @@ async fn prover(notary_socke client_socket.close().await.unwrap(); - let mut prover = prover_task.await.unwrap().unwrap(); + let prover = prover_task.await.unwrap().unwrap(); - let sent_len = prover.sent_transcript().data().len(); - let recv_len = prover.recv_transcript().data().len(); - - prover.add_commitment_sent(0..sent_len as u32).unwrap(); - prover.add_commitment_recv(0..recv_len as u32).unwrap(); - - _ = prover.finalize(None).await.unwrap(); + _ = prover.finalize(Box::new(TotalSpanner)).await.unwrap(); } #[instrument(skip(socket))] diff --git a/tlsn/tlsn-core/src/span/mod.rs b/tlsn/tlsn-core/src/span/mod.rs index 30d1c81a96..cc1efcca29 100644 --- a/tlsn/tlsn-core/src/span/mod.rs +++ b/tlsn/tlsn-core/src/span/mod.rs @@ -15,11 +15,31 @@ pub mod json; /// created pub trait SpanCommit { /// Identify byte ranges in the request to commit to - fn span_request(&mut self, request: &[u8]) -> Vec>; + fn span_request(&mut self, request: &[u8]) -> Result>, SpanError>; /// Identify byte ranges in the response to commit to - fn span_response(&mut self, response: &[u8]) -> Vec>; + fn span_response(&mut self, response: &[u8]) -> Result>, SpanError>; } +/// A Spanner that commits to the entire request and response +pub struct TotalSpanner; + +impl SpanCommit for TotalSpanner { + fn span_request(&mut self, request: &[u8]) -> Result>, SpanError> { + Ok(vec![Range { + start: 0, + end: request.len(), + }]) + } + + fn span_response(&mut self, response: &[u8]) -> Result>, SpanError> { + Ok(vec![Range { + start: 0, + end: response.len(), + }]) + } +} + +/// Inverts a set of ranges, i.e. returns the complement of the ranges pub fn invert_ranges( ranges: Vec>, len: usize, @@ -45,15 +65,34 @@ pub fn invert_ranges( } // Now invert ranges - let mut inverted = (0..len).collect::>(); + let mut inverted = vec![Range { start: 0, end: len }]; + + for range in ranges.iter() { + let inv = inverted + .iter_mut() + .find(|inv| range.start >= inv.start) + .unwrap(); + + let original_len = inv.end; + inv.end = range.start; + + inverted.push(Range { + start: range.end, + end: original_len, + }); + } + + Ok(inverted) } /// An error that can occur during span creation +#[allow(missing_docs)] #[derive(Debug, thiserror::Error)] pub enum SpanError { - /// The request or response could not be parsed #[error("Error during parsing")] ParseError, #[error("Found invalid ranges")] InvalidRange, + #[error("Custom error: {0}")] + Custom(String), } diff --git a/tlsn/tlsn-prover/src/error.rs b/tlsn/tlsn-prover/src/error.rs index bf963172cb..8b5ea25231 100644 --- a/tlsn/tlsn-prover/src/error.rs +++ b/tlsn/tlsn-prover/src/error.rs @@ -22,6 +22,8 @@ pub enum ProverError { ServerNoCloseNotify, #[error(transparent)] CommitmentError(#[from] CommitmentError), + #[error(transparent)] + SpanError(#[from] tlsn_core::span::SpanError), } impl From for ProverError { diff --git a/tlsn/tlsn-prover/src/lib.rs b/tlsn/tlsn-prover/src/lib.rs index 006d1b2273..2dea30ba03 100644 --- a/tlsn/tlsn-prover/src/lib.rs +++ b/tlsn/tlsn-prover/src/lib.rs @@ -309,11 +309,11 @@ where mut spanner: Box, ) -> Result { // Add commitments identified by the spanner - for range in spanner.span_request(self.state.transcript_tx.data()) { - self.add_commitment(range, Direction::Sent)?; + for range in spanner.span_request(self.state.transcript_tx.data())? { + self.add_commitment(range.start as u32..range.end as u32, Direction::Sent)?; } - for range in spanner.span_response(self.state.transcript_rx.data()) { - self.add_commitment(range, Direction::Received)?; + for range in spanner.span_response(self.state.transcript_rx.data())? { + self.add_commitment(range.start as u32..range.end as u32, Direction::Received)?; } let Notarize { From 98f716053aa394b009313ae11c282324983f4969 Mon Sep 17 00:00:00 2001 From: th4s Date: Wed, 23 Aug 2023 11:59:02 +0200 Subject: [PATCH 08/15] Fixed `invert_ranges` and added tests --- tlsn/tlsn-core/src/span/mod.rs | 73 +++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/tlsn/tlsn-core/src/span/mod.rs b/tlsn/tlsn-core/src/span/mod.rs index cc1efcca29..1411d653e4 100644 --- a/tlsn/tlsn-core/src/span/mod.rs +++ b/tlsn/tlsn-core/src/span/mod.rs @@ -44,7 +44,7 @@ pub fn invert_ranges( ranges: Vec>, len: usize, ) -> Result>, SpanError> { - for range in ranges.iter() { + for (k, range) in ranges.iter().enumerate() { // Check that there is no invalid or empty range if range.start >= range.end { return Err(SpanError::InvalidRange); @@ -58,7 +58,8 @@ pub fn invert_ranges( // Check that ranges are not overlapping if ranges .iter() - .any(|r| r.start < range.end && r.end > range.start) + .enumerate() + .any(|(l, r)| k != l && r.start < range.end && r.end > range.start) { return Err(SpanError::InvalidRange); } @@ -70,18 +71,21 @@ pub fn invert_ranges( for range in ranges.iter() { let inv = inverted .iter_mut() - .find(|inv| range.start >= inv.start) - .unwrap(); + .find(|inv| range.start >= inv.start && range.end <= inv.end) + .expect("Should have found range to invert"); - let original_len = inv.end; + let original_end = inv.end; inv.end = range.start; inverted.push(Range { start: range.end, - end: original_len, + end: original_end, }); } + // Remove empty ranges + inverted.retain(|r| r.start != r.end); + Ok(inverted) } @@ -96,3 +100,60 @@ pub enum SpanError { #[error("Custom error: {0}")] Custom(String), } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_invert_ranges_errors() { + let empty_range = Range { start: 0, end: 0 }; + let invalid_range = Range { start: 2, end: 1 }; + let out_of_bounds = Range { start: 4, end: 11 }; + + let ranges = vec![empty_range, invalid_range, out_of_bounds]; + + for range in ranges { + assert!(invert_ranges(vec![range], 10).is_err()); + } + } + + #[test] + fn test_invert_ranges_overlapping() { + let overlapping1 = vec![Range { start: 2, end: 5 }, Range { start: 4, end: 7 }]; + let overlapping2 = vec![Range { start: 2, end: 5 }, Range { start: 1, end: 4 }]; + let overlapping3 = vec![Range { start: 2, end: 5 }, Range { start: 3, end: 4 }]; + let overlapping4 = vec![Range { start: 2, end: 5 }, Range { start: 2, end: 5 }]; + + // this should not be an error + let ok1 = vec![Range { start: 2, end: 5 }, Range { start: 5, end: 8 }]; + let ok2 = vec![Range { start: 2, end: 5 }, Range { start: 7, end: 10 }]; + + let overlap = vec![overlapping1, overlapping2, overlapping3, overlapping4]; + let ok = vec![ok1, ok2]; + + for range in overlap { + assert!(invert_ranges(range, 10).is_err()); + } + + for range in ok { + assert!(invert_ranges(range, 10).is_ok()); + } + } + + #[test] + fn test_invert_ranges() { + let len = 20; + + let ranges = vec![ + Range { start: 0, end: 5 }, + Range { start: 5, end: 10 }, + Range { start: 12, end: 16 }, + Range { start: 18, end: 20 }, + ]; + + let expected = vec![Range { start: 10, end: 12 }, Range { start: 16, end: 18 }]; + + assert_eq!(invert_ranges(ranges, len).unwrap(), expected); + } +} From 7d1635c4123b45cb2c6f8ad3b136bce970ca962b Mon Sep 17 00:00:00 2001 From: th4s Date: Thu, 24 Aug 2023 16:33:33 +0200 Subject: [PATCH 09/15] Added pest json parser --- tlsn/tlsn-core/Cargo.toml | 2 ++ tlsn/tlsn-core/src/span/json.rs | 1 - tlsn/tlsn-core/src/span/json/json.pest | 41 ++++++++++++++++++++++++++ tlsn/tlsn-core/src/span/json/mod.rs | 40 +++++++++++++++++++++++++ tlsn/tlsn-core/src/span/mod.rs | 1 + 5 files changed, 84 insertions(+), 1 deletion(-) delete mode 100644 tlsn/tlsn-core/src/span/json.rs create mode 100644 tlsn/tlsn-core/src/span/json/json.pest create mode 100644 tlsn/tlsn-core/src/span/json/mod.rs diff --git a/tlsn/tlsn-core/Cargo.toml b/tlsn/tlsn-core/Cargo.toml index 2bad9a3bba..b46b239981 100644 --- a/tlsn/tlsn-core/Cargo.toml +++ b/tlsn/tlsn-core/Cargo.toml @@ -31,6 +31,8 @@ rstest = { workspace = true, optional = true} hex = { workspace = true, optional = true} tracing = { workspace = true, optional = true } httparse = "1" +pest = "2" +pest_derive = "2" [dev-dependencies] rstest.workspace = true diff --git a/tlsn/tlsn-core/src/span/json.rs b/tlsn/tlsn-core/src/span/json.rs deleted file mode 100644 index a325c9ae8b..0000000000 --- a/tlsn/tlsn-core/src/span/json.rs +++ /dev/null @@ -1 +0,0 @@ -//! Some support for redacting json using e.g. pest diff --git a/tlsn/tlsn-core/src/span/json/json.pest b/tlsn/tlsn-core/src/span/json/json.pest new file mode 100644 index 0000000000..60c3a651cf --- /dev/null +++ b/tlsn/tlsn-core/src/span/json/json.pest @@ -0,0 +1,41 @@ +// pest. The Elegant Parser +// Copyright (c) 2018 DragoČ™ Tiselice +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + +//! A parser for JSON file. +//! +//! And this is a example for JSON parser. +json = { SOI ~ value ~ EOI } + +/// Matches object, e.g.: `{ "foo": "bar" }` +/// Foobar +object = { "{" ~ pair ~ ("," ~ pair)* ~ "}" | "{" ~ "}" } +pair = { string ~ ":" ~ value } + +array = { "[" ~ value ~ ("," ~ value)* ~ "]" | "[" ~ "]" } + + +////////////////////// +/// Matches value, e.g.: `"foo"`, `42`, `true`, `null`, `[]`, `{}`. +////////////////////// +value = { string | number | object | array | bool | null } + +string = @{ "\"" ~ inner ~ "\"" } +inner = @{ (!("\"" | "\\") ~ ANY)* ~ (escape ~ inner)? } +escape = @{ "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t" | unicode) } +unicode = @{ "u" ~ ASCII_HEX_DIGIT{4} } + +number = @{ "-"? ~ int ~ ("." ~ ASCII_DIGIT+ ~ exp? | exp)? } +int = @{ "0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT* } +exp = @{ ("E" | "e") ~ ("+" | "-")? ~ ASCII_DIGIT+ } + +bool = { "true" | "false" } + +null = { "null" } + +WHITESPACE = _{ " " | "\t" | "\r" | "\n" } diff --git a/tlsn/tlsn-core/src/span/json/mod.rs b/tlsn/tlsn-core/src/span/json/mod.rs new file mode 100644 index 0000000000..83137f203c --- /dev/null +++ b/tlsn/tlsn-core/src/span/json/mod.rs @@ -0,0 +1,40 @@ +//! Some support for redacting json using e.g. pest + +#[derive(pest_derive::Parser)] +#[grammar = "span/json/json.pest"] +struct JsonSpanner; + +enum JSONValue<'a> { + Object(Vec<(&'a str, JSONValue<'a>)>), + Array(Vec>), + String(&'a str), + Number(f64), + Boolean(bool), + Null, +} + +#[cfg(test)] +mod tests { + use super::*; + use pest::Parser; + + struct JsonOuter { + foo: String, + baz: i32, + quux: JsonInner, + } + + struct JsonInner { + a: String, + b: String, + } + + #[test] + fn test_json_spanner() { + let test_json = r#"{"foo": "bar", "baz": 123, "quux": { "a": "b", "c": "d" }}"#; + + let mut pairs = + JsonSpanner::parse(Rule::json, test_json).unwrap_or_else(|e| panic!("{}", e)); + println!("{:#?}", pairs); + } +} diff --git a/tlsn/tlsn-core/src/span/mod.rs b/tlsn/tlsn-core/src/span/mod.rs index 1411d653e4..1e5cef08d1 100644 --- a/tlsn/tlsn-core/src/span/mod.rs +++ b/tlsn/tlsn-core/src/span/mod.rs @@ -9,6 +9,7 @@ use std::ops::Range; pub mod http; +#[allow(missing_docs)] pub mod json; /// A trait for identifying byte ranges in the request and response for which commitments will be From 82a036c280fda40e963ee3d57f7caec81e25a690 Mon Sep 17 00:00:00 2001 From: th4s Date: Thu, 24 Aug 2023 17:03:58 +0200 Subject: [PATCH 10/15] Modify grammar to our needs --- tlsn/tlsn-core/src/span/json/json.pest | 5 +++-- tlsn/tlsn-core/src/span/json/mod.rs | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tlsn/tlsn-core/src/span/json/json.pest b/tlsn/tlsn-core/src/span/json/json.pest index 60c3a651cf..5f492d2997 100644 --- a/tlsn/tlsn-core/src/span/json/json.pest +++ b/tlsn/tlsn-core/src/span/json/json.pest @@ -10,7 +10,8 @@ //! A parser for JSON file. //! //! And this is a example for JSON parser. -json = { SOI ~ value ~ EOI } +json = _{ SOI ~ value ~ eoi } +eoi = _{ !ANY } /// Matches object, e.g.: `{ "foo": "bar" }` /// Foobar @@ -23,7 +24,7 @@ array = { "[" ~ value ~ ("," ~ value)* ~ "]" | "[" ~ "]" } ////////////////////// /// Matches value, e.g.: `"foo"`, `42`, `true`, `null`, `[]`, `{}`. ////////////////////// -value = { string | number | object | array | bool | null } +value = _{ string | number | object | array | bool | null } string = @{ "\"" ~ inner ~ "\"" } inner = @{ (!("\"" | "\\") ~ ANY)* ~ (escape ~ inner)? } diff --git a/tlsn/tlsn-core/src/span/json/mod.rs b/tlsn/tlsn-core/src/span/json/mod.rs index 83137f203c..84c0a8ff6f 100644 --- a/tlsn/tlsn-core/src/span/json/mod.rs +++ b/tlsn/tlsn-core/src/span/json/mod.rs @@ -31,7 +31,8 @@ mod tests { #[test] fn test_json_spanner() { - let test_json = r#"{"foo": "bar", "baz": 123, "quux": { "a": "b", "c": "d" }}"#; + let test_json = + r#"{"foo": "bar", "baz": 123, "quux": { "a": "b", "c": "d" }, "arr": [1, 2, 3]}"#; let mut pairs = JsonSpanner::parse(Rule::json, test_json).unwrap_or_else(|e| panic!("{}", e)); From 67121df0561f8bd8e35b2af77f0ff6d91d47a84e Mon Sep 17 00:00:00 2001 From: th4s Date: Thu, 24 Aug 2023 18:04:26 +0200 Subject: [PATCH 11/15] WIP: Adding spanner for json --- tlsn/tlsn-core/src/span/json/mod.rs | 76 +++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/tlsn/tlsn-core/src/span/json/mod.rs b/tlsn/tlsn-core/src/span/json/mod.rs index 84c0a8ff6f..5f3454851a 100644 --- a/tlsn/tlsn-core/src/span/json/mod.rs +++ b/tlsn/tlsn-core/src/span/json/mod.rs @@ -1,15 +1,61 @@ //! Some support for redacting json using e.g. pest +use std::ops::Range; + +use pest::{iterators::Pairs, Parser}; + +pub struct JsonSpanner<'a> { + matches: Vec, + pairs: Pairs<'a, crate::span::json::Rule>, +} #[derive(pest_derive::Parser)] #[grammar = "span/json/json.pest"] -struct JsonSpanner; - -enum JSONValue<'a> { - Object(Vec<(&'a str, JSONValue<'a>)>), - Array(Vec>), - String(&'a str), - Number(f64), - Boolean(bool), +struct JsonParser; + +impl<'a> JsonSpanner<'a> { + pub fn new(input: &'a str) -> Self { + let pairs = JsonParser::parse(Rule::json, input).unwrap(); + Self { + pairs, + matches: vec![], + } + } + + pub fn add_match( + &mut self, + rule: JsonRule, + marker: fn(&str) -> bool, + nth: Option, + ) -> &mut Self { + self.matches.push(Match { + rule, + marker, + nth: nth.unwrap_or_default(), + }); + + self + } + + pub fn span_json(&self) -> Vec> { + // Iterate over json and apply matches. If marker returns true then add range for + // commitment + todo!() + } +} + +pub struct Match { + rule: JsonRule, + marker: fn(&str) -> bool, + nth: usize, +} + +pub enum JsonRule { + Object, + Pair, + Array, + String, + Number, + Bool, Null, } @@ -18,24 +64,12 @@ mod tests { use super::*; use pest::Parser; - struct JsonOuter { - foo: String, - baz: i32, - quux: JsonInner, - } - - struct JsonInner { - a: String, - b: String, - } - #[test] fn test_json_spanner() { let test_json = r#"{"foo": "bar", "baz": 123, "quux": { "a": "b", "c": "d" }, "arr": [1, 2, 3]}"#; - let mut pairs = - JsonSpanner::parse(Rule::json, test_json).unwrap_or_else(|e| panic!("{}", e)); + let pairs = JsonParser::parse(Rule::json, test_json).unwrap_or_else(|e| panic!("{}", e)); println!("{:#?}", pairs); } } From 9ff447051a553972400c8fbe1d40a5647cf23cee Mon Sep 17 00:00:00 2001 From: th4s Date: Thu, 24 Aug 2023 18:23:04 +0200 Subject: [PATCH 12/15] Improve `Match` --- tlsn/tlsn-core/src/span/json/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tlsn/tlsn-core/src/span/json/mod.rs b/tlsn/tlsn-core/src/span/json/mod.rs index 5f3454851a..d5841973d0 100644 --- a/tlsn/tlsn-core/src/span/json/mod.rs +++ b/tlsn/tlsn-core/src/span/json/mod.rs @@ -45,7 +45,7 @@ impl<'a> JsonSpanner<'a> { pub struct Match { rule: JsonRule, - marker: fn(&str) -> bool, + marker: Vec bool>, nth: usize, } From cfdb526cf81a438c706323ba8cde96b043128488 Mon Sep 17 00:00:00 2001 From: th4s Date: Thu, 24 Aug 2023 18:24:19 +0200 Subject: [PATCH 13/15] adapt to new `Match` --- tlsn/tlsn-core/src/span/json/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tlsn/tlsn-core/src/span/json/mod.rs b/tlsn/tlsn-core/src/span/json/mod.rs index d5841973d0..56a51308b5 100644 --- a/tlsn/tlsn-core/src/span/json/mod.rs +++ b/tlsn/tlsn-core/src/span/json/mod.rs @@ -24,7 +24,7 @@ impl<'a> JsonSpanner<'a> { pub fn add_match( &mut self, rule: JsonRule, - marker: fn(&str) -> bool, + marker: Vec bool>, nth: Option, ) -> &mut Self { self.matches.push(Match { From 76af93ce97afe45f568c75f4fe99cc582c014282 Mon Sep 17 00:00:00 2001 From: th4s Date: Thu, 24 Aug 2023 18:25:53 +0200 Subject: [PATCH 14/15] Remove `nth` field --- tlsn/tlsn-core/src/span/json/mod.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/tlsn/tlsn-core/src/span/json/mod.rs b/tlsn/tlsn-core/src/span/json/mod.rs index 56a51308b5..eacdbbe78f 100644 --- a/tlsn/tlsn-core/src/span/json/mod.rs +++ b/tlsn/tlsn-core/src/span/json/mod.rs @@ -21,17 +21,8 @@ impl<'a> JsonSpanner<'a> { } } - pub fn add_match( - &mut self, - rule: JsonRule, - marker: Vec bool>, - nth: Option, - ) -> &mut Self { - self.matches.push(Match { - rule, - marker, - nth: nth.unwrap_or_default(), - }); + pub fn add_match(&mut self, rule: JsonRule, marker: Vec bool>) -> &mut Self { + self.matches.push(Match { rule, marker }); self } @@ -46,7 +37,6 @@ impl<'a> JsonSpanner<'a> { pub struct Match { rule: JsonRule, marker: Vec bool>, - nth: usize, } pub enum JsonRule { From d0b0e948bad850c0b3fc809d6b3df3df241546c5 Mon Sep 17 00:00:00 2001 From: th4s Date: Fri, 25 Aug 2023 09:27:24 +0200 Subject: [PATCH 15/15] Remove `http` and `json` modules (moved to spansy) --- tlsn/tlsn-core/src/{span/mod.rs => span.rs} | 4 - tlsn/tlsn-core/src/span/http.rs | 188 -------------------- tlsn/tlsn-core/src/span/json/json.pest | 42 ----- tlsn/tlsn-core/src/span/json/mod.rs | 65 ------- 4 files changed, 299 deletions(-) rename tlsn/tlsn-core/src/{span/mod.rs => span.rs} (98%) delete mode 100644 tlsn/tlsn-core/src/span/http.rs delete mode 100644 tlsn/tlsn-core/src/span/json/json.pest delete mode 100644 tlsn/tlsn-core/src/span/json/mod.rs diff --git a/tlsn/tlsn-core/src/span/mod.rs b/tlsn/tlsn-core/src/span.rs similarity index 98% rename from tlsn/tlsn-core/src/span/mod.rs rename to tlsn/tlsn-core/src/span.rs index 1e5cef08d1..ee52353a44 100644 --- a/tlsn/tlsn-core/src/span/mod.rs +++ b/tlsn/tlsn-core/src/span.rs @@ -8,10 +8,6 @@ use std::ops::Range; -pub mod http; -#[allow(missing_docs)] -pub mod json; - /// A trait for identifying byte ranges in the request and response for which commitments will be /// created pub trait SpanCommit { diff --git a/tlsn/tlsn-core/src/span/http.rs b/tlsn/tlsn-core/src/span/http.rs deleted file mode 100644 index 8749542db4..0000000000 --- a/tlsn/tlsn-core/src/span/http.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! Some support for redacting http using httparse - -use super::SpanError; -use httparse::{Header, Request, Response, Status}; -use std::{ops::Range, panic}; - -/// A Spanner for HTTP -pub struct HttpSpanner<'a, 'b> { - body_start_request: Option, - body_start_response: Option, - request: Option>, - response: Option>, -} - -impl<'a, 'b> HttpSpanner<'a, 'b> { - /// Create a new HttpSpanner - pub fn new() -> Self { - HttpSpanner { - body_start_request: None, - body_start_response: None, - request: None, - response: None, - } - } -} - -impl<'a, 'b> Default for HttpSpanner<'a, 'b> { - fn default() -> Self { - HttpSpanner::new() - } -} - -impl<'a, 'b> HttpSpanner<'a, 'b> { - /// Parse a http request - pub fn parse_request( - &mut self, - headers: &'a mut [Header<'b>], - bytes: &'b [u8], - ) -> Result<(), SpanError> { - let mut request = Request::new(headers); - match request.parse(bytes) { - Ok(Status::Complete(body_start)) => { - self.body_start_request = Some(body_start); - self.request = Some(request); - Ok(()) - } - Ok(Status::Partial) => Err(SpanError::ParseError), - Err(_) => Err(SpanError::ParseError), - } - } - - /// Parse a http response - pub fn parse_response( - &mut self, - headers: &'a mut [Header<'b>], - bytes: &'b [u8], - ) -> Result<(), SpanError> { - let mut response = Response::new(headers); - match response.parse(bytes) { - Ok(Status::Complete(body_start)) => { - self.body_start_response = Some(body_start); - self.response = Some(response); - Ok(()) - } - Ok(Status::Partial) => Err(SpanError::ParseError), - Err(_) => Err(SpanError::ParseError), - } - } - - /// Return the byte offset where the request body starts - pub fn body_start_request(&self) -> Option { - self.body_start_request - } - - /// Return the byte offset where the response body starts - pub fn body_start_response(&self) -> Option { - self.body_start_response - } - - /// Return the byte range matching the value belonging to the specified header key in the - /// request - pub fn header_value_span_request(&self, key: &str, bytes: &[u8]) -> Option> { - let request = self.request.as_ref()?; - let header = request.headers.iter().find(|h| h.name == key)?; - Some(get_ptr_range(bytes, header.value)) - } - - /// Return the byte range matching the value belonging to the specified header key in the - /// response - pub fn header_value_span_response(&self, key: &str, bytes: &[u8]) -> Option> { - let request = self.response.as_ref()?; - let header = request.headers.iter().find(|h| h.name == key)?; - Some(get_ptr_range(bytes, header.value)) - } -} - -fn get_ptr_range(whole: &[u8], part: &[u8]) -> Range { - if part.as_ptr() < whole.as_ptr() { - panic!("part is not a part of whole"); - } - let start = unsafe { part.as_ptr().offset_from(whole.as_ptr()) as usize }; - let end = start + part.len(); - Range { start, end } -} - -#[cfg(test)] -mod tests { - use super::*; - - const TEST_REQUEST: &[u8] = b"\ - GET /home.html HTTP/1.1\n\ - Host: developer.mozilla.org\n\ - User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0\n\ - Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.\n\ - Accept-Language: en-US,en;q=0.\n\ - Accept-Encoding: gzip, deflate, b\n\ - Referer: https://developer.mozilla.org/testpage.htm\n\ - Connection: keep-alive\n\ - Cache-Control: max-age=0\n\n\ - Hello World!"; - - const TEST_RESPONSE: &[u8] = b"\ - HTTP/1.1 200 OK\n\ - Date: Mon, 27 Jul 2009 12:28:53 GMT\n\ - Server: Apache/2.2.14 (Win32)\n\ - Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT\n\ - Content-Length: 88\n\ - Content-Type: text/html\n\ - Connection: Closed\n\n\ - \n\ - \n\ -

Hello, World!

\n\ - \n\ - "; - - #[test] - fn test_parse_request() { - let mut spanner = HttpSpanner::new(); - let mut headers = vec![httparse::EMPTY_HEADER; 8]; - spanner.parse_request(&mut headers, TEST_REQUEST).unwrap(); - assert_eq!( - &TEST_REQUEST[spanner.body_start_request().unwrap()..], - b"Hello World!" - ); - } - - #[test] - fn test_header_value_span_request() { - let mut spanner = HttpSpanner::new(); - let mut headers = vec![httparse::EMPTY_HEADER; 8]; - spanner.parse_request(&mut headers, TEST_REQUEST).unwrap(); - assert_eq!( - &TEST_REQUEST[spanner - .header_value_span_request("Host", TEST_REQUEST) - .unwrap()], - b"developer.mozilla.org" - ); - } - - #[test] - fn test_parse_response() { - let mut spanner = HttpSpanner::new(); - let mut headers = vec![httparse::EMPTY_HEADER; 6]; - spanner.parse_response(&mut headers, TEST_RESPONSE).unwrap(); - assert_eq!( - &TEST_RESPONSE[spanner.body_start_response().unwrap()..], - b"\ - \n\ - \n\ -

Hello, World!

\n\ - \n\ - " - ); - } - - #[test] - fn test_header_value_span_response() { - let mut spanner = HttpSpanner::new(); - let mut headers = vec![httparse::EMPTY_HEADER; 6]; - spanner.parse_response(&mut headers, TEST_RESPONSE).unwrap(); - assert_eq!( - &TEST_RESPONSE[spanner - .header_value_span_response("Content-Type", TEST_RESPONSE) - .unwrap()], - b"text/html" - ); - } -} diff --git a/tlsn/tlsn-core/src/span/json/json.pest b/tlsn/tlsn-core/src/span/json/json.pest deleted file mode 100644 index 5f492d2997..0000000000 --- a/tlsn/tlsn-core/src/span/json/json.pest +++ /dev/null @@ -1,42 +0,0 @@ -// pest. The Elegant Parser -// Copyright (c) 2018 DragoČ™ Tiselice -// -// Licensed under the Apache License, Version 2.0 -// or the MIT -// license , at your -// option. All files in the project carrying such notice may not be copied, -// modified, or distributed except according to those terms. - -//! A parser for JSON file. -//! -//! And this is a example for JSON parser. -json = _{ SOI ~ value ~ eoi } -eoi = _{ !ANY } - -/// Matches object, e.g.: `{ "foo": "bar" }` -/// Foobar -object = { "{" ~ pair ~ ("," ~ pair)* ~ "}" | "{" ~ "}" } -pair = { string ~ ":" ~ value } - -array = { "[" ~ value ~ ("," ~ value)* ~ "]" | "[" ~ "]" } - - -////////////////////// -/// Matches value, e.g.: `"foo"`, `42`, `true`, `null`, `[]`, `{}`. -////////////////////// -value = _{ string | number | object | array | bool | null } - -string = @{ "\"" ~ inner ~ "\"" } -inner = @{ (!("\"" | "\\") ~ ANY)* ~ (escape ~ inner)? } -escape = @{ "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t" | unicode) } -unicode = @{ "u" ~ ASCII_HEX_DIGIT{4} } - -number = @{ "-"? ~ int ~ ("." ~ ASCII_DIGIT+ ~ exp? | exp)? } -int = @{ "0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT* } -exp = @{ ("E" | "e") ~ ("+" | "-")? ~ ASCII_DIGIT+ } - -bool = { "true" | "false" } - -null = { "null" } - -WHITESPACE = _{ " " | "\t" | "\r" | "\n" } diff --git a/tlsn/tlsn-core/src/span/json/mod.rs b/tlsn/tlsn-core/src/span/json/mod.rs deleted file mode 100644 index eacdbbe78f..0000000000 --- a/tlsn/tlsn-core/src/span/json/mod.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Some support for redacting json using e.g. pest -use std::ops::Range; - -use pest::{iterators::Pairs, Parser}; - -pub struct JsonSpanner<'a> { - matches: Vec, - pairs: Pairs<'a, crate::span::json::Rule>, -} - -#[derive(pest_derive::Parser)] -#[grammar = "span/json/json.pest"] -struct JsonParser; - -impl<'a> JsonSpanner<'a> { - pub fn new(input: &'a str) -> Self { - let pairs = JsonParser::parse(Rule::json, input).unwrap(); - Self { - pairs, - matches: vec![], - } - } - - pub fn add_match(&mut self, rule: JsonRule, marker: Vec bool>) -> &mut Self { - self.matches.push(Match { rule, marker }); - - self - } - - pub fn span_json(&self) -> Vec> { - // Iterate over json and apply matches. If marker returns true then add range for - // commitment - todo!() - } -} - -pub struct Match { - rule: JsonRule, - marker: Vec bool>, -} - -pub enum JsonRule { - Object, - Pair, - Array, - String, - Number, - Bool, - Null, -} - -#[cfg(test)] -mod tests { - use super::*; - use pest::Parser; - - #[test] - fn test_json_spanner() { - let test_json = - r#"{"foo": "bar", "baz": 123, "quux": { "a": "b", "c": "d" }, "arr": [1, 2, 3]}"#; - - let pairs = JsonParser::parse(Rule::json, test_json).unwrap_or_else(|e| panic!("{}", e)); - println!("{:#?}", pairs); - } -}