Class: Hyperion::Http2Handler::RequestStream

Inherits:
Protocol::HTTP2::Stream
  • Object
show all
Defined in:
lib/hyperion/http2_handler.rb

Overview

Per-stream subclass that captures decoded request pseudo-headers, regular headers, and any DATA frame body bytes for later dispatch. Also exposes a ‘window_available` notification fan-out so the response-writer fiber can sleep until WINDOW_UPDATE arrives.

Constant Summary collapse

VALID_REQUEST_PSEUDO_HEADERS =

RFC 7540 §8.1.2.1 — the only pseudo-headers a server MUST accept on a request. Anything else (notably ‘:status`, which is response-only, or an unknown `:foo`) is a malformed request that we reject with PROTOCOL_ERROR.

%w[:method :path :scheme :authority].freeze
FORBIDDEN_HEADERS =

RFC 7540 §8.1.2.2 — these connection-specific headers MUST NOT appear in HTTP/2 requests; their semantics are folded into HTTP/2 framing.

%w[connection transfer-encoding keep-alive upgrade proxy-connection].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeRequestStream

Returns a new instance of RequestStream.



51
52
53
54
55
56
57
58
59
60
# File 'lib/hyperion/http2_handler.rb', line 51

def initialize(*)
  super
  @request_headers = []
  @request_body = +''
  @request_body_bytes = 0
  @request_complete = false
  @window_available = ::Async::Notification.new
  @protocol_error_reason = nil
  @declared_content_length = nil
end

Instance Attribute Details

#protocol_error_reasonObject (readonly)

Returns the value of attribute protocol_error_reason.



49
50
51
# File 'lib/hyperion/http2_handler.rb', line 49

def protocol_error_reason
  @protocol_error_reason
end

#request_bodyObject (readonly)

Returns the value of attribute request_body.



49
50
51
# File 'lib/hyperion/http2_handler.rb', line 49

def request_body
  @request_body
end

#request_completeObject (readonly)

Returns the value of attribute request_complete.



49
50
51
# File 'lib/hyperion/http2_handler.rb', line 49

def request_complete
  @request_complete
end

#request_headersObject (readonly)

Returns the value of attribute request_headers.



49
50
51
# File 'lib/hyperion/http2_handler.rb', line 49

def request_headers
  @request_headers
end

Instance Method Details

#process_data(frame) ⇒ Object



90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/hyperion/http2_handler.rb', line 90

def process_data(frame)
  data = super
  # rubocop:disable Rails/Present
  if data && !data.empty?
    @request_body << data
    @request_body_bytes += data.bytesize
  end
  # rubocop:enable Rails/Present
  if frame.end_stream?
    validate_body_length! unless protocol_error?
    @request_complete = true
  end
  data
end

#process_headers(frame) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/hyperion/http2_handler.rb', line 69

def process_headers(frame)
  decoded = super
  # First HEADERS frame on a stream carries the request header block;
  # any later HEADERS frame is trailers (§8.1) and we deliberately do
  # not re-validate (re-running the validator would see the original
  # request pseudo-headers plus the new trailer block and falsely flag
  # them as misordered).
  first_block = @request_headers.empty?
  # decoded is an Array of [name, value] pairs (HPACK output).
  decoded.each { |pair| @request_headers << pair }
  # Run RFC 7540 §8.1.2 validation as soon as we have a complete header
  # block. We do it here (not at end_stream) so the dispatcher sees the
  # error flag before it spawns a fiber for the request.
  validate_request_headers! if first_block && !protocol_error?
  if frame.end_stream?
    validate_body_length! unless protocol_error?
    @request_complete = true
  end
  decoded
end

#protocol_error?Boolean

