Skip to content

Commit edcffb6

Browse files
committed
CA-406403: Do not return HTTP 500 when Accept header can't be parsed
/update_rrds returned a 500 HTTP code in some cases where the accept header was invalid. Now these cases are treated in the same way as a lack of Accept header. The parser has been changed to use Result.t, with precise errors about the part that failed to be parsed. Signed-off-by: Pau Ruiz Safont <[email protected]>
1 parent 87ba9c2 commit edcffb6

File tree

4 files changed

+142
-36
lines changed

4 files changed

+142
-36
lines changed

ocaml/libs/http-lib/http.ml

Lines changed: 103 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -463,43 +463,133 @@ module Accept = struct
463463
in
464464
loop (List.sort compare_user_preference types)
465465

466-
exception Parse_failure of string
466+
(* {|
467+
RFC 9110 defines a grammar for the Accept header:
468+
469+
Accept = #( media-range [ weight ] )
470+
471+
media-range = ( "*/*"
472+
/ ( type "/" "*" )
473+
/ ( type "/" subtype )
474+
) parameters
475+
476+
media-type = type "/" subtype parameters
477+
type = token
478+
subtype = token
479+
480+
parameters = *( OWS ";" OWS [ parameter ] )
481+
parameter = parameter-name "=" parameter-value
482+
parameter-name = token
483+
parameter-value = ( token / quoted-string )
484+
485+
weight = OWS ";" OWS "q=" qvalue
486+
qvalue = ( "0" [ "." 0*3DIGIT ] )
487+
/ ( "1" [ "." 0*3("0") ] )
488+
489+
OWS = *( SP / HTAB )
490+
491+
token = 1*tchar
492+
493+
tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
494+
/ "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
495+
/ DIGIT / ALPHA
496+
; any VCHAR, except delimiters
497+
498+
quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
499+
qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
500+
quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
501+
502+
obs-text = %x80-FF
503+
504+
|} *)
505+
506+
type parse_error =
507+
| MediaRange of string
508+
| MediaType of string
509+
| Parameter of string
510+
| Qvalue of string
511+
512+
let ( let* ) = Result.bind
513+
514+
(* transpose a list of results to Result of (list, err) *)
515+
let transpose_list_result list_result =
516+
list_result
517+
|> List.partition_map (function Ok x -> Left x | Error x -> Right x)
518+
|> function
519+
| [], [] ->
520+
Ok []
521+
| _, err :: _ ->
522+
Error err
523+
| oks, _ ->
524+
Ok oks
467525

468526
let of_string_single x =
469527
match Astring.String.cuts ~sep:";" x with
470528
| ty_subty :: params ->
471529
let ty_of_string = function "*" -> None | x -> Some x in
472-
let ty =
530+
let* ty =
473531
match Astring.String.cuts ~sep:"/" ty_subty with
474532
| [ty; subty] ->
475-
Option.map (fun ty -> (ty, ty_of_string subty)) (ty_of_string ty)
533+
Ok
534+
(Option.map
535+
(fun ty -> (ty, ty_of_string subty))
536+
(ty_of_string ty)
537+
)
476538
| _ ->
477-
raise (Parse_failure ty_subty)
539+
Error (MediaType ty_subty)
478540
in
479-
let params =
541+
let* params =
480542
List.map
481543
(fun x ->
482544
match Astring.String.cut ~sep:"=" x with
483545
| Some (k, v) ->
484-
(k, v)
546+
Ok (k, v)
485547
| _ ->
486-
raise (Parse_failure x)
548+
Error (Parameter x)
487549
)
488550
params
551+
|> transpose_list_result
489552
in
490-
let q =
553+
554+
let* q =
491555
match List.assoc_opt "q" params with
492-
| Some q ->
493-
int_of_float (1000. *. float_of_string q)
556+
| Some q -> (
557+
match Float.of_string_opt q with
558+
| None ->
559+
Error (Qvalue q)
560+
| Some q ->
561+
Ok (Float.to_int (1000. *. q))
562+
)
494563
| None ->
495-
1000
564+
Ok 1000
496565
in
497-
{ty; q}
566+
Ok {ty; q}
498567
| _ ->
499-
raise (Parse_failure x)
568+
Error (MediaRange x)
500569

501570
let of_string x =
502571
List.map of_string_single (Astring.String.cuts ~empty:false ~sep:"," x)
572+
|> transpose_list_result
573+
574+
let parse_error_equal a b =
575+
match (a, b) with
576+
| MediaRange a, MediaRange b
577+
| MediaType a, MediaType b
578+
| Parameter a, Parameter b
579+
| Qvalue a, Qvalue b ->
580+
String.equal a b
581+
| _ ->
582+
false
583+
584+
let parse_error_to_string = function
585+
| MediaRange a ->
586+
Printf.sprintf "MediaRange (%s)" a
587+
| MediaType a ->
588+
Printf.sprintf "Mimetype (%s)" a
589+
| Parameter a ->
590+
Printf.sprintf "Parameter (%s)" a
591+
| Qvalue a ->
592+
Printf.sprintf "Qvalue (%s)" a
503593
end
504594

