httpi 1.1.1 → 2.0.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|