forked from httprb/http
-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.rb
134 lines (104 loc) · 3.45 KB
/
client.rb
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
require 'cgi'
require 'uri'
require 'http/options'
require 'http/redirector'
module HTTP
# Clients make requests and receive responses
class Client
include Chainable
# Input buffer size
BUFFER_SIZE = 16_384
attr_reader :default_options
def initialize(default_options = {})
@default_options = HTTP::Options.new(default_options)
@parser = HTTP::Response::Parser.new
@socket = nil
end
# Make an HTTP request
def request(verb, uri, opts = {})
opts = @default_options.merge(opts)
uri = make_request_uri(uri, opts)
headers = opts.headers
proxy = opts.proxy
body = make_request_body(opts, headers)
req = HTTP::Request.new(verb, uri, headers, proxy, body)
res = perform req, opts
if opts.follow
res = Redirector.new(opts.follow).perform req, res do |request|
perform request, opts
end
end
res
end
# Perform a single (no follow) HTTP request
def perform(req, options)
# finish previous response if client was re-used
# TODO: this is pretty wrong, as socket shoud be part of response
# connection, so that re-use of client will not break multiple
# chunked responses
finish_response
uri = req.uri
# TODO: keep-alive support
@socket = options[:socket_class].open(req.socket_host, req.socket_port)
@socket = start_tls(@socket, options) if uri.is_a?(URI::HTTPS) && !req.using_proxy?
req.stream @socket
begin
read_more BUFFER_SIZE until @parser.headers
rescue IOError, Errno::ECONNRESET, Errno::EPIPE => ex
raise IOError, "problem making HTTP request: #{ex}"
end
body = Response::Body.new(self)
res = Response.new(@parser.status_code, @parser.http_version, @parser.headers, body, uri)
finish_response if :head == req.verb
res
end
# Read a chunk of the body
def readpartial(size = BUFFER_SIZE)
return unless @socket
read_more size
chunk = @parser.chunk
finish_response if @parser.finished?
chunk.to_s
end
private
# Initialize TLS connection
def start_tls(socket, options)
# TODO: abstract away SSLContexts so we can use other TLS libraries
context = options[:ssl_context] || OpenSSL::SSL::SSLContext.new
socket = options[:ssl_socket_class].new(socket, context)
socket.connect
socket
end
# Merges query params if needed
def make_request_uri(uri, options)
uri = URI uri.to_s unless uri.is_a? URI
if options.params && !options.params.empty?
params = CGI.parse(uri.query.to_s).merge(options.params || {})
uri.query = URI.encode_www_form params
end
uri
end
# Create the request body object to send
def make_request_body(opts, headers)
if opts.body
opts.body
elsif opts.form
headers['Content-Type'] ||= 'application/x-www-form-urlencoded'
URI.encode_www_form(opts.form)
elsif opts.json
headers['Content-Type'] ||= 'application/json'
MimeType[:json].encode opts.json
end
end
# Callback for when we've reached the end of a response
def finish_response
@socket.close if @socket && !@socket.closed?
@parser.reset
@socket = nil
end
# Feeds some more data into parser
def read_more(size)
@parser << @socket.readpartial(size) unless @parser.finished?
end
end
end