httpi 1.1.1 → 2.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/.rvmrc +1 -0
- data/CHANGELOG.md +51 -26
- data/Gemfile +6 -4
- data/README.md +19 -205
- data/httpi.gemspec +7 -10
- data/lib/httpi.rb +54 -56
- data/lib/httpi/adapter.rb +25 -18
- data/lib/httpi/adapter/base.rb +35 -0
- data/lib/httpi/adapter/curb.rb +59 -60
- data/lib/httpi/adapter/em_http.rb +126 -0
- data/lib/httpi/adapter/httpclient.rb +33 -63
- data/lib/httpi/adapter/net_http.rb +44 -62
- data/lib/httpi/auth/ssl.rb +26 -2
- data/lib/httpi/dime.rb +45 -29
- data/lib/httpi/request.rb +1 -1
- data/lib/httpi/response.rb +6 -4
- data/lib/httpi/version.rb +1 -1
- data/spec/httpi/adapter/base_spec.rb +23 -0
- data/spec/httpi/adapter/curb_spec.rb +107 -67
- data/spec/httpi/adapter/em_http_spec.rb +168 -0
- data/spec/httpi/adapter/httpclient_spec.rb +67 -56
- data/spec/httpi/adapter/net_http_spec.rb +62 -47
- data/spec/httpi/adapter_spec.rb +15 -2
- data/spec/httpi/auth/ssl_spec.rb +34 -1
- data/spec/httpi/httpi_spec.rb +80 -115
- data/spec/integration/fixtures/ca.pem +23 -0
- data/spec/integration/fixtures/ca_all.pem +44 -0
- data/spec/integration/fixtures/htdigest +1 -0
- data/spec/integration/fixtures/htpasswd +2 -0
- data/spec/integration/fixtures/server.cert +19 -0
- data/spec/integration/fixtures/server.key +15 -0
- data/spec/integration/fixtures/subca.pem +21 -0
- data/spec/integration/request_spec.rb +15 -2
- data/spec/integration/ssl_server.rb +70 -0
- data/spec/integration/ssl_spec.rb +102 -0
- data/spec/support/fixture.rb +1 -1
- metadata +60 -73
data/lib/httpi/dime.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
module HTTPI
|
2
|
-
|
3
|
-
|
2
|
+
|
3
|
+
DimeRecord = Struct.new(
|
4
|
+
'DimeRecord', :version, :first, :last, :chunked,
|
5
|
+
:type_format, :options, :id, :type, :data
|
6
|
+
)
|
4
7
|
|
5
8
|
class Dime < Array
|
9
|
+
|
6
10
|
BINARY = 1
|
7
11
|
XML = 2
|
8
12
|
|
@@ -11,40 +15,51 @@ module HTTPI
|
|
11
15
|
|
12
16
|
while bytes.length > 0
|
13
17
|
record = DimeRecord.new
|
18
|
+
configure_record(record, bytes)
|
14
19
|
|
15
|
-
|
16
|
-
|
17
|
-
record.version = (byte >> 3) & 31 # 5 bits DIME format version (always 1)
|
18
|
-
record.first = (byte >> 2) & 1 # 1 bit Set if this is the first part in the message
|
19
|
-
record.last = (byte >> 1) & 1 # 1 bit Set if this is the last part in the message
|
20
|
-
record.chunked = byte & 1 # 1 bit This file is broken into chunked parts
|
21
|
-
record.type_format = (bytes.shift >> 4) & 15 # 4 bits Type of file in the part (1 for binary data, 2 for XML)
|
22
|
-
# 4 bits Reserved (skipped in the above command)
|
23
|
-
|
24
|
-
# Fetch big-endian lengths
|
25
|
-
lengths = [] # we can't use a hash since the order will be screwed in Ruby 1.8
|
26
|
-
lengths << [:options, (bytes.shift << 8) | bytes.shift] # 2 bytes Length of the "options" field
|
27
|
-
lengths << [:id, (bytes.shift << 8) | bytes.shift] # 2 bytes Length of the "ID" or "name" field
|
28
|
-
lengths << [:type, (bytes.shift << 8) | bytes.shift] # 2 bytes Length of the "type" field
|
29
|
-
lengths << [:data, (bytes.shift << 24) | (bytes.shift << 16) | (bytes.shift << 8) | bytes.shift] # 4 bytes Size of the included file
|
30
|
-
|
31
|
-
# Read in padded data
|
32
|
-
lengths.each do |attribute_set|
|
33
|
-
attribute, length = attribute_set
|
34
|
-
content = bytes.slice!(0, length).pack('C*')
|
35
|
-
if attribute == :data && record.type_format == BINARY
|
36
|
-
content = StringIO.new(content)
|
37
|
-
end
|
38
|
-
|
39
|
-
record.send "#{attribute.to_s}=", content
|
40
|
-
|
41
|
-
bytes.slice!(0, 4 - (length & 3)) if (length & 3) != 0
|
20
|
+
big_endian_lengths(bytes).each do |attribute_set|
|
21
|
+
read_data(record, bytes, attribute_set)
|
42
22
|
end
|
43
23
|
|
44
24
|
self << record
|
45
25
|
end
|
46
26
|
end
|
47
27
|
|
28
|
+
# Shift out bitfields for the first fields.
|
29
|
+
def configure_record(record, bytes)
|
30
|
+
byte = bytes.shift
|
31
|
+
|
32
|
+
record.version = (byte >> 3) & 31 # 5 bits DIME format version (always 1)
|
33
|
+
record.first = (byte >> 2) & 1 # 1 bit Set if this is the first part in the message
|
34
|
+
record.last = (byte >> 1) & 1 # 1 bit Set if this is the last part in the message
|
35
|
+
record.chunked = byte & 1 # 1 bit This file is broken into chunked parts
|
36
|
+
record.type_format = (bytes.shift >> 4) & 15 # 4 bits Type of file in the part (1 for binary data, 2 for XML)
|
37
|
+
# 4 bits Reserved (skipped in the above command)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Fetch big-endian lengths.
|
41
|
+
def big_endian_lengths(bytes)
|
42
|
+
lengths = [] # we can't use a hash since the order will be screwed in Ruby 1.8
|
43
|
+
lengths << [:options, (bytes.shift << 8) | bytes.shift] # 2 bytes Length of the "options" field
|
44
|
+
lengths << [:id, (bytes.shift << 8) | bytes.shift] # 2 bytes Length of the "ID" or "name" field
|
45
|
+
lengths << [:type, (bytes.shift << 8) | bytes.shift] # 2 bytes Length of the "type" field
|
46
|
+
lengths << [:data, (bytes.shift << 24) | (bytes.shift << 16) | (bytes.shift << 8) | bytes.shift] # 4 bytes Size of the included file
|
47
|
+
lengths
|
48
|
+
end
|
49
|
+
|
50
|
+
# Read in padded data.
|
51
|
+
def read_data(record, bytes, attribute_set)
|
52
|
+
attribute, length = attribute_set
|
53
|
+
content = bytes.slice!(0, length).pack('C*')
|
54
|
+
|
55
|
+
if attribute == :data && record.type_format == BINARY
|
56
|
+
content = StringIO.new(content)
|
57
|
+
end
|
58
|
+
|
59
|
+
record.send "#{attribute.to_s}=", content
|
60
|
+
bytes.slice!(0, 4 - (length & 3)) if (length & 3) != 0
|
61
|
+
end
|
62
|
+
|
48
63
|
def xml_records
|
49
64
|
select { |r| r.type_format == XML }
|
50
65
|
end
|
@@ -52,5 +67,6 @@ module HTTPI
|
|
52
67
|
def binary_records
|
53
68
|
select { |r| r.type_format == BINARY }
|
54
69
|
end
|
70
|
+
|
55
71
|
end
|
56
72
|
end
|
data/lib/httpi/request.rb
CHANGED
data/lib/httpi/response.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
require "zlib"
|
2
2
|
require "stringio"
|
3
|
+
require "rack/utils"
|
4
|
+
|
3
5
|
require "httpi/dime"
|
4
6
|
require "httpi/cookie"
|
5
|
-
require "rack/utils"
|
6
7
|
|
7
8
|
module HTTPI
|
8
9
|
|
@@ -52,7 +53,7 @@ module HTTPI
|
|
52
53
|
|
53
54
|
attr_writer :body
|
54
55
|
|
55
|
-
|
56
|
+
private
|
56
57
|
|
57
58
|
def decode_body
|
58
59
|
return @body = "" if !raw_body || raw_body.empty?
|
@@ -73,8 +74,9 @@ module HTTPI
|
|
73
74
|
|
74
75
|
# Returns the gzip decoded response body.
|
75
76
|
def decoded_gzip_body
|
76
|
-
gzip = Zlib::GzipReader.new
|
77
|
-
|
77
|
+
unless gzip = Zlib::GzipReader.new(StringIO.new(raw_body))
|
78
|
+
raise ArgumentError, "Unable to create Zlib::GzipReader"
|
79
|
+
end
|
78
80
|
gzip.read
|
79
81
|
ensure
|
80
82
|
gzip.close if gzip
|
data/lib/httpi/version.rb
CHANGED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "httpi/adapter/base"
|
3
|
+
|
4
|
+
describe HTTPI::Adapter::Base do
|
5
|
+
|
6
|
+
subject(:base) { HTTPI::Adapter::Base.new(request) }
|
7
|
+
let(:request) { HTTPI::Request.new }
|
8
|
+
|
9
|
+
describe "#client" do
|
10
|
+
it "returns the adapter's client instance" do
|
11
|
+
expect { base.client }.
|
12
|
+
to raise_error(HTTPI::NotImplementedError, "Adapters need to implement a #client method")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#request" do
|
17
|
+
it "executes arbitrary HTTP requests" do
|
18
|
+
expect { base.request(:get) }.
|
19
|
+
to raise_error(HTTPI::NotImplementedError, "Adapters need to implement a #request method")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -4,13 +4,15 @@ require "httpi/request"
|
|
4
4
|
|
5
5
|
# curb does not run on jruby
|
6
6
|
unless RUBY_PLATFORM =~ /java/
|
7
|
-
|
7
|
+
HTTPI::Adapter.load_adapter(:curb)
|
8
8
|
|
9
9
|
describe HTTPI::Adapter::Curb do
|
10
|
-
let(:adapter) { HTTPI::Adapter::Curb.new }
|
11
|
-
let(:curb) { Curl::Easy.any_instance }
|
12
10
|
|
13
|
-
|
11
|
+
let(:adapter) { HTTPI::Adapter::Curb.new(request) }
|
12
|
+
let(:curb) { Curl::Easy.any_instance }
|
13
|
+
let(:request) { HTTPI::Request.new("http://example.com") }
|
14
|
+
|
15
|
+
describe "#request(:get)" do
|
14
16
|
before do
|
15
17
|
curb.expects(:http_get)
|
16
18
|
curb.expects(:response_code).returns(200)
|
@@ -19,11 +21,11 @@ unless RUBY_PLATFORM =~ /java/
|
|
19
21
|
end
|
20
22
|
|
21
23
|
it "returns a valid HTTPI::Response" do
|
22
|
-
adapter.get
|
24
|
+
adapter.request(:get).should match_response(:body => Fixture.xml)
|
23
25
|
end
|
24
26
|
end
|
25
27
|
|
26
|
-
describe "#post" do
|
28
|
+
describe "#request(:post)" do
|
27
29
|
before do
|
28
30
|
curb.expects(:http_post)
|
29
31
|
curb.expects(:response_code).returns(200)
|
@@ -32,18 +34,20 @@ unless RUBY_PLATFORM =~ /java/
|
|
32
34
|
end
|
33
35
|
|
34
36
|
it "returns a valid HTTPI::Response" do
|
35
|
-
adapter.post
|
37
|
+
adapter.request(:post).should match_response(:body => Fixture.xml)
|
36
38
|
end
|
37
39
|
end
|
38
40
|
|
39
|
-
describe "#post" do
|
41
|
+
describe "#request(:post)" do
|
40
42
|
it "sends the body in the request" do
|
41
|
-
curb.expects(:http_post).with(
|
42
|
-
|
43
|
+
curb.expects(:http_post).with("xml=hi&name=123")
|
44
|
+
|
45
|
+
request.body = "xml=hi&name=123"
|
46
|
+
adapter.request(:post)
|
43
47
|
end
|
44
48
|
end
|
45
49
|
|
46
|
-
describe "#head" do
|
50
|
+
describe "#request(:head)" do
|
47
51
|
before do
|
48
52
|
curb.expects(:http_head)
|
49
53
|
curb.expects(:response_code).returns(200)
|
@@ -52,11 +56,11 @@ unless RUBY_PLATFORM =~ /java/
|
|
52
56
|
end
|
53
57
|
|
54
58
|
it "returns a valid HTTPI::Response" do
|
55
|
-
adapter.head
|
59
|
+
adapter.request(:head).should match_response(:body => Fixture.xml)
|
56
60
|
end
|
57
61
|
end
|
58
62
|
|
59
|
-
describe "#put" do
|
63
|
+
describe "#request(:put)" do
|
60
64
|
before do
|
61
65
|
curb.expects(:http_put)
|
62
66
|
curb.expects(:response_code).returns(200)
|
@@ -65,18 +69,20 @@ unless RUBY_PLATFORM =~ /java/
|
|
65
69
|
end
|
66
70
|
|
67
71
|
it "returns a valid HTTPI::Response" do
|
68
|
-
adapter.put
|
72
|
+
adapter.request(:put).should match_response(:body => Fixture.xml)
|
69
73
|
end
|
70
74
|
end
|
71
75
|
|
72
|
-
describe "#put" do
|
76
|
+
describe "#request(:put)" do
|
73
77
|
it "sends the body in the request" do
|
74
78
|
curb.expects(:http_put).with('xml=hi&name=123')
|
75
|
-
|
79
|
+
|
80
|
+
request.body = 'xml=hi&name=123'
|
81
|
+
adapter.request(:put)
|
76
82
|
end
|
77
83
|
end
|
78
84
|
|
79
|
-
describe "#delete" do
|
85
|
+
describe "#request(:delete)" do
|
80
86
|
before do
|
81
87
|
curb.expects(:http_delete)
|
82
88
|
curb.expects(:response_code).returns(200)
|
@@ -85,7 +91,14 @@ unless RUBY_PLATFORM =~ /java/
|
|
85
91
|
end
|
86
92
|
|
87
93
|
it "returns a valid HTTPI::Response" do
|
88
|
-
adapter.delete
|
94
|
+
adapter.request(:delete).should match_response(:body => "")
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "#request(:custom)" do
|
99
|
+
it "raises a NotSupportedError" do
|
100
|
+
expect { adapter.request(:custom) }.
|
101
|
+
to raise_error(HTTPI::NotSupportedError, "Curb does not support custom HTTP methods")
|
89
102
|
end
|
90
103
|
end
|
91
104
|
|
@@ -94,145 +107,172 @@ unless RUBY_PLATFORM =~ /java/
|
|
94
107
|
|
95
108
|
describe "url" do
|
96
109
|
it "always sets the request url" do
|
97
|
-
curb.expects(:url=).with(
|
98
|
-
adapter.get
|
110
|
+
curb.expects(:url=).with(request.url.to_s)
|
111
|
+
adapter.request(:get)
|
99
112
|
end
|
100
113
|
end
|
101
114
|
|
102
115
|
describe "proxy_url" do
|
103
116
|
it "is not set unless it's specified" do
|
104
117
|
curb.expects(:proxy_url=).never
|
105
|
-
adapter.get
|
118
|
+
adapter.request(:get)
|
106
119
|
end
|
107
120
|
|
108
121
|
it "is set if specified" do
|
109
|
-
request
|
110
|
-
|
122
|
+
request.proxy = "http://proxy.example.com"
|
111
123
|
curb.expects(:proxy_url=).with(request.proxy.to_s)
|
112
|
-
|
124
|
+
|
125
|
+
adapter.request(:get)
|
113
126
|
end
|
114
127
|
end
|
115
128
|
|
116
129
|
describe "timeout" do
|
117
130
|
it "is not set unless it's specified" do
|
118
131
|
curb.expects(:timeout=).never
|
119
|
-
adapter.get
|
132
|
+
adapter.request(:get)
|
120
133
|
end
|
121
134
|
|
122
135
|
it "is set if specified" do
|
123
|
-
request
|
136
|
+
request.read_timeout = 30
|
137
|
+
curb.expects(:timeout=).with(request.read_timeout)
|
124
138
|
|
125
|
-
|
126
|
-
adapter.get(request)
|
139
|
+
adapter.request(:get)
|
127
140
|
end
|
128
141
|
end
|
129
142
|
|
130
143
|
describe "connect_timeout" do
|
131
144
|
it "is not set unless it's specified" do
|
132
145
|
curb.expects(:connect_timeout=).never
|
133
|
-
adapter.get
|
146
|
+
adapter.request(:get)
|
134
147
|
end
|
135
148
|
|
136
149
|
it "is set if specified" do
|
137
|
-
request
|
138
|
-
|
150
|
+
request.open_timeout = 30
|
139
151
|
curb.expects(:connect_timeout=).with(30)
|
140
|
-
|
152
|
+
|
153
|
+
adapter.request(:get)
|
141
154
|
end
|
142
155
|
end
|
143
156
|
|
144
157
|
describe "headers" do
|
145
158
|
it "is always set" do
|
146
159
|
curb.expects(:headers=).with({})
|
147
|
-
adapter.get
|
160
|
+
adapter.request(:get)
|
148
161
|
end
|
149
162
|
end
|
150
163
|
|
151
164
|
describe "verbose" do
|
152
165
|
it "is always set to false" do
|
153
166
|
curb.expects(:verbose=).with(false)
|
154
|
-
adapter.get
|
167
|
+
adapter.request(:get)
|
155
168
|
end
|
156
169
|
end
|
157
170
|
|
158
171
|
describe "http_auth_types" do
|
159
172
|
it "is set to :basic for HTTP basic auth" do
|
160
|
-
request
|
161
|
-
|
173
|
+
request.auth.basic "username", "password"
|
162
174
|
curb.expects(:http_auth_types=).with(:basic)
|
163
|
-
|
175
|
+
|
176
|
+
adapter.request(:get)
|
164
177
|
end
|
165
178
|
|
166
179
|
it "is set to :digest for HTTP digest auth" do
|
167
|
-
request
|
168
|
-
|
180
|
+
request.auth.digest "username", "password"
|
169
181
|
curb.expects(:http_auth_types=).with(:digest)
|
170
|
-
|
182
|
+
|
183
|
+
adapter.request(:get)
|
171
184
|
end
|
172
185
|
|
173
186
|
it "is set to :gssnegotiate for HTTP Negotiate auth" do
|
174
|
-
request
|
175
|
-
|
187
|
+
request.auth.gssnegotiate
|
176
188
|
curb.expects(:http_auth_types=).with(:gssnegotiate)
|
177
|
-
|
189
|
+
|
190
|
+
adapter.request(:get)
|
178
191
|
end
|
179
192
|
end
|
180
193
|
|
181
194
|
describe "username and password" do
|
182
195
|
it "is set for HTTP basic auth" do
|
183
|
-
request
|
196
|
+
request.auth.basic "username", "password"
|
184
197
|
|
185
198
|
curb.expects(:username=).with("username")
|
186
199
|
curb.expects(:password=).with("password")
|
187
|
-
adapter.get
|
200
|
+
adapter.request(:get)
|
188
201
|
end
|
189
202
|
|
190
203
|
it "is set for HTTP digest auth" do
|
191
|
-
request
|
204
|
+
request.auth.digest "username", "password"
|
192
205
|
|
193
206
|
curb.expects(:username=).with("username")
|
194
207
|
curb.expects(:password=).with("password")
|
195
|
-
adapter.get
|
208
|
+
adapter.request(:get)
|
196
209
|
end
|
197
210
|
end
|
198
211
|
|
199
212
|
context "(for SSL client auth)" do
|
200
|
-
let(:
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
213
|
+
let(:request) do
|
214
|
+
request = HTTPI::Request.new("http://example.com")
|
215
|
+
request.auth.ssl.cert_key_file = "spec/fixtures/client_key.pem"
|
216
|
+
request.auth.ssl.cert_file = "spec/fixtures/client_cert.pem"
|
217
|
+
request
|
205
218
|
end
|
206
219
|
|
207
220
|
it "cert_key, cert and ssl_verify_peer should be set" do
|
208
|
-
curb.expects(:cert_key=).with(
|
209
|
-
curb.expects(:cert=).with(
|
221
|
+
curb.expects(:cert_key=).with(request.auth.ssl.cert_key_file)
|
222
|
+
curb.expects(:cert=).with(request.auth.ssl.cert_file)
|
210
223
|
curb.expects(:ssl_verify_peer=).with(true)
|
211
|
-
curb.expects(:certtype=).with(
|
224
|
+
curb.expects(:certtype=).with(request.auth.ssl.cert_type.to_s.upcase)
|
212
225
|
|
213
|
-
adapter.get
|
226
|
+
adapter.request(:get)
|
214
227
|
end
|
215
228
|
|
216
229
|
it "sets the cert_type to DER if specified" do
|
217
|
-
|
230
|
+
request.auth.ssl.cert_type = :der
|
218
231
|
curb.expects(:certtype=).with(:der.to_s.upcase)
|
219
232
|
|
220
|
-
adapter.get
|
233
|
+
adapter.request(:get)
|
234
|
+
end
|
235
|
+
|
236
|
+
it "raise if an invalid cert type was set" do
|
237
|
+
expect { request.auth.ssl.cert_type = :invalid }.
|
238
|
+
to raise_error(ArgumentError, "Invalid SSL cert type :invalid\nPlease specify one of [:pem, :der]")
|
221
239
|
end
|
222
240
|
|
223
241
|
it "sets the cacert if specified" do
|
224
|
-
|
225
|
-
curb.expects(:cacert=).with(
|
242
|
+
request.auth.ssl.ca_cert_file = "spec/fixtures/client_cert.pem"
|
243
|
+
curb.expects(:cacert=).with(request.auth.ssl.ca_cert_file)
|
226
244
|
|
227
|
-
adapter.get
|
245
|
+
adapter.request(:get)
|
228
246
|
end
|
229
|
-
end
|
230
|
-
end
|
231
247
|
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
248
|
+
context 'sets ssl_version' do
|
249
|
+
it 'defaults to nil when no ssl_version is specified' do
|
250
|
+
curb.expects(:ssl_version=).with(nil)
|
251
|
+
adapter.request(:get)
|
252
|
+
end
|
253
|
+
|
254
|
+
it 'to 1 when ssl_version is specified as TLSv1' do
|
255
|
+
request.auth.ssl.ssl_version = :TLSv1
|
256
|
+
curb.expects(:ssl_version=).with(1)
|
257
|
+
|
258
|
+
adapter.request(:get)
|
259
|
+
end
|
260
|
+
|
261
|
+
it 'to 2 when ssl_version is specified as SSLv2' do
|
262
|
+
request.auth.ssl.ssl_version = :SSLv2
|
263
|
+
curb.expects(:ssl_version=).with(2)
|
264
|
+
|
265
|
+
adapter.request(:get)
|
266
|
+
end
|
267
|
+
|
268
|
+
it 'to 3 when ssl_version is specified as SSLv3' do
|
269
|
+
request.auth.ssl.ssl_version = :SSLv3
|
270
|
+
curb.expects(:ssl_version=).with(3)
|
271
|
+
|
272
|
+
adapter.request(:get)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
236
276
|
end
|
237
277
|
|
238
278
|
end
|