diff --git a/lib/protocol/http2/client.rb b/lib/protocol/http2/client.rb index fc7bd77..de4def2 100644 --- a/lib/protocol/http2/client.rb +++ b/lib/protocol/http2/client.rb @@ -7,23 +7,45 @@ 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 @@ -42,10 +64,15 @@ def send_connection_preface(settings = []) 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!" diff --git a/lib/protocol/http2/connection.rb b/lib/protocol/http2/connection.rb index 1308f03..381a5b7 100644 --- a/lib/protocol/http2/connection.rb +++ b/lib/protocol/http2/connection.rb @@ -12,9 +12,14 @@ 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() @@ -41,10 +46,15 @@ def initialize(framer, local_stream_id) @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 @@ -89,6 +99,9 @@ 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 @@ -106,10 +119,17 @@ def close(error = 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 @@ -141,6 +161,9 @@ def ignore_frame?(frame) 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 @@ -171,6 +194,8 @@ def read_frame 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) @@ -197,6 +222,9 @@ def send_goaway(error_code = 0, message = "") 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 @@ -209,6 +237,8 @@ def receive_goaway(frame) 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) @@ -217,6 +247,10 @@ def write_frame(frame) @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 @@ -229,6 +263,8 @@ def write_frames 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 @@ -239,6 +275,8 @@ def update_local_settings(changes) @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 @@ -273,12 +311,17 @@ def process_settings(frame) 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: @@ -290,6 +333,8 @@ def receive_settings(frame) 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 @@ -301,6 +346,9 @@ def send_ping(data) 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`: @@ -318,6 +366,9 @@ def receive_ping(frame) 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) @@ -330,6 +381,10 @@ def receive_data(frame) 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 @@ -366,6 +421,10 @@ def create_stream(id = next_stream_id, &block) 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 @@ -397,10 +456,16 @@ def receive_headers(frame) 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}" @@ -414,14 +479,25 @@ def receive_priority_update(frame) 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. @@ -450,6 +526,9 @@ def closed_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!" @@ -475,6 +554,8 @@ def consume_window(size = self.available_size) 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 @@ -494,10 +575,15 @@ def receive_window_update(frame) 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 diff --git a/lib/protocol/http2/continuation_frame.rb b/lib/protocol/http2/continuation_frame.rb index 1b9f9f2..a826660 100644 --- a/lib/protocol/http2/continuation_frame.rb +++ b/lib/protocol/http2/continuation_frame.rb @@ -7,21 +7,31 @@ module Protocol module HTTP2 + # Module for frames that can be continued with CONTINUATION frames. module Continued + # 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. + # @parameter stream [IO] The stream to read from. + # @parameter maximum_frame_size [Integer] Maximum allowed frame size. def read(stream, maximum_frame_size) super @@ -44,6 +54,8 @@ def read(stream, maximum_frame_size) end end + # Write the frame and any continuation frames to the stream. + # @parameter stream [IO] The stream to write to. def write(stream) super @@ -54,6 +66,9 @@ def write(stream) 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] @@ -75,6 +90,8 @@ def pack(data, **options) end end + # Unpack data from this frame and any continuation frames. + # @returns [String] The complete unpacked data. def unpack if @continuation.nil? super @@ -100,6 +117,8 @@ 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 diff --git a/lib/protocol/http2/data_frame.rb b/lib/protocol/http2/data_frame.rb index 1eeba48..d3eece8 100644 --- a/lib/protocol/http2/data_frame.rb +++ b/lib/protocol/http2/data_frame.rb @@ -25,10 +25,16 @@ class DataFrame < Frame 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 @@ -38,10 +44,14 @@ def pack(data, *arguments, **options) 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 diff --git a/lib/protocol/http2/error.rb b/lib/protocol/http2/error.rb index 03fca6f..cfb6fcd 100644 --- a/lib/protocol/http2/error.rb +++ b/lib/protocol/http2/error.rb @@ -62,6 +62,9 @@ class HandshakeError < Error # 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) @@ -71,26 +74,36 @@ def initialize(message, code = PROTOCOL_ERROR) 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 @@ -98,6 +111,8 @@ def initialize(message) # 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 diff --git a/lib/protocol/http2/flow_controlled.rb b/lib/protocol/http2/flow_controlled.rb index 0f200e5..8cc5201 100644 --- a/lib/protocol/http2/flow_controlled.rb +++ b/lib/protocol/http2/flow_controlled.rb @@ -8,7 +8,11 @@ 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 @@ -40,17 +44,22 @@ def consume_remote_window(frame) 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) @@ -67,6 +76,9 @@ def send_window_update(window_increment) @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 diff --git a/lib/protocol/http2/frame.rb b/lib/protocol/http2/frame.rb index 301da65..690d3dc 100644 --- a/lib/protocol/http2/frame.rb +++ b/lib/protocol/http2/frame.rb @@ -17,6 +17,9 @@ module HTTP2 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 @@ -43,14 +46,21 @@ def initialize(stream_id = 0, flags = 0, type = self.class::TYPE, length = nil, @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 @@ -73,10 +83,16 @@ def to_ary 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 @@ -86,14 +102,21 @@ def pack(payload, maximum_size: nil) 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 @@ -101,7 +124,7 @@ def flag_set?(mask) # Check if frame is a connection frame: SETTINGS, PING, GOAWAY, and any # frame addressed to stream ID = 0. # - # @return [Boolean] + # @return [Boolean] If this is a connection frame. def connection? @stream_id.zero? end @@ -145,6 +168,9 @@ def self.parse_header(buffer) 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) @@ -154,6 +180,9 @@ def read_header(stream) 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 @@ -162,6 +191,11 @@ def read_payload(stream) 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 @@ -172,14 +206,21 @@ def read(stream, maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE) 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? @@ -196,10 +237,14 @@ def write(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 diff --git a/lib/protocol/http2/framer.rb b/lib/protocol/http2/framer.rb index 964d414..dc710ff 100644 --- a/lib/protocol/http2/framer.rb +++ b/lib/protocol/http2/framer.rb @@ -42,28 +42,40 @@ module HTTP2 # 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) @@ -105,6 +117,9 @@ def write_frame(frame) 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 diff --git a/lib/protocol/http2/goaway_frame.rb b/lib/protocol/http2/goaway_frame.rb index e8ade17..ab0bd77 100644 --- a/lib/protocol/http2/goaway_frame.rb +++ b/lib/protocol/http2/goaway_frame.rb @@ -21,10 +21,14 @@ 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 @@ -33,10 +37,16 @@ def unpack 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 diff --git a/lib/protocol/http2/headers_frame.rb b/lib/protocol/http2/headers_frame.rb index b653033..9fb5ec8 100644 --- a/lib/protocol/http2/headers_frame.rb +++ b/lib/protocol/http2/headers_frame.rb @@ -28,14 +28,20 @@ class HeadersFrame < Frame 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 @@ -47,6 +53,10 @@ def unpack 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 @@ -55,10 +65,14 @@ def pack(data, *arguments, **options) 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 diff --git a/lib/protocol/http2/padded.rb b/lib/protocol/http2/padded.rb index c17ab2e..78fb5a7 100644 --- a/lib/protocol/http2/padded.rb +++ b/lib/protocol/http2/padded.rb @@ -18,11 +18,19 @@ module HTTP2 # | 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) @@ -44,6 +52,9 @@ def pack(data, padding_size: nil, maximum_size: nil) 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 diff --git a/lib/protocol/http2/ping_frame.rb b/lib/protocol/http2/ping_frame.rb index c18e4c3..679f57c 100644 --- a/lib/protocol/http2/ping_frame.rb +++ b/lib/protocol/http2/ping_frame.rb @@ -9,15 +9,22 @@ 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 @@ -41,14 +48,20 @@ class PingFrame < Frame 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 @@ -57,6 +70,9 @@ def acknowledge 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 diff --git a/lib/protocol/http2/priority_update_frame.rb b/lib/protocol/http2/priority_update_frame.rb index 49436ae..919a557 100644 --- a/lib/protocol/http2/priority_update_frame.rb +++ b/lib/protocol/http2/priority_update_frame.rb @@ -21,6 +21,8 @@ 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 @@ -29,10 +31,16 @@ def unpack 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 diff --git a/lib/protocol/http2/push_promise_frame.rb b/lib/protocol/http2/push_promise_frame.rb index f4e360f..b166a4c 100644 --- a/lib/protocol/http2/push_promise_frame.rb +++ b/lib/protocol/http2/push_promise_frame.rb @@ -27,6 +27,8 @@ class PushPromiseFrame < Frame 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 @@ -35,10 +37,17 @@ def unpack 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 diff --git a/lib/protocol/http2/reset_stream_frame.rb b/lib/protocol/http2/reset_stream_frame.rb index 09f924c..0822b1a 100644 --- a/lib/protocol/http2/reset_stream_frame.rb +++ b/lib/protocol/http2/reset_stream_frame.rb @@ -32,19 +32,28 @@ 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 diff --git a/lib/protocol/http2/server.rb b/lib/protocol/http2/server.rb index 634a05a..9213138 100644 --- a/lib/protocol/http2/server.rb +++ b/lib/protocol/http2/server.rb @@ -7,23 +7,44 @@ 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 @@ -40,10 +61,15 @@ def read_connection_preface(settings = []) 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 diff --git a/lib/protocol/http2/settings_frame.rb b/lib/protocol/http2/settings_frame.rb index 811401d..37dfa7c 100644 --- a/lib/protocol/http2/settings_frame.rb +++ b/lib/protocol/http2/settings_frame.rb @@ -7,6 +7,7 @@ module Protocol module HTTP2 + # HTTP/2 connection settings container and management. class Settings HEADER_TABLE_SIZE = 0x1 ENABLE_PUSH = 0x2 @@ -30,6 +31,7 @@ class Settings :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 @@ -49,6 +51,9 @@ def initialize # 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 @@ -57,6 +62,8 @@ def 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 @@ -67,6 +74,9 @@ def enable_push? # 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 @@ -78,6 +88,9 @@ def initial_window_size= value # 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}" @@ -93,6 +106,9 @@ def maximum_frame_size= value 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 @@ -101,12 +117,17 @@ def 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 @@ -115,10 +136,14 @@ def 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] @@ -128,7 +153,10 @@ def update(changes) 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 @@ -139,11 +167,14 @@ def initialize(current = Settings.new) 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) @@ -154,30 +185,44 @@ def acknowledge 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 @@ -197,10 +242,14 @@ class SettingsFrame < Frame 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. @@ -210,14 +259,22 @@ def unpack 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 diff --git a/lib/protocol/http2/stream.rb b/lib/protocol/http2/stream.rb index 74879db..0d45725 100644 --- a/lib/protocol/http2/stream.rb +++ b/lib/protocol/http2/stream.rb @@ -60,6 +60,10 @@ module HTTP2 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) @@ -68,6 +72,10 @@ def self.create(connection, id) 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 @@ -95,18 +103,26 @@ def initialize(connection, id, state = :idle) # @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 @@ -170,6 +186,8 @@ def send_headers(*arguments) 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 @@ -187,6 +205,9 @@ def consume_remote_window(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) @@ -205,6 +226,9 @@ def send_data(*arguments, **options) 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 @@ -234,6 +258,8 @@ def close!(error_code = nil) 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) @@ -247,6 +273,9 @@ def send_reset_stream(error_code = 0) 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 @@ -258,6 +287,8 @@ def process_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? @@ -295,6 +326,8 @@ 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 @@ -325,6 +358,10 @@ def receive_data(frame) 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. @@ -355,6 +392,9 @@ def receive_reset_stream(frame) 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 @@ -365,6 +405,9 @@ def reserved_local! 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 @@ -402,6 +445,8 @@ 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) @@ -412,6 +457,8 @@ def receive_push_promise(frame) 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 diff --git a/lib/protocol/http2/window.rb b/lib/protocol/http2/window.rb index a4d3a86..ff7950d 100644 --- a/lib/protocol/http2/window.rb +++ b/lib/protocol/http2/window.rb @@ -5,10 +5,12 @@ 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: @@ -40,6 +42,8 @@ def capacity= value @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 @@ -47,10 +51,15 @@ def consume(amount) 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 @@ -63,14 +72,20 @@ def expand(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 @@ -80,6 +95,9 @@ def inspect # 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) @@ -91,6 +109,8 @@ def initialize(capacity = DEFAULT_CAPACITY, desired: nil) # 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. @@ -100,6 +120,8 @@ def wanted 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: @@ -109,6 +131,8 @@ def limited? 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 diff --git a/lib/protocol/http2/window_update_frame.rb b/lib/protocol/http2/window_update_frame.rb index 72417b3..045d7e6 100644 --- a/lib/protocol/http2/window_update_frame.rb +++ b/lib/protocol/http2/window_update_frame.rb @@ -18,14 +18,21 @@ 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 @@ -34,6 +41,8 @@ def read_payload(stream) 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