123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657(*----------------------------------------------------------------------------
Copyright (c) 2017 Inhabited Type LLC.
Copyright (c) 2019 Antonio N. Monteiro.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the author nor the names of his contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
----------------------------------------------------------------------------*)openAngstromtypeparse_context={mutableframe_header:Frame.frame_headeroption;mutableremaining_bytes_to_skip:int;mutabledid_report_stream_error:bool}letframe_length=lift3(funxyz->(* From RFC7540§4.1:
Length: The length of the frame payload expressed as an unsigned
24-bit integer. *)(xlsl16)lor(ylsl8)lorz)any_uint8any_uint8any_uint8letframe_type=(* From RFC7540§4.1:
Type: The 8-bit type of the frame. The frame type determines the format
and semantics of the frame. Implementations MUST ignore and discard any
frame that has a type that is unknown. *)liftFrame.FrameType.parseany_uint8letframe_flags=(* From RFC7540§4.1:
Flags: An 8-bit field reserved for boolean flags specific to the frame
type. *)any_uint8letextract_stream_ids=(* From RFC7540§4.1:
Stream Identifier: A stream identifier (see Section 5.1.1) expressed as
an unsigned 31-bit integer. The value 0x0 is reserved for frames that
are associated with the connection as a whole as opposed to an
individual stream. *)Int32.(logands(sub(shift_left1l31)1l))letstream_identifier=liftextract_stream_idBE.any_int32letparse_frame_header=lift4(funpayload_lengthframe_typeflagsstream_id->{Frame.flags;payload_length;stream_id;frame_type})frame_lengthframe_typeframe_flagsstream_identifier<?>"frame_header"<*commitletparse_payload_with_padding{Frame.payload_length;flags;_}parse_fn=ifFlags.test_paddedflagsthenany_uint8>>=funpad_length->(* From RFC7540§6.1:
Pad Length: An 8-bit field containing the length of the frame padding
in units of octets.
Data: Application data. The amount of data is the remainder of the
frame payload after subtracting the length of the other fields that
are present.
*)letbody_length=payload_length-pad_length-1inifbody_length<0then(* From RFC7540§6.1:
If the length of the padding is the length of the frame payload or
greater, the recipient MUST treat this as a connection error
(Section 5.4.1) of type PROTOCOL_ERROR. *)advance(payload_length-1)>>|fun()->ErrorError.(ConnectionError(ProtocolError,"Padding size exceeds payload size"))elsebeginparse_fnbody_length<*commit<*advancepad_lengthendelseparse_fnpayload_lengthletparse_data_frame({Frame.stream_id;payload_length;_}asframe_header)=ifStream_identifier.is_connectionstream_idthen(* From RFC7540§6.1:
DATA frames MUST be associated with a stream. If a DATA frame is
received whose stream identifier field is 0x0, the recipient MUST
respond with a connection error (Section 5.4.1) of type
PROTOCOL_ERROR. *)advancepayload_length>>|fun()->ErrorError.(ConnectionError(ProtocolError,"Data frames must be associated with a stream"))elseletparse_datalength=lift(funx->Ok(Frame.Datax))(take_bigstringlength)inparse_payload_with_paddingframe_headerparse_dataletparse_priority=lift2(funstream_dependencyw->lete=Priority.test_exclusivestream_dependencyin(* From RFC7540§6.3:
An unsigned 8-bit integer representing a priority weight for the
stream (see Section 5.3). Add one to the value to obtain a weight
between 1 and 256. *)letweight=w+1inletp={Priority.exclusive=e;weight;stream_dependency=extract_stream_idstream_dependency}inp)BE.any_int32any_uint8letparse_priority_frame{Frame.payload_length;stream_id;_}=ifStream_identifier.is_connectionstream_idthen(* From RFC7540§6.3:
The PRIORITY frame always identifies a stream. If a PRIORITY frame is
received with a stream identifier of 0x0, the recipient MUST respond
with a connection error (Section 5.4.1) of type PROTOCOL_ERROR. *)advancepayload_length>>|fun()->ErrorError.(ConnectionError(ProtocolError,"PRIORITY must be associated with a stream"))elseifpayload_length<>5then(* From RFC7540§6.3:
A PRIORITY frame with a length other than 5 octets MUST be treated as
a stream error (Section 5.4.2) of type FRAME_SIZE_ERROR. *)advancepayload_length>>|fun()->ErrorError.(StreamError(stream_id,FrameSizeError))elseparse_priority>>|funx->Ok(Frame.Priorityx)letparse_headers_frameframe_header=let{Frame.payload_length;stream_id;flags;_}=frame_headerinifStream_identifier.is_connectionstream_idthen(* From RFC7540§6.2:
HEADERS frames MUST be associated with a stream. If a HEADERS frame is
received whose stream identifier field is 0x0, the recipient MUST
respond with a connection error (Section 5.4.1) of type
PROTOCOL_ERROR. *)advancepayload_length>>|fun()->ErrorError.(ConnectionError(ProtocolError,"HEADERS must be associated with a stream"))elseletparse_fn=ifFlags.test_priorityflagsthenfunlength->lift2(funpriorityheaders->Ok(Frame.Headers(Somepriority,headers)))parse_priority(take_bigstring(length-5))elsefunlength->lift(funx->Ok(Frame.Headers(None,x)))(take_bigstringlength)inparse_payload_with_paddingframe_headerparse_fnletparse_error_code=liftError.parseBE.any_int32letparse_rst_stream_frame{Frame.payload_length;stream_id;_}=ifStream_identifier.is_connectionstream_idthen(* From RFC7540§6.4:
RST_STREAM frames MUST be associated with a stream. If a RST_STREAM
frame is received with a stream identifier of 0x0, the recipient MUST
treat this as a connection error (Section 5.4.1) of type
PROTOCOL_ERROR. *)advancepayload_length>>|fun()->ErrorError.(ConnectionError(ProtocolError,"RST_STREAM must be associated with a stream"))elseifpayload_length!=4then(* From RFC7540§6.4:
A RST_STREAM frame with a length other than 4 octets MUST be treated
as a connection error (Section 5.4.1) of type FRAME_SIZE_ERROR. *)advancepayload_length>>|fun()->ErrorError.(ConnectionError(FrameSizeError,"RST_STREAM payload must be 4 octets in length"))elselift(funx->Ok(Frame.RSTStreamx))parse_error_codeletparse_settings_frame{Frame.payload_length;stream_id;flags;_}=ifnot(Stream_identifier.is_connectionstream_id)then(* From RFC7540§6.5:
If an endpoint receives a SETTINGS frame whose stream identifier field
is anything other than 0x0, the endpoint MUST respond with a
connection error (Section 5.4.1) of type PROTOCOL_ERROR. *)advancepayload_length>>|fun()->ErrorError.(ConnectionError(ProtocolError,"SETTINGS must be associated with stream id 0x0"))elseifpayload_lengthmod6!=0then(* From RFC7540§6.5:
A SETTINGS frame with a length other than a multiple of 6 octets MUST
be treated as a connection error (Section 5.4.1) of type
FRAME_SIZE_ERROR. *)advancepayload_length>>|fun()->ErrorError.(ConnectionError(FrameSizeError,"SETTINGS payload size must be a multiple of 6"))elseifFlags.test_ackflags&&payload_length!=0then(* From RFC7540§6.5:
Receipt of a SETTINGS frame with the ACK flag set and a length field
value other than 0 MUST be treated as a connection error
(Section 5.4.1) of type FRAME_SIZE_ERROR. *)advancepayload_length>>|fun()->ErrorError.(ConnectionError(FrameSizeError,"SETTINGS with ACK must be empty"))elseletnum_settings=payload_length/6inletparse_setting=lift2(funkv->matchSettings.to_keykwith|Somes->Some(s,Int32.to_intv)|None->None)BE.any_uint16BE.any_int32in(* Note: This ignores unknown settings.
From RFC7540§6.5.3:
Unsupported parameters MUST be ignored.
*)countnum_settingsparse_setting>>|funxs->letrecfilter_optacc=function|[]->acc[]|Somex::xs->(filter_opt[@ocaml.tailcall])(funys->acc(x::ys))xs|None::xs->filter_optaccxsin(* From RFC7540§6.5.3:
The values in the SETTINGS frame MUST be processed in the order they
appear, with no other frame processing between values. *)Ok(Frame.Settings(filter_opt(funx->x)xs))letparse_push_promise_frame({Frame.payload_length;stream_id;_}asframe_header)=ifStream_identifier.is_connectionstream_idthen(* From RFC7540§6.6:
The stream identifier of a PUSH_PROMISE frame indicates the
stream it is associated with. If the stream identifier field
specifies the value 0x0, a recipient MUST respond with a
connection error (Section 5.4.1) of type PROTOCOL_ERROR. *)advancepayload_length>>|fun()->ErrorError.(ConnectionError(ProtocolError,"PUSH must be associated with a stream"))elseletparse_fnlength=lift2(funpromised_stream_idfragment->ifStream_identifier.is_connectionpromised_stream_idthen(* From RFC7540§6.6:
A receiver MUST treat the receipt of a PUSH_PROMISE that
promises an illegal stream identifier (Section 5.1.1) as a
connection error (Section 5.4.1) of type PROTOCOL_ERROR. *)ErrorError.(ConnectionError(ProtocolError,"PUSH must not promise stream id 0x0"))elseifStream_identifier.is_requestpromised_stream_idthen(* From RFC7540§6.6:
A receiver MUST treat the receipt of a PUSH_PROMISE that
promises an illegal stream identifier (Section 5.1.1) as a
connection error (Section 5.4.1) of type PROTOCOL_ERROR.
Note: An odd-numbered stream is an invalid stream identifier for
the server, and only the server can send PUSH_PROMISE frames. *)ErrorError.(ConnectionError(ProtocolError,"PUSH must be associated with an even-numbered stream id"))elseOkFrame.(PushPromise(promised_stream_id,fragment)))stream_identifier(take_bigstring(length-4))inparse_payload_with_paddingframe_headerparse_fnletparse_ping_frame{Frame.payload_length;stream_id;_}=ifnot(Stream_identifier.is_connectionstream_id)then(* From RFC7540§6.7:
PING frames are not associated with any individual stream. If a PING
frame is received with a stream identifier field value other than
0x0, the recipient MUST respond with a connection error
(Section 5.4.1) of type PROTOCOL_ERROR. *)advancepayload_length>>|fun()->ErrorError.(ConnectionError(ProtocolError,"PING must be associated with stream id 0x0"))elseifpayload_length!=8then(* From RFC7540§6.7:
Receipt of a PING frame with a length field value other than 8 MUST
be treated as a connection error (Section 5.4.1) of type
FRAME_SIZE_ERROR. *)advancepayload_length>>|fun()->ErrorError.(ConnectionError(FrameSizeError,"PING payload must be 8 octets in length"))elselift(funx->Ok(Frame.Pingx))(take_bigstringpayload_length)letparse_go_away_frame{Frame.payload_length;stream_id;_}=ifnot(Stream_identifier.is_connectionstream_id)then(* From RFC7540§6.8:
The GOAWAY frame applies to the connection, not a specific stream. An
endpoint MUST treat a GOAWAY frame with a stream identifier other than
0x0 as a connection error (Section 5.4.1) of type PROTOCOL_ERROR. *)advancepayload_length>>|fun()->ErrorError.(ConnectionError(ProtocolError,"GOAWAY must be associated with stream id 0x0"))elselift3(funlast_stream_iderrdebug_data->Ok(Frame.GoAway(last_stream_id,err,debug_data)))stream_identifierparse_error_code(take_bigstring(payload_length-8))letparse_window_update_frame{Frame.stream_id;payload_length;_}=(* From RFC7540§6.9:
A WINDOW_UPDATE frame with a length other than 4 octets MUST be treated
as a connection error (Section 5.4.1) of type FRAME_SIZE_ERROR. *)ifpayload_length!=4thenadvancepayload_length>>|fun()->ErrorError.(ConnectionError(FrameSizeError,"WINDOW_UPDATE payload must be 4 octets in length"))elselift(funuint->letwindow_size_increment=Util.clear_bit(Int32.to_intuint)31inifwindow_size_increment==0thenbegin(* From RFC7540§6.9:
A receiver MUST treat the receipt of a WINDOW_UPDATE frame with an
flow-control window increment of 0 as a stream error (Section 5.4.2)
of type PROTOCOL_ERROR; errors on the connection flow-control window
MUST be treated as a connection error (Section 5.4.1). *)leterror=ifStream_identifier.is_connectionstream_idthenError.(ConnectionError(ProtocolError,"Window update must not be 0"))elseError.(StreamError(stream_id,ProtocolError))inErrorerrorendelseOk(Frame.WindowUpdatewindow_size_increment))BE.any_int32letparse_continuation_frame{Frame.payload_length;stream_id;_}=ifStream_identifier.is_connectionstream_idthen(* From RFC7540§6.10:
CONTINUATION frames MUST be associated with a stream. If a
CONTINUATION frame is received whose stream identifier field is 0x0,
the recipient MUST respond with a connection error (Section 5.4.1) of
type PROTOCOL_ERROR. *)advancepayload_length>>|fun()->ErrorError.(ConnectionError(ProtocolError,"CONTINUATION must be associated with a stream"))elselift(funblock_fragment->Ok(Frame.Continuationblock_fragment))(take_bigstringpayload_length)letparse_unknown_frametyp{Frame.payload_length;_}=lift(funbigstring->Ok(Frame.Unknown(typ,bigstring)))(take_bigstringpayload_length)letparse_frame_payload({Frame.frame_type;_}asframe_header)=beginmatchframe_typewith|Frame.FrameType.Data->parse_data_frameframe_header|Headers->parse_headers_frameframe_header|Priority->parse_priority_frameframe_header|RSTStream->parse_rst_stream_frameframe_header|Settings->parse_settings_frameframe_header|PushPromise->parse_push_promise_frameframe_header|Ping->parse_ping_frameframe_header|GoAway->parse_go_away_frameframe_header|WindowUpdate->parse_window_update_frameframe_header|Continuation->parse_continuation_frameframe_header|Unknowntyp->parse_unknown_frametypframe_headerend<?>"frame_payload"letparse_frameparse_context=parse_frame_header>>=fun({Frame.payload_length;_}asframe_header)->parse_context.frame_header<-Someframe_header;(* If we're parsing a new frame, we didn't yet send a stream error on it *)parse_context.did_report_stream_error<-false;(* Payload could be 0 (e.g. empty SETTINGS frame). This always succeeds. *)Angstrom.Unsafe.peek0(funbs~off:_~len:_->(* We do unbuffered parsing and the bigarray we read input from is
allocated based on the maximum frame payload negotiated by HTTP/2
communication. If the underlying buffer is smaller than what
the frame can fit, we want to skip the remaining input and skip to the
next frame.
From RFC7540§5.4.2:
A stream error is an error related to a specific stream that does not
affect processing of other streams.
*)letis_frame_size_error=payload_length>Bigstringaf.lengthbsinifis_frame_size_errorthenparse_context.remaining_bytes_to_skip<-parse_context.remaining_bytes_to_skip+payload_length)>>=fun()->parse_frame_payloadframe_header>>|function|Okframe_payload->Ok{Frame.frame_header;frame_payload}|Errore->Erroreletconnection_preface=(* From RFC7540§3.5:
In HTTP/2, each endpoint is required to send a connection preface as a
final confirmation of the protocol in use and to establish the initial
settings for the HTTP/2 connection. *)string"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"<?>"connection preface"moduleReader=structmoduleAU=Angstrom.Unbufferedtypeparse_error=(* Parse error reported by Angstrom *)[`Parseofstringlist*string(* Full error information *)|`ErrorofError.t(* Just the error code, need to puzzle back connection or stream info *)|`ErrorCodeofError.error_code]type'errorparse_state=|Done|Failof'error|Partialof(Bigstringaf.t->off:int->len:int->AU.more->(unit,'error)resultAU.state)type'errort={parser:(unit,'error)resultAngstrom.t;mutableparse_state:'errorparse_state(* The state of the parse for the current request *);mutableclosed:bool(* Whether the input source has left the building, indicating that no
* further input will be received. *);parse_context:parse_context(* The current stream identifier being processed, in order to discern
* whether the error that needs to be assembled is a stream or connection
* error. *)}typeconnection_preface=parse_errorttypeframe=parse_errortletcreateparserparse_context={parser;parse_state=Done;closed=false;parse_context}letconnection_prefacehandler=letparse_context={frame_header=None;remaining_bytes_to_skip=0;did_report_stream_error=false}inletparser=connection_preface*>commit>>=fun()->(* From RFC7540§3.5:
[...] the connection preface starts with the string
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n). This sequence MUST be followed by
a SETTINGS frame (Section 6.5), which MAY be empty. *)parse_frameparse_context>>|function|Ok({frame_payload=Frame.Settingssettings_list;_}asframe)->handlerframesettings_list;Ok()|Ok_->(* From RFC7540§3.5:
Clients and servers MUST treat an invalid connection preface as a
connection error (Section 5.4.1) of type PROTOCOL_ERROR. A GOAWAY
frame (Section 6.8) MAY be omitted in this case, since an invalid
preface indicates that the peer is not using HTTP/2. *)Error(`ErrorError.(ConnectionError(ProtocolError,"Invalid connection preface")))|Errore->Error(`Errore)increateparserparse_contextletframehandler=letparse_context={frame_header=None;remaining_bytes_to_skip=0;did_report_stream_error=false}inletparser=skip_many(parse_frameparse_context<*commit>>|handler)>>|fun()->Ok()increateparserparse_contextletis_closedt=t.closedlettransitiontstate=matchstatewith|AU.Done(consumed,Ok())->t.parse_state<-Done;consumed|Done(consumed,Errorerror)->t.parse_state<-Failerror;consumed|Fail(consumed,marks,msg)->t.parse_state<-Fail(`Parse(marks,msg));consumed|Partial{committed;continue}->(* If we have bytes to skip over then it means we've spotted a
* FRAME_SIZE_ERROR, a case where, due to our unbuffered parsing, the
* payload length declared in a frame header is larger than the
* underlying buffer can fit. *)ift.parse_context.remaining_bytes_to_skip>0thent.parse_state<-Fail(`ErrorCodeError.FrameSizeError)elset.parse_state<-Partialcontinue;committedletstarttstate=matchstatewith|AU.Done_->failwith"h2.Parse.unable to start parser"|Fail(0,marks,msg)->t.parse_state<-Fail(`Parse(marks,msg))|Partial{committed=0;continue}->t.parse_state<-Partialcontinue|Partial_|Fail_->assertfalseletrecread_with_moretbs~off~lenmore=letconsumed=matcht.parse_statewith|Fail_->letparser_ctx=t.parse_contextinletremaining_bytes=parser_ctx.remaining_bytes_to_skipinifremaining_bytes>0thenbegin(* Just skip input if we need to *)assert(remaining_bytes>=len);letremaining_bytes'=remaining_bytes-leninparser_ctx.remaining_bytes_to_skip<-remaining_bytes';assert(remaining_bytes'>=0);ifremaining_bytes'=0then(* Reset the parser state to `Done` so that we can read the next
* frame (after skipping through the bad input) *)t.parse_state<-Done;lenendelse0|Done->startt(AU.parset.parser);read_with_moretbs~off~lenmore;|Partialcontinue->transitiont(continuebsmore~off~len)inbeginmatchmorewith|Complete->t.closed<-true;|Incomplete->()end;consumedletforce_closet=ignore(read_with_moretBigstringaf.empty~off:0~len:0Complete:int)letstate_to_string=function|Fail_->"fail"|Done->"done"|Partial_->"partial"letfail_to_stringmarkserr=String.concat" > "marks^": "^errletnext_from_errort?(msg="")error_code=matcht.parse_context,error_codewith|{frame_header=Some{frame_type=Headers|PushPromise|Continuation|Settings;_};_},Error.FrameSizeError|{frame_header=Some{Frame.stream_id=0x0l;_};_},_|{frame_header=None;_},_->(* From RFC7540§4.2:
A frame size error in a frame that could alter the state of the
entire connection MUST be treated as a connection error (Section
5.4.1); this includes any frame carrying a header block (Section
4.3) (that is, HEADERS, PUSH_PROMISE, and CONTINUATION), SETTINGS,
and any frame with a stream identifier of 0. *)`ErrorError.(ConnectionError(error_code,msg))|{frame_header=Some_;did_report_stream_error=true;_},_->(* If the parser is in a `Fail` state and would report a stream error,
* just issue a `Read` operation if we've already reported that error. *)ift.closedthen`Closeelse`Read|{frame_header=Some{Frame.stream_id;_};_},_->t.parse_context.did_report_stream_error<-true;`ErrorError.(StreamError(stream_id,error_code))letnextt=matcht.parse_statewith|Partial_->`Read|Done->ift.closedthen`Closeelse`Read|Fail(`Errore)->`Errore|Fail(`ErrorCodeerror_code)->next_from_errorterror_code|Fail(`Parse(marks,msg))->leterror_code=matchmarks,msgwith|["frame_payload"],"not enough input"->(* From RFC7540§4.2:
An endpoint MUST send an error code of FRAME_SIZE_ERROR if a frame
exceeds the size defined in SETTINGS_MAX_FRAME_SIZE, exceeds any
limit defined for the frame type, or is too small to contain
mandatory frame data. *)Error.FrameSizeError|_->Error.ProtocolErrorinnext_from_errort~msg:(fail_to_stringmarksmsg)error_codeend