# frozen_string_literal: true# Released under the MIT License.# Copyright, 2018-2024, by Samuel Williams.require_relative"../response"require_relative"stream"moduleAsyncmoduleHTTPmoduleProtocolmoduleHTTP2# Typically used on the client side for writing a request and reading the incoming response.classResponse<Protocol::ResponseclassStream<HTTP2::Streamdefinitialize(*)super@response=Response.new(self)@notification=Async::Notification.new@exception=nilendattr:responsedefwait_for_input# The input isn't ready until the response headers have been received:@response.wait# There is a possible race condition if you try to access @input - it might already be closed and nil.return@response.bodyenddefaccept_push_promise_stream(promised_stream_id,headers)raiseProtocolError,"Cannot accept push promise stream!"end# This should be invoked from the background reader, and notifies the task waiting for the headers that we are done.defreceive_initial_headers(headers,end_stream)# While in theory, the response pseudo-headers may be extended in the future, currently they only response pseudo-header is :status, so we can assume it is always the first header.status_header=headers.shiftifstatus_header.first!=":status"raiseProtocolError,"Invalid response headers: #{headers.inspect}"endstatus=Integer(status_header.last)ifstatus>=100&&status<200returnreceive_interim_headers(status,headers)end@response.status=status@headers=::Protocol::HTTP::Headers.new# If the protocol request was successful, ensure the response protocol matches:ifstatus==200andprotocol=@response.request.protocol@response.protocol=Array(protocol).firstendheaders.eachdo|key,value|# It's guaranteed that this should be the first header:ifkey==CONTENT_LENGTH@length=Integer(value)elseadd_header(key,value)endend@response.headers=@headersif@response.valid?if!end_stream# We only construct the input/body if data is coming.@response.body=prepare_input(@length)elsif@response.head?@response.body=::Protocol::HTTP::Body::Head.new(@length)endelsesend_reset_stream(::Protocol::HTTP2::Error::PROTOCOL_ERROR)endself.notify!returnheadersenddefreceive_interim_headers(status,headers)ifheaders.any?headers=::Protocol::HTTP::Headers[headers]elseheaders=nilend@response.request.send_interim_response(status,headers)end# Notify anyone waiting on the response headers to be received (or failure).defnotify!ifnotification=@notification@notification=nilnotification.signalendend# Wait for the headers to be received or for stream reset.defwait# If you call wait after the headers were already received, it should return immediately:@notification&.waitif@exceptionraise@exceptionendenddefclosed(error)superif@response@response=nilend@exception=errorself.notify!endenddefinitialize(stream)super(stream.connection.version,nil,nil)@stream=stream@request=nilendattr:streamattr:requestdefpool=(pool)# If we are already closed, the stream can be released now:if@stream.closed?pool.release(@stream.connection)else# Otherwise, we will release the stream when it is closed:@stream.pool=poolendenddefconnection@stream.connectionenddefwait@stream.waitenddefhead?@request&.head?enddefvalid?!!@statusenddefbuild_request(headers)request=::Protocol::HTTP::Request.newrequest.headers=::Protocol::HTTP::Headers.newheaders.eachdo|key,value|ifkey==SCHEMEraise::Protocol::HTTP2::HeaderError,"Request scheme already specified!"ifrequest.schemerequest.scheme=valueelsifkey==AUTHORITYraise::Protocol::HTTP2::HeaderError,"Request authority already specified!"ifrequest.authorityrequest.authority=valueelsifkey==METHODraise::Protocol::HTTP2::HeaderError,"Request method already specified!"ifrequest.methodrequest.method=valueelsifkey==PATHraise::Protocol::HTTP2::HeaderError,"Request path is empty!"ifvalue.empty?raise::Protocol::HTTP2::HeaderError,"Request path already specified!"ifrequest.pathrequest.path=valueelsifkey.start_with?":"raise::Protocol::HTTP2::HeaderError,"Invalid pseudo-header #{key}!"elserequest.headers[key]=valueendend@request=requestend# Send a request and read it into this response.defsend_request(request)@request=request# https://http2.github.io/http2-spec/#rfc.section.8.1.2.3# All HTTP/2 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header fields, unless it is a CONNECT request (Section 8.3). An HTTP request that omits mandatory pseudo-header fields is malformed (Section 8.1.2.6).pseudo_headers=[[SCHEME,request.scheme],[METHOD,request.method],[PATH,request.path],]# To ensure that the HTTP/1.1 request line can be reproduced accurately, this pseudo-header field MUST be omitted when translating from an HTTP/1.1 request that has a request target in origin or asterisk form (see [RFC7230], Section 5.3). Clients that generate HTTP/2 requests directly SHOULD use the :authority pseudo-header field instead of the Host header field.ifauthority=request.authoritypseudo_headers<<[AUTHORITY,authority]endifprotocol=request.protocolpseudo_headers<<[PROTOCOL,protocol]endheaders=::Protocol::HTTP::Headers::Merged.new(pseudo_headers,request.headers)ifrequest.body.nil?@stream.send_headers(nil,headers,::Protocol::HTTP2::END_STREAM)elseiflength=request.body.length# This puts it at the end of the pseudo-headers:pseudo_headers<<[CONTENT_LENGTH,length]end# This function informs the headers object that any subsequent headers are going to be trailer. Therefore, it must be called *before* sending the headers, to avoid any race conditions.trailer=request.headers.trailer!begin@stream.send_headers(nil,headers)rescueraiseRequestFailedend@stream.send_body(request.body,trailer)endendendendendendend