Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 42 additions & 6 deletions lib/http/2/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ def initialize(settings = {})
@last_stream_id = 0
@streams = {}
@streams_recently_closed = {}
@idle_stream_priorities = {}
@oldest_stream_recently_closed = nil
@last_promised_stream_id = 0
@pending_settings = []

@framer = Framer.new(@local_settings[:settings_max_frame_size])
Expand Down Expand Up @@ -316,11 +318,19 @@ def receive(data)
verify_pseudo_headers(frame)

verify_stream_order(stream_id)
priority = @idle_stream_priorities.delete(stream_id) || {}
if frame[:flags].anybits?(PRIORITY)
priority = {
weight: frame[:weight],
dependency: frame[:dependency],
exclusive: frame[:exclusive]
}
end
Comment on lines +321 to +328

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
priority = @idle_stream_priorities.delete(stream_id) || {}
if frame[:flags].anybits?(PRIORITY)
priority = {
weight: frame[:weight],
dependency: frame[:dependency],
exclusive: frame[:exclusive]
}
end
if frame[:flags].anybits?(PRIORITY)
priority = {
weight: frame[:weight],
dependency: frame[:dependency],
exclusive: frame[:exclusive]
}
else
priority = @idle_stream_priorities.delete(stream_id)
end

stream = activate_stream(
id: stream_id,
weight: frame[:weight] || DEFAULT_WEIGHT,
dependency: frame[:dependency] || 0,
exclusive: frame[:exclusive] || false
weight: priority.fetch(:weight, DEFAULT_WEIGHT),
dependency: priority.fetch(:dependency, 0),
exclusive: priority.fetch(:exclusive, false)
Comment on lines +331 to +333

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
weight: priority.fetch(:weight, DEFAULT_WEIGHT),
dependency: priority.fetch(:dependency, 0),
exclusive: priority.fetch(:exclusive, false)
weight: priority&[:weight] || DEFAULT_WEIGHT,
dependency: priority&[:dependency] || 0,
exclusive: priority&[:exclusive] || false

)
emit(:stream, stream)
end
Expand Down Expand Up @@ -352,6 +362,8 @@ def receive(data)
parent = @streams[stream_id]
pid = frame[:promise_stream]

validate_push_promise(pid)

# if PUSH parent is recently closed, RST_STREAM the push
if @streams_recently_closed[stream_id]
send(type: :rst_stream, stream: pid, error: :refused_stream)
Expand All @@ -377,6 +389,13 @@ def receive(data)

_verify_pseudo_headers(frame, REQUEST_MANDATORY_HEADERS)
verify_stream_order(pid)

promised_stream_count = @streams.each_value.count(&:parent)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of counting on the fly and in order to avoid a O(n), bookkeep this in an ivar instead.

if promised_stream_count >= @local_settings[:settings_max_concurrent_streams]
send(type: :rst_stream, stream: pid, error: :refused_stream)
next
end

stream = activate_stream(id: pid, parent: parent)
emit(:promise, stream)
stream << frame
Expand All @@ -399,12 +418,17 @@ def receive(data)
# the draining connection open.
next if closed? && stream_id <= @last_stream_id

stream = activate_stream(
id: stream_id,
unless @idle_stream_priorities.key?(stream_id)
next if @idle_stream_priorities.size >= @local_settings[:settings_max_concurrent_streams]
end

priority = {
weight: frame[:weight] || DEFAULT_WEIGHT,
dependency: frame[:dependency] || 0,
exclusive: frame[:exclusive] || false
)
}
@idle_stream_priorities[stream_id] = priority
stream = Stream.new(connection: self, id: stream_id, **priority)

emit(:stream, stream)
stream << frame
Expand Down Expand Up @@ -577,6 +601,18 @@ def ping_management(frame)
end
end

def validate_push_promise(stream_id)
connection_error(:protocol_error, msg: "clients cannot send PUSH_PROMISE") if @local_role == :server

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this clause is easier to read with if @remote_role == :client

if @local_settings[:settings_enable_push].zero?
connection_error(:protocol_error, msg: "received PUSH_PROMISE while push is disabled")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
connection_error(:protocol_error, msg: "received PUSH_PROMISE while push is disabled")
connection_error(:protocol_error, msg: "push promises are disabled")

end
unless stream_id.positive? && stream_id.even? && stream_id > @last_promised_stream_id
connection_error(:protocol_error, msg: "invalid promised stream ID")
end

@last_promised_stream_id = stream_id
end

# Validate settings parameters. See sepc Section 6.5.2.
#
# @param role [Symbol] The sender's role: :client or :server
Expand Down
4 changes: 4 additions & 0 deletions sig/connection.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ module HTTP2

@streams: Hash[Integer, Stream]
@streams_recently_closed: Hash[Integer, Numeric]
@idle_stream_priorities: Hash[Integer, Hash[Symbol, Integer | bool]]
@oldest_stream_recently_closed: Numeric
@last_promised_stream_id: Integer

@framer: Framer

Expand Down Expand Up @@ -91,6 +93,8 @@ module HTTP2

def ping_management: (frame) -> void

def validate_push_promise: (Integer stream_id) -> void

def validate_settings: (role_type, settings_enum) -> void

def connection_settings: (settings_frame) -> void
Expand Down
13 changes: 13 additions & 0 deletions spec/server_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,19 @@
expect(stream.weight).to eq 20
end

it "should preserve updated PRIORITY values when an idle stream opens" do
srv << CONNECTION_PREFACE_MAGIC
srv << f.generate(settings_frame)

streams = []
srv.on(:stream) { |stream| streams << stream }
srv << f.generate(priority_frame.merge(stream: 1, weight: 20))
srv << f.generate(priority_frame.merge(stream: 1, weight: 30))
srv << f.generate(headers_frame)

expect(streams.last.weight).to eq 30
end

it "should process connection management frames after GOAWAY" do
srv << CONNECTION_PREFACE_MAGIC
srv << f.generate(settings_frame)
Expand Down
Loading