pax_global_header00006660000000000000000000000064150634070220014511gustar00rootroot0000000000000052 comment=9f38daa9169561ab0b5c0083e284c5f7bc417a3f protocol-http2-0.23.0/000077500000000000000000000000001506340702200144735ustar00rootroot00000000000000protocol-http2-0.23.0/.editorconfig000066400000000000000000000001511506340702200171450ustar00rootroot00000000000000root = true [*] indent_style = tab indent_size = 2 [*.{yml,yaml}] indent_style = space indent_size = 2 protocol-http2-0.23.0/.github/000077500000000000000000000000001506340702200160335ustar00rootroot00000000000000protocol-http2-0.23.0/.github/copilot-instructions.md000066400000000000000000000016431506340702200225740ustar00rootroot00000000000000# GitHub Copilot Instructions ## Use Agent Context When working on this project, consult the `agents.md` file for project-specific guidelines, architecture decisions, and development patterns. This file contains curated information that will help you make better decisions aligned with the project's goals and standards. If the file does not exist, you will need to install it, by running the following command: ```bash $ bundle install $ bundle exec bake agent:context:install ``` This command will set up the necessary context files that help you understand the project structure, dependencies, and conventions. ## Ignoring Files The `.gitignore` file is split into two sections, separated by a blank line. The first section is automatically generated, while the second section is user controlled. While working on pull requests, you should not add unrelated changes to the `.gitignore` file as part of the pull request. protocol-http2-0.23.0/.github/workflows/000077500000000000000000000000001506340702200200705ustar00rootroot00000000000000protocol-http2-0.23.0/.github/workflows/documentation-coverage.yaml000066400000000000000000000006511506340702200254200ustar00rootroot00000000000000name: Documentation Coverage on: [push, pull_request] permissions: contents: read env: COVERAGE: PartialSummary jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ruby bundler-cache: true - name: Validate coverage timeout-minutes: 5 run: bundle exec bake decode:index:coverage lib protocol-http2-0.23.0/.github/workflows/documentation.yaml000066400000000000000000000021311506340702200236220ustar00rootroot00000000000000name: Documentation on: push: branches: - main # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages: permissions: contents: read pages: write id-token: write # Allow one concurrent deployment: concurrency: group: "pages" cancel-in-progress: true env: BUNDLE_WITH: maintenance jobs: generate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ruby bundler-cache: true - name: Installing packages run: sudo apt-get install wget - name: Generate documentation timeout-minutes: 5 run: bundle exec bake utopia:project:static --force no - name: Upload documentation artifact uses: actions/upload-pages-artifact@v3 with: path: docs deploy: runs-on: ubuntu-latest environment: name: github-pages url: ${{steps.deployment.outputs.page_url}} needs: generate steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 protocol-http2-0.23.0/.github/workflows/rubocop.yaml000066400000000000000000000005321506340702200224250ustar00rootroot00000000000000name: RuboCop on: [push, pull_request] permissions: contents: read jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ruby bundler-cache: true - name: Run RuboCop timeout-minutes: 10 run: bundle exec rubocop protocol-http2-0.23.0/.github/workflows/test-coverage.yaml000066400000000000000000000022031506340702200235210ustar00rootroot00000000000000name: Test Coverage on: [push, pull_request] permissions: contents: read env: COVERAGE: PartialSummary jobs: test: name: ${{matrix.ruby}} on ${{matrix.os}} runs-on: ${{matrix.os}}-latest strategy: matrix: os: - ubuntu - macos ruby: - ruby steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Run tests timeout-minutes: 5 run: bundle exec bake test - uses: actions/upload-artifact@v4 with: include-hidden-files: true if-no-files-found: error name: coverage-${{matrix.os}}-${{matrix.ruby}} path: .covered.db validate: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ruby bundler-cache: true - uses: actions/download-artifact@v4 - name: Validate coverage timeout-minutes: 5 run: bundle exec bake covered:validate --paths */.covered.db \; protocol-http2-0.23.0/.github/workflows/test-external.yaml000066400000000000000000000011101506340702200235440ustar00rootroot00000000000000name: Test External on: [push, pull_request] permissions: contents: read jobs: test: name: ${{matrix.ruby}} on ${{matrix.os}} runs-on: ${{matrix.os}}-latest strategy: matrix: os: - ubuntu - macos ruby: - "3.2" - "3.3" - "3.4" steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Run tests timeout-minutes: 10 run: bundle exec bake test:external protocol-http2-0.23.0/.github/workflows/test.yaml000066400000000000000000000016261506340702200217400ustar00rootroot00000000000000name: Test on: [push, pull_request] permissions: contents: read jobs: test: name: ${{matrix.ruby}} on ${{matrix.os}} runs-on: ${{matrix.os}}-latest continue-on-error: ${{matrix.experimental}} strategy: matrix: os: - ubuntu - macos ruby: - "3.2" - "3.3" - "3.4" experimental: [false] include: - os: ubuntu ruby: truffleruby experimental: true - os: ubuntu ruby: jruby experimental: true - os: ubuntu ruby: head experimental: true steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Run tests timeout-minutes: 10 run: bundle exec bake test protocol-http2-0.23.0/.gitignore000066400000000000000000000001071506340702200164610ustar00rootroot00000000000000/agents.md /.context /.bundle /pkg /gems.locked /.covered.db /external protocol-http2-0.23.0/.rubocop.yml000066400000000000000000000022551506340702200167510ustar00rootroot00000000000000plugins: - rubocop-socketry AllCops: DisabledByDefault: true Layout/ConsistentBlankLineIndentation: Enabled: true Layout/IndentationStyle: Enabled: true EnforcedStyle: tabs Layout/InitialIndentation: Enabled: true Layout/IndentationWidth: Enabled: true Width: 1 Layout/IndentationConsistency: Enabled: true EnforcedStyle: normal Layout/BlockAlignment: Enabled: true Layout/EndAlignment: Enabled: true EnforcedStyleAlignWith: start_of_line Layout/BeginEndAlignment: Enabled: true EnforcedStyleAlignWith: start_of_line Layout/ElseAlignment: Enabled: true Layout/DefEndAlignment: Enabled: true Layout/CaseIndentation: Enabled: true Layout/CommentIndentation: Enabled: true Layout/EmptyLinesAroundClassBody: Enabled: true Layout/EmptyLinesAroundModuleBody: Enabled: true Layout/EmptyLineAfterMagicComment: Enabled: true Layout/SpaceInsideBlockBraces: Enabled: true EnforcedStyle: no_space SpaceBeforeBlockParameters: false Layout/SpaceAroundBlockParameters: Enabled: true EnforcedStyleInsidePipes: no_space Style/FrozenStringLiteralComment: Enabled: true Style/StringLiterals: Enabled: true EnforcedStyle: double_quotes protocol-http2-0.23.0/bake.rb000066400000000000000000000005471506340702200157300ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2025, by Samuel Williams. # Update the project documentation with the new version number. # # @parameter version [String] The new version number. def after_gem_release_version_increment(version) context["releases:update"].call(version) context["utopia:project:update"].call end protocol-http2-0.23.0/config/000077500000000000000000000000001506340702200157405ustar00rootroot00000000000000protocol-http2-0.23.0/config/external.yaml000066400000000000000000000002521506340702200204450ustar00rootroot00000000000000async-http: url: https://github.com/socketry/async-http.git command: bundle exec sus falcon: url: https://github.com/socketry/falcon.git command: bundle exec sus protocol-http2-0.23.0/config/sus.rb000066400000000000000000000007241506340702200171020ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2024, by Samuel Williams. require "covered/sus" include Covered::Sus ENV["TRACES_BACKEND"] ||= "traces/backend/test" ENV["METRICS_BACKEND"] ||= "metrics/backend/test" def prepare_instrumentation! require "traces" require "metrics" # Enable coverage of all tracing: Traces.trace_context = Traces::Context.local end def before_tests(...) prepare_instrumentation! super end protocol-http2-0.23.0/config/traces.rb000066400000000000000000000002431506340702200175450ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2024, by Samuel Williams. def prepare require "traces/provider/protocol/http2" end protocol-http2-0.23.0/context/000077500000000000000000000000001506340702200161575ustar00rootroot00000000000000protocol-http2-0.23.0/context/getting-started.md000066400000000000000000000034361506340702200216140ustar00rootroot00000000000000# Getting Started This guide explains how to use the `protocol-http2` gem to implement a basic HTTP/2 client. ## Installation Add the gem to your project: ``` bash $ bundle add protocol-http2 ``` ## Usage This gem provides a low-level implementation of the HTTP/2 protocol. It is designed to be used in conjunction with other libraries to provide a complete HTTP/2 client or server. However, it is straight forward to give examples of how to use the library directly. ### Client Here is a basic HTTP/2 client: ``` ruby require 'async' require 'async/io/stream' require 'async/http/endpoint' require 'protocol/http2/client' Async do endpoint = Async::HTTP::Endpoint.parse("https://www.google.com/search?q=kittens") peer = endpoint.connect puts "Connected to #{peer.inspect}" # IO Buffering: stream = Async::IO::Stream.new(peer) framer = Protocol::HTTP2::Framer.new(stream) client = Protocol::HTTP2::Client.new(framer) puts "Sending connection preface..." client.send_connection_preface puts "Creating stream..." stream = client.create_stream headers = [ [":scheme", endpoint.scheme], [":method", "GET"], [":authority", "www.google.com"], [":path", endpoint.path], ["accept", "*/*"], ] puts "Sending request on stream id=#{stream.id} state=#{stream.state}..." stream.send_headers(headers, Protocol::HTTP2::END_STREAM) puts "Waiting for response..." $count = 0 def stream.process_headers(frame) headers = super puts "Got response headers: #{headers} (#{frame.end_stream?})" end def stream.receive_data(frame) data = super $count += data.scan(/kittens/).count puts "Got response data: #{data.bytesize}" end until stream.closed? frame = client.read_frame end puts "Got #{$count} kittens!" puts "Closing client..." client.close end ``` protocol-http2-0.23.0/context/index.yaml000066400000000000000000000011041506340702200201460ustar00rootroot00000000000000# Automatically generated context index for Utopia::Project guides. # Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`. --- description: A low level implementation of the HTTP/2 protocol. metadata: documentation_uri: https://socketry.github.io/protocol-http2/ source_code_uri: https://github.com/socketry/protocol-http2.git files: - path: getting-started.md title: Getting Started description: This guide explains how to use the `protocol-http2` gem to implement a basic HTTP/2 client. protocol-http2-0.23.0/examples/000077500000000000000000000000001506340702200163115ustar00rootroot00000000000000protocol-http2-0.23.0/examples/http2/000077500000000000000000000000001506340702200173525ustar00rootroot00000000000000protocol-http2-0.23.0/examples/http2/request.rb000066400000000000000000000027121506340702200213710ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. $LOAD_PATH.unshift File.expand_path("../../lib", __dir__) require "async" require "async/io/stream" require "async/http/endpoint" require "protocol/http2/client" Async do endpoint = Async::HTTP::Endpoint.parse("https://www.google.com/search?q=kittens") peer = endpoint.connect puts "Connected to #{peer.inspect}" # IO Buffering... stream = Async::IO::Stream.new(peer) framer = Protocol::HTTP2::Framer.new(stream) client = Protocol::HTTP2::Client.new(framer) puts "Sending connection preface..." client.send_connection_preface puts "Creating stream..." stream = client.create_stream headers = [ [":scheme", endpoint.scheme], [":method", "GET"], [":authority", "www.google.com"], [":path", endpoint.path], ["accept", "*/*"], ] puts "Sending request on stream id=#{stream.id} state=#{stream.state}..." stream.send_headers(headers, Protocol::HTTP2::END_STREAM) puts "Waiting for response..." $count = 0 def stream.process_headers(frame) headers = super puts "Got response headers: #{headers} (#{frame.end_stream?})" end def stream.process_data(frame) if data = super $count += data.scan(/kittens/).size puts "Got response data: #{data.bytesize}" end end until stream.closed? frame = client.read_frame end puts "Got #{$count} kittens!" puts "Closing client..." client.close end puts "Exiting." protocol-http2-0.23.0/examples/http2/requests.rb000066400000000000000000000027241506340702200215570ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. $LOAD_PATH.unshift File.expand_path("../../lib", __dir__) require "async" require "async/io/stream" require "async/http/endpoint" require "protocol/http2/client" queries = ["apple", "orange", "teapot", "async"] Async do endpoint = Async::HTTP::Endpoint.parse("https://www.google.com") peer = endpoint.connect stream = Async::IO::Stream.new(peer) framer = Protocol::HTTP2::Framer.new(stream) client = Protocol::HTTP2::Client.new(framer) puts "Sending connection preface..." client.send_connection_preface puts "Creating stream..." streams = queries.collect do |keyword| client.create_stream.tap do |stream| headers = [ [":scheme", endpoint.scheme], [":method", "GET"], [":authority", "www.google.com"], [":path", "/search?q=#{keyword}"], ["accept", "*/*"], ] puts "Sending request on stream id=#{stream.id} state=#{stream.state}..." stream.send_headers(headers, Protocol::HTTP2::END_STREAM) def stream.process_headers(frame) headers = super puts "Stream #{self.id}: Got response headers: #{headers} (#{frame.end_stream?})" end def stream.receive_data(frame) data = super puts "Stream #{self.id}: Got response data: #{data.bytesize}" end end end until streams.all?{|stream| stream.closed?} frame = client.read_frame end puts "Closing client..." client.close end puts "Exiting." protocol-http2-0.23.0/fixtures/000077500000000000000000000000001506340702200163445ustar00rootroot00000000000000protocol-http2-0.23.0/fixtures/protocol/000077500000000000000000000000001506340702200202055ustar00rootroot00000000000000protocol-http2-0.23.0/fixtures/protocol/http2/000077500000000000000000000000001506340702200212465ustar00rootroot00000000000000protocol-http2-0.23.0/fixtures/protocol/http2/a_frame.rb000066400000000000000000000012121506340702200231610ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. require "socket" require "protocol/http2/framer" module Protocol module HTTP2 AFrame = Sus::Shared("a frame") do let(:pipe) {Socket.pair(:UNIX, :STREAM)} let(:remote) {pipe[0]} let(:stream) {pipe[1]} let(:framer) {Protocol::HTTP2::Framer.new(stream, {subject::TYPE => subject})} let(:frame) {subject.new} it "is a valid frame type" do expect(frame).to be(:valid_type?) end it "can write the frame" do frame.write(remote) expect(framer.read_frame).to be == frame end end end end protocol-http2-0.23.0/fixtures/protocol/http2/connection_context.rb000066400000000000000000000010771506340702200255030ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. require "protocol/http2/client" require "protocol/http2/server" require "protocol/http2/stream" require "socket" module Protocol module HTTP2 ConnectionContext = Sus::Shared("a connection") do let(:sockets) {Socket.pair(Socket::PF_UNIX, Socket::SOCK_STREAM)} let(:client) {Protocol::HTTP2::Client.new(Protocol::HTTP2::Framer.new(sockets.first))} let(:server) {Protocol::HTTP2::Server.new(Protocol::HTTP2::Framer.new(sockets.last))} end end end protocol-http2-0.23.0/fuzz/000077500000000000000000000000001506340702200154715ustar00rootroot00000000000000protocol-http2-0.23.0/fuzz/framer/000077500000000000000000000000001506340702200167455ustar00rootroot00000000000000protocol-http2-0.23.0/fuzz/framer/bake.rb000066400000000000000000000006771506340702200202060ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2020-2024, by Samuel Williams. # Run the fuzz test. def run system("AFL_SKIP_BIN_CHECK=1 afl-fuzz -i input/ -o output/ -m 100 -- ruby script.rb") end def generate require_relative "../../lib/protocol/http2/framer" framer = Protocol::HTTP2::Framer.new($stdout) frame = Protocol::HTTP2::DataFrame.new frame.pack("Hello World") framer.write_frame(frame) end protocol-http2-0.23.0/fuzz/framer/input/000077500000000000000000000000001506340702200201045ustar00rootroot00000000000000protocol-http2-0.23.0/fuzz/framer/input/data.txt000066400000000000000000000000241506340702200215520ustar00rootroot00000000000000 Hello Worldprotocol-http2-0.23.0/fuzz/framer/script.rb000077500000000000000000000010631506340702200206010ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # Released under the MIT License. # Copyright, 2020-2024, by Samuel Williams. require "socket" require_relative "../../lib/protocol/http2/framer" def test framer = Protocol::HTTP2::Framer.new($stdin) while frame = framer.read_frame pp frame end rescue EOFError # Ignore. end if ENV["_"] =~ /afl/ require "kisaten" Kisaten.crash_at [Exception], [EOFError, Protocol::HTTP2::FrameSizeError, Protocol::HTTP2::ProtocolError], Signal.list["USR1"] while Kisaten.loop 10_000 test end else test end protocol-http2-0.23.0/gems.rb000066400000000000000000000007341506340702200157570ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. source "https://rubygems.org" gemspec group :maintenance, optional: true do gem "bake-modernize" gem "bake-gem" gem "bake-releases" gem "agent-context" gem "utopia-project" end group "test" do gem "sus" gem "covered" gem "decode" gem "rubocop" gem "rubocop-socketry" gem "traces" gem "metrics" gem "bake-test" gem "bake-test-external" end protocol-http2-0.23.0/guides/000077500000000000000000000000001506340702200157535ustar00rootroot00000000000000protocol-http2-0.23.0/guides/getting-started/000077500000000000000000000000001506340702200210605ustar00rootroot00000000000000protocol-http2-0.23.0/guides/getting-started/readme.md000066400000000000000000000034361506340702200226450ustar00rootroot00000000000000# Getting Started This guide explains how to use the `protocol-http2` gem to implement a basic HTTP/2 client. ## Installation Add the gem to your project: ``` bash $ bundle add protocol-http2 ``` ## Usage This gem provides a low-level implementation of the HTTP/2 protocol. It is designed to be used in conjunction with other libraries to provide a complete HTTP/2 client or server. However, it is straight forward to give examples of how to use the library directly. ### Client Here is a basic HTTP/2 client: ``` ruby require 'async' require 'async/io/stream' require 'async/http/endpoint' require 'protocol/http2/client' Async do endpoint = Async::HTTP::Endpoint.parse("https://www.google.com/search?q=kittens") peer = endpoint.connect puts "Connected to #{peer.inspect}" # IO Buffering: stream = Async::IO::Stream.new(peer) framer = Protocol::HTTP2::Framer.new(stream) client = Protocol::HTTP2::Client.new(framer) puts "Sending connection preface..." client.send_connection_preface puts "Creating stream..." stream = client.create_stream headers = [ [":scheme", endpoint.scheme], [":method", "GET"], [":authority", "www.google.com"], [":path", endpoint.path], ["accept", "*/*"], ] puts "Sending request on stream id=#{stream.id} state=#{stream.state}..." stream.send_headers(headers, Protocol::HTTP2::END_STREAM) puts "Waiting for response..." $count = 0 def stream.process_headers(frame) headers = super puts "Got response headers: #{headers} (#{frame.end_stream?})" end def stream.receive_data(frame) data = super $count += data.scan(/kittens/).count puts "Got response data: #{data.bytesize}" end until stream.closed? frame = client.read_frame end puts "Got #{$count} kittens!" puts "Closing client..." client.close end ``` protocol-http2-0.23.0/guides/links.yaml000066400000000000000000000000341506340702200177540ustar00rootroot00000000000000getting-started: order: 1 protocol-http2-0.23.0/lib/000077500000000000000000000000001506340702200152415ustar00rootroot00000000000000protocol-http2-0.23.0/lib/protocol/000077500000000000000000000000001506340702200171025ustar00rootroot00000000000000protocol-http2-0.23.0/lib/protocol/http2.rb000066400000000000000000000002631506340702200204710ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. require_relative "http2/version" require_relative "http2/connection" protocol-http2-0.23.0/lib/protocol/http2/000077500000000000000000000000001506340702200201435ustar00rootroot00000000000000protocol-http2-0.23.0/lib/protocol/http2/client.rb000066400000000000000000000061371506340702200217550ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. require_relative "connection" module Protocol module HTTP2 # Represents an HTTP/2 client connection. # Manages client-side protocol semantics including stream ID allocation, # connection preface handling, and push promise processing. class Client < Connection # Initialize a new HTTP/2 client connection. # @parameter framer [Framer] The frame handler for reading/writing HTTP/2 frames. def initialize(framer) super(framer, 1) end # Check if the given stream ID represents a locally-initiated stream. # Client streams have odd numbered IDs. # @parameter id [Integer] The stream ID to check. # @returns [bool] True if the stream ID is locally-initiated. def local_stream_id?(id) id.odd? end # Check if the given stream ID represents a remotely-initiated stream. # Server streams have even numbered IDs. # @parameter id [Integer] The stream ID to check. # @returns [bool] True if the stream ID is remotely-initiated. def remote_stream_id?(id) id.even? end # Check if the given stream ID is valid for remote initiation. # Server-initiated streams must have even numbered IDs. # @parameter stream_id [Integer] The stream ID to validate. # @returns [bool] True if the stream ID is valid for remote initiation. def valid_remote_stream_id?(stream_id) stream_id.even? end # Send the HTTP/2 connection preface and initial settings. # This must be called once when the connection is first established. # @parameter settings [Array] Optional settings to send with the connection preface. # @raises [ProtocolError] If called when not in the new state. # @yields Allows custom processing during preface exchange. def send_connection_preface(settings = []) if @state == :new @framer.write_connection_preface send_settings(settings) yield if block_given? read_frame do |frame| unless frame.is_a? SettingsFrame raise ProtocolError, "First frame must be #{SettingsFrame}, but got #{frame.class}" end end else raise ProtocolError, "Cannot send connection preface in state #{@state}" end end # Clients cannot create push promise streams. # @raises [ProtocolError] Always, as clients cannot initiate push promises. def create_push_promise_stream raise ProtocolError, "Cannot create push promises from client!" end # Process a push promise frame received from the server. # @parameter frame [PushPromiseFrame] The push promise frame to process. # @returns [Array(Stream, Hash) | Nil] The promised stream and request headers, or nil if no associated stream. def receive_push_promise(frame) if frame.stream_id == 0 raise ProtocolError, "Cannot receive headers for stream 0!" end if stream = @streams[frame.stream_id] # This is almost certainly invalid: promised_stream, request_headers = stream.receive_push_promise(frame) return promised_stream, request_headers end end end end end protocol-http2-0.23.0/lib/protocol/http2/connection.rb000066400000000000000000000475221506340702200226410ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. # Copyright, 2023, by Marco Concetto Rudilosso. require_relative "framer" require_relative "flow_controlled" require "protocol/hpack" require "protocol/http/header/priority" module Protocol module HTTP2 # This is the core connection class that handles HTTP/2 protocol semantics including # stream management, settings negotiation, and frame processing. class Connection include FlowControlled # Initialize a new HTTP/2 connection. # @parameter framer [Framer] The frame handler for reading/writing HTTP/2 frames. # @parameter local_stream_id [Integer] The starting stream ID for locally-initiated streams. def initialize(framer, local_stream_id) super() @state = :new # Hash(Integer, Stream) @streams = {} @framer = framer # The next stream id to use: @local_stream_id = local_stream_id # The biggest remote stream id seen thus far: @remote_stream_id = 0 @local_settings = PendingSettings.new @remote_settings = Settings.new @decoder = HPACK::Context.new @encoder = HPACK::Context.new @local_window = LocalWindow.new @remote_window = Window.new end # The connection stream ID (always 0 for connection-level operations). # @returns [Integer] Always returns 0 for the connection itself. def id 0 end # Access streams by ID, with 0 returning the connection itself. # @parameter id [Integer] The stream ID to look up. # @returns [Connection | Stream | Nil] The connection (if id=0), stream, or nil. def [] id if id.zero? self else @streams[id] end end # The size of a frame payload is limited by the maximum size that a receiver advertises in the SETTINGS_MAX_FRAME_SIZE setting. def maximum_frame_size @remote_settings.maximum_frame_size end # The maximum number of concurrent streams that this connection can initiate. This is a setting that can be changed by the remote peer. # # It is not the same as the number of streams that can be accepted by the connection. The number of streams that can be accepted is determined by the local settings, and the number of streams that can be initiated is determined by the remote settings. def maximum_concurrent_streams @remote_settings.maximum_concurrent_streams end attr :framer # Connection state (:new, :open, :closed). attr_accessor :state # Current settings value for local and peer attr_accessor :local_settings attr_accessor :remote_settings # Our window for receiving data. When we receive data, it reduces this window. # If the window gets too small, we must send a window update. attr :local_window # Our window for sending data. When we send data, it reduces this window. attr :remote_window # The highest stream_id that has been successfully accepted by this connection. attr :remote_stream_id # Whether the connection is effectively or actually closed. def closed? @state == :closed || @framer.nil? end # Remove a stream from the active streams collection. # @parameter id [Integer] The stream ID to remove. # @returns [Stream | Nil] The removed stream, or nil if not found. def delete(id) @streams.delete(id) end # Close the underlying framer and all streams. def close(error = nil) # The underlying socket may already be closed by this point. @streams.each_value{|stream| stream.close(error)} @streams.clear ensure if @framer @framer.close @framer = nil end end # Encode headers using HPACK compression. # @parameter headers [Array] The headers to encode. # @parameter buffer [String] Optional buffer for encoding output. # @returns [String] The encoded header block. def encode_headers(headers, buffer = String.new.b) HPACK::Compressor.new(buffer, @encoder, table_size_limit: @remote_settings.header_table_size).encode(headers) end # Decode headers using HPACK decompression. # @parameter data [String] The encoded header block data. # @returns [Array] The decoded headers. def decode_headers(data) HPACK::Decompressor.new(data, @decoder, table_size_limit: @local_settings.header_table_size).decode end # Streams are identified with an unsigned 31-bit integer. Streams initiated by a client MUST use odd-numbered stream identifiers; those initiated by the server MUST use even-numbered stream identifiers. A stream identifier of zero (0x0) is used for connection control messages; the stream identifier of zero cannot be used to establish a new stream. def next_stream_id id = @local_stream_id @local_stream_id += 2 return id end attr :streams attr :dependencies attr :dependency # 6.8. GOAWAY # There is an inherent race condition between an endpoint starting new streams and the remote sending a GOAWAY frame. To deal with this case, the GOAWAY contains the stream identifier of the last peer-initiated stream that was or might be processed on the sending endpoint in this connection. For instance, if the server sends a GOAWAY frame, the identified stream is the highest-numbered stream initiated by the client. # Once sent, the sender will ignore frames sent on streams initiated by the receiver if the stream has an identifier higher than the included last stream identifier. Receivers of a GOAWAY frame MUST NOT open additional streams on the connection, although a new connection can be established for new streams. def ignore_frame?(frame) if self.closed? # puts "ignore_frame? #{frame.stream_id} -> #{valid_remote_stream_id?(frame.stream_id)} > #{@remote_stream_id}" if valid_remote_stream_id?(frame.stream_id) return frame.stream_id > @remote_stream_id end end end # Execute a block within a synchronized context. # This method provides a synchronization primitive for thread safety. # @yields The block to execute within the synchronized context. def synchronize yield end # Reads one frame from the network and processes. Processing the frame updates the state of the connection and related streams. If the frame triggers an error, e.g. a protocol error, the connection will typically emit a goaway frame and re-raise the exception. You should continue processing frames until the underlying connection is closed. def read_frame frame = @framer.read_frame(@local_settings.maximum_frame_size) # puts "#{self.class} #{@state} read_frame: class=#{frame.class} stream_id=#{frame.stream_id} flags=#{frame.flags} length=#{frame.length} (remote_stream_id=#{@remote_stream_id})" # puts "Windows: local_window=#{@local_window.inspect}; remote_window=#{@remote_window.inspect}" return if ignore_frame?(frame) yield frame if block_given? frame.apply(self) return frame rescue GoawayError => error # Go directly to jail. Do not pass go, do not collect $200. raise rescue ProtocolError => error send_goaway(error.code || PROTOCOL_ERROR, error.message) raise rescue HPACK::Error => error send_goaway(COMPRESSION_ERROR, error.message) raise end # Send updated settings to the remote peer. # @parameter changes [Hash] The settings changes to send. def send_settings(changes) @local_settings.append(changes) frame = SettingsFrame.new frame.pack(changes) write_frame(frame) end # Transition the connection into the closed state. def close! @state = :closed return self end # Tell the remote end that the connection is being shut down. If the `error_code` is 0, this is a graceful shutdown. The other end of the connection should not make any new streams, but existing streams may be completed. def send_goaway(error_code = 0, message = "") frame = GoawayFrame.new frame.pack @remote_stream_id, error_code, message write_frame(frame) ensure self.close! end # Process a GOAWAY frame from the remote peer. # @parameter frame [GoawayFrame] The GOAWAY frame to process. # @raises [GoawayError] If the frame indicates a connection error. def receive_goaway(frame) # We capture the last stream that was processed. @remote_stream_id, error_code, message = frame.unpack self.close! if error_code != 0 # Shut down immediately. raise GoawayError.new(message, error_code) end end # Write a single frame to the connection. # @parameter frame [Frame] The frame to write. def write_frame(frame) synchronize do @framer.write_frame(frame) end @framer.flush end # Write multiple frames within a synchronized block. # @yields {|framer| ...} The framer for writing multiple frames. # @parameter framer [Framer] The framer instance. # @raises [EOFError] If the connection is closed. def write_frames if @framer synchronize do yield @framer end @framer.flush else raise EOFError, "Connection closed!" end end # Update local settings and adjust stream window capacities. # @parameter changes [Hash] The settings changes to apply locally. def update_local_settings(changes) capacity = @local_settings.initial_window_size @streams.each_value do |stream| stream.local_window.capacity = capacity end @local_window.desired = capacity end # Update remote settings and adjust stream window capacities. # @parameter changes [Hash] The settings changes to apply to remote peer. def update_remote_settings(changes) capacity = @remote_settings.initial_window_size @streams.each_value do |stream| stream.remote_window.capacity = capacity end end # In addition to changing the flow-control window for streams that are not yet active, a SETTINGS frame can alter the initial flow-control window size for streams with active flow-control windows (that is, streams in the "open" or "half-closed (remote)" state). When the value of SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST adjust the size of all stream flow-control windows that it maintains by the difference between the new value and the old value. # # @return [Boolean] whether the frame was an acknowledgement def process_settings(frame) if frame.acknowledgement? # The remote end has confirmed the settings have been received: changes = @local_settings.acknowledge update_local_settings(changes) return true else # The remote end is updating the settings, we reply with acknowledgement: reply = frame.acknowledge write_frame(reply) changes = frame.unpack @remote_settings.update(changes) update_remote_settings(changes) return false end end # Transition the connection to the open state. # @returns [Connection] Self for method chaining. def open! @state = :open return self end # Receive and process a SETTINGS frame from the remote peer. # @parameter frame [SettingsFrame] The settings frame to process. # @raises [ProtocolError] If the connection is in an invalid state. def receive_settings(frame) if @state == :new # We transition to :open when we receive acknowledgement of first settings frame: open! if process_settings(frame) elsif @state != :closed process_settings(frame) else raise ProtocolError, "Cannot receive settings in state #{@state}" end end # Send a PING frame to the remote peer. # @parameter data [String] The 8-byte ping payload data. def send_ping(data) if @state != :closed frame = PingFrame.new frame.pack data write_frame(frame) else raise ProtocolError, "Cannot send ping in state #{@state}" end end # Process a PING frame from the remote peer. # @parameter frame [PingFrame] The ping frame to process. # @raises [ProtocolError] If ping is received in invalid state. def receive_ping(frame) if @state != :closed # This is handled in `read_payload`: # if frame.stream_id != 0 # raise ProtocolError, "Ping received for non-zero stream!" # end unless frame.acknowledgement? reply = frame.acknowledge write_frame(reply) end else raise ProtocolError, "Cannot receive ping in state #{@state}" end end # Process a DATA frame from the remote peer. # @parameter frame [DataFrame] The data frame to process. # @raises [ProtocolError] If data is received for invalid stream. def receive_data(frame) update_local_window(frame) if stream = @streams[frame.stream_id] stream.receive_data(frame) elsif closed_stream_id?(frame.stream_id) # This can occur if one end sent a stream reset, while the other end was sending a data frame. It's mostly harmless. else raise ProtocolError, "Cannot receive data for stream id #{frame.stream_id}" end end # Check if the given stream ID is valid for remote initiation. # This method should be overridden by client/server implementations. # @parameter stream_id [Integer] The stream ID to validate. # @returns [Boolean] True if the stream ID is valid for remote initiation. def valid_remote_stream_id?(stream_id) false end # Accept an incoming stream from the other side of the connnection. # On the server side, we accept requests. def accept_stream(stream_id, &block) unless valid_remote_stream_id?(stream_id) raise ProtocolError, "Invalid stream id: #{stream_id}" end create_stream(stream_id, &block) end # Accept an incoming push promise from the other side of the connection. # On the client side, we accept push promise streams. # On the server side, existing streams create push promise streams. def accept_push_promise_stream(stream_id, &block) accept_stream(stream_id, &block) end # Create a stream, defaults to an outgoing stream. # On the client side, we create requests. # @return [Stream] the created stream. def create_stream(id = next_stream_id, &block) if @streams.key?(id) raise ProtocolError, "Cannot create stream with id #{id}, already exists!" end if block_given? return yield(self, id) else return Stream.create(self, id) end end # Create a push promise stream. # This method should be overridden by client/server implementations. # @yields {|stream| ...} Optional block to configure the created stream. # @returns [Stream] The created push promise stream. def create_push_promise_stream(&block) create_stream(&block) end # On the server side, starts a new request. def receive_headers(frame) stream_id = frame.stream_id if stream_id.zero? raise ProtocolError, "Cannot receive headers for stream 0!" end if stream = @streams[stream_id] stream.receive_headers(frame) else if stream_id <= @remote_stream_id raise ProtocolError, "Invalid stream id: #{stream_id} <= #{@remote_stream_id}!" end # We need to validate that we have less streams than the specified maximum: if @streams.size < @local_settings.maximum_concurrent_streams stream = accept_stream(stream_id) @remote_stream_id = stream_id stream.receive_headers(frame) else raise ProtocolError, "Exceeded maximum concurrent streams" end end end # Receive and process a PUSH_PROMISE frame. # @parameter frame [PushPromiseFrame] The push promise frame. # @raises [ProtocolError] Always raises as push promises are not supported. def receive_push_promise(frame) raise ProtocolError, "Unable to receive push promise!" end # Receive and process a PRIORITY_UPDATE frame. # @parameter frame [PriorityUpdateFrame] The priority update frame. # @raises [ProtocolError] If the stream ID is invalid. def receive_priority_update(frame) if frame.stream_id != 0 raise ProtocolError, "Invalid stream id: #{frame.stream_id}" end stream_id, value = frame.unpack # Apparently you can set the priority of idle streams, but I'm not sure why that makes sense, so for now let's ignore it. if stream = @streams[stream_id] stream.priority = Protocol::HTTP::Header::Priority.new(value) end end # Check if the given stream ID represents a client-initiated stream. # Client streams always have odd numbered IDs. # @parameter id [Integer] The stream ID to check. # @returns [Boolean] True if the stream ID is client-initiated. def client_stream_id?(id) id.odd? end # Check if the given stream ID represents a server-initiated stream. # Server streams always have even numbered IDs. # @parameter id [Integer] The stream ID to check. # @returns [Boolean] True if the stream ID is server-initiated. def server_stream_id?(id) id.even? end # Check if the given stream ID represents an idle stream. # @parameter id [Integer] The stream ID to check. # @returns [Boolean] True if the stream ID is idle (not yet used). def idle_stream_id?(id) if id.even? # Server-initiated streams are even. if @local_stream_id.even? id >= @local_stream_id else id > @remote_stream_id end elsif id.odd? # Client-initiated streams are odd. if @local_stream_id.odd? id >= @local_stream_id else id > @remote_stream_id end end end # This is only valid if the stream doesn't exist in `@streams`. def closed_stream_id?(id) if id.zero? # The connection "stream id" can never be closed: false else !idle_stream_id?(id) end end # Receive and process a RST_STREAM frame. # @parameter frame [ResetStreamFrame] The reset stream frame. # @raises [ProtocolError] If the frame is invalid for connection context. def receive_reset_stream(frame) if frame.connection? raise ProtocolError, "Cannot reset connection!" elsif stream = @streams[frame.stream_id] stream.receive_reset_stream(frame) elsif closed_stream_id?(frame.stream_id) # Ignore. else raise StreamClosed, "Cannot reset stream #{frame.stream_id}" end end # Traverse active streams and allow them to consume the available flow-control window. # @parameter amount [Integer] the amount of data to write. Defaults to the current window capacity. def consume_window(size = self.available_size) # Return if there is no window to consume: return unless size > 0 @streams.each_value do |stream| if stream.active? stream.window_updated(size) end end end # Receive and process a WINDOW_UPDATE frame. # @parameter frame [WindowUpdateFrame] The window update frame. def receive_window_update(frame) if frame.connection? super self.consume_window elsif stream = @streams[frame.stream_id] begin stream.receive_window_update(frame) rescue ProtocolError => error stream.send_reset_stream(error.code) end elsif closed_stream_id?(frame.stream_id) # Ignore. else # Receiving any frame other than HEADERS or PRIORITY on a stream in this state (idle) MUST be treated as a connection error of type PROTOCOL_ERROR. raise ProtocolError, "Cannot update window of idle stream #{frame.stream_id}" end end # Receive and process a CONTINUATION frame. # @parameter frame [ContinuationFrame] The continuation frame. # @raises [ProtocolError] Always raises as unexpected continuation frames are not supported. def receive_continuation(frame) raise ProtocolError, "Received unexpected continuation: #{frame.class}" end # Receive and process a generic frame (default handler). # @parameter frame [Frame] The frame to receive. def receive_frame(frame) # ignore. end end end end protocol-http2-0.23.0/lib/protocol/http2/continuation_frame.rb000066400000000000000000000111641506340702200243570ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. require_relative "frame" module Protocol module HTTP2 # Module for frames that can be continued with CONTINUATION frames. module Continued # @constant [Integer] The maximum number of continuation frames to read to prevent resource exhaustion. LIMIT = 8 # Initialize a continuable frame. # @parameter arguments [Array] Arguments passed to parent constructor. def initialize(*) super @continuation = nil end # Check if this frame has continuation frames. # @returns [Boolean] True if there are continuation frames. def continued? !!@continuation end # Check if this is the last header block fragment. # @returns [Boolean] True if the END_HEADERS flag is set. def end_headers? flag_set?(END_HEADERS) end # Read the frame and any continuation frames from the stream. # # There is an upper limit to the number of continuation frames that can be read to prevent resource exhaustion. If the limit is 0, only one frame will be read (the initial frame). Otherwise, the limit decrements with each continuation frame read. # # @parameter stream [IO] The stream to read from. # @parameter maximum_frame_size [Integer] Maximum allowed frame size. # @parameter limit [Integer] The maximum number of continuation frames to read. def read(stream, maximum_frame_size, limit = LIMIT) super(stream, maximum_frame_size) unless end_headers? if limit.zero? raise ProtocolError, "Too many continuation frames!" end continuation = ContinuationFrame.new continuation.read_header(stream) # We validate the frame type here: unless continuation.valid_type? raise ProtocolError, "Invalid frame type: #{@type}!" end if continuation.stream_id != @stream_id raise ProtocolError, "Invalid stream id: #{continuation.stream_id} for continuation of stream id: #{@stream_id}!" end continuation.read(stream, maximum_frame_size, limit - 1) @continuation = continuation end end # Write the frame and any continuation frames to the stream. # @parameter stream [IO] The stream to write to. def write(stream) super if continuation = self.continuation continuation.write(stream) end end attr_accessor :continuation # Pack data into this frame, creating continuation frames if needed. # @parameter data [String] The data to pack. # @parameter options [Hash] Options including maximum_size. def pack(data, **options) maximum_size = options[:maximum_size] if maximum_size and data.bytesize > maximum_size clear_flags(END_HEADERS) super(data.byteslice(0, maximum_size), **options) remainder = data.byteslice(maximum_size, data.bytesize-maximum_size) @continuation = ContinuationFrame.new @continuation.pack(remainder, maximum_size: maximum_size) else set_flags(END_HEADERS) super data, **options @continuation = nil end end # Unpack data from this frame and any continuation frames. # @returns [String] The complete unpacked data. def unpack if @continuation.nil? super else super + @continuation.unpack end end end # The CONTINUATION frame is used to continue a sequence of header block fragments. Any number of CONTINUATION frames can be sent, as long as the preceding frame is on the same stream and is a HEADERS, PUSH_PROMISE, or CONTINUATION frame without the END_HEADERS flag set. # # +---------------------------------------------------------------+ # | Header Block Fragment (*) ... # +---------------------------------------------------------------+ # class ContinuationFrame < Frame include Continued TYPE = 0x9 # Read the frame and any continuation frames from the stream. # @parameter stream [IO] The stream to read from. # @parameter maximum_frame_size [Integer] Maximum allowed frame size. # @parameter limit [Integer] The maximum number of continuation frames to read. def read(stream, maximum_frame_size, limit = 8) super end # This is only invoked if the continuation is received out of the normal flow. def apply(connection) connection.receive_continuation(self) end # Get a string representation of the continuation frame. # @returns [String] Human-readable frame information. def inspect "\#<#{self.class} stream_id=#{@stream_id} flags=#{@flags} length=#{@length || 0}b>" end end end end protocol-http2-0.23.0/lib/protocol/http2/data_frame.rb000066400000000000000000000037301506340702200225560ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. require_relative "frame" require_relative "padded" module Protocol module HTTP2 # DATA frames convey arbitrary, variable-length sequences of octets associated with a stream. One or more DATA frames are used, for instance, to carry HTTP request or response payloads. # # DATA frames MAY also contain padding. Padding can be added to DATA frames to obscure the size of messages. # # +---------------+ # |Pad Length? (8)| # +---------------+-----------------------------------------------+ # | Data (*) ... # +---------------------------------------------------------------+ # | Padding (*) ... # +---------------------------------------------------------------+ # class DataFrame < Frame include Padded TYPE = 0x0 # Check if this frame marks the end of the stream. # @returns [Boolean] True if the END_STREAM flag is set. def end_stream? flag_set?(END_STREAM) end # Pack data into the frame, handling empty data as stream end. # @parameter data [String | Nil] The data to pack into the frame. # @parameter arguments [Array] Additional arguments passed to super. # @parameter options [Hash] Additional options passed to super. def pack(data, *arguments, **options) if data super else @length = 0 set_flags(END_STREAM) end end # Apply this DATA frame to a connection for processing. # @parameter connection [Connection] The connection to apply the frame to. def apply(connection) connection.receive_data(self) end # Provide a readable representation of the frame for debugging. # @returns [String] A formatted string representation of the frame. def inspect "\#<#{self.class} stream_id=#{@stream_id} flags=#{@flags} #{@length || 0}b>" end end end end protocol-http2-0.23.0/lib/protocol/http2/error.rb000066400000000000000000000074611506340702200216310ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. require "protocol/http/error" module Protocol module HTTP2 # Status codes as defined by . class Error < HTTP::Error # The associated condition is not a result of an error. For example, a GOAWAY might include this code to indicate graceful shutdown of a connection. NO_ERROR = 0x0 # The endpoint detected an unspecific protocol error. This error is for use when a more specific error code is not available. PROTOCOL_ERROR = 0x1 # The endpoint encountered an unexpected internal error. INTERNAL_ERROR = 0x2 # The endpoint detected that its peer violated the flow-control protocol. FLOW_CONTROL_ERROR = 0x3 # The endpoint sent a SETTINGS frame but did not receive a response in a timely manner. SETTINGS_TIMEOUT = 0x4 # The endpoint received a frame after a stream was half-closed. STREAM_CLOSED = 0x5 # The endpoint received a frame with an invalid size. FRAME_SIZE_ERROR = 0x6 # The endpoint refused the stream prior to performing any application processing. REFUSED_STREAM = 0x7 # Used by the endpoint to indicate that the stream is no longer needed. CANCEL = 0x8 # The endpoint is unable to maintain the header compression context for the connection. COMPRESSION_ERROR = 0x9 # The connection established in response to a CONNECT request was reset or abnormally closed. CONNECT_ERROR = 0xA # The endpoint detected that its peer is exhibiting a behavior that might be generating excessive load. ENHANCE_YOUR_CALM = 0xB # The underlying transport has properties that do not meet minimum security requirements. INADEQUATE_SECURITY = 0xC # The endpoint requires that HTTP/1.1 be used instead of HTTP/2. HTTP_1_1_REQUIRED = 0xD end # Raised if connection header is missing or invalid indicating that # this is an invalid HTTP 2.0 request - no frames are emitted and the # connection must be aborted. class HandshakeError < Error end # Raised by stream or connection handlers, results in GOAWAY frame # which signals termination of the current connection. You *cannot* # recover from this exception, or any exceptions subclassed from it. class ProtocolError < Error # Initialize a protocol error with message and error code. # @parameter message [String] The error message. # @parameter code [Integer] The HTTP/2 error code. def initialize(message, code = PROTOCOL_ERROR) super(message) @code = code end attr :code end # Represents an error specific to stream operations. class StreamError < ProtocolError end # Represents an error for operations on closed streams. class StreamClosed < StreamError # Initialize a stream closed error. # @parameter message [String] The error message. def initialize(message) super message, STREAM_CLOSED end end # Represents a GOAWAY-related protocol error. class GoawayError < ProtocolError end # When the frame payload does not match expectations. class FrameSizeError < ProtocolError # Initialize a frame size error. # @parameter message [String] The error message. def initialize(message) super message, FRAME_SIZE_ERROR end end # Represents a header processing error. class HeaderError < StreamClosed # Initialize a header error. # @parameter message [String] The error message. def initialize(message) super(message) end end # Raised on invalid flow control frame or command. class FlowControlError < ProtocolError # Initialize a flow control error. # @parameter message [String] The error message. def initialize(message) super message, FLOW_CONTROL_ERROR end end end end protocol-http2-0.23.0/lib/protocol/http2/flow_controlled.rb000066400000000000000000000064041506340702200236700ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. # Copyright, 2019, by Yuta Iwama. require_relative "window_update_frame" module Protocol module HTTP2 # Provides flow control functionality for HTTP/2 connections and streams. # This module implements window-based flow control as defined in RFC 7540. module FlowControlled # Get the available window size for sending data. # @returns [Integer] The number of bytes that can be sent. def available_size @remote_window.available end # This could be negative if the window has been overused due to a change in initial window size. def available_frame_size(maximum_frame_size = self.maximum_frame_size) available_size = self.available_size # puts "available_size=#{available_size} maximum_frame_size=#{maximum_frame_size}" if available_size < maximum_frame_size return available_size else return maximum_frame_size end end # Keep track of the amount of data sent, and fail if is too much. def consume_remote_window(frame) amount = frame.length # Frames with zero length with the END_STREAM flag set (that is, an empty DATA frame) MAY be sent if there is no available space in either flow-control window. if amount.zero? and frame.end_stream? # It's okay, we can send. No need to consume, it's empty anyway. elsif amount >= 0 and amount <= @remote_window.available @remote_window.consume(amount) else raise FlowControlError, "Trying to send #{frame.length} bytes, exceeded window size: #{@remote_window.available} (#{@remote_window})" end end # Update the local window after receiving data. # @parameter frame [Frame] The frame that was received. def update_local_window(frame) consume_local_window(frame) request_window_update end # Consume local window space for a received frame. # @parameter frame [Frame] The frame that consumed window space. def consume_local_window(frame) # For flow-control calculations, the 9-octet frame header is not counted. amount = frame.length @local_window.consume(amount) end # Request a window update if the local window is limited. def request_window_update if @local_window.limited? self.send_window_update(@local_window.wanted) end end # Notify the remote end that we are prepared to receive more data: def send_window_update(window_increment) frame = WindowUpdateFrame.new(self.id) frame.pack window_increment write_frame(frame) @local_window.expand(window_increment) end # Process a received WINDOW_UPDATE frame. # @parameter frame [WindowUpdateFrame] The window update frame to process. # @raises [ProtocolError] If the window increment is invalid. def receive_window_update(frame) amount = frame.unpack if amount != 0 @remote_window.expand(amount) else raise ProtocolError, "Invalid window size increment: #{amount}!" end end # The window has been expanded by the given amount. # @parameter size [Integer] the maximum amount of data to send. # @return [Boolean] whether the window update was used or not. def window_updated(size) return false end end end end protocol-http2-0.23.0/lib/protocol/http2/frame.rb000066400000000000000000000175551506340702200215770ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. # Copyright, 2019, by Yuta Iwama. require_relative "error" module Protocol module HTTP2 END_STREAM = 0x1 END_HEADERS = 0x4 PADDED = 0x8 PRIORITY = 0x20 MAXIMUM_ALLOWED_WINDOW_SIZE = 0x7FFFFFFF MINIMUM_ALLOWED_FRAME_SIZE = 0x4000 MAXIMUM_ALLOWED_FRAME_SIZE = 0xFFFFFF # Represents the base class for all HTTP/2 frames. # This class provides common functionality for frame parsing, serialization, # and manipulation according to RFC 7540. class Frame include Comparable # Stream Identifier cannot be bigger than this: # https://http2.github.stream/http2-spec/#rfc.section.4.1 VALID_STREAM_ID = 0..0x7fffffff # The absolute maximum bounds for the length field: VALID_LENGTH = 0..0xffffff # Used for generating 24-bit frame length: LENGTH_HISHIFT = 16 LENGTH_LOMASK = 0xFFFF # The base class does not have any specific type index: TYPE = nil # @parameter length [Integer] the length of the payload, or nil if the header has not been read yet. def initialize(stream_id = 0, flags = 0, type = self.class::TYPE, length = nil, payload = nil) @stream_id = stream_id @flags = flags @type = type @length = length @payload = payload end # Check if the frame has a valid type identifier. # @returns [Boolean] True if the frame type is valid. def valid_type? @type == self.class::TYPE end # Compare frames based on their essential properties. # @parameter other [Frame] The frame to compare with. # @returns [Integer] -1, 0, or 1 for comparison result. def <=> other to_ary <=> other.to_ary end # Convert frame to array representation for comparison. # @returns [Array] Frame properties as an array. def to_ary [@length, @type, @flags, @stream_id, @payload] end # The generic frame header uses the following binary representation: # # +-----------------------------------------------+ # | Length (24) | # +---------------+---------------+---------------+ # | Type (8) | Flags (8) | # +-+-------------+---------------+-------------------------------+ # |R| Stream Identifier (31) | # +=+=============================================================+ # | Frame Payload (0...) ... # +---------------------------------------------------------------+ attr_accessor :length attr_accessor :type attr_accessor :flags attr_accessor :stream_id attr_accessor :payload # Unpack the frame payload data. # @returns [String] The frame payload. def unpack @payload end # Pack payload data into the frame. # @parameter payload [String] The payload data to pack. # @parameter maximum_size [Integer | Nil] Optional maximum payload size. # @raises [ProtocolError] If payload exceeds maximum size. def pack(payload, maximum_size: nil) @payload = payload @length = payload.bytesize if maximum_size and @length > maximum_size raise ProtocolError, "Frame length bigger than maximum allowed: #{@length} > #{maximum_size}" end end # Set specific flags on the frame. # @parameter mask [Integer] The flag bits to set. def set_flags(mask) @flags |= mask end # Clear specific flags on the frame. # @parameter mask [Integer] The flag bits to clear. def clear_flags(mask) @flags &= ~mask end # Check if specific flags are set on the frame. # @parameter mask [Integer] The flag bits to check. # @returns [Boolean] True if any of the flags are set. def flag_set?(mask) @flags & mask != 0 end # Check if frame is a connection frame: SETTINGS, PING, GOAWAY, and any # frame addressed to stream ID = 0. # # @return [Boolean] If this is a connection frame. def connection? @stream_id.zero? end HEADER_FORMAT = "CnCCN".freeze STREAM_ID_MASK = 0x7fffffff # Generates common 9-byte frame header. # - http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-4.1 # # @return [String] def header unless VALID_LENGTH.include? @length raise ProtocolError, "Invalid frame length: #{@length.inspect}" end unless VALID_STREAM_ID.include? @stream_id raise ProtocolError, "Invalid stream identifier: #{@stream_id.inspect}" end [ # These are guaranteed correct due to the length check above. @length >> LENGTH_HISHIFT, @length & LENGTH_LOMASK, @type, @flags, @stream_id ].pack(HEADER_FORMAT) end # Decodes common 9-byte header. # # @parameter buffer [String] def self.parse_header(buffer) length_hi, length_lo, type, flags, stream_id = buffer.unpack(HEADER_FORMAT) length = (length_hi << LENGTH_HISHIFT) | length_lo stream_id = stream_id & STREAM_ID_MASK # puts "parse_header: length=#{length} type=#{type} flags=#{flags} stream_id=#{stream_id}" return length, type, flags, stream_id end # Read the frame header from a stream. # @parameter stream [IO] The stream to read from. # @raises [EOFError] If the header cannot be read completely. def read_header(stream) if buffer = stream.read(9) and buffer.bytesize == 9 @length, @type, @flags, @stream_id = Frame.parse_header(buffer) # puts "read_header: #{@length} #{@type} #{@flags} #{@stream_id}" else raise EOFError, "Could not read frame header!" end end # Read the frame payload from a stream. # @parameter stream [IO] The stream to read from. # @raises [EOFError] If the payload cannot be read completely. def read_payload(stream) if payload = stream.read(@length) and payload.bytesize == @length @payload = payload else raise EOFError, "Could not read frame payload!" end end # Read the complete frame (header and payload) from a stream. # @parameter stream [IO] The stream to read from. # @parameter maximum_frame_size [Integer] The maximum allowed frame size. # @raises [FrameSizeError] If the frame exceeds the maximum size. # @returns [Frame] Self for method chaining. def read(stream, maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE) read_header(stream) unless @length if @length > maximum_frame_size raise FrameSizeError, "#{self.class} (type=#{@type}) frame length #{@length} exceeds maximum frame size #{maximum_frame_size}!" end read_payload(stream) end # Write the frame header to a stream. # @parameter stream [IO] The stream to write to. def write_header(stream) stream.write self.header end # Write the frame payload to a stream. # @parameter stream [IO] The stream to write to. def write_payload(stream) stream.write(@payload) if @payload end # Write the complete frame (header and payload) to a stream. # @parameter stream [IO] The stream to write to. # @raises [ProtocolError] If frame validation fails. def write(stream) # Validate the payload size: if @payload.nil? if @length != 0 raise ProtocolError, "Invalid frame length: #{@length} != 0" end else if @length != @payload.bytesize raise ProtocolError, "Invalid payload size: #{@length} != #{@payload.bytesize}" end end self.write_header(stream) self.write_payload(stream) end # Apply the frame to a connection for processing. # @parameter connection [Connection] The connection to apply the frame to. def apply(connection) connection.receive_frame(self) end # Provide a readable representation of the frame for debugging. # @returns [String] A formatted string representation of the frame. def inspect "\#<#{self.class} stream_id=#{@stream_id} flags=#{@flags} payload=#{self.unpack}>" end end end end protocol-http2-0.23.0/lib/protocol/http2/framer.rb000066400000000000000000000073511506340702200217520ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. require_relative "error" require_relative "data_frame" require_relative "headers_frame" require_relative "reset_stream_frame" require_relative "settings_frame" require_relative "push_promise_frame" require_relative "ping_frame" require_relative "goaway_frame" require_relative "window_update_frame" require_relative "continuation_frame" require_relative "priority_update_frame" module Protocol module HTTP2 # HTTP/2 frame type mapping as defined by the spec FRAMES = [ DataFrame, HeadersFrame, nil, # PriorityFrame is deprecated and ignored, instead consider using PriorityUpdateFrame instead. ResetStreamFrame, SettingsFrame, PushPromiseFrame, PingFrame, GoawayFrame, WindowUpdateFrame, ContinuationFrame, nil, nil, nil, nil, nil, nil, PriorityUpdateFrame, ].freeze # Default connection "fast-fail" preamble string as defined by the spec. CONNECTION_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".freeze # Handles frame serialization and deserialization for HTTP/2 connections. # This class manages the reading and writing of HTTP/2 frames to/from a stream. class Framer # Initialize a new framer with a stream and frame definitions. # @parameter stream [IO] The underlying stream for frame I/O. # @parameter frames [Array] Frame type definitions to use. def initialize(stream, frames = FRAMES) @stream = stream @frames = frames end # Flush the underlying stream. def flush @stream.flush end # Close the underlying stream. def close @stream.close end # Check if the underlying stream is closed. # @returns [Boolean] True if the stream is closed. def closed? @stream.closed? end # Write the HTTP/2 connection preface to the stream. def write_connection_preface @stream.write(CONNECTION_PREFACE) end # Read and validate the HTTP/2 connection preface from the stream. # @raises [HandshakeError] If the preface is invalid. def read_connection_preface string = @stream.read(CONNECTION_PREFACE.bytesize) unless string == CONNECTION_PREFACE raise HandshakeError, "Invalid connection preface: #{string.inspect}" end return string end # @return [Frame] the frame that has been read from the underlying IO. # @raise if the underlying IO fails for some reason. def read_frame(maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE) # Read the header: length, type, flags, stream_id = read_header # Console.debug(self) {"read_frame: length=#{length} type=#{type} flags=#{flags} stream_id=#{stream_id} -> klass=#{@frames[type].inspect}"} # Allocate the frame: klass = @frames[type] || Frame frame = klass.new(stream_id, flags, type, length) # Read the payload: frame.read(@stream, maximum_frame_size) # Console.debug(self, name: "read") {frame.inspect} return frame end # Write a frame to the underlying IO. # After writing one or more frames, you should call flush to ensure the frames are sent to the remote peer. # @parameter frame [Frame] the frame to write. def write_frame(frame) # Console.debug(self, name: "write") {frame.inspect} frame.write(@stream) return frame end # Read a frame header from the stream. # @returns [Array] Parsed frame header components: length, type, flags, stream_id. # @raises [EOFError] If the header cannot be read completely. def read_header if buffer = @stream.read(9) if buffer.bytesize == 9 return Frame.parse_header(buffer) end end raise EOFError, "Could not read frame header!" end end end end protocol-http2-0.23.0/lib/protocol/http2/goaway_frame.rb000066400000000000000000000040041506340702200231270ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. require_relative "frame" module Protocol module HTTP2 # The GOAWAY frame is used to initiate shutdown of a connection or to signal serious error conditions. GOAWAY allows an endpoint to gracefully stop accepting new streams while still finishing processing of previously established streams. This enables administrative actions, like server maintenance. # # +-+-------------------------------------------------------------+ # |R| Last-Stream-ID (31) | # +-+-------------------------------------------------------------+ # | Error Code (32) | # +---------------------------------------------------------------+ # | Additional Debug Data (*) | # +---------------------------------------------------------------+ # class GoawayFrame < Frame TYPE = 0x7 FORMAT = "NN" # Check if this frame applies to the connection level. # @returns [Boolean] Always returns true for GOAWAY frames. def connection? true end # Unpack the GOAWAY frame payload. # @returns [Array] Last stream ID, error code, and debug data. def unpack data = super last_stream_id, error_code = data.unpack(FORMAT) return last_stream_id, error_code, data.slice(8, data.bytesize-8) end # Pack GOAWAY frame data into payload. # @parameter last_stream_id [Integer] The last processed stream ID. # @parameter error_code [Integer] The error code for connection termination. # @parameter data [String] Additional debug data. def pack(last_stream_id, error_code, data) super [last_stream_id, error_code].pack(FORMAT) + data end # Apply this GOAWAY frame to a connection for processing. # @parameter connection [Connection] The connection to apply the frame to. def apply(connection) connection.receive_goaway(self) end end end end protocol-http2-0.23.0/lib/protocol/http2/headers_frame.rb000066400000000000000000000050511506340702200232560ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. require_relative "frame" require_relative "padded" require_relative "continuation_frame" module Protocol module HTTP2 # The HEADERS frame is used to open a stream, and additionally carries a header block fragment. HEADERS frames can be sent on a stream in the "idle", "reserved (local)", "open", or "half-closed (remote)" state. # # +---------------+ # |Pad Length? (8)| # +-+-------------+-----------------------------------------------+ # |E| Stream Dependency? (31) | # +-+-------------+-----------------------------------------------+ # | Weight? (8) | # +-+-------------+-----------------------------------------------+ # | Header Block Fragment (*) ... # +---------------------------------------------------------------+ # | Padding (*) ... # +---------------------------------------------------------------+ # class HeadersFrame < Frame include Continued, Padded TYPE = 0x1 # Check if this frame contains priority information. # @returns [Boolean] True if the PRIORITY flag is set. def priority? flag_set?(PRIORITY) end # Check if this frame ends the stream. # @returns [Boolean] True if the END_STREAM flag is set. def end_stream? flag_set?(END_STREAM) end # Unpack the header block fragment from the frame. # @returns [String] The unpacked header block data. def unpack data = super if priority? # We no longer support priority frames, so strip the data: data = data.byteslice(5, data.bytesize - 5) end return data end # Pack header block data into the frame. # @parameter data [String] The header block data to pack. # @parameter arguments [Array] Additional arguments. # @parameter options [Hash] Options for packing. def pack(data, *arguments, **options) buffer = String.new.b buffer << data super(buffer, *arguments, **options) end # Apply this HEADERS frame to a connection for processing. # @parameter connection [Connection] The connection to apply the frame to. def apply(connection) connection.receive_headers(self) end # Get a string representation of the headers frame. # @returns [String] Human-readable frame information. def inspect "\#<#{self.class} stream_id=#{@stream_id} flags=#{@flags} #{@length || 0}b>" end end end end protocol-http2-0.23.0/lib/protocol/http2/padded.rb000066400000000000000000000041441506340702200217140ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. require_relative "frame" module Protocol module HTTP2 # Certain frames can have padding: # https://http2.github.io/http2-spec/#padding # # +---------------+ # |Pad Length? (8)| # +---------------+-----------------------------------------------+ # | Data (*) ... # +---------------------------------------------------------------+ # | Padding (*) ... # +---------------------------------------------------------------+ # # Provides padding functionality for HTTP/2 frames. # Padding can be used to obscure the actual size of frame payloads. module Padded # Check if the frame has padding enabled. # @returns [Boolean] True if the PADDED flag is set. def padded? flag_set?(PADDED) end # Pack data with optional padding into the frame. # @parameter data [String] The data to pack. # @parameter padding_size [Integer | Nil] Number of padding bytes to add. # @parameter maximum_size [Integer | Nil] Maximum frame size limit. def pack(data, padding_size: nil, maximum_size: nil) if padding_size set_flags(PADDED) buffer = String.new.b buffer << padding_size buffer << data if padding_size buffer << ("\0" * padding_size) end super buffer else clear_flags(PADDED) super data end end # Unpack frame data, removing padding if present. # @returns [String] The unpacked data without padding. # @raises [ProtocolError] If padding length is invalid. def unpack if padded? padding_size = @payload[0].ord # 1 byte for the padding octet, and padding_size bytes for the padding itself: data_size = @payload.bytesize - (1 + padding_size) if data_size < 0 raise ProtocolError, "Invalid padding length: #{padding_size}" end return @payload.byteslice(1, data_size) else return @payload end end end end end protocol-http2-0.23.0/lib/protocol/http2/ping_frame.rb000066400000000000000000000050441506340702200226020ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. require_relative "frame" module Protocol module HTTP2 ACKNOWLEDGEMENT = 0x1 # Provides acknowledgement functionality for frames that support it. # This module handles setting and checking acknowledgement flags on frames. module Acknowledgement # Check if the frame is an acknowledgement. # @returns [Boolean] True if the acknowledgement flag is set. def acknowledgement? flag_set?(ACKNOWLEDGEMENT) end # Mark this frame as an acknowledgement. def acknowledgement! set_flags(ACKNOWLEDGEMENT) end # Create an acknowledgement frame for this frame. # @returns [Frame] A new frame marked as an acknowledgement. def acknowledge frame = self.class.new frame.length = 0 frame.set_flags(ACKNOWLEDGEMENT) return frame end end # The PING frame is a mechanism for measuring a minimal round-trip time from the sender, as well as determining whether an idle connection is still functional. PING frames can be sent from any endpoint. # # +---------------------------------------------------------------+ # | | # | Opaque Data (64) | # | | # +---------------------------------------------------------------+ # class PingFrame < Frame TYPE = 0x6 include Acknowledgement # Check if this frame applies to the connection level. # @returns [Boolean] Always returns true for PING frames. def connection? true end # Apply this PING frame to a connection for processing. # @parameter connection [Connection] The connection to apply the frame to. def apply(connection) connection.receive_ping(self) end # Create an acknowledgement PING frame with the same payload. # @returns [PingFrame] A new PING frame marked as an acknowledgement. def acknowledge frame = super frame.pack self.unpack return frame end # Read and validate the PING frame payload. # @parameter stream [IO] The stream to read from. # @raises [ProtocolError] If validation fails. def read_payload(stream) super if @stream_id != 0 raise ProtocolError, "Settings apply to connection only, but stream_id was given" end if @length != 8 raise FrameSizeError, "Invalid frame length: #{@length} != 8!" end end end end end protocol-http2-0.23.0/lib/protocol/http2/priority_update_frame.rb000066400000000000000000000035641506340702200250750ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2024-2025, by Samuel Williams. require_relative "frame" require_relative "padded" require_relative "continuation_frame" module Protocol module HTTP2 # The PRIORITY_UPDATE frame is used by clients to signal the initial priority of a response, or to reprioritize a response or push stream. It carries the stream ID of the response and the priority in ASCII text, using the same representation as the Priority header field value. # # +-+-------------+-----------------------------------------------+ # |R| Prioritized Stream ID (31) | # +-+-----------------------------+-------------------------------+ # | Priority Field Value (*) ... # +---------------------------------------------------------------+ # class PriorityUpdateFrame < Frame TYPE = 0x10 FORMAT = "N".freeze # Unpack the prioritized stream ID and priority field value. # @returns [Array] An array containing the prioritized stream ID and priority field value. def unpack data = super prioritized_stream_id = data.unpack1(FORMAT) return prioritized_stream_id, data.byteslice(4, data.bytesize - 4) end # Pack the prioritized stream ID and priority field value into the frame. # @parameter prioritized_stream_id [Integer] The stream ID to prioritize. # @parameter data [String] The priority field value. # @parameter options [Hash] Options for packing. def pack(prioritized_stream_id, data, **options) super([prioritized_stream_id].pack(FORMAT) + data, **options) end # Apply this PRIORITY_UPDATE frame to a connection for processing. # @parameter connection [Connection] The connection to apply the frame to. def apply(connection) connection.receive_priority_update(self) end end end end protocol-http2-0.23.0/lib/protocol/http2/push_promise_frame.rb000066400000000000000000000041241506340702200243600ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. require_relative "frame" require_relative "padded" require_relative "continuation_frame" module Protocol module HTTP2 # The PUSH_PROMISE frame is used to notify the peer endpoint in advance of streams the sender intends to initiate. The PUSH_PROMISE frame includes the unsigned 31-bit identifier of the stream the endpoint plans to create along with a set of headers that provide additional context for the stream. # # +---------------+ # |Pad Length? (8)| # +-+-------------+-----------------------------------------------+ # |R| Promised Stream ID (31) | # +-+-----------------------------+-------------------------------+ # | Header Block Fragment (*) ... # +---------------------------------------------------------------+ # | Padding (*) ... # +---------------------------------------------------------------+ # class PushPromiseFrame < Frame include Continued, Padded TYPE = 0x5 FORMAT = "N".freeze # Unpack the promised stream ID and header block fragment. # @returns [Array] An array containing the promised stream ID and header block data. def unpack data = super stream_id = data.unpack1(FORMAT) return stream_id, data.byteslice(4, data.bytesize - 4) end # Pack the promised stream ID and header block data into the frame. # @parameter stream_id [Integer] The promised stream ID. # @parameter data [String] The header block data. # @parameter arguments [Array] Additional arguments. # @parameter options [Hash] Options for packing. def pack(stream_id, data, *arguments, **options) super([stream_id].pack(FORMAT) + data, *arguments, **options) end # Apply this PUSH_PROMISE frame to a connection for processing. # @parameter connection [Connection] The connection to apply the frame to. def apply(connection) connection.receive_push_promise(self) end end end end protocol-http2-0.23.0/lib/protocol/http2/reset_stream_frame.rb000066400000000000000000000034601506340702200243420ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. require_relative "frame" module Protocol module HTTP2 NO_ERROR = 0 PROTOCOL_ERROR = 1 INTERNAL_ERROR = 2 FLOW_CONTROL_ERROR = 3 TIMEOUT = 4 STREAM_CLOSED = 5 FRAME_SIZE_ERROR = 6 REFUSED_STREAM = 7 CANCEL = 8 COMPRESSION_ERROR = 9 CONNECT_ERROR = 10 ENHANCE_YOUR_CALM = 11 INADEQUATE_SECURITY = 12 HTTP_1_1_REQUIRED = 13 # The RST_STREAM frame allows for immediate termination of a stream. RST_STREAM is sent to request cancellation of a stream or to indicate that an error condition has occurred. # # +---------------------------------------------------------------+ # | Error Code (32) | # +---------------------------------------------------------------+ # class ResetStreamFrame < Frame TYPE = 0x3 FORMAT = "N".freeze # Unpack the error code from the frame payload. # @returns [Integer] The error code. def unpack @payload.unpack1(FORMAT) end # Pack an error code into the frame payload. # @parameter error_code [Integer] The error code to pack. def pack(error_code = NO_ERROR) @payload = [error_code].pack(FORMAT) @length = @payload.bytesize end # Apply this RST_STREAM frame to a connection for processing. # @parameter connection [Connection] The connection to apply the frame to. def apply(connection) connection.receive_reset_stream(self) end # Read and validate the RST_STREAM frame payload. # @parameter stream [IO] The stream to read from. # @raises [FrameSizeError] If the frame length is invalid. def read_payload(stream) super if @length != 4 raise FrameSizeError, "Invalid frame length" end end end end end protocol-http2-0.23.0/lib/protocol/http2/server.rb000066400000000000000000000053051506340702200220010ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. require_relative "connection" module Protocol module HTTP2 # Represents an HTTP/2 server connection. # Manages server-side protocol semantics including stream ID allocation, # connection preface handling, and settings negotiation. class Server < Connection # Initialize a new HTTP/2 server connection. # @parameter framer [Framer] The frame handler for reading/writing HTTP/2 frames. def initialize(framer) super(framer, 2) end # Check if the given stream ID represents a locally-initiated stream. # Server streams have even numbered IDs. # @parameter id [Integer] The stream ID to check. # @returns [Boolean] True if the stream ID is locally-initiated. def local_stream_id?(id) id.even? end # Check if the given stream ID represents a remotely-initiated stream. # Client streams have odd numbered IDs. # @parameter id [Integer] The stream ID to check. # @returns [Boolean] True if the stream ID is remotely-initiated. def remote_stream_id?(id) id.odd? end # Check if the given stream ID is valid for remote initiation. # Client-initiated streams must have odd numbered IDs. # @parameter stream_id [Integer] The stream ID to validate. # @returns [Boolean] True if the stream ID is valid for remote initiation. def valid_remote_stream_id?(stream_id) stream_id.odd? end # Read the HTTP/2 connection preface from the client and send initial settings. # This must be called once when the connection is first established. # @parameter settings [Array] Optional settings to send during preface exchange. # @raises [ProtocolError] If called when not in the new state or preface is invalid. def read_connection_preface(settings = []) if @state == :new @framer.read_connection_preface send_settings(settings) read_frame do |frame| unless frame.is_a? SettingsFrame raise ProtocolError, "First frame must be #{SettingsFrame}, but got #{frame.class}" end end else raise ProtocolError, "Cannot read connection preface in state #{@state}" end end # Servers cannot accept push promise streams from clients. # @parameter stream_id [Integer] The stream ID (unused). # @raises [ProtocolError] Always, as servers cannot accept push promises. def accept_push_promise_stream(stream_id, &block) raise ProtocolError, "Cannot accept push promises on server!" end # Check if server push is enabled by the client. # @returns [Boolean] True if push promises are enabled. def enable_push? @remote_settings.enable_push? end end end end protocol-http2-0.23.0/lib/protocol/http2/settings_frame.rb000066400000000000000000000225021506340702200235030ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. require_relative "ping_frame" module Protocol module HTTP2 # HTTP/2 connection settings container and management. class Settings HEADER_TABLE_SIZE = 0x1 ENABLE_PUSH = 0x2 MAXIMUM_CONCURRENT_STREAMS = 0x3 INITIAL_WINDOW_SIZE = 0x4 MAXIMUM_FRAME_SIZE = 0x5 MAXIMUM_HEADER_LIST_SIZE = 0x6 ENABLE_CONNECT_PROTOCOL = 0x8 NO_RFC7540_PRIORITIES = 0x9 ASSIGN = [ nil, :header_table_size=, :enable_push=, :maximum_concurrent_streams=, :initial_window_size=, :maximum_frame_size=, :maximum_header_list_size=, nil, :enable_connect_protocol=, :no_rfc7540_priorities=, ] # Initialize settings with default values from HTTP/2 specification. def initialize # These limits are taken from the RFC: # https://tools.ietf.org/html/rfc7540#section-6.5.2 @header_table_size = 4096 @enable_push = 1 @maximum_concurrent_streams = 0xFFFFFFFF @initial_window_size = 0xFFFF # 2**16 - 1 @maximum_frame_size = 0x4000 # 2**14 @maximum_header_list_size = 0xFFFFFFFF @enable_connect_protocol = 0 @no_rfc7540_priorities = 0 end # Allows the sender to inform the remote endpoint of the maximum size of the header compression table used to decode header blocks, in octets. attr_accessor :header_table_size # This setting can be used to disable server push. An endpoint MUST NOT send a PUSH_PROMISE frame if it receives this parameter set to a value of 0. attr :enable_push # Set the server push enable flag. # @parameter value [Integer] Must be 0 (disabled) or 1 (enabled). # @raises [ProtocolError] If the value is invalid. def enable_push= value if value == 0 or value == 1 @enable_push = value else raise ProtocolError, "Invalid value for enable_push: #{value}" end end # Check if server push is enabled. # @returns [Boolean] True if server push is enabled. def enable_push? @enable_push == 1 end # Indicates the maximum number of concurrent streams that the sender will allow. attr_accessor :maximum_concurrent_streams # Indicates the sender's initial window size (in octets) for stream-level flow control. attr :initial_window_size # Set the initial window size for stream-level flow control. # @parameter value [Integer] The window size in octets. # @raises [ProtocolError] If the value exceeds the maximum allowed. def initial_window_size= value if value <= MAXIMUM_ALLOWED_WINDOW_SIZE @initial_window_size = value else raise ProtocolError, "Invalid value for initial_window_size: #{value} > #{MAXIMUM_ALLOWED_WINDOW_SIZE}" end end # Indicates the size of the largest frame payload that the sender is willing to receive, in octets. attr :maximum_frame_size # Set the maximum frame size the sender is willing to receive. # @parameter value [Integer] The maximum frame size in octets. # @raises [ProtocolError] If the value is outside the allowed range. def maximum_frame_size= value if value > MAXIMUM_ALLOWED_FRAME_SIZE raise ProtocolError, "Invalid value for maximum_frame_size: #{value} > #{MAXIMUM_ALLOWED_FRAME_SIZE}" elsif value < MINIMUM_ALLOWED_FRAME_SIZE raise ProtocolError, "Invalid value for maximum_frame_size: #{value} < #{MINIMUM_ALLOWED_FRAME_SIZE}" else @maximum_frame_size = value end end # This advisory setting informs a peer of the maximum size of header list that the sender is prepared to accept, in octets. attr_accessor :maximum_header_list_size attr :enable_connect_protocol # Set the CONNECT protocol enable flag. # @parameter value [Integer] Must be 0 (disabled) or 1 (enabled). # @raises [ProtocolError] If the value is invalid. def enable_connect_protocol= value if value == 0 or value == 1 @enable_connect_protocol = value else raise ProtocolError, "Invalid value for enable_connect_protocol: #{value}" end end # Check if CONNECT protocol is enabled. # @returns [Boolean] True if CONNECT protocol is enabled. def enable_connect_protocol? @enable_connect_protocol == 1 end attr :no_rfc7540_priorities # Set the RFC 7540 priorities disable flag. # @parameter value [Integer] Must be 0 (enabled) or 1 (disabled). # @raises [ProtocolError] If the value is invalid. def no_rfc7540_priorities= value if value == 0 or value == 1 @no_rfc7540_priorities = value else raise ProtocolError, "Invalid value for no_rfc7540_priorities: #{value}" end end # Check if RFC 7540 priorities are disabled. # @returns [Boolean] True if RFC 7540 priorities are disabled. def no_rfc7540_priorities? @no_rfc7540_priorities == 1 end # Update settings with a hash of changes. # @parameter changes [Hash] Hash of setting keys and values to update. def update(changes) changes.each do |key, value| if name = ASSIGN[key] self.send(name, value) end end end end # Manages pending settings changes that haven't been acknowledged yet. class PendingSettings # Initialize with current settings. # @parameter current [Settings] The current settings object. def initialize(current = Settings.new) @current = current @pending = current.dup @queue = [] end attr :current attr :pending # Append changes to the pending queue. # @parameter changes [Hash] Hash of setting changes to queue. def append(changes) @queue << changes @pending.update(changes) end # Acknowledge the next set of pending changes. def acknowledge if changes = @queue.shift @current.update(changes) return changes else raise ProtocolError, "Cannot acknowledge settings, no changes pending" end end # Get the current header table size setting. # @returns [Integer] The header table size in octets. def header_table_size @current.header_table_size end # Get the current enable push setting. # @returns [Integer] 1 if push is enabled, 0 if disabled. def enable_push @current.enable_push end # Get the current maximum concurrent streams setting. # @returns [Integer] The maximum number of concurrent streams. def maximum_concurrent_streams @current.maximum_concurrent_streams end # Get the current initial window size setting. # @returns [Integer] The initial window size in octets. def initial_window_size @current.initial_window_size end # Get the current maximum frame size setting. # @returns [Integer] The maximum frame size in octets. def maximum_frame_size @current.maximum_frame_size end # Get the current maximum header list size setting. # @returns [Integer] The maximum header list size in octets. def maximum_header_list_size @current.maximum_header_list_size end # Get the current CONNECT protocol enable setting. # @returns [Integer] 1 if CONNECT protocol is enabled, 0 if disabled. def enable_connect_protocol @current.enable_connect_protocol end end # The SETTINGS frame conveys configuration parameters that affect how endpoints communicate, such as preferences and constraints on peer behavior. The SETTINGS frame is also used to acknowledge the receipt of those parameters. Individually, a SETTINGS parameter can also be referred to as a "setting". # # +-------------------------------+ # | Identifier (16) | # +-------------------------------+-------------------------------+ # | Value (32) | # +---------------------------------------------------------------+ # class SettingsFrame < Frame TYPE = 0x4 FORMAT = "nN".freeze include Acknowledgement # Check if this frame applies to the connection level. # @returns [Boolean] Always returns true for SETTINGS frames. def connection? true end # Unpack settings parameters from the frame payload. # @returns [Array] Array of [key, value] pairs representing settings. def unpack if buffer = super # TODO String#each_slice, or #each_unpack would be nice. buffer.scan(/....../m).map{|s| s.unpack(FORMAT)} else [] end end # Pack settings parameters into the frame payload. # @parameter settings [Array] Array of [key, value] pairs to pack. def pack(settings = []) super(settings.map{|s| s.pack(FORMAT)}.join) end # Apply this SETTINGS frame to a connection for processing. # @parameter connection [Connection] The connection to apply the frame to. def apply(connection) connection.receive_settings(self) end # Read and validate the SETTINGS frame payload. # @parameter stream [IO] The stream to read from. # @raises [ProtocolError] If the frame is invalid. # @raises [FrameSizeError] If the frame length is invalid. def read_payload(stream) super if @stream_id != 0 raise ProtocolError, "Settings apply to connection only, but stream_id was given" end if acknowledgement? and @length != 0 raise FrameSizeError, "Settings acknowledgement must not contain payload: #{@payload.inspect}" end if (@length % 6) != 0 raise FrameSizeError, "Invalid frame length" end end end end end protocol-http2-0.23.0/lib/protocol/http2/stream.rb000066400000000000000000000400031506340702200217600ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. require_relative "connection" module Protocol module HTTP2 # A single HTTP/2 connection can multiplex multiple streams in parallel: # multiple requests and responses can be in flight simultaneously and stream # data can be interleaved and prioritized. # # This class encapsulates all of the state, transition, flow-control, and # error management as defined by the HTTP 2.0 specification. All you have # to do is subscribe to appropriate events (marked with ":" prefix in # diagram below) and provide your application logic to handle request # and response processing. # # ``` # ┌────────┐ # send PP │ │ recv PP # ┌──────────┤ idle ├──────────┐ # │ │ │ │ # ▼ └───┬────┘ ▼ # ┌──────────┐ │ ┌──────────┐ # │ │ │ send H / │ │ # ┌──────┼ reserved │ │ recv H │ reserved ├──────┐ # │ │ (local) │ │ │ (remote) │ │ # │ └───┬──────┘ ▼ └──────┬───┘ │ # │ │ ┌────────┐ │ │ # │ │ recv ES │ │ send ES │ │ # │ send H │ ┌─────────┤ open ├─────────┐ │ recv H │ # │ │ │ │ │ │ │ │ # │ ▼ ▼ └───┬────┘ ▼ ▼ │ # │ ┌──────────┐ │ ┌──────────┐ │ # │ │ half │ │ │ half │ │ # │ │ closed │ │ send R / │ closed │ │ # │ │ (remote) │ │ recv R │ (local) │ │ # │ └────┬─────┘ │ └─────┬────┘ │ # │ │ │ │ │ # │ │ send ES / │ recv ES / │ │ # │ │ send R / ▼ send R / │ │ # │ │ recv R ┌────────┐ recv R │ │ # │ send R / └───────────►│ │◄───────────┘ send R / │ # │ recv R │ closed │ recv R │ # └───────────────────────►│ │◄───────────────────────┘ # └────────┘ # ``` # # - `send`: endpoint sends this frame # - `recv`: endpoint receives this frame # # - H: HEADERS frame (with implied CONTINUATIONs) # - PP: PUSH_PROMISE frame (with implied CONTINUATIONs) # - ES: END_STREAM flag # - R: RST_STREAM frame # # State transition methods use a trailing "!". class Stream include FlowControlled # Create a new stream and add it to the connection. # @parameter connection [Connection] The connection this stream belongs to. # @parameter id [Integer] The stream identifier. # @returns [Stream] The newly created stream. def self.create(connection, id) stream = self.new(connection, id) connection.streams[id] = stream return stream end # Initialize a new stream. # @parameter connection [Connection] The connection this stream belongs to. # @parameter id [Integer] The stream identifier. # @parameter state [Symbol] The initial stream state. def initialize(connection, id, state = :idle) @connection = connection @id = id @state = state @local_window = Window.new(@connection.local_settings.initial_window_size) @remote_window = Window.new(@connection.remote_settings.initial_window_size) @priority = nil end # The connection this stream belongs to. attr :connection # Stream ID (odd for client initiated streams, even otherwise). attr :id # Stream state, e.g. `idle`, `closed`. attr_accessor :state attr :local_window attr :remote_window # @attribute [Protocol::HTTP::Header::Priority | Nil] the priority of the stream. attr_accessor :priority # Get the maximum frame size for this stream. # @returns [Integer] The maximum frame size from connection settings. def maximum_frame_size @connection.available_frame_size end # Write a frame to the connection for this stream. # @parameter frame [Frame] The frame to write. def write_frame(frame) @connection.write_frame(frame) end # Check if the stream is active (not idle or closed). # @returns [Boolean] True if the stream is active. def active? @state != :closed && @state != :idle end # Check if the stream is closed. # @returns [Boolean] True if the stream is in closed state. def closed? @state == :closed end # Transition directly to closed state. Do not pass go, do not collect $200. # This method should only be used by `Connection#close`. def close(error = nil) unless closed? @state = :closed self.closed(error) end end # HEADERS frames can be sent on a stream in the "idle", "reserved (local)", "open", or "half-closed (remote)" state. Despite it's name, it can also be used for trailers. def send_headers? @state == :idle or @state == :reserved_local or @state == :open or @state == :half_closed_remote end private def write_headers(headers, flags = 0) frame = HeadersFrame.new(@id, flags) @connection.write_frames do |framer| data = @connection.encode_headers(headers) frame.pack(data, maximum_size: @connection.maximum_frame_size) framer.write_frame(frame) end return frame end # The HEADERS frame is used to open a stream, and additionally carries a header block fragment. HEADERS frames can be sent on a stream in the "idle", "reserved (local)", "open", or "half-closed (remote)" state. def send_headers(*arguments) if @state == :idle frame = write_headers(*arguments) if frame.end_stream? @state = :half_closed_local else open! end elsif @state == :reserved_local frame = write_headers(*arguments) @state = :half_closed_remote elsif @state == :open frame = write_headers(*arguments) if frame.end_stream? @state = :half_closed_local end elsif @state == :half_closed_remote frame = write_headers(*arguments) if frame.end_stream? close! end else raise ProtocolError, "Cannot send headers in state: #{@state}" end end # Consume from the remote window for both stream and connection. # @parameter frame [Frame] The frame that consumes window space. def consume_remote_window(frame) super @connection.consume_remote_window(frame) end private def write_data(data, flags = 0, **options) frame = DataFrame.new(@id, flags) frame.pack(data, **options) # This might fail if the data payload was too big: consume_remote_window(frame) write_frame(frame) return frame end # Send data over this stream. # @parameter arguments [Array] Arguments passed to write_data. # @parameter options [Hash] Options passed to write_data. def send_data(*arguments, **options) if @state == :open frame = write_data(*arguments, **options) if frame.end_stream? @state = :half_closed_local end elsif @state == :half_closed_remote frame = write_data(*arguments, **options) if frame.end_stream? close! end else raise ProtocolError, "Cannot send data in state: #{@state}" end end # Open the stream by transitioning from idle to open state. # @returns [Stream] Returns self for chaining. # @raises [ProtocolError] If the stream cannot be opened from current state. def open! if @state == :idle @state = :open else raise ProtocolError, "Cannot open stream in state: #{@state}" end return self end # The stream has been closed. If closed due to a stream reset, the error will be set. def closed(error = nil) end # Transition the stream into the closed state. # @parameter error_code [Integer] the error code if the stream was closed due to a stream reset. def close!(error_code = nil) @state = :closed @connection.delete(@id) if error_code error = StreamError.new("Stream closed!", error_code) end self.closed(error) return self end # Send a RST_STREAM frame to reset this stream. # @parameter error_code [Integer] The error code to send. def send_reset_stream(error_code = 0) if @state != :idle and @state != :closed frame = ResetStreamFrame.new(@id) frame.pack(error_code) write_frame(frame) close! else raise ProtocolError, "Cannot send reset stream (#{error_code}) in state: #{@state}" end end # Process headers frame and decode the header block. # @parameter frame [HeadersFrame] The headers frame to process. # @returns [Array] The decoded headers. def process_headers(frame) # Receiving request headers: data = frame.unpack @connection.decode_headers(data) end protected def ignore_headers(frame) # Console.warn(self) {"Received headers in state: #{@state}!"} end # Receive and process a headers frame on this stream. # @parameter frame [HeadersFrame] The headers frame to receive. def receive_headers(frame) if @state == :idle if frame.end_stream? @state = :half_closed_remote else open! end process_headers(frame) elsif @state == :reserved_remote process_headers(frame) @state = :half_closed_local elsif @state == :open process_headers(frame) if frame.end_stream? @state = :half_closed_remote end elsif @state == :half_closed_local process_headers(frame) if frame.end_stream? close! end elsif self.closed? ignore_headers(frame) else self.send_reset_stream(Error::STREAM_CLOSED) end end # @return [String] the data that was received. def process_data(frame) frame.unpack end # Ignore data frame when in an invalid state. # @parameter frame [DataFrame] The data frame to ignore. def ignore_data(frame) # Console.warn(self) {"Received headers in state: #{@state}!"} end # DATA frames are subject to flow control and can only be sent when a stream is in the "open" or "half-closed (remote)" state. The entire DATA frame payload is included in flow control, including the Pad Length and Padding fields if present. If a DATA frame is received whose stream is not in "open" or "half-closed (local)" state, the recipient MUST respond with a stream error of type STREAM_CLOSED. def receive_data(frame) if @state == :open update_local_window(frame) if frame.end_stream? @state = :half_closed_remote end process_data(frame) elsif @state == :half_closed_local update_local_window(frame) process_data(frame) if frame.end_stream? close! end elsif self.closed? ignore_data(frame) else # If a DATA frame is received whose stream is not in "open" or "half-closed (local)" state, the recipient MUST respond with a stream error (Section 5.4.2) of type STREAM_CLOSED. self.send_reset_stream(Error::STREAM_CLOSED) end end # Receive and process a RST_STREAM frame on this stream. # @parameter frame [ResetStreamFrame] The reset stream frame to receive. # @returns [Integer] The error code from the reset frame. # @raises [ProtocolError] If reset is received on an idle stream. def receive_reset_stream(frame) if @state == :idle # If a RST_STREAM frame identifying an idle stream is received, the recipient MUST treat this as a connection error (Section 5.4.1) of type PROTOCOL_ERROR. raise ProtocolError, "Cannot receive reset stream in state: #{@state}!" else error_code = frame.unpack close!(error_code) return error_code end end # A normal request is client request -> server response -> client. # A push promise is server request -> client -> server response -> client. # The server generates the same set of headers as if the client was sending a request, and sends these to the client. The client can reject the request by resetting the (new) stream. Otherwise, the server will start sending a response as if the client had send the request. private def write_push_promise(stream_id, headers, flags = 0, **options) frame = PushPromiseFrame.new(@id, flags) @connection.write_frames do |framer| data = @connection.encode_headers(headers) frame.pack(stream_id, data, maximum_size: @connection.maximum_frame_size) framer.write_frame(frame) end return frame end # Transition stream to reserved local state. # @returns [Stream] Returns self for chaining. # @raises [ProtocolError] If the stream cannot be reserved from current state. def reserved_local! if @state == :idle @state = :reserved_local else raise ProtocolError, "Cannot reserve stream in state: #{@state}" end return self end # Transition stream to reserved remote state. # @returns [Stream] Returns self for chaining. # @raises [ProtocolError] If the stream cannot be reserved from current state. def reserved_remote! if @state == :idle @state = :reserved_remote else raise ProtocolError, "Cannot reserve stream in state: #{@state}" end return self end # Override this function to implement your own push promise logic. def create_push_promise_stream(headers) @connection.create_push_promise_stream end # Server push is semantically equivalent to a server responding to a request; however, in this case, that request is also sent by the server, as a PUSH_PROMISE frame. # @parameter headers [Hash] contains a complete set of request header fields that the server attributes to the request. def send_push_promise(headers) if @state == :open or @state == :half_closed_remote promised_stream = self.create_push_promise_stream(headers) promised_stream.reserved_local! # The headers are the same as if the client had sent a request: write_push_promise(promised_stream.id, headers) # The server should call send_headers on the promised stream to begin sending the response: return promised_stream else raise ProtocolError, "Cannot send push promise in state: #{@state}" end end # Override this function to implement your own push promise logic. def accept_push_promise_stream(stream_id, headers) @connection.accept_push_promise_stream(stream_id) end # Receive and process a PUSH_PROMISE frame on this stream. # @parameter frame [PushPromiseFrame] The push promise frame to receive. def receive_push_promise(frame) promised_stream_id, data = frame.unpack headers = @connection.decode_headers(data) stream = self.accept_push_promise_stream(promised_stream_id, headers) stream.reserved_remote! return stream, headers end # Get a string representation of the stream. # @returns [String] Human-readable stream information. def inspect "\#<#{self.class} id=#{@id} state=#{@state}>" end end end end protocol-http2-0.23.0/lib/protocol/http2/version.rb000066400000000000000000000003051506340702200221530ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. # @namespace module Protocol # @namespace module HTTP2 VERSION = "0.23.0" end end protocol-http2-0.23.0/lib/protocol/http2/window.rb000066400000000000000000000114161506340702200220020ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2024-2025, by Samuel Williams. module Protocol module HTTP2 # Flow control window for managing HTTP/2 data flow. class Window # When an HTTP/2 connection is first established, new streams are created with an initial flow-control window size of 65,535 octets. The connection flow-control window is also 65,535 octets. DEFAULT_CAPACITY = 0xFFFF # Initialize a new flow control window. # @parameter capacity [Integer] The initial window size, typically from the settings. def initialize(capacity = DEFAULT_CAPACITY) # This is the main field required: @available = capacity # These two fields are primarily used for efficiently sending window updates: @used = 0 @capacity = capacity end # The window is completely full? def full? @available <= 0 end attr :used attr :capacity # When the value of SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST adjust the size of all stream flow-control windows that it maintains by the difference between the new value and the old value. def capacity= value difference = value - @capacity # An endpoint MUST treat a change to SETTINGS_INITIAL_WINDOW_SIZE that causes any flow-control window to exceed the maximum size as a connection error of type FLOW_CONTROL_ERROR. if (@available + difference) > MAXIMUM_ALLOWED_WINDOW_SIZE raise FlowControlError, "Changing window size by #{difference} caused overflow: #{@available + difference} > #{MAXIMUM_ALLOWED_WINDOW_SIZE}!" end @available += difference @capacity = value end # Consume a specific amount from the available window. # @parameter amount [Integer] The amount to consume from the window. def consume(amount) @available -= amount @used += amount end attr :available # Check if there is available window capacity. # @returns [Boolean] True if there is available capacity. def available? @available > 0 end # Expand the window by a specific amount. # @parameter amount [Integer] The amount to expand the window by. # @raises [FlowControlError] If expansion would cause overflow. def expand(amount) available = @available + amount if available > MAXIMUM_ALLOWED_WINDOW_SIZE raise FlowControlError, "Expanding window by #{amount} caused overflow: #{available} > #{MAXIMUM_ALLOWED_WINDOW_SIZE}!" end # puts "expand(#{amount}) @available=#{@available}" @available += amount @used -= amount end # Get the amount of window that should be reclaimed. # @returns [Integer] The amount of used window space. def wanted @used end # Check if the window is limited and needs updating. # @returns [Boolean] True if available capacity is less than half of total capacity. def limited? @available < (@capacity / 2) end # Get a string representation of the window. # @returns [String] Human-readable window information. def inspect "\#<#{self.class} available=#{@available} used=#{@used} capacity=#{@capacity}#{limited? ? " limited" : nil}>" end alias to_s inspect end # This is a window which efficiently maintains a desired capacity. class LocalWindow < Window # Initialize a local window with optional desired capacity. # @parameter capacity [Integer] The initial window capacity. # @parameter desired [Integer] The desired window capacity. def initialize(capacity = DEFAULT_CAPACITY, desired: nil) super(capacity) # The desired capacity of the window, may be bigger than the initial capacity. # If that is the case, we will likely send a window update to the remote end to increase the capacity. @desired = desired end # The desired capacity of the window. attr_accessor :desired # Get the amount of window that should be reclaimed, considering desired capacity. # @returns [Integer] The amount needed to reach desired capacity or used space. def wanted if @desired # We must send an update which allows at least @desired bytes to be sent. (@desired - @capacity) + @used else super end end # Check if the window is limited, considering desired capacity. # @returns [Boolean] True if window needs updating based on desired capacity. def limited? if @desired # Do not send window updates until we are less than half the desired capacity: @available < (@desired / 2) else super end end # Get a string representation of the local window. # @returns [String] Human-readable local window information. def inspect "\#<#{self.class} available=#{@available} used=#{@used} capacity=#{@capacity} desired=#{@desired} #{limited? ? "limited" : nil}>" end end end end protocol-http2-0.23.0/lib/protocol/http2/window_update_frame.rb000066400000000000000000000027241506340702200245200ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. require_relative "frame" require_relative "window" module Protocol module HTTP2 # The WINDOW_UPDATE frame is used to implement flow control. # # +-+-------------------------------------------------------------+ # |R| Window Size Increment (31) | # +-+-------------------------------------------------------------+ # class WindowUpdateFrame < Frame TYPE = 0x8 FORMAT = "N" # Pack a window size increment into the frame. # @parameter window_size_increment [Integer] The window size increment value. def pack(window_size_increment) super [window_size_increment].pack(FORMAT) end # Unpack the window size increment from the frame payload. # @returns [Integer] The window size increment value. def unpack super.unpack1(FORMAT) end # Read and validate the WINDOW_UPDATE frame payload. # @parameter stream [IO] The stream to read from. # @raises [FrameSizeError] If the frame length is invalid. def read_payload(stream) super if @length != 4 raise FrameSizeError, "Invalid frame length: #{@length} != 4!" end end # Apply this WINDOW_UPDATE frame to a connection for processing. # @parameter connection [Connection] The connection to apply the frame to. def apply(connection) connection.receive_window_update(self) end end end end protocol-http2-0.23.0/lib/traces/000077500000000000000000000000001506340702200165225ustar00rootroot00000000000000protocol-http2-0.23.0/lib/traces/provider/000077500000000000000000000000001506340702200203545ustar00rootroot00000000000000protocol-http2-0.23.0/lib/traces/provider/protocol/000077500000000000000000000000001506340702200222155ustar00rootroot00000000000000protocol-http2-0.23.0/lib/traces/provider/protocol/http2.rb000066400000000000000000000002111506340702200235750ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2024, by Samuel Williams. require_relative "http2/framer" protocol-http2-0.23.0/lib/traces/provider/protocol/http2/000077500000000000000000000000001506340702200232565ustar00rootroot00000000000000protocol-http2-0.23.0/lib/traces/provider/protocol/http2/framer.rb000066400000000000000000000025321506340702200250610ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2024-2025, by Samuel Williams. require "traces/provider" require_relative "../../../../protocol/http2/framer" Traces::Provider(Protocol::HTTP2::Framer) do def write_connection_preface return super unless Traces.active? Traces.trace("protocol.http2.framer.write_connection_preface") do super end end def read_connection_preface return super unless Traces.active? Traces.trace("protocol.http2.framer.read_connection_preface") do super end end def write_frame(frame) return super unless Traces.active? attributes = { "frame.length" => frame.length, "frame.class" => frame.class.name, "frame.type" => frame.type, "frame.flags" => frame.flags, "frame.stream_id" => frame.stream_id, } Traces.trace("protocol.http2.framer.write_frame", attributes: attributes) do super end end def read_frame(...) return super unless Traces.active? Traces.trace("protocol.http2.framer.read_frame") do |span| super.tap do |frame| span["frame.length"] = frame.length span["frame.type"] = frame.type span["frame.flags"] = frame.flags span["frame.stream_id"] = frame.stream_id end end end def flush return super unless Traces.active? Traces.trace("protocol.http2.framer.flush") do super end end end protocol-http2-0.23.0/license.md000066400000000000000000000023231506340702200164370ustar00rootroot00000000000000# MIT License Copyright, 2019-2025, by Samuel Williams. Copyright, 2019, by Yuta Iwama. Copyright, 2020, by Olle Jonsson. Copyright, 2023, by Marco Concetto Rudilosso. Copyright, 2024, by Adam Petro. 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. protocol-http2-0.23.0/protocol-http2.gemspec000066400000000000000000000016641506340702200207470ustar00rootroot00000000000000# frozen_string_literal: true require_relative "lib/protocol/http2/version" Gem::Specification.new do |spec| spec.name = "protocol-http2" spec.version = Protocol::HTTP2::VERSION spec.summary = "A low level implementation of the HTTP/2 protocol." spec.authors = ["Samuel Williams", "Yuta Iwama", "Adam Petro", "Marco Concetto Rudilosso", "Olle Jonsson"] spec.license = "MIT" spec.cert_chain = ["release.cert"] spec.signing_key = File.expand_path("~/.gem/release.pem") spec.homepage = "https://github.com/socketry/protocol-http2" spec.metadata = { "documentation_uri" => "https://socketry.github.io/protocol-http2/", "source_code_uri" => "https://github.com/socketry/protocol-http2.git", } spec.files = Dir.glob(["{context,lib}/**/*", "*.md"], File::FNM_DOTMATCH, base: __dir__) spec.required_ruby_version = ">= 3.2" spec.add_dependency "protocol-hpack", "~> 1.4" spec.add_dependency "protocol-http", "~> 0.47" end protocol-http2-0.23.0/readme.md000066400000000000000000000047141506340702200162600ustar00rootroot00000000000000# Protocol::HTTP2 Provides a low-level implementation of the HTTP/2 protocol. [![Development Status](https://github.com/socketry/protocol-http2/workflows/Test/badge.svg)](https://github.com/socketry/protocol-http2/actions?workflow=Test) ## Usage Please see the [project documentation](https://socketry.github.io/protocol-http2/) for more details. - [Getting Started](https://socketry.github.io/protocol-http2/guides/getting-started/index) - This guide explains how to use the `protocol-http2` gem to implement a basic HTTP/2 client. ## Releases Please see the [project releases](https://socketry.github.io/protocol-http2/releases/index) for all releases. ### v0.23.0 - Introduce a limit to the number of CONTINUATION frames that can be read to prevent resource exhaustion. The default limit is 8 continuation frames, which means a total of 9 frames (1 initial + 8 continuation). This limit can be adjusted by passing a different value to the `limit` parameter in the `Continued.read` method. Setting the limit to 0 will only read the initial frame without any continuation frames. In order to change the default, you can redefine the `LIMIT` constant in the `Protocol::HTTP2::Continued` module, OR you can pass a different frame class to the framer. ### v0.22.0 - [Added Priority Update Frame and Stream Priority](https://socketry.github.io/protocol-http2/releases/index#added-priority-update-frame-and-stream-priority) ## See Also - [Async::HTTP](https://github.com/socketry/async-http) - A high-level HTTP client and server implementation. ## Contributing We welcome contributions to this project. 1. Fork it. 2. Create your feature branch (`git checkout -b my-new-feature`). 3. Commit your changes (`git commit -am 'Add some feature'`). 4. Push to the branch (`git push origin my-new-feature`). 5. Create new Pull Request. ### Developer Certificate of Origin In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed. ### Community Guidelines This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers. protocol-http2-0.23.0/release.cert000066400000000000000000000033141506340702200167730ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11 ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/ c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8 voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg= -----END CERTIFICATE----- protocol-http2-0.23.0/releases.md000066400000000000000000000022431506340702200166210ustar00rootroot00000000000000# Releases ## v0.23.0 - Introduce a limit to the number of CONTINUATION frames that can be read to prevent resource exhaustion. The default limit is 8 continuation frames, which means a total of 9 frames (1 initial + 8 continuation). This limit can be adjusted by passing a different value to the `limit` parameter in the `Continued.read` method. Setting the limit to 0 will only read the initial frame without any continuation frames. In order to change the default, you can redefine the `LIMIT` constant in the `Protocol::HTTP2::Continued` module, OR you can pass a different frame class to the framer. ## v0.22.0 ### Added Priority Update Frame and Stream Priority HTTP/2 has deprecated the priority frame and stream dependency tracking. This feature has been effectively removed from the protocol. As a consequence, the internal implementation is greatly simplified. The `Protocol::HTTP2::Stream` class no longer tracks dependencies, and this includes `Stream#send_headers` which no longer takes `priority` as the first argument. Optional per-request priority can be set using the `priority` header instead, and this value can be manipulated using the priority update frame. protocol-http2-0.23.0/test/000077500000000000000000000000001506340702200154525ustar00rootroot00000000000000protocol-http2-0.23.0/test/protocol/000077500000000000000000000000001506340702200173135ustar00rootroot00000000000000protocol-http2-0.23.0/test/protocol/http2.rb000066400000000000000000000004171506340702200207030ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2024, by Samuel Williams. require "protocol/http2/version" describe Protocol::HTTP2 do it "has a version number" do expect(Protocol::HTTP2::VERSION).to be =~ /\d+\.\d+\.\d+/ end end protocol-http2-0.23.0/test/protocol/http2/000077500000000000000000000000001506340702200203545ustar00rootroot00000000000000protocol-http2-0.23.0/test/protocol/http2/client.rb000066400000000000000000000051151506340702200221610ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. require "protocol/http2/connection_context" describe Protocol::HTTP2::Client do include_context Protocol::HTTP2::ConnectionContext let(:framer) {server.framer} let(:settings) do [[Protocol::HTTP2::Settings::HEADER_TABLE_SIZE, 1024]] end it "has an id of 0" do expect(client.id).to be == 0 expect(client[0]).to be == client end it "can lookup stream by id" do stream = client.create_stream expect(client[stream.id]).to be == stream end it "should start in new state" do expect(client.state).to be == :new end it "should send connection preface followed by settings frame" do client.send_connection_preface(settings) do expect(framer.read_connection_preface).to be == Protocol::HTTP2::CONNECTION_PREFACE client_settings_frame = framer.read_frame expect(client_settings_frame).to be_a Protocol::HTTP2::SettingsFrame expect(client_settings_frame.unpack).to be == settings # Fake (empty) server settings: server_settings_frame = Protocol::HTTP2::SettingsFrame.new server_settings_frame.pack framer.write_frame(server_settings_frame) framer.write_frame(client_settings_frame.acknowledge) end expect(client.state).to be == :new client.read_frame expect(client.state).to be == :open expect(client.local_settings.header_table_size).to be == 1024 end it "should fail if the server does not reply with settings frame" do data_frame = Protocol::HTTP2::DataFrame.new data_frame.pack("Hello, World!") expect do client.send_connection_preface(settings) do framer.write_frame(data_frame) end end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /First frame must be Protocol::HTTP2::SettingsFrame/) end it "can generate a stream id" do id = client.next_stream_id expect(id).to be == 1 expect(client).to be(:local_stream_id?, id) expect(client).not.to be(:remote_stream_id?, id) end it "can't send connection preface in open state" do client.open! expect do client.send_connection_preface(settings) end.to raise_exception(Protocol::HTTP2::ProtocolError) end it "can't generate push promise stream" do expect do client.create_push_promise_stream end.to raise_exception(Protocol::HTTP2::ProtocolError) end it "can't receive push promise stream for stream id 0" do frame = Protocol::HTTP2::PushPromiseFrame.new frame.pack(0, "Hello World") expect do client.receive_push_promise(frame) end.to raise_exception(Protocol::HTTP2::ProtocolError) end end protocol-http2-0.23.0/test/protocol/http2/connection.rb000066400000000000000000000327211506340702200230450ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. # Copyright, 2023, by Marco Concetto Rudilosso. require "protocol/http2/connection_context" describe Protocol::HTTP2::Connection do let(:stream) {StringIO.new} let(:framer) {Protocol::HTTP2::Framer.new(stream)} let(:connection) {subject.new(framer, 1)} with "#maximum_concurrent_streams" do it "is the remote peer's maximum concurrent streams" do connection.remote_settings.maximum_concurrent_streams = 10 connection.local_settings.current.maximum_concurrent_streams = 5 expect(connection.maximum_concurrent_streams).to be == 10 end end with "#receive_headers" do it "fails with protocol error if exceeding the maximum concurrent connections" do connection.local_settings.current.maximum_concurrent_streams = 1 # Create a stream to connection.streams[1] = Protocol::HTTP2::Stream.new(connection, 1) frame = Protocol::HTTP2::HeadersFrame.new(3) expect do connection.receive_headers(frame) end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Exceeded maximum concurrent streams/) end end it "reports the connection id 0 is not closed" do expect(connection).not.to be(:closed_stream_id?, 0) end it "does not report any stream_id as being remote" do expect(connection).not.to be(:valid_remote_stream_id?, 1) end it "rejects a push promise" do frame = Protocol::HTTP2::PushPromiseFrame.new expect do connection.receive_push_promise(frame) end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Unable to receive push promise/) end it "rejects a stream reset to stream id 0" do frame = Protocol::HTTP2::ResetStreamFrame.new(0) expect do connection.receive_reset_stream(frame) end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot reset connection/) end it "rejects headers to stream id 0" do frame = Protocol::HTTP2::HeadersFrame.new(0) expect do connection.receive_headers(frame) end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot receive headers for stream 0/) end it "rejects continuation" do frame = Protocol::HTTP2::ContinuationFrame.new(1) expect do connection.receive_continuation(frame) end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Received unexpected continuation/) end it "rejects incorrectly encoded headers" do expect(connection).to receive(:valid_remote_stream_id?).and_return(true) invalid_headers_frame = Protocol::HTTP2::HeadersFrame.new(1) invalid_headers_frame.pack("\xFF") invalid_headers_frame.write(stream) stream.rewind expect(connection).to receive(:send_goaway) expect do connection.read_frame end.to raise_exception(Protocol::HPACK::Error) end it "can't write frames to a closed connection" do connection.close expect do connection.write_frames do |framer| end end.to raise_exception(EOFError, message: be =~ /Connection closed/) end end with "client and server" do include_context Protocol::HTTP2::ConnectionContext it "can negotiate connection" do first_server_frame = nil first_client_frame = client.send_connection_preface do first_server_frame = server.read_connection_preface([]) end expect(first_client_frame).to be_a Protocol::HTTP2::SettingsFrame expect(first_client_frame).not.to be(:acknowledgement?) expect(first_server_frame).to be_a Protocol::HTTP2::SettingsFrame expect(first_server_frame).not.to be(:acknowledgement?) frame = client.read_frame expect(frame).to be_a Protocol::HTTP2::SettingsFrame expect(frame).to be(:acknowledgement?) frame = server.read_frame expect(frame).to be_a Protocol::HTTP2::SettingsFrame expect(frame).to be(:acknowledgement?) expect(client.state).to be == :open expect(server.state).to be == :open end describe Protocol::HTTP2::PingFrame do def before client.open! server.open! super end it "can send ping and receive pong" do expect(server).to receive(:receive_ping) client.send_ping("12345678") server.read_frame expect(client).to receive(:receive_ping) client.read_frame end end describe Protocol::HTTP2::Stream do def before client.open! server.open! super end let(:request_data) {"Hello World!"} let(:stream) {client.create_stream} let(:request_headers) {[[":method", "GET"], [":path", "/"], [":authority", "localhost"]]} let(:response_headers) {[[":status", "200"]]} it "can determine who initiated stream" do expect(client).to be(:client_stream_id?, stream.id) expect(client).not.to be(:server_stream_id?, stream.id) end it "can determine closed streams" do expect(client).not.to be(:idle_stream_id?, stream.id) expect(server).to be(:idle_stream_id?, stream.id) stream.send_headers(request_headers) server.read_frame # `closed_stream_id?` is true even if the stream is still open. It implies that if the stream is not otherwise open, it is closed. expect(server[stream.id]).not.to be_nil expect(server).to be(:closed_stream_id?, stream.id) end with "server created stream" do let(:stream) {server.create_stream} it "can determine who initiated stream" do expect(client).to be(:idle_stream_id?, stream.id) expect(server).not.to be(:idle_stream_id?, stream.id) end end it "can create new stream and send response" do # First argument is deprecated priority. stream.send_headers(request_headers) expect(stream.id).to be == 1 expect(server).to receive(:receive_headers) do |frame| headers = super(frame) expect(headers).to be == request_headers end server.read_frame expect(server.streams).not.to be(:empty?) expect(server.streams[1].state).to be == :open stream.send_data(request_data, Protocol::HTTP2::END_STREAM) expect(stream.state).to be == :half_closed_local expect(server).to receive(:receive_data) data_frame = server.read_frame expect(data_frame.unpack).to be == request_data expect(server.streams[1].state).to be == :half_closed_remote server.streams[1].send_headers(response_headers, Protocol::HTTP2::END_STREAM) expect(stream).to receive(:process_headers) do |frame| headers = super(frame) expect(headers).to be == response_headers end client.read_frame expect(stream.state).to be == :closed expect(stream.remote_window.used).to be == data_frame.length end it "can update settings while sending response" do stream.send_headers(request_headers) server.read_frame client.send_settings([ [Protocol::HTTP2::Settings::INITIAL_WINDOW_SIZE, 100] ]) frame = server.read_frame expect(frame).to be_a(Protocol::HTTP2::SettingsFrame) frame = client.read_frame expect(frame).to be_a(Protocol::HTTP2::SettingsFrame) expect(frame).to be(:acknowledgement?) end it "can update concurrent streams" do initial_maximum_concurrent_streams = server.maximum_concurrent_streams inform "Initial maximum concurrent streams: #{initial_maximum_concurrent_streams}" server.send_settings([ [Protocol::HTTP2::Settings::MAXIMUM_CONCURRENT_STREAMS, 1] ]) # It's not acknowledged until the client sends an acknowledgement: expect(server).to have_attributes( maximum_concurrent_streams: be == initial_maximum_concurrent_streams ) expect(client).to have_attributes( maximum_concurrent_streams: be == initial_maximum_concurrent_streams ) frame = client.read_frame expect(frame).to be_a(Protocol::HTTP2::SettingsFrame) expect(client).to have_attributes( maximum_concurrent_streams: be == 1 ) frame = server.read_frame expect(frame).to be_a(Protocol::HTTP2::SettingsFrame) expect(frame).to be(:acknowledgement?) expect(server.local_settings).to have_attributes( maximum_concurrent_streams: be == 1 ) # The client maximum concurrent streams was not changed because the client did not `send_settings`: expect(server).to have_attributes( maximum_concurrent_streams: be == initial_maximum_concurrent_streams ) end it "doesn't accept headers for an existing stream" do stream.send_headers(request_headers) expect(stream.id).to be == 1 server.read_frame expect(server.streams).not.to be(:empty?) expect(server.streams[1].state).to be == :open stream.send_headers(request_headers) # Simulate a closed stream: server.streams.clear expect do server.read_frame end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Invalid stream id/) end it "client can handle graceful shutdown" do stream.send_headers(request_headers, Protocol::HTTP2::END_STREAM) # Establish request stream on server: server.read_frame # Graceful shutdown server.send_goaway(0) another_stream = client.create_stream another_stream.send_headers(request_headers, Protocol::HTTP2::END_STREAM) expect(client.read_frame).to be_a Protocol::HTTP2::GoawayFrame expect(client.remote_stream_id).to be == 1 expect(client).to be(:closed?) # The server will ignore this frame as it was sent after the graceful shutdown: server.read_frame expect(server.streams[another_stream.id]).to be_nil # The pre-existing stream is still functional: expect(server.streams[1].state).to be == :half_closed_remote server.streams[1].send_headers(response_headers, Protocol::HTTP2::END_STREAM) client.read_frame expect(stream.state).to be == :closed end it "client can handle non-graceful shutdown" do stream.send_headers(request_headers, Protocol::HTTP2::END_STREAM) # Establish request stream on server: server.read_frame # Send connection error to client: server.send_goaway(1, "Bugger off!") expect(stream).to receive(:close) expect do client.read_frame end.to raise_exception(Protocol::HTTP2::GoawayError) client.close expect(client.remote_stream_id).to be == 1 expect(client).to be(:closed?) end it "can stream data" do stream.send_headers(request_headers) stream.send_data("A") stream.send_data("B") stream.send_data("C") stream.send_data("", Protocol::HTTP2::END_STREAM) frame = server.read_frame expect(frame).to be_a(Protocol::HTTP2::HeadersFrame) expect(stream.available_frame_size).to be >= 3 frame = server.read_frame expect(frame).to be_a(Protocol::HTTP2::DataFrame) expect(frame.unpack).to be == "A" frame = server.read_frame expect(frame).to be_a(Protocol::HTTP2::DataFrame) expect(frame.unpack).to be == "B" frame = server.read_frame expect(frame).to be_a(Protocol::HTTP2::DataFrame) expect(frame.unpack).to be == "C" frame = server.read_frame expect(frame).to be_a(Protocol::HTTP2::DataFrame) expect(frame.unpack).to be == "" expect(frame).to be(:end_stream?) end end it "allows client to create new stream and send headers when client maximum concurrent streams is 0" do client.local_settings.current.maximum_concurrent_streams = 0 client_stream = client.create_stream request_headers = [[":method", "GET"], [":path", "/"], [":authority", "localhost"]] client_stream.send_headers(request_headers) expect(server).to receive(:receive_headers) do |frame| headers = super(frame) expect(headers).to be == request_headers end server.read_frame end it "does not allow client to create new stream and send headers when server maximum concurrent streams is 0" do server.local_settings.current.maximum_concurrent_streams = 0 client_stream = client.create_stream request_headers = [[":method", "GET"], [":path", "/"], [":authority", "localhost"]] client_stream.send_headers(request_headers) expect {server.read_frame}.to raise_exception(Protocol::HTTP2::ProtocolError) end it "allows server to create new stream and send headers when server maximum concurrent streams is 0" do server.local_settings.current.maximum_concurrent_streams = 0 server_stream = server.create_stream request_headers = [[":method", "GET"], [":path", "/"], [":authority", "localhost"]] server_stream.send_headers(request_headers) expect(client).to receive(:receive_headers) do |frame| headers = super(frame) expect(headers).to be == request_headers end client.read_frame end it "does not allow server to create new stream send headers when client maximum concurrent streams is 0" do client.local_settings.current.maximum_concurrent_streams = 0 server_stream = server.create_stream request_headers = [[":method", "GET"], [":path", "/"], [":authority", "localhost"]] server_stream.send_headers(request_headers) expect {client.read_frame}.to raise_exception(Protocol::HTTP2::ProtocolError) end with "closed client" do def before client.close! super end it "cannot receive settings" do expect do settings_frame = Protocol::HTTP2::SettingsFrame.new client.receive_settings(settings_frame) end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot receive settings/) end it "cannot send ping" do expect do client.send_ping("Hello World!") end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot send ping/) end it "cannot receive ping" do expect do ping_frame = Protocol::HTTP2::PingFrame.new client.receive_ping(ping_frame) end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot receive ping/) end end end protocol-http2-0.23.0/test/protocol/http2/continuation_frame.rb000066400000000000000000000042341506340702200245700ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. require "protocol/http2/continuation_frame" require "protocol/http2/a_frame" describe Protocol::HTTP2::ContinuationFrame do let(:data) {"Hello World!"} let(:frame) {subject.new} it_behaves_like Protocol::HTTP2::AFrame do def before frame.pack data super end end with "#pack" do it "packs data" do frame.pack data expect(frame.length).to be == data.bytesize end it "packs data over multiple continuation frames" do frame.pack data, maximum_size: 6 expect(frame.continuation).not.to be_nil end end with "#unpack" do it "unpacks data" do frame.pack data expect(frame.unpack).to be == data end it "unpacks data over multiple continuations" do frame.pack data, maximum_size: 2 expect(frame.unpack).to be == data end end with "#apply" do let(:connection) {Protocol::HTTP2::Connection.new(nil, 0)} it "applies the frame to a connection" do expect(connection).to receive(:receive_continuation).and_return(true) frame.pack data frame.apply(connection) end end with "#inspect" do it "can generate a string representation" do expect(frame.inspect).to be =~ /stream_id=0 flags=0 length=0/ end end with "read(limit)" do it "reads a single frame" do frame.pack data buffer = StringIO.new frame.write(buffer) buffer.rewind new_frame = subject.new new_frame.read(buffer, 128, 0) expect(new_frame.unpack).to be == data end it "raises if too many continuation frames" do frame.pack data, maximum_size: 2 buffer = StringIO.new frame.write(buffer) buffer.rewind expect do new_frame = subject.new new_frame.read(buffer, 128, 1) end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Too many continuation frames!/) end it "reads multiple continuation frames" do frame.pack data, maximum_size: 2 buffer = StringIO.new frame.write(buffer) buffer.rewind new_frame = subject.new new_frame.read(buffer, 128, 8) expect(new_frame.unpack).to be == data end end end protocol-http2-0.23.0/test/protocol/http2/data_frame.rb000066400000000000000000000052461506340702200227730ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. require "protocol/http2/data_frame" require "protocol/http2/a_frame" describe Protocol::HTTP2::DataFrame do let(:frame) {subject.new} it_behaves_like Protocol::HTTP2::AFrame do def before frame.pack "Hello World!" super end end with "wire representation" do let(:stream) {StringIO.new} let(:payload) {"Hello World!"} let(:data) do [0, 12, 0x0, 0x1, 0x1].pack("CnCCNC*") + payload end it "should write frame to buffer" do frame.set_flags(Protocol::HTTP2::END_STREAM) frame.stream_id = 1 frame.payload = payload frame.length = payload.bytesize frame.write(stream) expect(stream.string).to be == data end it "should read frame from buffer" do stream.write(data) stream.seek(0) frame.read(stream) expect(frame.length).to be == payload.bytesize expect(frame.flags).to be == Protocol::HTTP2::END_STREAM expect(frame.stream_id).to be == 1 expect(frame.payload).to be == payload end end with "#pack" do it "adds appropriate padding" do frame.pack "Hello World!", padding_size: 4 expect(frame.length).to be == 17 expect(frame.payload[0].ord).to be == 4 expect(frame.unpack).to be == "Hello World!" stream = StringIO.new frame.write(stream) expect(stream.string.bytesize).to be == 26 stream.rewind frame2 = subject.new frame2.read(stream) expect(frame2.unpack).to be == "Hello World!" expect(frame2).to be(:padded?) end it "detects invalid padding" do frame.pack "Hello World!", padding_size: 4 expect(frame.length).to be == 17 expect(frame.payload[0].ord).to be == 4 expect(frame.unpack).to be == "Hello World!" # Artifically set the padding to be the entire payload: frame.payload[0] = (frame.payload.bytesize - 1).chr expect(frame.unpack).to be == "" # Artifically set the padding to be larger than the payload: frame.payload[0] = (frame.payload.bytesize + 1).chr expect do frame.unpack end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Invalid padding length/) end it "can pack end frame" do frame.pack(nil) expect(frame.length).to be == 0 expect(frame.flags).to be == Protocol::HTTP2::END_STREAM expect(frame).to be(:end_stream?) end end with "#unpack" do it "removes padding" do frame.pack "Hello World!" expect(frame.unpack).to be == "Hello World!" end end with "#inspect" do it "can generate a string representation" do expect(frame.inspect).to be =~ /Protocol::HTTP2::DataFrame stream_id=0 flags=0 0b/ end end end protocol-http2-0.23.0/test/protocol/http2/error.rb000066400000000000000000000006571506340702200220420ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2024, by Samuel Williams. require "protocol/http2/error" describe Protocol::HTTP2::HeaderError do let(:error) {subject.new("Invalid header key")} it "should have a message" do expect(error.message).to be == "Invalid header key" end it "should be a kind of StreamClosed" do expect(error).to be_a Protocol::HTTP2::StreamClosed end end protocol-http2-0.23.0/test/protocol/http2/frame.rb000066400000000000000000000056631506340702200220050ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2024, by Samuel Williams. require "protocol/http2/frame" describe Protocol::HTTP2::Frame do let(:frame) {subject.new(0, 0, 0)} with "#read_header" do it "can't read a frame header with an empty stream" do buffer = StringIO.new expect do frame.read_header(buffer) end.to raise_exception(EOFError) end end with "#read_payload" do it "can read a frame with payload and matching length" do buffer = StringIO.new("Hello World!") frame.length = 12 frame.read_payload(buffer) expect(frame.payload).to be == "Hello World!" end it "can't read a frame with an empty stream" do buffer = StringIO.new frame.length = 1 expect do frame.read_payload(buffer) end.to raise_exception(EOFError) end end with "#header" do it "can generate a frame header" do frame.length = 0 expect(frame.header).to be == "\x00\x00\x00\x00\x00\x00\x00\x00\x00" end it "can't generate a frame header with an invalid length" do frame.length = 2**24 expect do frame.header end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Invalid frame length/) end it "can't generate a frame header with an invalid stream_id" do frame.length = 0 frame.stream_id = 2**31 expect do frame.header end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Invalid stream identifier/) end end with "#write_payload" do it "can write a frame with payload and matching length" do buffer = StringIO.new frame.payload = "Hello World!" frame.length = 12 frame.write_payload(buffer) expect(buffer.string).to be == "Hello World!" end end it "isn't a valid frame type" do expect(frame).not.to be(:valid_type?) end it "cannot pack frame with payload larger than maximum frame size" do expect do frame.pack("Hello World!", maximum_size: 6) end.to raise_exception( Protocol::HTTP2::ProtocolError, message: be =~ /Frame length bigger than maximum allowed/ ) end it "cannot write a frame with missing payload and non-zero length" do expect do frame.length = 1 frame.write(StringIO.new) end.to raise_exception( Protocol::HTTP2::ProtocolError, message: be =~ /Invalid frame length/ ) end it "cannot write frame with payload and mismatched length" do expect do frame.payload = "Hello World!" frame.length = 1 frame.write(StringIO.new) end.to raise_exception( Protocol::HTTP2::ProtocolError, message: be =~ /Invalid payload size/ ) end with "a connection" do let(:connection) {Protocol::HTTP2::Connection.new(nil, 0)} it "can apply a frame to the connection" do expect(connection).to receive(:receive_frame) frame.apply(connection) end end with "#inspect" do it "can generate a string representation" do expect(frame.inspect).to be =~ /Protocol::HTTP2::Frame stream_id=0/ end end end protocol-http2-0.23.0/test/protocol/http2/framer.rb000066400000000000000000000023671506340702200221650ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2024, by Samuel Williams. require "protocol/http2/framer" describe Protocol::HTTP2::Framer do let(:stream) {StringIO.new} let(:framer) {subject.new(stream)} with "#flush" do it "flushes the underlying stream" do expect(stream).to receive(:flush) framer.flush end end with "#closed?" do it "reports the status of the underlying stream" do expect(stream).to receive(:closed?).and_return(true) expect(framer).to be(:closed?) end end with "#read_connection_preface" do with "invalid preface" do let(:stream) {StringIO.new("Hello World!")} it "fails with a handshake error" do expect{framer.read_connection_preface}.to raise_exception(Protocol::HTTP2::HandshakeError) end end with "valid preface" do let(:stream) {StringIO.new(Protocol::HTTP2::CONNECTION_PREFACE)} it "reads the connection preface" do expect(framer.read_connection_preface).to be == Protocol::HTTP2::CONNECTION_PREFACE end end end with "#read_frame" do with "invalid frame" do let(:stream) {StringIO.new("!")} it "fails with a end-of-file error" do expect{framer.read_frame}.to raise_exception(EOFError) end end end end protocol-http2-0.23.0/test/protocol/http2/goaway_frame.rb000066400000000000000000000013661506340702200233500ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. require "protocol/http2/goaway_frame" require "protocol/http2/a_frame" describe Protocol::HTTP2::GoawayFrame do let(:data) {"Hikikomori desu!"} let(:frame) {subject.new} it_behaves_like Protocol::HTTP2::AFrame do def before frame.pack 1, 2, data super end end it "applies to the connection" do frame.pack(0, 0, "") expect(frame).to be(:connection?) end with "#pack" do it "packs data" do frame.pack 3, 2, data expect(frame.length).to be == 8+data.bytesize end end with "#unpack" do it "unpacks data" do frame.pack 3, 2, data expect(frame.unpack).to be == [3, 2, data] end end end protocol-http2-0.23.0/test/protocol/http2/headers_frame.rb000066400000000000000000000065471506340702200235020ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. require "protocol/http2/headers_frame" require "protocol/http2/connection_context" require "protocol/http2/a_frame" describe Protocol::HTTP2::HeadersFrame do let(:data) {"Hello World!"} let(:frame) {subject.new} it_behaves_like Protocol::HTTP2::AFrame do def before frame.set_flags(Protocol::HTTP2::END_HEADERS) frame.pack data super end end with "#pack" do it "adds appropriate padding" do frame.pack data expect(frame.length).to be == 12 expect(frame).not.to be(:priority?) end it "ignores priority data" do frame.pack "xxxxx" + data frame.set_flags(Protocol::HTTP2::PRIORITY) expect(frame.length).to be == 17 expect(frame).to be(:priority?) expect(frame.unpack).to be == data end end with "#unpack" do it "removes padding" do frame.pack data expect(frame.unpack).to be == data end end with "#continuation" do let(:stream) {StringIO.new} def before frame.pack "Hello World", maximum_size: 8 super end it "generates chain of frames" do expect(frame).to be(:continued?) expect(frame.continuation).to be(:end_headers?) expect(frame.length).to be == 8 expect(frame.continuation).not.to be_nil expect(frame.continuation.length).to be == 3 end it "can read and write continuation frames" do frame.write(stream) stream.rewind frame2 = subject.new frame2.read(stream, 128) expect(frame).to be(:continued?) expect(frame.continuation).to be(:end_headers?) expect(frame.length).to be == 8 expect(frame.continuation).not.to be_nil expect(frame.continuation.length).to be == 3 end it "fails if the stream id of the continuation doesn't match" do frame.continuation.stream_id = frame.stream_id+1 frame.write(stream) stream.rewind frame2 = subject.new expect do frame2.read(stream, 128) end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Invalid stream id/) end it "fails if the frame type of the continuation doesn't match" do frame.continuation.type = frame.type+1 frame.write(stream) stream.rewind frame2 = subject.new expect do frame2.read(stream, 128) end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Invalid frame type/) end end with "client/server connection" do include_context Protocol::HTTP2::ConnectionContext def before client.open! server.open! # We force this to something low so we can exceed it without hitting the socket buffer: server.local_settings.current.instance_variable_set(:@maximum_frame_size, 128) super end let(:stream) {client.create_stream} it "rejects headers frame that exceeds maximum frame size" do frame.stream_id = stream.id frame.pack "\0" * (server.local_settings.maximum_frame_size + 1) client.write_frame(frame) expect do server.read_frame end.to raise_exception(Protocol::HTTP2::FrameSizeError) expect(client).to receive(:receive_goaway) expect do client.read_frame end.to raise_exception(Protocol::HTTP2::GoawayError) end end with "#inspect" do it "can generate a string representation" do expect(frame.inspect).to be =~ /Protocol::HTTP2::HeadersFrame stream_id=0 flags=0 0b/ end end end protocol-http2-0.23.0/test/protocol/http2/ping_frame.rb000066400000000000000000000025161506340702200230140ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. require "protocol/http2/ping_frame" require "protocol/http2/a_frame" describe Protocol::HTTP2::PingFrame do let(:data) {"PingPong"} let(:frame) {subject.new} it_behaves_like Protocol::HTTP2::AFrame do def before frame.pack data super end end it "applies to the connection" do expect(frame).to be(:connection?) end with "#pack" do it "packs data" do frame.pack data expect(frame.length).to be == 8 end end with "#unpack" do it "unpacks data" do frame.pack data expect(frame.unpack).to be == data end end with "#read_payload" do let(:stream) {StringIO.new([0, 0, 0, 0, 0, 0, 0, 0].pack("C*"))} with "invalid stream id" do it "raises an error" do frame.stream_id = 1 frame.length = 0 expect{frame.read_payload(stream)}.to raise_exception( Protocol::HTTP2::ProtocolError, message: be =~ /Settings apply to connection only, but stream_id was given/ ) end end with "a length other than 8" do it "raises an error" do frame.stream_id = 0 frame.length = 4 expect{frame.read_payload(stream)}.to raise_exception( Protocol::HTTP2::FrameSizeError, message: be =~ /Invalid frame length/ ) end end end end protocol-http2-0.23.0/test/protocol/http2/priority_update_frame.rb000066400000000000000000000027151506340702200253030ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2024-2025, by Samuel Williams. require "protocol/http2/priority_update_frame" require "protocol/http2/a_frame" require "protocol/http2/connection_context" describe Protocol::HTTP2::PriorityUpdateFrame do let(:frame) {subject.new} it_behaves_like Protocol::HTTP2::AFrame do before do frame.pack 1, "u=1, i" end it "applies to the connection" do expect(frame).to be(:connection?) end end with "client/server connection" do include_context Protocol::HTTP2::ConnectionContext def before client.open! server.open! super end it "fails with protocol error if stream id is not zero" do # This isn't a valid for the frame stream_id: frame.stream_id = 1 # This is a valid stream payload: frame.pack stream.id, "u=1, i" expect do frame.apply(server) end.to raise_exception(Protocol::HTTP2::ProtocolError) end let(:stream) {client.create_stream} it "updates the priority of a stream" do stream.send_headers [["content-type", "text/plain"]] server.read_frame expect(server).to receive(:receive_priority_update) expect(stream.priority).to be_nil frame.pack stream.id, "u=1, i" client.write_frame(frame) inform server.read_frame server_stream = server.streams[stream.id] expect(server_stream).not.to be_nil expect(server_stream.priority).to be == ["u=1", "i"] end end end protocol-http2-0.23.0/test/protocol/http2/push_promise_frame.rb000066400000000000000000000052531506340702200245750ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. require "protocol/http2/push_promise_frame" require "protocol/http2/connection_context" require "protocol/http2/a_frame" describe Protocol::HTTP2::PushPromiseFrame do let(:stream_id) {5} let(:data) {"Hello World!"} let(:frame) {subject.new} it_behaves_like Protocol::HTTP2::AFrame do def before frame.set_flags(Protocol::HTTP2::END_HEADERS) frame.pack stream_id, data super end end with "#pack" do it "packs stream_id and data with padding" do frame.pack stream_id, data expect(frame.padded?).to be_falsey expect(frame.length).to be == 16 end end with "#unpack" do it "unpacks stream_id and data" do frame.pack stream_id, data expect(frame.unpack).to be == [stream_id, data] end end with "client/server connection" do include_context Protocol::HTTP2::ConnectionContext def before client.open! server.open! super end let(:stream) {client.create_stream} let(:request_headers) do [[":method", "GET"], [":authority", "Earth"], [":path", "/index.html"]] end let(:push_promise_headers) do [[":method", "GET"], [":authority", "Earth"], [":path", "/index.css"]] end it "sends push promise to client" do # Client sends a request: stream.send_headers(request_headers) # Server receives request: expect(server.read_frame).to be_a Protocol::HTTP2::HeadersFrame # Get the request stream on the server: server_stream = server.streams[stream.id] # Push a promise back through the stream: promised_stream = server_stream.send_push_promise(push_promise_headers) expect(client).to receive(:receive_push_promise) do |frame| stream, headers = super(frame) expect(stream.id).to be == promised_stream.id expect(headers).to be == push_promise_headers end expect(client.read_frame).to be_a Protocol::HTTP2::PushPromiseFrame end it "fails if the same push promise is sent twice" do # Client sends a request: stream.send_headers(request_headers) # Server receives request: expect(server.read_frame).to be_a Protocol::HTTP2::HeadersFrame # Get the request stream on the server: server_stream = server.streams[stream.id] # Push a promise back through the stream: promised_stream = server_stream.send_push_promise(push_promise_headers) push_promise_frame = client.read_frame expect(push_promise_frame).to be_a Protocol::HTTP2::PushPromiseFrame # Force the stream to be sent a 2nd time: expect do push_promise_frame.apply(client) end.to raise_exception(Protocol::HTTP2::Error) end end end protocol-http2-0.23.0/test/protocol/http2/reset_stream_frame.rb000066400000000000000000000016461506340702200245570ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. require "protocol/http2/reset_stream_frame" require "protocol/http2/a_frame" describe Protocol::HTTP2::ResetStreamFrame do let(:error) {Protocol::HTTP2::INTERNAL_ERROR} let(:frame) {subject.new} it_behaves_like Protocol::HTTP2::AFrame do def before frame.pack error super end end with "#pack" do it "packs error" do frame.pack error expect(frame.length).to be == 4 end end with "#unpack" do it "unpacks error" do frame.pack error expect(frame.unpack).to be == error end end with "#read_payload" do let(:stream) {StringIO.new("!!!")} it "must be 4 bytes long" do frame.pack(error) expect(frame.length).to be == 4 frame.length = 3 expect do frame.read_payload(stream) end.to raise_exception(Protocol::HTTP2::FrameSizeError) end end end protocol-http2-0.23.0/test/protocol/http2/server.rb000066400000000000000000000056121506340702200222130ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. require "protocol/http2/connection_context" describe Protocol::HTTP2::Client do include_context Protocol::HTTP2::ConnectionContext let(:framer) {client.framer} let(:client_settings) do [[Protocol::HTTP2::Settings::HEADER_TABLE_SIZE, 1024]] end let(:server_settings) do [[Protocol::HTTP2::Settings::HEADER_TABLE_SIZE, 2048]] end it "has an id of 0" do expect(server.id).to be == 0 expect(server[0]).to be == server end it "can lookup stream by id" do stream = server.create_stream expect(server[stream.id]).to be == stream end it "can optionally support push promises" do expect(server).to be(:enable_push?) end it "cannot accept push promises" do expect do server.accept_push_promise_stream(1) end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot accept push promises/) end it "should start in new state" do expect(server.state).to be == :new end it "fails with protocol error if first frame is not settings frame" do framer.write_connection_preface data_frame = Protocol::HTTP2::DataFrame.new data_frame.pack("Hello, World!") framer.write_frame(data_frame) expect do server.read_connection_preface end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /First frame must be #{Protocol::HTTP2::SettingsFrame}/) end it "cannot read connection preface in open state" do server.open! expect do server.read_connection_preface end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot read connection preface in state open/) end it "should receive connection preface followed by settings frame" do # The client must write the connection preface followed immediately by the first settings frame: framer.write_connection_preface settings_frame = Protocol::HTTP2::SettingsFrame.new settings_frame.pack(client_settings) framer.write_frame(settings_frame) expect(server.state).to be == :new # The server should read the preface and settings... server.read_connection_preface(server_settings) expect(server.remote_settings.header_table_size).to be == 1024 # The server immediately sends its own settings frame... frame = framer.read_frame expect(frame).to be_a Protocol::HTTP2::SettingsFrame expect(frame.unpack).to be == server_settings # And then it acknowledges the client settings: frame = framer.read_frame expect(frame).to be_a Protocol::HTTP2::SettingsFrame expect(frame).to be(:acknowledgement?) # We reply with acknolwedgement: framer.write_frame(frame.acknowledge) server.read_frame expect(server.state).to be == :open end it "can generate a stream id" do id = server.next_stream_id expect(id).to be == 2 expect(server).to be(:local_stream_id?, id) expect(server).not.to be(:remote_stream_id?, id) end end protocol-http2-0.23.0/test/protocol/http2/settings.rb000066400000000000000000000121741506340702200225460ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2024, by Samuel Williams. require "protocol/http2/settings_frame" describe Protocol::HTTP2::Settings do let(:settings) {subject.new} with "#enable_push" do it "is enabled by default" do expect(settings.enable_push).to be == 1 expect(settings).to be(:enable_push?) end it "can be disabled" do settings.enable_push = 0 expect(settings.enable_push).to be == 0 expect(settings).not.to be(:enable_push?) end it "only accepts valid values" do expect{settings.enable_push = 2}.to raise_exception(Protocol::HTTP2::ProtocolError) end end with "#initial_window_size" do it "is 65535 by default" do expect(settings.initial_window_size).to be == 0xFFFF end it "can be updated" do settings.initial_window_size = 1000 expect(settings.initial_window_size).to be == 1000 end it "only accepts valid values" do expect{settings.initial_window_size = (Protocol::HTTP2::MAXIMUM_ALLOWED_WINDOW_SIZE + 1)}.to raise_exception(Protocol::HTTP2::ProtocolError) end end with "#maximum_frame_size" do it "is 16384 by default" do expect(settings.maximum_frame_size).to be == 0x4000 end it "can be updated" do settings.maximum_frame_size = 20000 expect(settings.maximum_frame_size).to be == 20000 end it "only accepts valid values" do expect{settings.maximum_frame_size = (Protocol::HTTP2::MAXIMUM_ALLOWED_FRAME_SIZE + 1)}.to raise_exception(Protocol::HTTP2::ProtocolError) expect{settings.maximum_frame_size = (Protocol::HTTP2::MINIMUM_ALLOWED_FRAME_SIZE - 1)}.to raise_exception(Protocol::HTTP2::ProtocolError) end end with "#enable_connect_protocol" do it "is disabled by default" do expect(settings.enable_connect_protocol).to be == 0 expect(settings).not.to be(:enable_connect_protocol?) end it "can be enabled" do settings.enable_connect_protocol = 1 expect(settings.enable_connect_protocol).to be == 1 expect(settings).to be(:enable_connect_protocol?) end it "only accepts valid values" do expect{settings.enable_connect_protocol = 2}.to raise_exception(Protocol::HTTP2::ProtocolError) end end with "#no_rfc7540_priorities" do it "is disabled by default" do expect(settings.no_rfc7540_priorities).to be == 0 expect(settings).not.to be(:no_rfc7540_priorities?) end it "can be enabled" do settings.no_rfc7540_priorities = 1 expect(settings.no_rfc7540_priorities).to be == 1 expect(settings).to be(:no_rfc7540_priorities?) end it "only accepts valid values" do expect{settings.no_rfc7540_priorities = 2}.to raise_exception(Protocol::HTTP2::ProtocolError) end end it "has the expected defaults" do expect(settings).to have_attributes( header_table_size: be == 4096, enable_push: be == 1, maximum_concurrent_streams: be == 0xFFFFFFFF, initial_window_size: be == 0xFFFF, maximum_frame_size: be == 0x4000, maximum_header_list_size: be == 0xFFFFFFFF, enable_connect_protocol: be == 0, no_rfc7540_priorities: be == 0, ) end it "can be updated" do settings.update({ Protocol::HTTP2::Settings::HEADER_TABLE_SIZE => 100, Protocol::HTTP2::Settings::ENABLE_PUSH => 0, Protocol::HTTP2::Settings::MAXIMUM_CONCURRENT_STREAMS => 10, Protocol::HTTP2::Settings::INITIAL_WINDOW_SIZE => 1000, Protocol::HTTP2::Settings::MAXIMUM_FRAME_SIZE => 20000, Protocol::HTTP2::Settings::MAXIMUM_HEADER_LIST_SIZE => 100000, Protocol::HTTP2::Settings::ENABLE_CONNECT_PROTOCOL => 1 }) expect(settings).to have_attributes( header_table_size: be == 100, enable_push: be == 0, maximum_concurrent_streams: be == 10, initial_window_size: be == 1000, maximum_frame_size: be == 20000, maximum_header_list_size: be == 100000, enable_connect_protocol: be == 1 ) end end describe Protocol::HTTP2::PendingSettings do let(:pending_settings) {subject.new} it "has the expected defaults" do expect(pending_settings).to have_attributes( header_table_size: be == 4096, enable_push: be == 1, maximum_concurrent_streams: be == 0xFFFFFFFF, initial_window_size: be == 0xFFFF, maximum_frame_size: be == 0x4000, maximum_header_list_size: be == 0xFFFFFFFF, enable_connect_protocol: be == 0 ) end it "can append changes" do pending_settings.append([ [Protocol::HTTP2::Settings::HEADER_TABLE_SIZE, 100], [Protocol::HTTP2::Settings::ENABLE_PUSH, 0], ]) expect(pending_settings).to have_attributes( header_table_size: be == 4096, enable_push: be == 1 ) pending_settings.acknowledge expect(pending_settings).to have_attributes( header_table_size: be == 100, enable_push: be == 0 ) end it "can't acknowledge changes that haven't been appended" do expect{pending_settings.acknowledge}.to raise_exception(Protocol::HTTP2::ProtocolError) end it "can't acknowledge changes that have already been acknowledged" do pending_settings.append([ [Protocol::HTTP2::Settings::HEADER_TABLE_SIZE, 100], [Protocol::HTTP2::Settings::ENABLE_PUSH, 0], ]) pending_settings.acknowledge expect{pending_settings.acknowledge}.to raise_exception(Protocol::HTTP2::ProtocolError) end end protocol-http2-0.23.0/test/protocol/http2/settings_frame.rb000066400000000000000000000042411506340702200237140ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. require "protocol/http2/settings_frame" require "protocol/http2/a_frame" describe Protocol::HTTP2::SettingsFrame do let(:settings) {[ [3, 10], [5, 1048576], [4, 2147483647], [8, 1] ]} let(:frame) {subject.new} it_behaves_like Protocol::HTTP2::AFrame do def before frame.pack settings super end end it "applies to the connection" do expect(frame).to be(:connection?) end with "#pack" do it "packs settings" do frame.pack settings expect(frame.length).to be == 6*settings.size end end with "#unpack" do it "unpacks settings" do frame.pack settings expect(frame.unpack).to be == settings end it "can generate acknowledgment" do frame.pack settings acknowledgement_frame = frame.acknowledge expect(acknowledgement_frame).to be_a(Protocol::HTTP2::SettingsFrame) expect(acknowledgement_frame).to be(:acknowledgement?) expect(acknowledgement_frame.length).to be == 0 settings = acknowledgement_frame.unpack expect(settings).to be == [] end it "can unpack empty settings" do end end with "#read_payload" do let(:stream) {StringIO.new([0, 0, 0, 0, 0, 0].pack("C*"))} with "invalid stream id" do it "raises an error" do frame.stream_id = 1 frame.length = 0 expect{frame.read_payload(stream)}.to raise_exception( Protocol::HTTP2::ProtocolError, message: be =~ /Settings apply to connection only, but stream_id was given/ ) end end with "non-zero length acknowledgement" do it "raises an error" do frame.acknowledgement! frame.stream_id = 0 frame.length = 6 expect{frame.read_payload(stream)}.to raise_exception( Protocol::HTTP2::ProtocolError, message: be =~ /Settings acknowledgement must not contain payload/ ) end end with "invalid length modulo 6" do it "raises an error" do frame.stream_id = 0 frame.length = 5 expect{frame.read_payload(stream)}.to raise_exception( Protocol::HTTP2::ProtocolError, message: be =~ /Invalid frame length/ ) end end end end protocol-http2-0.23.0/test/protocol/http2/stream.rb000066400000000000000000000213471506340702200222030ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2025, by Samuel Williams. require "protocol/http2/connection_context" describe Protocol::HTTP2::Stream do include_context Protocol::HTTP2::ConnectionContext def before client.open! server.open! super end it "can create a stream" do stream = client.create_stream expect(stream).to be_a(Protocol::HTTP2::Stream) end it "can create a stream with a block" do stream = client.create_stream do |connection, stream_id| expect(connection).to be_equal(client) expect(stream_id).to be_a(Integer) [connection, stream_id] end expect(stream).to be == [client, 1] end with "idle stream" do let(:stream) {client.create_stream} it "is not active" do expect(stream).not.to be(:active?) end it "can send headers" do expect(stream).to be(:send_headers?) end it "can't receive stream reset" do frame = Protocol::HTTP2::ResetStreamFrame.new(stream.id) expect do stream.receive_reset_stream(frame) end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot receive reset stream/) end it "can inspect the stream" do expect(stream.inspect).to be =~ /id=1 state=idle/ end it "can send headers" do stream.send_headers([["foo", "bar"]]) # Pre-create the stream in idle state: server_stream = server.create_stream(stream.id) expect(server_stream).to receive(:receive_headers) frame = server.read_frame expect(frame).to be_a(Protocol::HTTP2::HeadersFrame) expect(frame).not.to be(:end_stream?) end it "can send request and read response" do stream.send_headers([["foo", "bar"]], Protocol::HTTP2::END_STREAM) server_stream = server.create_stream(stream.id) expect(server_stream).to receive(:receive_headers) server.read_frame expect(server_stream.state).to be == :half_closed_remote server_stream.send_headers([[":status", "200"]]) frame = client.read_frame expect(frame).to be_a(Protocol::HTTP2::HeadersFrame) expect(frame).not.to be(:end_stream?) server_stream.send_data("Hello World!", Protocol::HTTP2::END_STREAM) frame = client.read_frame expect(frame).to be_a(Protocol::HTTP2::DataFrame) expect(frame).to be(:end_stream?) end it "cannot send data" do expect do stream.send_data("Hello World!") end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot send data in state: idle/) end it "cannot send reset stream" do expect do stream.send_reset_stream end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot send reset stream/) end it "cannot receive data" do stream.open! stream.send_data("Hello World!") expect do server.read_frame end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot receive data/) end it "cannot receive stream reset" do stream.open! stream.send_reset_stream expect do server.read_frame end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot reset stream/) end end with "open stream" do let(:stream) {client.create_stream} let(:headers) {[[":method", "GET"], [":path", "/"], [":scheme", "https"], [":authority", "example.com"]]} let(:server_stream) {server[stream.id]} def before stream.send_headers(headers) server.read_frame super end it "is active" do expect(stream).to be(:active?) end it "can send data" do stream.send_data("Hello World!") expect(server_stream).to receive(:receive_data).and_return(nil) server.read_frame end it "can send reset stream" do stream.send_reset_stream expect(server_stream).to receive(:receive_reset_stream).and_return(nil) server.read_frame end it "can send request trailers" do stream.send_headers([["foo", "bar"]], Protocol::HTTP2::END_STREAM) expect(server_stream).to receive(:receive_headers) server.read_frame expect(server_stream.state).to be == :half_closed_remote end it "can send response and end stream" do server_stream.send_headers([["foo", "bar"]], Protocol::HTTP2::END_STREAM) expect(stream).to receive(:receive_headers) client.read_frame expect(stream.state).to be == :half_closed_remote end it "can't be opened (again)" do expect do stream.open! end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot open stream/) end end with "half closed local stream" do let(:stream) {client.create_stream} let(:headers) {[[":method", "GET"], [":path", "/"], [":scheme", "https"], [":authority", "example.com"]]} let(:server_stream) {server[stream.id]} def before stream.send_headers(headers, Protocol::HTTP2::END_STREAM) server.read_frame super end it "is active" do expect(stream).to be(:active?) end it "can send data from server side" do expect(server_stream.state).to be == :half_closed_remote server_stream.send_data("Hello World!", Protocol::HTTP2::END_STREAM) expect(server_stream).to be(:closed?) expect(stream).to receive(:receive_data).and_return(nil) client.read_frame end it "can't send data" do expect do stream.send_data("Hello World!") end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot send data in state: half_closed_local/) end end with "pushed stream" do let(:origin_stream) {client.create_stream} let(:server_stream) {server[origin_stream.id]} let(:promised_stream) {@promised_stream} let(:stream) {client[@pushed_stream_id]} let(:headers) {[[":method", "GET"], [":path", "/"], [":scheme", "https"], [":authority", "example.com"]]} let(:response_headers) {[[":status", "200"]]} def before origin_stream.send_headers(headers) server.read_frame @promised_stream = server_stream.send_push_promise(headers) frame = client.read_frame @pushed_stream_id, data = frame.unpack super end it "can't send headers" do expect(stream).not.to be(:send_headers?) expect do stream.send_headers([["foo", "bar"]]) end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot send headers in state: reserved_remote/) end it "receives stream reset when writing headers in invalid state" do stream.state = :open stream.send_headers([["foo", "bar"]]) frame = server.read_frame frame = client.read_frame expect(frame).to be_a(Protocol::HTTP2::ResetStreamFrame) end it "receives stream reset when writing data in invalid state" do stream.state = :open stream.send_data("Hello World!") frame = server.read_frame frame = client.read_frame expect(frame).to be_a(Protocol::HTTP2::ResetStreamFrame) end it "can send response and end stream" do promised_stream.send_headers(response_headers, Protocol::HTTP2::END_STREAM) expect(promised_stream.state).to be == :half_closed_remote expect(stream).to receive(:receive_headers) client.read_frame expect(stream.state).to be == :half_closed_local end end with "closed stream" do let(:stream) {client.create_stream.close!} it "is not active" do expect(stream).not.to be(:active?) end it "can't send headers" do expect(stream).not.to be(:send_headers?) expect do stream.send_headers([["foo", "bar"]]) end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot send headers in state: closed/) end it "cannot send reset stream" do expect do stream.send_reset_stream end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot send reset stream/) end it "won't accept the same stream" do expect do client.accept_stream(stream.id) end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Invalid stream id/) end it "cannot reserve local" do expect do stream.reserved_local! end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot reserve stream/) end it "cannot reserve remote" do expect do stream.reserved_remote! end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot reserve stream/) end it "cannot send push promise" do expect do stream.send_push_promise([]) end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot send push promise/) end it "ignores headers" do expect(stream).to receive(:ignore_headers) stream.receive_headers(nil) end it "ignores data" do expect(stream).to receive(:ignore_data) stream.receive_data(nil) end it "ignores reset stream" do server_stream = server.create_stream(stream.id) server_stream.open! server_stream.send_reset_stream client.read_frame end end end protocol-http2-0.23.0/test/protocol/http2/window.rb000066400000000000000000000040311506340702200222060ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. require "protocol/http2/connection_context" require "json" describe Protocol::HTTP2::Window do let(:window) {subject.new} with "#consume" do it "can consume available capacity" do expect(window).not.to be(:full?) expect(window).to be(:available?) expect(window.consume(100)).to be == 100 expect(window).not.to be(:full?) window.consume(window.available) expect(window).to be(:full?) expect(window).not.to be(:available?) end end with "#capacity=" do it "fails if assignment to capacity causes the window to exceed the maximum flow control window size" do expect do window.capacity = Protocol::HTTP2::MAXIMUM_ALLOWED_WINDOW_SIZE + 1 end.to raise_exception(Protocol::HTTP2::FlowControlError) end end with "#inspect" do it "can generate a string representation" do expect(window.inspect).to be =~ /available=65535 used=0 capacity=65535/ end end end describe Protocol::HTTP2::LocalWindow do let(:window) {subject.new} with "#consume" do it "can consume available capacity" do window.consume(100) expect(window.wanted).to be == 100 end end with "desired window size" do let(:window) {subject.new(desired: 200)} it "can consume available capacity" do window.consume(window.available) expect(window.wanted).to be == 200 end it "is not limited if the half the desired capacity is available" do expect(window).not.to be(:limited?) # Consume the entire window: window.consume(window.available) expect(window).to be(:limited?) # Expand the window by at least half the desired capacity: window.expand(window.desired / 2) expect(window).not.to be(:limited?) end end with "#limited?" do it "becomes limited after half the capacity is consumed" do expect(window).not.to be(:limited?) # Consume a little more than half: window.consume(window.capacity / 2 + 2) expect(window).to be(:limited?) end end end protocol-http2-0.23.0/test/protocol/http2/window_update_frame.rb000066400000000000000000000173631506340702200247360ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. require "protocol/http2/window_update_frame" require "protocol/http2/connection_context" require "protocol/http2/a_frame" describe Protocol::HTTP2::WindowUpdateFrame do let(:window_size_increment) {1024} let(:frame) {subject.new} it_behaves_like Protocol::HTTP2::AFrame do def before frame.pack window_size_increment super end end with "#pack" do it "packs data" do frame.pack window_size_increment expect(frame.length).to be == 4 end end with "#unpack" do it "unpacks data" do frame.pack window_size_increment expect(frame.unpack).to be == window_size_increment end end with "#read_payload" do let(:stream) {StringIO.new([0, 0, 0, 0].pack("C*"))} with "a length other than 4" do it "raises an error" do frame.stream_id = 0 frame.length = 2 expect{frame.read_payload(stream)}.to raise_exception( Protocol::HTTP2::FrameSizeError, message: be =~ /Invalid frame length/ ) end end end with "a connection" do include_context Protocol::HTTP2::ConnectionContext let(:framer) {client.framer} let(:settings) do [[Protocol::HTTP2::Settings::INITIAL_WINDOW_SIZE, 200]] end let(:headers) do [[":method", "GET"], [":authority", "Earth"]] end let(:stream) do client.create_stream end def before client.send_connection_preface do server.read_connection_preface(settings) end client.read_frame until client.state == :open server.read_frame until server.state == :open stream.send_headers(headers) expect(server.read_frame).to be_a Protocol::HTTP2::HeadersFrame super end it "can determine available frame size" do expect(client.available_frame_size).to be == 16384 expect(server.available_frame_size).to be == 16384 # Fake the maximum frame size: maximum_frame_size = server.available_size + 1 expect(server.available_frame_size(maximum_frame_size)).to be == server.available_size expect(stream.maximum_frame_size).to be == 16384 end it "can consume data frames" do frame = Protocol::HTTP2::DataFrame.new frame.length = client.available_size client.consume_remote_window(frame) expect(client.available_size).to be == 0 end it "fails if it tries to consume more than the available window" do frame = Protocol::HTTP2::DataFrame.new frame.length = client.available_size + 1 expect do client.consume_remote_window(frame) end.to raise_exception(Protocol::HTTP2::FlowControlError, message: be =~ /exceeded window size/) end it "should assign capacity according to settings" do expect(client.remote_settings.initial_window_size).to be == 200 expect(server.local_settings.initial_window_size).to be == 200 expect(client.remote_window.capacity).to be == 0xFFFF expect(server.local_window.capacity).to be == 0xFFFF expect(client.local_window.capacity).to be == 0xFFFF expect(server.remote_window.capacity).to be == 0xFFFF expect(client.local_settings.initial_window_size).to be == 0xFFFF expect(server.remote_settings.initial_window_size).to be == 0xFFFF end it "should send window update after exhausting half of the available window" do # Write 60 bytes of data. stream.send_data("*" * 60) expect(stream.remote_window.used).to be == 60 expect(client.remote_window.used).to be == 60 # puts "Server #{server} #{server.remote_window.inspect} reading frame..." expect(server.read_frame).to be_a Protocol::HTTP2::DataFrame expect(server.local_window.used).to be == 60 # Write another 60 bytes which passes the 50% threshold. stream.send_data("*" * 60) # The server receives a data frame... expect(server.read_frame).to be_a Protocol::HTTP2::DataFrame # ...and must respond with a window update: frame = client.read_frame expect(frame).to be_a Protocol::HTTP2::WindowUpdateFrame expect(frame.unpack).to be == 120 end with "#expand" do it "should expand the window" do expect(client.remote_window.used).to be == 0 expect(client.remote_window.capacity).to be == 0xFFFF expect(client.remote_window).not.to be(:full?) client.remote_window.expand(100) expect(client.remote_window.used).to be == -100 expect(client.remote_window.capacity).to be == 0xFFFF end it "should not expand the window beyond the maximum" do expect(client.remote_window.used).to be == 0 expect(client.remote_window.capacity).to be == 0xFFFF expect do client.remote_window.expand(Protocol::HTTP2::MAXIMUM_ALLOWED_WINDOW_SIZE + 1) end.to raise_exception(Protocol::HTTP2::FlowControlError) expect(client.remote_window.used).to be == 0 expect(client.remote_window.capacity).to be == 0xFFFF end end with "#receive_window_update" do it "should be invoked when window update is received" do # Write 200 bytes of data (client -> server) which exhausts server local window stream.send_data("*" * 200) expect(server.read_frame).to be_a Protocol::HTTP2::DataFrame expect(server.local_window.used).to be == 200 expect(client.remote_window.used).to be == 200 # Window update was sent, and used data was zeroed: server_stream = server.streams[stream.id] expect(server_stream.local_window.used).to be == 0 # ...and must respond with a window update for the stream: expect(stream).to receive(:receive_window_update).once frame = client.read_frame expect(frame).to be_a Protocol::HTTP2::WindowUpdateFrame expect(frame.unpack).to be == 200 end it "should be invoked when window update is received for the connection" do frame = nil (client.available_size / 200).times do stream.send_data("*" * 200) expect(server.read_frame).to be_a Protocol::HTTP2::DataFrame frame = client.read_frame expect(frame).to be_a Protocol::HTTP2::WindowUpdateFrame end expect(client).to receive(:receive_window_update).twice stream.send_data("*" * client.available_size) expect(server.read_frame).to be_a Protocol::HTTP2::DataFrame frame = client.read_frame expect(frame).to be_a(Protocol::HTTP2::WindowUpdateFrame) expect(frame).to be(:connection?) # stream_id = 0 frame = client.read_frame expect(frame).to be_a Protocol::HTTP2::WindowUpdateFrame expect(frame).to have_attributes(stream_id: be == stream.id) stream.send_data("*" * 200) expect(server.read_frame).to be_a Protocol::HTTP2::DataFrame end it "should send stream reset if window update is invalid" do window_update_frame = Protocol::HTTP2::WindowUpdateFrame.new(stream.id) window_update_frame.pack(0) server.write_frame(window_update_frame) expect(stream).to receive(:send_reset_stream) client.read_frame end it "should fail when window update is received for an idle stream" do window_update_frame = Protocol::HTTP2::WindowUpdateFrame.new(stream.id+2) window_update_frame.pack(100) server.write_frame(window_update_frame) expect do frame = client.read_frame end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /Cannot update window of idle stream/) end end with "desired capacity" do it "should send window updates only as needed" do expect(client.local_window.desired).to be == 0xFFFF server_stream = server[stream.id] # Send a data frame that will consume less than half of the desired capacity: server_stream.send_data("*" * 0xFF) expect(client.read_frame).to be_a Protocol::HTTP2::DataFrame expect(client.local_window.used).to be == 0xFF expect(client.local_window).not.to be(:limited?) end end end end