Used by the dispatch loop to decide whether to invoke the app or send RST_STREAM PROTOCOL_ERROR. Set by ‘validate_request_headers!` and `validate_body_length!`.

Returns:

  • (Boolean)


65
66
67
# File 'lib/hyperion/http2_handler.rb', line 65

def protocol_error?
  !@protocol_error_reason.nil?
end

#validate_body_length!Object

RFC 7540 §8.1.2.6 — if ‘content-length` was advertised, the actual number of DATA bytes received (across all DATA frames) MUST match.



167
168
169
170
171
172
173
174
# File 'lib/hyperion/http2_handler.rb', line 167

def validate_body_length!
  return if @declared_content_length.nil?
  return if @declared_content_length == @request_body_bytes

  fail_validation!(
    "content-length mismatch: declared #{@declared_content_length}, received #{@request_body_bytes}"
  )
end

#validate_request_headers!Object

RFC 7540 §8.1.2 — request header validation. Sets ‘@protocol_error_reason` on the first violation we hit; the dispatch loop turns that into RST_STREAM PROTOCOL_ERROR.



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/hyperion/http2_handler.rb', line 108

def validate_request_headers!
  seen_regular = false
  pseudo_counts = Hash.new(0)
  @request_headers.each do |pair|
    name, value = pair
    name = name.to_s
    if name.start_with?(':')
      # §8.1.2.1: pseudo-headers MUST precede regular headers.
      return fail_validation!('pseudo-header after regular header') if seen_regular
      # §8.1.2.1: only the four request pseudo-headers are valid; in
      # particular, `:status` is response-only.
      unless VALID_REQUEST_PSEUDO_HEADERS.include?(name)
        return fail_validation!("invalid request pseudo-header: #{name}")
      end

      pseudo_counts[name] += 1
    else
      seen_regular = true
      # §8.1.2: header names must be lowercase in HTTP/2.
      return fail_validation!('uppercase header name') if /[A-Z]/.match?(name)
      # §8.1.2.2: connection-specific headers are forbidden.
      return fail_validation!("forbidden connection-specific header: #{name}") if FORBIDDEN_HEADERS.include?(name)
      # §8.1.2.2: TE may only carry the value `trailers`.
      if name == 'te' && value.to_s.downcase.strip != 'trailers'
        return fail_validation!('TE header with non-trailers value')
      end

      # Track declared content-length for later body-byte cross-check.
      @declared_content_length = value.to_s.to_i if name == 'content-length'
    end
  end

  # §8.1.2.3: every pseudo-header may appear at most once.
  pseudo_counts.each do |name, count|
    return fail_validation!("duplicated pseudo-header: #{name}") if count > 1
  end

  method = pseudo_value(':method')
  # CONNECT (§8.3) has its own rules; everything else MUST carry
  # :method, :scheme and a non-empty :path.
  if method == 'CONNECT'
    return fail_validation!('CONNECT with :scheme') if pseudo_value(':scheme')
    return fail_validation!('CONNECT with :path') if pseudo_value(':path')
    return fail_validation!('CONNECT without :authority') unless pseudo_value(':authority')
  else
    return fail_validation!('missing :method') if method.nil? || method.empty?

    scheme = pseudo_value(':scheme')
    return fail_validation!('missing :scheme') if scheme.nil? || scheme.empty?

    path = pseudo_value(':path')
    return fail_validation!('missing or empty :path') if path.nil? || path.empty?
  end

  nil
end

#wait_for_windowObject

Block the calling fiber until the remote window grows. Cheap no-op signal each time ‘window_updated` fires; the caller re-checks available_frame_size in a loop.



188
189
190
# File 'lib/hyperion/http2_handler.rb', line 188

def wait_for_window
  @window_available.wait
end

#window_updated(size) ⇒ Object

Called by protocol-http2 whenever the remote peer’s flow-control window opens up — either via a stream-level WINDOW_UPDATE or via the connection-level fan-out in ‘Connection#consume_window`. We poke the notification so any fiber waiting in `wait_for_window` resumes.



180
181
182
183
# File 'lib/hyperion/http2_handler.rb', line 180

def window_updated(size)
  @window_available.signal
  super
end