# frozen_string_literal: true module HTTPX module Parser Error = Class.new(Error) class HTTP1 VERSIONS = %w[1.0 1.1].freeze attr_reader :status_code, :http_version, :headers def initialize(observer, header_separator: ":") @observer = observer @state = :idle @header_separator = header_separator @buffer = "".b @headers = {} end def <<(chunk) @buffer << chunk parse end def reset! @state = :idle @headers.clear @content_length = nil @_has_trailers = nil end def upgrade? @upgrade end def upgrade_data @buffer end private def parse state = @state case @state when :idle parse_headline when :headers parse_headers when :trailers parse_headers when :data parse_data end parse if !@buffer.empty? && state != @state end def parse_headline idx = @buffer.index("\n") return unless idx (m = %r{\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?}in.match(@buffer)) || raise(Error, "wrong head line format") version, code, _ = m.captures raise(Error, "unsupported HTTP version (HTTP/#{version})") unless VERSIONS.include?(version) @http_version = version.split(".").map(&:to_i) @status_code = code.to_i raise(Error, "wrong status code (#{@status_code})") unless (100..599).cover?(@status_code) @buffer.slice!(0, idx + 1) nextstate(:headers) end def parse_headers headers = @headers while (idx = @buffer.index("\n")) line = @buffer.slice!(0, idx + 1).sub(/\s+\z/, "") if line.empty? case @state when :headers prepare_data(headers) @observer.on_headers(headers) return unless @state == :headers # state might have been reset # in the :headers callback nextstate(:data) headers.clear when :trailers @observer.on_trailers(headers) headers.clear nextstate(:complete) else raise Error, "wrong header format" end return end separator_index = line.index(@header_separator) raise Error, "wrong header format" unless separator_index key = line[0..separator_index - 1] raise Error, "wrong header format" if key.start_with?("\s", "\t") key.strip! value = line[separator_index + 1..-1] value.strip! raise Error, "wrong header format" if value.nil? (headers[key.downcase] ||= []) << value end end def parse_data if @buffer.respond_to?(:each) @buffer.each do |chunk| @observer.on_data(chunk) end elsif @content_length data = @buffer.slice!(0, @content_length) @content_length -= data.bytesize @observer.on_data(data) data.clear else @observer.on_data(@buffer) @buffer.clear end return unless no_more_data? @buffer = @buffer.to_s if @_has_trailers nextstate(:trailers) else nextstate(:complete) end end def prepare_data(headers) @upgrade = headers.key?("upgrade") @_has_trailers = headers.key?("trailer") if (tr_encodings = headers["transfer-encoding"]) tr_encodings.reverse_each do |tr_encoding| tr_encoding.split(/ *, */).each do |encoding| case encoding when "chunked" @buffer = Transcoder::Chunker::Decoder.new(@buffer, @_has_trailers) end end end else @content_length = headers["content-length"][0].to_i if headers.key?("content-length") end end def no_more_data? if @content_length @content_length <= 0 elsif @buffer.respond_to?(:finished?) @buffer.finished? else false end end def nextstate(state) @state = state case state when :headers @observer.on_start when :complete @observer.on_complete reset! nextstate(:idle) unless @buffer.empty? end end end end end