# frozen_string_literal: true## Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com># # Permission is hereby granted, free of charge, to any person obtaining a copy# of this software and associated documentation files (the "Software"), to deal# in the Software without restriction, including without limitation the rights# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell# copies of the Software, and to permit persons to whom the Software is# furnished to do so, subject to the following conditions:# # The above copyright notice and this permission notice shall be included in# all copies or substantial portions of the Software.# # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN# THE SOFTWARE.require'protocol/http2/stream'require_relative'input'require_relative'output'moduleAsyncmoduleHTTPmoduleProtocolmoduleHTTP2classStream<::Protocol::HTTP2::Streamdefinitialize(*)super@headers=nil@trailer=nil# Input buffer, reading request body, or response body (receive_data):@length=nil@input=nil# Output buffer, writing request body or response body (window_updated):@output=nilendattr_accessor:headersattr:inputdefadd_header(key,value)ifkey==CONNECTIONraise::Protocol::HTTP2::HeaderError,"Connection header is not allowed!"elsifkey.start_with?':'raise::Protocol::HTTP2::HeaderError,"Invalid pseudo-header #{key}!"elsifkey=~/[A-Z]/raise::Protocol::HTTP2::HeaderError,"Invalid upper-case characters in header #{key}!"else@headers.add(key,value)endenddefreceive_trailing_headers(headers,end_stream)headers.eachdo|key,value|if@trailer.include?(key)add_header(key,value)elseraise::Protocol::HTTP2::HeaderError,"Cannot add trailer #{key} as it was not specified as a trailer!"endendenddefprocess_headers(frame)if@headers.nil?@headers=::Protocol::HTTP::Headers.newself.receive_initial_headers(super,frame.end_stream?)@trailer=@headers[TRAILER]elsif@trailerandframe.end_stream?self.receive_trailing_headers(super,frame.end_stream?)elseraise::Protocol::HTTP2::HeaderError,"Unable to process headers!"end# TODO this might need to be in an ensure block:if@inputandframe.end_stream?@input.close($!)@input=nilendrescue::Protocol::HTTP2::HeaderError=>errorAsync.logger.error(self,error)send_reset_stream(error.code)enddefwait_for_inputreturn@inputend# Prepare the input stream which will be used for incoming data frames.# @return [Input] the input body.defprepare_input(length)if@input.nil?@input=Input.new(self,length)elseraiseArgumentError,"Input body already prepared!"endenddefupdate_local_window(frame)consume_local_window(frame)# This is done on demand in `Input#read`:# request_window_updateenddefprocess_data(frame)data=frame.unpackif@inputunlessdata.empty?@input.write(data)endifframe.end_stream?@input.close@input=nilendendreturndatarescue::Protocol::HTTP2::ProtocolErrorraiserescue# Anything else...send_reset_stream(::Protocol::HTTP2::Error::INTERNAL_ERROR)end# Set the body and begin sending it.defsend_body(body,trailer=nil)@output=Output.new(self,body,trailer)@output.startend# Called when the output terminates normally.deffinish_output(error=nil)trailer=@output&.trailer@output=niliferrorsend_reset_stream(::Protocol::HTTP2::Error::INTERNAL_ERROR)else# Write trailer?iftrailersend_headers(nil,trailer,::Protocol::HTTP2::END_STREAM)elsesend_data(nil,::Protocol::HTTP2::END_STREAM)endendenddefwindow_updated(size)super@output&.window_updated(size)end# When the stream transitions to the closed state, this method is called. There are roughly two ways this can happen:# - A frame is received which causes this stream to enter the closed state. This method will be invoked from the background reader task.# - A frame is sent which causes this stream to enter the closed state. This method will be invoked from that task.# While the input stream is relatively straight forward, the output stream can trigger the second case abovedefclosed(error)superif@input@input.close(error)@input=nilendif@output@output.stop(error)@output=nilendreturnselfendendendendendend