505595
module Request = struct

ocaml/libs/http-lib/http.mli

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,15 @@ val read_http_response_header : bytes -> Unix.file_descr -> int
5050
module Accept : sig
5151
type t = {ty: (string * string option) option (** None means '*' *); q: int}
5252

53-
exception Parse_failure of string
53+
type parse_error =
54+
| MediaRange of string
55+
| Mimetype of string
56+
| Parameter of string
57+
| Qvalue of string
5458

5559
val equal : t -> t -> bool
5660

57-
val of_string : string -> t list
61+
val of_string : string -> (t list, parse_error) Result.t
5862

5963
val to_string : t -> string
6064

@@ -63,6 +67,10 @@ module Accept : sig
6367
val preferred : from:string list -> t list -> string list
6468
(** [preferred ~from accepted] returns the content types in [~from]
6569
that are accepted by elements of [accepted] in priority order *)
70+
71+
val parse_error_equal : parse_error -> parse_error -> bool
72+
73+
val parse_error_to_string : parse_error -> string
6674
end
6775

6876
(** Parsed form of the HTTP request line plus cookie info *)

ocaml/libs/http-lib/http_test.ml

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,32 @@ module Accept = struct
1818
let accept =
1919
Alcotest.testable (Fmt.of_to_string Accept.to_string) Accept.equal
2020

21+
let parse_error =
22+
Alcotest.testable
23+
(Fmt.of_to_string Accept.parse_error_to_string)
24+
Accept.parse_error_equal
25+
2126
let test_accept_simple () =
2227
let data = "application/json" in
23-
let expected = [Accept.{ty= Some ("application", Some "json"); q= 1000}] in
28+
let expected =
29+
Ok [Accept.{ty= Some ("application", Some "json"); q= 1000}]
30+
in
2431
let actual = Accept.of_string data in
25-
Alcotest.(check @@ list accept) data expected actual
32+
Alcotest.(check @@ result (list accept) parse_error) data expected actual
2633

2734
let test_invalid () =
2835
let data = "text/html, image/gif, image/jpeg, ; q=.2, */; q=.2" in
29-
let expected = Accept.Parse_failure " " in
30-
let actual () = let _ = Accept.of_string data in () in
31-
Alcotest.check_raises "Raises Parse failure" expected actual
36+
let expected = Error (Accept.Mimetype " ") in
37+
let actual = Accept.of_string data in
38+
Alcotest.(check @@ result (list accept) parse_error) data expected actual
3239

3340
let test_accept_complex () =
3441
let data =
3542
"application/xml;q=0.9,text/html,application/xhtml+xml,*/*;q=0.8"
3643
in
44+
let content_types = Accept.of_string data |> Result.get_ok in
45+
3746
let expected = ["text/html"] in
38-
let content_types = Accept.of_string data in
3947
let actual = Accept.preferred ~from:["text/html"] content_types in
4048
Alcotest.(check @@ list string) data expected actual ;
4149

@@ -73,7 +81,7 @@ module Accept = struct
7381
in
7482
let test (name, data, from, expected) =
7583
let test () =
76-
let content_types = Accept.of_string data in
84+
let content_types = Accept.of_string data |> Result.get_ok in
7785
let actual = Accept.preferred ~from content_types in
7886
Alcotest.(check @@ list string) data expected actual
7987
in

ocaml/xcp-rrdd/bin/rrdd/rrdd_http_handler.ml

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,20 @@ let content_xml = content_hdr_of_mime mime_xml
1818

1919
let client_prefers_json req =
2020
let module Accept = Http.Accept in
21-
match req.Http.Request.accept with
22-
| None ->
23-
List.mem_assoc "json" req.Http.Request.query
24-
| Some accept -> (
25-
let accepted = Accept.of_string accept in
26-
let negotiated = Accept.preferred ~from:[mime_json; mime_xml] accepted in
27-
match negotiated with
28-
| x :: _ when String.equal x mime_json ->
29-
true
30-
| [] ->
31-
List.mem_assoc "json" req.Http.Request.query
32-
| _ ->
33-
false
34-
)
21+
let preferred_of accept =
22+
let ( let* ) = Option.bind in
23+
let* accepted = Accept.of_string accept |> Result.to_option in
24+
let negotiated = Accept.preferred ~from:[mime_json; mime_xml] accepted in
25+
match negotiated with
26+
| x :: _ when String.equal x mime_json ->
27+
Some true
28+
| [] ->
29+
None
30+
| _ ->
31+
Some false
32+
in
33+
Option.bind req.Http.Request.accept preferred_of
34+
|> Option.value ~default:(List.mem_assoc "json" req.Http.Request.query)
3535

3636
let content_type json = if json then content_json else content_xml
3737

0 commit comments

Comments
 (0)