File: http1.rb

package info (click to toggle)
ruby-httpx 1.7.2-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,816 kB
  • sloc: ruby: 12,209; makefile: 4
file content (187 lines) | stat: -rw-r--r-- 4,852 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# frozen_string_literal: true

module HTTPX
  module Parser
    class Error < Error; end

    class HTTP1
      VERSIONS = %w[1.0 1.1].freeze

      attr_reader :status_code, :http_version, :headers

      def initialize(observer)
        @observer = observer
        @state = :idle
        @buffer = "".b
        @headers = {}
      end

      def <<(chunk)
        @buffer << chunk
        parse
      end

      def reset!
        @state = :idle
        @headers = {}
        @content_length = nil
        @_has_trailers = nil
        @buffer.clear
      end

      def upgrade?
        @upgrade
      end

      def upgrade_data
        @buffer
      end

      private

      def parse
        loop do
          state = @state
          case @state
          when :idle
            parse_headline
          when :headers, :trailers
            parse_headers
          when :data
            parse_data
          end
          return if @buffer.empty? || state == @state
        end
      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 version && 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 = @buffer.byteslice((idx + 1)..-1)
        nextstate(:headers)
      end

      def parse_headers
        headers = @headers
        buffer = @buffer

        while (idx = buffer.index("\n"))
          # @type var line: String
          line = buffer.byteslice(0..idx)
          raise Error, "wrong header format" if line.start_with?("\s", "\t")

          line.lstrip!
          buffer = @buffer = buffer.byteslice((idx + 1)..-1)
          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)
            end
            return
          end
          separator_index = line.index(":")
          raise Error, "wrong header format" unless separator_index

          # @type var key: String
          key = line.byteslice(0..(separator_index - 1))

          key.rstrip! # was lstripped previously!
          # @type var value: String
          value = line.byteslice((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
          # @type var data: String
          data = @buffer.byteslice(0, @content_length)
          @buffer = @buffer.byteslice(@content_length..-1) || "".b
          @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