@@ -23,10 +23,9 @@ pub(crate) trait UrlExt {
2323impl UrlExt for Url {
2424 /// Retrieve the receiver's public key from the URL fragment
2525 fn receiver_pubkey ( & self ) -> Result < HpkePublicKey , ParseReceiverPubkeyParamError > {
26- let value = get_param ( self , "RK1" , |v| Some ( v. to_owned ( ) ) )
27- . ok_or ( ParseReceiverPubkeyParamError :: MissingPubkey ) ?;
26+ let value = get_param ( self , "RK1" ) ?. ok_or ( ParseReceiverPubkeyParamError :: MissingPubkey ) ?;
2827
29- let ( hrp, bytes) = crate :: bech32:: nochecksum:: decode ( & value)
28+ let ( hrp, bytes) = crate :: bech32:: nochecksum:: decode ( value)
3029 . map_err ( ParseReceiverPubkeyParamError :: DecodeBech32 ) ?;
3130
3231 let rk_hrp: Hrp = Hrp :: parse ( "RK" ) . unwrap ( ) ;
@@ -52,21 +51,19 @@ impl UrlExt for Url {
5251
5352 /// Retrieve the ohttp parameter from the URL fragment
5453 fn ohttp ( & self ) -> Result < OhttpKeys , ParseOhttpKeysParamError > {
55- let value = get_param ( self , "OH1" , |v| Some ( v. to_owned ( ) ) )
56- . ok_or ( ParseOhttpKeysParamError :: MissingOhttpKeys ) ?;
57- OhttpKeys :: from_str ( & value) . map_err ( ParseOhttpKeysParamError :: InvalidOhttpKeys )
54+ let value = get_param ( self , "OH1" ) ?. ok_or ( ParseOhttpKeysParamError :: MissingOhttpKeys ) ?;
55+ OhttpKeys :: from_str ( value) . map_err ( ParseOhttpKeysParamError :: InvalidOhttpKeys )
5856 }
5957
6058 /// Set the ohttp parameter in the URL fragment
6159 fn set_ohttp ( & mut self , ohttp : OhttpKeys ) { set_param ( self , "OH1" , & ohttp. to_string ( ) ) }
6260
6361 /// Retrieve the exp parameter from the URL fragment
6462 fn exp ( & self ) -> Result < std:: time:: SystemTime , ParseExpParamError > {
65- let value =
66- get_param ( self , "EX1" , |v| Some ( v. to_owned ( ) ) ) . ok_or ( ParseExpParamError :: MissingExp ) ?;
63+ let value = get_param ( self , "EX1" ) ?. ok_or ( ParseExpParamError :: MissingExp ) ?;
6764
6865 let ( hrp, bytes) =
69- crate :: bech32:: nochecksum:: decode ( & value) . map_err ( ParseExpParamError :: DecodeBech32 ) ?;
66+ crate :: bech32:: nochecksum:: decode ( value) . map_err ( ParseExpParamError :: DecodeBech32 ) ?;
7067
7168 let ex_hrp: Hrp = Hrp :: parse ( "EX" ) . unwrap ( ) ;
7269 if hrp != ex_hrp {
@@ -110,22 +107,57 @@ pub fn parse_with_fragment(endpoint: &str) -> Result<Url, BadEndpointError> {
110107 Ok ( url)
111108}
112109
113- fn get_param < F , T > ( url : & Url , prefix : & str , parse : F ) -> Option < T >
114- where
115- F : Fn ( & str ) -> Option < T > ,
116- {
117- if let Some ( fragment) = url. fragment ( ) {
118- let mut delim = '-' ;
119-
120- // For backwards compatibility, also accept `+` as a
121- // fragment parameter delimiter. This was previously
122- // specified, but may be interpreted as ` ` by some
123- // URI parsoing libraries. Therefore if `-` is missing,
124- // assume the URI was generated following the older
125- // version of the spec.
126- if !fragment. contains ( delim) {
127- delim = '+' ;
110+ #[ derive( Debug ) ]
111+ enum ParseFragmentError {
112+ InvalidChar ( char ) ,
113+ AmbiguousDelimiter ,
114+ }
115+
116+ fn check_fragment_delimiter ( fragment : & str ) -> Result < char , ParseFragmentError > {
117+ // For backwards compatibility, also accept `+` as a
118+ // fragment parameter delimiter. This was previously
119+ // specified, but may be interpreted as ` ` by some
120+ // URI parsoing libraries. Therefore if `-` is missing,
121+ // assume the URI was generated following the older
122+ // version of the spec.
123+ if fragment. is_empty ( ) {
124+ return Ok ( '-' ) ;
125+ }
126+
127+ let mut has_plus = false ;
128+ let mut has_dash = false ;
129+
130+ // Even though fragment is a &str, it should be ascii so bytes() correspond
131+ // to chars(), except that it's easier to check that they are in range
132+ for c in fragment. bytes ( ) {
133+ // check for allowed delimiters
134+ if c == b'-' {
135+ has_dash = true ;
136+ } else if c == b'+' {
137+ has_plus = true ;
138+ }
139+ // Check that the character is in the uppercase bech32 character set unioned with '1' for HRP
140+ else if !( 48 ..57 + 1 ) . contains ( & c)
141+ && c != 65
142+ && !( 67 ..72 + 1 ) . contains ( & c)
143+ && !( 74 ..79 + 1 ) . contains ( & c)
144+ && !( 80 ..90 + 1 ) . contains ( & c)
145+ {
146+ return Err ( ParseFragmentError :: InvalidChar ( c. into ( ) ) ) ;
128147 }
148+ }
149+
150+ match ( has_dash, has_plus) {
151+ ( true , false ) => Ok ( '-' ) ,
152+ ( false , false ) => Ok ( '-' ) ,
153+ ( false , true ) => Ok ( '+' ) ,
154+ ( true , true ) => Err ( ParseFragmentError :: AmbiguousDelimiter ) ,
155+ }
156+ }
157+
158+ fn get_param < ' a > ( url : & ' a Url , prefix : & str ) -> Result < Option < & ' a str > , ParseFragmentError > {
159+ if let Some ( fragment) = url. fragment ( ) {
160+ let delim = check_fragment_delimiter ( fragment) ?;
129161
130162 // The spec says these MUST be ordered lexicographically.
131163 // However, this was a late spec change, and only matters
@@ -134,21 +166,17 @@ where
134166 // of the parameters.
135167 for param in fragment. split ( delim) {
136168 if param. starts_with ( prefix) {
137- return parse ( param) ;
169+ return Ok ( Some ( param) ) ;
138170 }
139171 }
140172 }
141- None
173+ Ok ( None )
142174}
143175
144176fn set_param ( url : & mut Url , prefix : & str , param : & str ) {
145177 let fragment = url. fragment ( ) . unwrap_or ( "" ) ;
146-
147- // See above for `-` vs `+` backwards compatibility
148- let mut delim = '-' ;
149- if !fragment. contains ( delim) {
150- delim = '+' ;
151- }
178+ let delim = check_fragment_delimiter ( fragment)
179+ . expect ( "set_param must be called on a URL with a valid fragment" ) ;
152180
153181 // In case of an invalid fragment parameter the following will still attempt
154182 // to retain the existing data
@@ -175,6 +203,7 @@ fn set_param(url: &mut Url, prefix: &str, param: &str) {
175203pub ( crate ) enum ParseOhttpKeysParamError {
176204 MissingOhttpKeys ,
177205 InvalidOhttpKeys ( crate :: ohttp:: ParseOhttpKeysError ) ,
206+ AmbiguousDelimiter ,
178207}
179208
180209impl std:: fmt:: Display for ParseOhttpKeysParamError {
@@ -184,6 +213,27 @@ impl std::fmt::Display for ParseOhttpKeysParamError {
184213 match & self {
185214 MissingOhttpKeys => write ! ( f, "ohttp keys are missing" ) ,
186215 InvalidOhttpKeys ( o) => write ! ( f, "invalid ohttp keys: {o}" ) ,
216+ AmbiguousDelimiter =>
217+ write ! ( f, "invalid URL fragment (both + and - fragment parameter delimiters)" ) ,
218+ }
219+ }
220+ }
221+
222+ impl From < ParseFragmentError > for ParseOhttpKeysParamError {
223+ fn from ( err : ParseFragmentError ) -> Self {
224+ match err {
225+ ParseFragmentError :: AmbiguousDelimiter => ParseOhttpKeysParamError :: AmbiguousDelimiter ,
226+
227+ // TODO CharError.into() -> UncheckedHrpstringError is available, but .into().into() needs more finessing
228+ ParseFragmentError :: InvalidChar ( c) => ParseOhttpKeysParamError :: InvalidOhttpKeys (
229+ crate :: ohttp:: ParseOhttpKeysError :: DecodeBech32 (
230+ bitcoin:: bech32:: primitives:: decode:: CheckedHrpstringError :: Parse (
231+ bitcoin:: bech32:: primitives:: decode:: UncheckedHrpstringError :: Char (
232+ bitcoin:: bech32:: primitives:: decode:: CharError :: InvalidChar ( c) ,
233+ ) ,
234+ ) ,
235+ ) ,
236+ ) ,
187237 }
188238 }
189239}
@@ -194,6 +244,7 @@ pub(crate) enum ParseExpParamError {
194244 InvalidHrp ( bitcoin:: bech32:: Hrp ) ,
195245 DecodeBech32 ( bitcoin:: bech32:: primitives:: decode:: CheckedHrpstringError ) ,
196246 InvalidExp ( bitcoin:: consensus:: encode:: Error ) ,
247+ AmbiguousDelimiter ,
197248}
198249
199250impl std:: fmt:: Display for ParseExpParamError {
@@ -206,17 +257,54 @@ impl std::fmt::Display for ParseExpParamError {
206257 DecodeBech32 ( d) => write ! ( f, "exp is not valid bech32: {d}" ) ,
207258 InvalidExp ( i) =>
208259 write ! ( f, "exp param does not contain a bitcoin consensus encoded u32: {i}" ) ,
260+ AmbiguousDelimiter =>
261+ write ! ( f, "invalid URL fragment (both + and - fragment parameter delimiters)" ) ,
262+ }
263+ }
264+ }
265+
266+ impl From < ParseFragmentError > for ParseExpParamError {
267+ fn from ( err : ParseFragmentError ) -> Self {
268+ match err {
269+ ParseFragmentError :: AmbiguousDelimiter => ParseExpParamError :: AmbiguousDelimiter ,
270+
271+ // TODO CharError.into() -> UncheckedHrpstringError is available, but .into().into() needs more finessing
272+ ParseFragmentError :: InvalidChar ( c) => ParseExpParamError :: DecodeBech32 (
273+ bitcoin:: bech32:: primitives:: decode:: CheckedHrpstringError :: Parse (
274+ bitcoin:: bech32:: primitives:: decode:: UncheckedHrpstringError :: Char (
275+ bitcoin:: bech32:: primitives:: decode:: CharError :: InvalidChar ( c) ,
276+ ) ,
277+ ) ,
278+ ) ,
209279 }
210280 }
211281}
212282
213- #[ cfg( feature = "v2" ) ]
214283#[ derive( Debug ) ]
215284pub ( crate ) enum ParseReceiverPubkeyParamError {
216285 MissingPubkey ,
217286 InvalidHrp ( bitcoin:: bech32:: Hrp ) ,
218287 DecodeBech32 ( bitcoin:: bech32:: primitives:: decode:: CheckedHrpstringError ) ,
219288 InvalidPubkey ( crate :: hpke:: HpkeError ) ,
289+ AmbiguousDelimiter ,
290+ }
291+
292+ impl From < ParseFragmentError > for ParseReceiverPubkeyParamError {
293+ fn from ( err : ParseFragmentError ) -> Self {
294+ match err {
295+ ParseFragmentError :: AmbiguousDelimiter =>
296+ ParseReceiverPubkeyParamError :: AmbiguousDelimiter ,
297+
298+ // TODO CharError.into() -> UncheckedHrpstringError is available, but .into().into() needs more finessing
299+ ParseFragmentError :: InvalidChar ( c) => ParseReceiverPubkeyParamError :: DecodeBech32 (
300+ bitcoin:: bech32:: primitives:: decode:: CheckedHrpstringError :: Parse (
301+ bitcoin:: bech32:: primitives:: decode:: UncheckedHrpstringError :: Char (
302+ bitcoin:: bech32:: primitives:: decode:: CharError :: InvalidChar ( c) ,
303+ ) ,
304+ ) ,
305+ ) ,
306+ }
307+ }
220308}
221309
222310impl std:: fmt:: Display for ParseReceiverPubkeyParamError {
@@ -229,6 +317,8 @@ impl std::fmt::Display for ParseReceiverPubkeyParamError {
229317 DecodeBech32 ( e) => write ! ( f, "receiver public is not valid base64: {e}" ) ,
230318 InvalidPubkey ( e) =>
231319 write ! ( f, "receiver public key does not represent a valid pubkey: {e}" ) ,
320+ AmbiguousDelimiter =>
321+ write ! ( f, "invalid URL fragment (both + and - fragment parameter delimiters)" ) ,
232322 }
233323 }
234324}
@@ -242,6 +332,7 @@ impl std::error::Error for ParseReceiverPubkeyParamError {
242332 InvalidHrp ( _) => None ,
243333 DecodeBech32 ( error) => Some ( error) ,
244334 InvalidPubkey ( error) => Some ( error) ,
335+ AmbiguousDelimiter => None ,
245336 }
246337 }
247338}
@@ -281,7 +372,9 @@ mod tests {
281372 . unwrap ( ) ;
282373 assert ! ( matches!(
283374 invalid_ohttp_url. ohttp( ) ,
284- Err ( ParseOhttpKeysParamError :: InvalidOhttpKeys ( _) )
375+ Err ( ParseOhttpKeysParamError :: InvalidOhttpKeys (
376+ crate :: ohttp:: ParseOhttpKeysError :: DecodeBech32 ( _)
377+ ) )
285378 ) ) ;
286379 }
287380
0 commit comments