lib/http/2/framer.rb in http-2-0.6.3 vs lib/http/2/framer.rb in http-2-0.7.0

- old
+ new

@@ -1,108 +1,141 @@ module HTTP2 - # Performs encoding, decoding, and validation of binary HTTP 2.0 frames. + # Performs encoding, decoding, and validation of binary HTTP/2 frames. # class Framer include Error - # Maximum frame size (65535 bytes) - MAX_PAYLOAD_SIZE = 2**16-1 + # Default value of max frame size (16384 bytes) + DEFAULT_MAX_FRAME_SIZE = 2**14 + # Current maximum frame size + attr_accessor :max_frame_size + # Maximum stream ID (2^31) MAX_STREAM_ID = 0x7fffffff # Maximum window increment value (2^31) MAX_WINDOWINC = 0x7fffffff - # HTTP 2.0 frame type mapping as defined by the spec + # HTTP/2 frame type mapping as defined by the spec FRAME_TYPES = { data: 0x0, headers: 0x1, priority: 0x2, rst_stream: 0x3, settings: 0x4, push_promise: 0x5, ping: 0x6, goaway: 0x7, - window_update: 0x9, - continuation: 0xa + window_update: 0x8, + continuation: 0x9, + altsvc: 0xa, } + FRAME_TYPES_WITH_PADDING = [ :data, :headers, :push_promise ] + # Per frame flags as defined by the spec FRAME_FLAGS = { data: { - end_stream: 0, reserved: 1 + end_stream: 0, + padded: 3, compressed: 5 }, headers: { - end_stream: 0, reserved: 1, - end_headers: 2, priority: 3 + end_stream: 0, end_headers: 2, + padded: 3, priority: 5, }, priority: {}, rst_stream: {}, - settings: {}, - push_promise: { end_push_promise: 0 }, - ping: { pong: 0 }, + settings: { ack: 0 }, + push_promise: { + end_headers: 2, + padded: 3, + }, + ping: { ack: 0 }, goaway: {}, window_update:{}, - continuation: { - end_stream: 0, end_headers: 1 - } + continuation: { end_headers: 2 }, + altsvc: {}, } # Default settings as defined by the spec DEFINED_SETTINGS = { - settings_max_concurrent_streams: 4, - settings_initial_window_size: 7, - settings_flow_control_options: 10 + settings_header_table_size: 1, + settings_enable_push: 2, + settings_max_concurrent_streams: 3, + settings_initial_window_size: 4, + settings_max_frame_size: 5, + settings_max_header_list_size: 6, } # Default error types as defined by the spec DEFINED_ERRORS = { no_error: 0, protocol_error: 1, internal_error: 2, flow_control_error: 3, + settings_timeout: 4, stream_closed: 5, - frame_too_large: 6, + frame_size_error: 6, refused_stream: 7, cancel: 8, - compression_error: 9 + compression_error: 9, + connect_error: 10, + enhance_your_calm: 11, + inadequate_security: 12, } RBIT = 0x7fffffff RBYTE = 0x0fffffff - HEADERPACK = "nCCN" - UINT32 = "N" + EBIT = 0x80000000 + UINT32 = "N".freeze + UINT16 = "n".freeze + UINT8 = "C".freeze + HEADERPACK = (UINT8 + UINT16 + UINT8 + UINT8 + UINT32).freeze + FRAME_LENGTH_HISHIFT = 16 + FRAME_LENGTH_LOMASK = 0xFFFF + BINARY = 'binary'.freeze - private_constant :RBIT, :RBYTE, :HEADERPACK, :UINT32 + private_constant :RBIT, :RBYTE, :EBIT, :HEADERPACK, :UINT32, :UINT16, :UINT8, :BINARY - # Generates common 8-byte frame header. - # - http://tools.ietf.org/html/draft-ietf-httpbis-http2-04#section-4.1 + # Initializes new framer object. # + def initialize + @max_frame_size = DEFAULT_MAX_FRAME_SIZE + end + + # Generates common 9-byte frame header. + # - http://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-4.1 + # # @param frame [Hash] # @return [String] def commonHeader(frame) header = [] if !FRAME_TYPES[frame[:type]] raise CompressionError.new("Invalid frame type (#{frame[:type]})") end - if frame[:length] > MAX_PAYLOAD_SIZE + if frame[:length] > @max_frame_size raise CompressionError.new("Frame size is too large: #{frame[:length]}") end + if frame[:length] < 0 + raise CompressionError.new("Frame size is invalid: #{frame[:length]}") + end + if frame[:stream] > MAX_STREAM_ID raise CompressionError.new("Stream ID (#{frame[:stream]}) is too large") end if frame[:type] == :window_update && frame[:increment] > MAX_WINDOWINC raise CompressionError.new("Window increment (#{frame[:increment]}) is too large") end - header << frame[:length] + header << (frame[:length] >> FRAME_LENGTH_HISHIFT) + header << (frame[:length] & FRAME_LENGTH_LOMASK) header << FRAME_TYPES[frame[:type]] header << frame[:flags].reduce(0) do |acc, f| position = FRAME_FLAGS[frame[:type]][f] if !position raise CompressionError.new("Invalid frame flag (#{f}) for #{frame[:type]}") @@ -111,31 +144,34 @@ acc |= (1 << position) acc end header << frame[:stream] - header.pack(HEADERPACK) # 16,8,8,32 + header.pack(HEADERPACK) # 8+16,8,8,32 end - # Decodes common 8-byte header. + # Decodes common 9-byte header. # # @param buf [Buffer] def readCommonHeader(buf) frame = {} - frame[:length], type, flags, stream = buf.slice(0,8).unpack(HEADERPACK) + len_hi, len_lo, type, flags, stream = buf.slice(0,9).unpack(HEADERPACK) + frame[:length] = (len_hi << FRAME_LENGTH_HISHIFT) | len_lo frame[:type], _ = FRAME_TYPES.select { |t,pos| type == pos }.first - frame[:flags] = FRAME_FLAGS[frame[:type]].reduce([]) do |acc, (name, pos)| - acc << name if (flags & (1 << pos)) > 0 - acc + if frame[:type] + frame[:flags] = FRAME_FLAGS[frame[:type]].reduce([]) do |acc, (name, pos)| + acc << name if (flags & (1 << pos)) > 0 + acc + end end frame[:stream] = stream & RBIT frame end - # Generates encoded HTTP 2.0 frame. + # Generates encoded HTTP/2 frame. # - http://tools.ietf.org/html/draft-ietf-httpbis-http2 # # @param frame [Hash] def generate(frame) bytes = Buffer.new @@ -148,25 +184,35 @@ when :data bytes << frame[:payload] length += frame[:payload].bytesize when :headers - if frame[:priority] + if frame[:weight] || frame[:stream_dependency] || !frame[:exclusive].nil? + unless frame[:weight] && frame[:stream_dependency] && !frame[:exclusive].nil? + raise CompressionError.new("Must specify all of priority parameters for #{frame[:type]}") + end frame[:flags] += [:priority] if !frame[:flags].include? :priority end if frame[:flags].include? :priority - bytes << [frame[:priority] & RBIT].pack(UINT32) - length += 4 + bytes << [(frame[:exclusive] ? EBIT : 0) | + (frame[:stream_dependency] & RBIT)].pack(UINT32) + bytes << [frame[:weight] - 1].pack(UINT8) + length += 5 end bytes << frame[:payload] length += frame[:payload].bytesize when :priority - bytes << [frame[:priority] & RBIT].pack(UINT32) - length += 4 + unless frame[:weight] && frame[:stream_dependency] && !frame[:exclusive].nil? + raise CompressionError.new("Must specify all of priority parameters for #{frame[:type]}") + end + bytes << [(frame[:exclusive] ? EBIT : 0) | + (frame[:stream_dependency] & RBIT)].pack(UINT32) + bytes << [frame[:weight] - 1].pack(UINT8) + length += 5 when :rst_stream bytes << pack_error(frame[:error]) length += 4 @@ -174,21 +220,23 @@ if frame[:stream] != 0 raise CompressionError.new("Invalid stream ID (#{frame[:stream]})") end frame[:payload].each do |(k,v)| - if !k.is_a? Integer + if k.is_a? Integer + DEFINED_SETTINGS.has_value?(k) or next + else k = DEFINED_SETTINGS[k] if k.nil? raise CompressionError.new("Unknown settings ID for #{k}") end end - bytes << [k & RBYTE].pack(UINT32) + bytes << [k].pack(UINT16) bytes << [v].pack(UINT32) - length += 8 + length += 6 end when :push_promise bytes << [frame[:promise_stream] & RBIT].pack(UINT32) bytes << frame[:payload] @@ -217,79 +265,177 @@ length += 4 when :continuation bytes << frame[:payload] length += frame[:payload].bytesize + + when :altsvc + bytes << [frame[:max_age], frame[:port]].pack(UINT32 + UINT16) + length += 6 + if frame[:proto] + frame[:proto].bytesize > 255 and raise CompressionError.new("Proto too long") + bytes << [frame[:proto].bytesize].pack(UINT8) << frame[:proto].force_encoding(BINARY) + length += 1 + frame[:proto].bytesize + else + bytes << [0].pack(UINT8) + length += 1 + end + if frame[:host] + frame[:host].bytesize > 255 and raise CompressionError.new("Host too long") + bytes << [frame[:host].bytesize].pack(UINT8) << frame[:host].force_encoding(BINARY) + length += 1 + frame[:host].bytesize + else + bytes << [0].pack(UINT8) + length += 1 + end + if frame[:origin] + bytes << frame[:origin] + length += frame[:origin].bytesize + end end + # Process padding. + # frame[:padding] gives number of extra octets to be added. + # - http://tools.ietf.org/html/draft-ietf-httpbis-http2-12#section-6.1 + if frame[:padding] + unless FRAME_TYPES_WITH_PADDING.include?(frame[:type]) + raise CompressionError.new("Invalid padding flag for #{frame[:type]}") + end + + padlen = frame[:padding] + + if padlen <= 0 || padlen > 256 || padlen + length > @max_frame_size + raise CompressionError.new("Invalid padding #{padlen}") + end + + length += padlen + bytes.prepend([padlen -= 1].pack(UINT8)) + frame[:flags] << :padded + + # Padding: Padding octets that contain no application semantic value. + # Padding octets MUST be set to zero when sending and ignored when + # receiving. + bytes << "\0" * padlen + end + frame[:length] = length bytes.prepend(commonHeader(frame)) end - # Decodes complete HTTP 2.0 frame from provided buffer. If the buffer + # Decodes complete HTTP/2 frame from provided buffer. If the buffer # does not contain enough data, no further work is performed. # # @param buf [Buffer] def parse(buf) - return nil if buf.size < 8 + return nil if buf.size < 9 frame = readCommonHeader(buf) - return nil if buf.size < 8 + frame[:length] + return nil if buf.size < 9 + frame[:length] - buf.read(8) + buf.read(9) payload = buf.read(frame[:length]) + # Implementations MUST discard frames + # that have unknown or unsupported types. + # - http://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-5.5 + return nil if frame[:type].nil? + + # Process padding + padlen = 0 + if FRAME_TYPES_WITH_PADDING.include?(frame[:type]) + padded = frame[:flags].include?(:padded) + if padded + padlen = payload.read(1).unpack(UINT8).first + frame[:padding] = padlen + 1 + padlen > payload.bytesize and raise ProtocolError.new("padding too long") + padlen > 0 and payload.slice!(-padlen,padlen) + frame[:length] -= frame[:padding] + frame[:flags].delete(:padded) + end + end + case frame[:type] when :data frame[:payload] = payload.read(frame[:length]) when :headers if frame[:flags].include? :priority - frame[:priority] = payload.read_uint32 & RBIT + e_sd = payload.read_uint32 + frame[:stream_dependency] = e_sd & RBIT + frame[:exclusive] = (e_sd & EBIT) != 0 + frame[:weight] = payload.getbyte + 1 end frame[:payload] = payload.read(frame[:length]) when :priority - frame[:priority] = payload.read_uint32 & RBIT + e_sd = payload.read_uint32 + frame[:stream_dependency] = e_sd & RBIT + frame[:exclusive] = (e_sd & EBIT) != 0 + frame[:weight] = payload.getbyte + 1 when :rst_stream frame[:error] = unpack_error payload.read_uint32 when :settings - frame[:payload] = {} - (frame[:length] / 8).times do - id = payload.read_uint32 & RBYTE + # NOTE: frame[:length] might not match the number of frame[:payload] + # because unknown extensions are ignored. + frame[:payload] = [] + unless frame[:length] % 6 == 0 + raise ProtocolError.new("Invalid settings payload length") + end + + if frame[:stream] != 0 + raise ProtocolError.new("Invalid stream ID (#{frame[:stream]})") + end + + (frame[:length] / 6).times do + id = payload.read(2).unpack(UINT16).first val = payload.read_uint32 # Unsupported or unrecognized settings MUST be ignored. + # Here we send it along. name, _ = DEFINED_SETTINGS.select { |name, v| v == id }.first - frame[:payload][name] = val if name + frame[:payload] << [name, val] if name end when :push_promise frame[:promise_stream] = payload.read_uint32 & RBIT frame[:payload] = payload.read(frame[:length]) when :ping frame[:payload] = payload.read(frame[:length]) when :goaway frame[:last_stream] = payload.read_uint32 & RBIT frame[:error] = unpack_error payload.read_uint32 - size = frame[:length] - 8 + size = frame[:length] - 8 # for last_stream and error frame[:payload] = payload.read(size) if size > 0 when :window_update frame[:increment] = payload.read_uint32 & RBIT when :continuation frame[:payload] = payload.read(frame[:length]) + when :altsvc + frame[:max_age], frame[:port] = payload.read(6).unpack(UINT32 + UINT16) + + len = payload.getbyte + len > 0 and frame[:proto] = payload.read(len) + + len = payload.getbyte + len > 0 and frame[:host] = payload.read(len) + + if payload.size > 0 + frame[:origin] = payload.read(payload.size) + end + else + # Unknown frame type is explicitly allowed end frame end private def pack_error(e) if !e.is_a? Integer - e = DEFINED_ERRORS[e] - - if e.nil? + if DEFINED_ERRORS[e].nil? raise CompressionError.new("Unknown error ID for #{e}") end + + e = DEFINED_ERRORS[e] end [e].pack(UINT32) end