@@ -3,10 +3,12 @@ use std::collections::BTreeMap;
33use serde:: { Deserialize , Serialize } ;
44
55use crate :: config:: ConfigError ;
6- use crate :: config_header_validation:: { valid_http_header_name , validate_optional_header_value} ;
6+ use crate :: config_header_validation:: validate_optional_header_value;
77use crate :: config_http:: valid_http_endpoint_url;
88
99const MAX_REPORTING_ENDPOINTS : usize = 16 ;
10+ const MAX_REPORTING_ENDPOINT_NAME_BYTES : usize = 64 ;
11+ const MAX_REPORTING_ENDPOINTS_HEADER_BYTES : usize = 16_384 ;
1012
1113#[ derive( Debug , Clone , Copy , Default , Eq , PartialEq , Deserialize , Serialize ) ]
1214#[ serde( rename_all = "kebab-case" ) ]
@@ -148,28 +150,88 @@ pub(crate) fn validate_reporting_endpoints(
148150 return Err ( ConfigError :: InvalidResponseHeaderValue { field } ) ;
149151 }
150152 for ( name, endpoint) in endpoints {
151- if !valid_http_header_name ( name) || !valid_http_endpoint_url ( endpoint) {
153+ if !valid_structured_field_key ( name)
154+ || !endpoint. starts_with ( "https://" )
155+ || !valid_http_endpoint_url ( endpoint)
156+ || serialize_sf_string ( endpoint) . is_none ( )
157+ {
152158 return Err ( ConfigError :: InvalidHeaderValue {
153159 field,
154160 name : name. clone ( ) ,
155161 } ) ;
156162 }
157163 validate_optional_header_value ( field, Some ( endpoint) ) ?;
158164 }
165+ if reporting_endpoints_header_value ( endpoints) . is_none ( ) && !endpoints. is_empty ( ) {
166+ return Err ( ConfigError :: InvalidResponseHeaderValue { field } ) ;
167+ }
159168 Ok ( ( ) )
160169}
161170
162171pub fn reporting_endpoints_header_value ( endpoints : & BTreeMap < String , String > ) -> Option < String > {
163172 if endpoints. is_empty ( ) {
164173 return None ;
165174 }
166- Some (
167- endpoints
168- . iter ( )
169- . map ( |( name, endpoint) | format ! ( "{name}=\" {endpoint}\" " ) )
170- . collect :: < Vec < _ > > ( )
171- . join ( ", " ) ,
172- )
175+ let mut serialized = String :: new ( ) ;
176+ for ( name, endpoint) in endpoints {
177+ if !valid_structured_field_key ( name)
178+ || !endpoint. starts_with ( "https://" )
179+ || !valid_http_endpoint_url ( endpoint)
180+ {
181+ return None ;
182+ }
183+ let endpoint = serialize_sf_string ( endpoint) ?;
184+ let separator_bytes = usize:: from ( !serialized. is_empty ( ) ) * 2 ;
185+ let additional_bytes = separator_bytes
186+ . checked_add ( name. len ( ) ) ?
187+ . checked_add ( 1 ) ?
188+ . checked_add ( endpoint. len ( ) ) ?;
189+ if serialized. len ( ) . checked_add ( additional_bytes) ? > MAX_REPORTING_ENDPOINTS_HEADER_BYTES {
190+ return None ;
191+ }
192+ if !serialized. is_empty ( ) {
193+ serialized. push_str ( ", " ) ;
194+ }
195+ serialized. push_str ( name) ;
196+ serialized. push ( '=' ) ;
197+ serialized. push_str ( & endpoint) ;
198+ }
199+ Some ( serialized)
200+ }
201+
202+ fn valid_structured_field_key ( name : & str ) -> bool {
203+ if name. len ( ) > MAX_REPORTING_ENDPOINT_NAME_BYTES {
204+ return false ;
205+ }
206+ let mut bytes = name. bytes ( ) ;
207+ matches ! ( bytes. next( ) , Some ( b'a' ..=b'z' | b'*' ) )
208+ && bytes. all ( |byte| {
209+ matches ! (
210+ byte,
211+ b'a' ..=b'z' | b'0' ..=b'9' | b'_' | b'-' | b'.' | b'*'
212+ )
213+ } )
214+ }
215+
216+ fn serialize_sf_string ( value : & str ) -> Option < String > {
217+ if !value. is_ascii ( ) || value. bytes ( ) . any ( |byte| !( 0x20 ..=0x7e ) . contains ( & byte) ) {
218+ return None ;
219+ }
220+ let escaped_bytes = value
221+ . bytes ( )
222+ . filter ( |byte| matches ! ( byte, b'"' | b'\\' ) )
223+ . count ( ) ;
224+ let capacity = value. len ( ) . checked_add ( escaped_bytes) ?. checked_add ( 2 ) ?;
225+ let mut serialized = String :: with_capacity ( capacity) ;
226+ serialized. push ( '"' ) ;
227+ for character in value. chars ( ) {
228+ if matches ! ( character, '"' | '\\' ) {
229+ serialized. push ( '\\' ) ;
230+ }
231+ serialized. push ( character) ;
232+ }
233+ serialized. push ( '"' ) ;
234+ Some ( serialized)
173235}
174236
175237fn default_true ( ) -> bool {
0 commit comments