File: client.rb

package info (click to toggle)
ruby-em-http-request 1.1.2-2
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 628 kB
  • ctags: 243
  • sloc: ruby: 3,478; makefile: 2
file content (318 lines) | stat: -rw-r--r-- 8,706 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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
require 'cookiejar'

module EventMachine


  class HttpClient
    include Deferrable
    include HttpEncoding
    include HttpStatus

    TRANSFER_ENCODING="TRANSFER_ENCODING"
    CONTENT_ENCODING="CONTENT_ENCODING"
    CONTENT_LENGTH="CONTENT_LENGTH"
    CONTENT_TYPE="CONTENT_TYPE"
    LAST_MODIFIED="LAST_MODIFIED"
    KEEP_ALIVE="CONNECTION"
    SET_COOKIE="SET_COOKIE"
    LOCATION="LOCATION"
    HOST="HOST"
    ETAG="ETAG"

    CRLF="\r\n"

    attr_accessor :state, :response
    attr_reader   :response_header, :error, :content_charset, :req, :cookies

    def initialize(conn, options)
      @conn = conn
      @req  = options

      @stream    = nil
      @headers   = nil
      @cookies   = []
      @cookiejar = CookieJar.new

      reset!
    end

    def reset!
      @response_header = HttpResponseHeader.new
      @state = :response_header

      @response = ''
      @error = nil
      @content_decoder = nil
      @content_charset = nil
    end

    def last_effective_url; @req.uri; end
    def redirects; @req.followed; end
    def peer; @conn.peer; end

    def connection_completed
      @state = :response_header

      head, body = build_request, @req.body
      @conn.middleware.each do |m|
        head, body = m.request(self, head, body) if m.respond_to?(:request)
      end

      send_request(head, body)
    end

    def on_request_complete
      begin
        @content_decoder.finalize! if @content_decoder
      rescue HttpDecoders::DecoderError
        on_error "Content-decoder error"
      end

      unbind
    end

    def continue?
      @response_header.status == 100 && (@req.method == 'POST' || @req.method == 'PUT')
    end

    def finished?
      @state == :finished || (@state == :body && @response_header.content_length.nil?)
    end

    def redirect?
      @response_header.redirection? && @req.follow_redirect?
    end

    def unbind(reason = nil)
      if finished?
        if redirect?

          begin
            @conn.middleware.each do |m|
              m.response(self) if m.respond_to?(:response)
            end

            # one of the injected middlewares could have changed
            # our redirect settings, check if we still want to
            # follow the location header
            if redirect?
              @req.followed += 1

              @cookies.clear
              @cookies = @cookiejar.get(@response_header.location).map(&:to_s) if @req.pass_cookies
              @req.set_uri(@response_header.location)

              @conn.redirect(self)
            else
              succeed(self)
            end

          rescue Exception => e
            on_error(e.message)
          end
        else
          succeed(self)
        end

      else
        on_error(reason || 'connection closed by server')
      end
    end

    def on_error(msg = nil)
      @error = msg
      fail(self)
    end
    alias :close :on_error

    def stream(&blk); @stream = blk; end
    def headers(&blk); @headers = blk; end

    def normalize_body(body)
      body.is_a?(Hash) ? form_encode_body(body) : body
    end

    def build_request
      head    = @req.headers ? munge_header_keys(@req.headers) : {}

      if @conn.connopts.http_proxy?
        proxy = @conn.connopts.proxy
        head['proxy-authorization'] = proxy[:authorization] if proxy[:authorization]
      end

      # Set the cookie header if provided
      if cookie = head['cookie']
        @cookies << encode_cookie(cookie) if cookie
      end
      head['cookie'] = @cookies.compact.uniq.join("; ").squeeze(";") unless @cookies.empty?

      # Set connection close unless keepalive
      if !@req.keepalive
        head['connection'] = 'close'
      end

      # Set the Host header if it hasn't been specified already
      head['host'] ||= encode_host

      # Set the User-Agent if it hasn't been specified
      if !head.key?('user-agent')
        head['user-agent'] = "EventMachine HttpClient"
      elsif head['user-agent'].nil?
        head.delete('user-agent')
      end

      # Set the auth from the URI if given
      head['Authorization'] = @req.uri.userinfo.split(/:/, 2) if @req.uri.userinfo

      head
    end

    def send_request(head, body)
      body    = normalize_body(body)
      file    = @req.file
      query   = @req.query

      # Set the Content-Length if file is given
      head['content-length'] = File.size(file) if file

      # Set the Content-Length if body is given,
      # or we're doing an empty post or put
      if body
        head['content-length'] = body.bytesize
      elsif @req.method == 'POST' or @req.method == 'PUT'
        # wont happen if body is set and we already set content-length above
        head['content-length'] ||= 0
      end

      # Set content-type header if missing and body is a Ruby hash
      if !head['content-type'] and @req.body.is_a? Hash
        head['content-type'] = 'application/x-www-form-urlencoded'
      end

      request_header ||= encode_request(@req.method, @req.uri, query, @conn.connopts.proxy)
      request_header << encode_headers(head)
      request_header << CRLF
      @conn.send_data request_header

      if body
        @conn.send_data body
      elsif @req.file
        @conn.stream_file_data @req.file, :http_chunks => false
      end
    end

    def on_body_data(data)
      if @content_decoder
        begin
          @content_decoder << data
        rescue HttpDecoders::DecoderError
          on_error "Content-decoder error"
        end
      else
        on_decoded_body_data(data)
      end
    end

    def on_decoded_body_data(data)
      data.force_encoding @content_charset if @content_charset
      if @stream
        @stream.call(data)
      else
        @response << data
      end
    end

    def parse_response_header(header, version, status)
      @response_header.raw = header
      header.each do |key, val|
        @response_header[key.upcase.gsub('-','_')] = val
      end

      @response_header.http_version = version.join('.')
      @response_header.http_status  = status
      @response_header.http_reason  = CODE[status] || 'unknown'

      # invoke headers callback after full parse
      # if one is specified by the user
      @headers.call(@response_header) if @headers

      unless @response_header.http_status and @response_header.http_reason
        @state = :invalid
        on_error "no HTTP response"
        return
      end

      # add set-cookie's to cookie list
      if @response_header.cookie && @req.pass_cookies
        [@response_header.cookie].flatten.each {|cookie| @cookiejar.set(cookie, @req.uri)}
      end

      # correct location header - some servers will incorrectly give a relative URI
      if @response_header.location
        begin
          location = Addressable::URI.parse(@response_header.location)
          location.path = "/" if location.path.empty?

          if location.relative?
            location = @req.uri.join(location)
          else
            # if redirect is to an absolute url, check for correct URI structure
            raise if location.host.nil?
          end

          @response_header[LOCATION] = location.to_s

        rescue
          on_error "Location header format error"
          return
        end
      end

      # Fire callbacks immediately after recieving header requests
      # if the request method is HEAD. In case of a redirect, terminate
      # current connection and reinitialize the process.
      if @req.method == "HEAD"
        @state = :finished
        return
      end

      if @response_header.chunked_encoding?
        @state = :chunk_header
      elsif @response_header.content_length
        @state = :body
      else
        @state = :body
      end

      if @req.decoding && decoder_class = HttpDecoders.decoder_for_encoding(response_header[CONTENT_ENCODING])
        begin
          @content_decoder = decoder_class.new do |s| on_decoded_body_data(s) end
        rescue HttpDecoders::DecoderError
          on_error "Content-decoder error"
        end
      end

      # handle malformed header - Content-Type repetitions.
      content_type = [response_header[CONTENT_TYPE]].flatten.first

      if String.method_defined?(:force_encoding) && /;\s*charset=\s*(.+?)\s*(;|$)/.match(content_type)
        @content_charset = Encoding.find($1.gsub(/^\"|\"$/, '')) rescue Encoding.default_external
      end
    end

    class CookieJar
      def initialize
        @jar = ::CookieJar::Jar.new
      end

      def set string, uri
        @jar.set_cookie(uri, string) rescue nil # drop invalid cookies
      end

      def get uri
        uri = URI.parse(uri) rescue nil
        uri ? @jar.get_cookies(uri) : []
      end
    end # CookieJar
  end
end