http-client 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 53ce70d44f5b7cf0dbc85317ce3d6f6606b9c8e1
4
+ data.tar.gz: 4eaa5aee72f9e60d288b6c9b52ba883ef88798a4
5
+ SHA512:
6
+ metadata.gz: fae9eb821a1e374769f9dd8c2134f00817f62de88d77fcfaf3f02db284c45fc0e3c2faaef9bb88bec627c9dd055e3ac20a69af2f522133e955f1c667f081cb71
7
+ data.tar.gz: 8fb4cbc0035594e46e4f2d66b76e59cd639dc95d41906c6757c77e2162ab5576a2db4f5f507cdd3e2908adcd331bebe1280ea76d9c12eaf2bd0720c000883520
data/CHANGELOG ADDED
@@ -0,0 +1,3 @@
1
+ == 0.1.0 (2016-06-08)
2
+
3
+ * Initial version.
data/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # HTTP Client
2
+
3
+ A simple Net::HTTP wrapper with multi-part and cookie-jar support.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ gem install http-client
9
+ ```
10
+
11
+ ## Example
12
+
13
+ ```ruby
14
+ require 'http-client'
15
+ res = HTTP::Client::Request.new(:get, "http://www.example.org", max_redirects: 2).execute
16
+
17
+ # save a few keystrokes.
18
+ res = HTTP::Client.get("http://www.example.org/", max_redirects: 2)
19
+ res = HTTP::Client.post("http://www.example.org/", files: {pic: "kittens.jpg"}, query: {title: "the usual suspects"})
20
+ ```
21
+
22
+ ## API
23
+
24
+ ```
25
+ HTTP::Client::Request
26
+ .new(verb, uri, arguments = {})
27
+ #execute
28
+
29
+ HTTP::Client::Response
30
+ .new net_http_response, last_effective_uri
31
+ #code
32
+ #body
33
+ #headers
34
+ #last_effective_uri
35
+ ```
36
+
37
+ ### Request parameters
38
+
39
+ Required:
40
+
41
+ | Name | Type | Description |
42
+ |------|------|-------------|
43
+ | verb | Symbol | HTTP verb, one of :get, :head, :put, :post, :delete, :options, :trace. |
44
+ | uri | String | Remote URI |
45
+
46
+ Optional arguments hash:
47
+
48
+ | Name | Type | Description |
49
+ |------|------|-------------|
50
+ | headers | Hash | Net::HTTP headers, in key-value pairs. |
51
+ | query | Hash | Net::HTTP query-string in key-value pairs. |
52
+ | files | Hash | Multi-part file uploads, in key-value pairs of {name => path_to_file} or {name => File} |
53
+ | body | String | Request body. |
54
+ | auth | Hash | Basic-Auth hash. {username: "...", password: "..."} |
55
+ | timeout | Integer | Fixed timeout for connection, read and ssl handshake in seconds. |
56
+ | open_timeout | Integer | Connection timeout in seconds. |
57
+ | read_timeout | Integer | Read timeout in seconds. |
58
+ | ssl_timeout | Integer | SSL handshake timeout in seconds. |
59
+ | max_redirects | Integer | Max redirect follow, default: 0 |
60
+ | ssl_verify | Integer | OpenSSL verification, HTTP::Client::SSL_VERIFY_PEER or HTTP::Client::SSL_VERIFY_NONE, defaults to SSL_VERIFY_PEER. |
61
+ | jar | HTTP::CookieJar | Optional cookie jar to use. Relies on HTTP::CookieJar from http-cookie gem. |
62
+
63
+ ## License
64
+
65
+ MIT
@@ -0,0 +1,3 @@
1
+ require 'http/simple'
2
+
3
+
@@ -0,0 +1,333 @@
1
+ require 'securerandom'
2
+ require 'net/http'
3
+ require 'openssl'
4
+ require 'uri'
5
+ require 'mime/types'
6
+ require 'http-cookie'
7
+
8
+ module HTTP
9
+ module Client
10
+ VERSION = '0.1.0'
11
+
12
+ GET = Net::HTTP::Get
13
+ HEAD = Net::HTTP::Head
14
+ PUT = Net::HTTP::Put
15
+ POST = Net::HTTP::Post
16
+ DELETE = Net::HTTP::Delete
17
+ OPTIONS = Net::HTTP::Options
18
+ TRACE = Net::HTTP::Trace
19
+ VALID_VERBS = [GET, HEAD, PUT, POST, DELETE, OPTIONS, TRACE]
20
+
21
+ SSL_VERIFY_NONE = OpenSSL::SSL::VERIFY_NONE
22
+ SSL_VERIFY_PEER = OpenSSL::SSL::VERIFY_PEER
23
+ VALID_SSL_VERIFICATIONS = [SSL_VERIFY_NONE, SSL_VERIFY_PEER]
24
+
25
+ DEFAULT_HEADERS = {'User-Agent' => 'HTTP Client API/1.0'}
26
+
27
+ class Request
28
+ attr_reader :uri
29
+
30
+ VALID_PARAMETERS = %w(headers files query body auth timeout open_timeout ssl_timeout read_timeout max_redirects ssl_verify jar)
31
+
32
+ # Create a new HTTP Client Request.
33
+ #
34
+ # @param verb [Symbol] HTTP verb, one of :get, :head, :put, :post, :delete, :options, :trace.
35
+ # @param uri [String] Remote URI
36
+ # @param headers [Hash] Net::HTTP headers, in key-value pairs.
37
+ # @param query [Hash] Net::HTTP query-string in key-value pairs.
38
+ # @param files [Hash] Multi-part file uploads, in key-value pairs of {name => path_to_file} or {name => File}
39
+ # @param body [String] Request body.
40
+ # @param auth [Hash] Basic-Auth hash. {username: "...", password: "..."}
41
+ # @param timeout [Integer] Fixed timeout for connection, read and ssl handshake in seconds.
42
+ # @param open_timeout [Integer] Connection timeout in seconds.
43
+ # @param read_timeout [Integer] Read timeout in seconds.
44
+ # @param ssl_timeout [Integer] SSL handshake timeout in seconds.
45
+ # @param max_redirects [Integer] Max redirect follow, default: 0
46
+ # @param ssl_verify [Integer] OpenSSL verification, HTTP::Client::SSL_VERIFY_PEER or HTTP::Client::SSL_VERIFY_NONE, defaults to SSL_VERIFY_PEER.
47
+ # @param jar [HTTP::CookieJar] Optional cookie jar to use. Relies on HTTP::CookieJar from http-cookie gem.
48
+ #
49
+ # @return [HTTP::Client::Request]
50
+ #
51
+ def initialize verb, uri, args = {}
52
+ args.each do |k, v|
53
+ raise ArgumentError, "unknown argument #{k}" unless VALID_PARAMETERS.include?(k.to_s)
54
+ end
55
+
56
+ parse_uri! uri
57
+ setup_request_delegate! verb, args
58
+
59
+ if body = args[:body]
60
+ raise ArgumentError, "#{verb} cannot have body" unless klass.const_get(:REQUEST_HAS_BODY)
61
+ @delegate.body = body
62
+ end
63
+
64
+ if auth = args[:auth]
65
+ @delegate.basic_auth(auth.fetch(:username), auth.fetch(:password))
66
+ end
67
+
68
+ # generic timeout
69
+ if timeout = args[:timeout]
70
+ @open_timeout = timeout
71
+ @ssl_timeout = timeout
72
+ @read_timeout = timeout
73
+ end
74
+
75
+ # overrides
76
+ @open_timeout = args[:open_timeout] if args[:open_timeout]
77
+ @ssl_timeout = args[:ssl_timeout] if args[:ssl_timeout]
78
+ @read_timeout = args[:read_timeout] if args[:read_timeout]
79
+
80
+ @redirects = args.fetch(:max_redirects, 0)
81
+ @ssl_verify = args.fetch(:ssl_verify, SSL_VERIFY_PEER)
82
+ @jar = args.fetch(:jar, HTTP::CookieJar.new)
83
+ end
84
+
85
+ # Executes a request.
86
+ #
87
+ # @return [HTTP::Client::Response]
88
+ #
89
+ def execute
90
+ @last_effective_uri = uri
91
+
92
+ cookie = HTTP::Cookie.cookie_value(@jar.cookies(uri))
93
+ if cookie && !cookie.empty?
94
+ @delegate.add_field('Cookie', cookie)
95
+ end
96
+
97
+ response = request!(uri, @delegate)
98
+ @jar.parse(response['set-cookie'].to_s, uri)
99
+
100
+ while @redirects > 0 && [301, 302, 307].include?(response.code.to_i)
101
+ @redirects -= 1
102
+ redirect = redirect_to(response['location'])
103
+
104
+ cookie = HTTP::Cookie.cookie_value(@jar.cookies(@last_effective_uri))
105
+ if cookie && !cookie.empty?
106
+ redirect.add_field('Cookie', cookie)
107
+ end
108
+
109
+ response = request!(@last_effective_uri, redirect)
110
+ @jar.parse(response['set-cookie'].to_s, @last_effective_uri)
111
+ end
112
+
113
+ Response.new(response, @last_effective_uri)
114
+ end
115
+
116
+ private
117
+ def parse_uri! uri
118
+ @uri = URI.parse(uri)
119
+ case @uri
120
+ when URI::HTTP, URI::HTTPS
121
+ # ok
122
+ else
123
+ raise ArgumentError, "Invalid URI #{uri}"
124
+ end
125
+ end
126
+
127
+ def setup_request_delegate! verb, args
128
+ klass = find_delegate_class(verb)
129
+ @headers = DEFAULT_HEADERS.merge(args.fetch(:headers, {}))
130
+
131
+ files = args[:files]
132
+ qs = args[:query]
133
+
134
+ if files
135
+ raise ArgumentError, "#{verb} cannot have body" unless klass.const_get(:REQUEST_HAS_BODY)
136
+ multipart = Multipart.new(files, qs)
137
+ @delegate = klass.new(@uri.request_uri, headers_for(@uri))
138
+ @delegate.content_type = multipart.content_type
139
+ @delegate.body = multipart.body
140
+ elsif qs
141
+ if klass.const_get(:REQUEST_HAS_BODY)
142
+ @delegate = klass.new(@uri.request_uri, headers_for(@uri))
143
+ @delegate.set_form_data(qs)
144
+ else
145
+ @uri.query = URI.encode_www_form(qs)
146
+ @delegate = klass.new(@uri.request_uri, headers_for(@uri))
147
+ end
148
+ else
149
+ @delegate = klass.new(@uri.request_uri, headers_for(@uri))
150
+ end
151
+ end
152
+
153
+ def request! uri, delegate
154
+ http = Net::HTTP.new(uri.host, uri.port)
155
+ if uri.scheme == 'https'
156
+ http.use_ssl = true
157
+ http.verify_mode = @ssl_verify
158
+ end
159
+
160
+ http.open_timeout = @open_timeout if @open_timeout
161
+ http.read_timeout = @read_timeout if @read_timeout
162
+ http.ssl_timeout = @ssl_timeout if @ssl_timeout
163
+
164
+ response = http.request(delegate)
165
+ http.finish if http.started?
166
+ response
167
+ end
168
+
169
+ def redirect_to uri
170
+ @last_effective_uri = URI.parse(uri)
171
+ GET.new(@last_effective_uri.request_uri, headers_for(@last_effective_uri))
172
+ end
173
+
174
+ def headers_for uri
175
+ @headers
176
+ end
177
+
178
+ def find_delegate_class verb
179
+ if VALID_VERBS.include?(verb)
180
+ verb
181
+ else
182
+ find_verb_class(verb.to_s)
183
+ end
184
+ end
185
+
186
+ def find_verb_class string
187
+ case string
188
+ when /^get$/i then GET
189
+ when /^head$/i then HEAD
190
+ when /^put$/i then PUT
191
+ when /^post$/i then POST
192
+ when /^delete$/i then DELETE
193
+ else
194
+ raise ArgumentError, "Invalid verb #{string}"
195
+ end
196
+ end
197
+ end # Request
198
+
199
+ class Response
200
+ attr_reader :last_effective_uri
201
+
202
+ def initialize response, last_effective_uri
203
+ @response = response
204
+ @last_effective_uri = last_effective_uri
205
+ end
206
+
207
+ def code
208
+ @response.code.to_i
209
+ end
210
+
211
+ def headers
212
+ @headers ||= @response.each_header.entries
213
+ end
214
+
215
+ def body
216
+ @response.body
217
+ end
218
+
219
+ def inspect
220
+ "#<#{self.class} @code=#{code} @last_effective_uri=#{last_effective_uri}>"
221
+ end
222
+ end # Response
223
+
224
+ class Multipart
225
+ attr_reader :boundary
226
+
227
+ EOL = "\r\n"
228
+ DEFAULT_MIME_TYPE = 'application/octet-stream'
229
+
230
+ def initialize files, query = {}
231
+ @files = files
232
+ @query = query
233
+ @boundary = generate_boundary
234
+ end
235
+
236
+ def content_type
237
+ "multipart/form-data; boundary=#{boundary}"
238
+ end
239
+
240
+ def body
241
+ body = ''.encode('ASCII-8BIT')
242
+ separator = "--#{boundary}"
243
+
244
+ @query.each do |key, value|
245
+ body << separator << EOL
246
+ body << %Q{Content-Disposition: form-data; name="#{key}"} << EOL
247
+ body << EOL
248
+ body << value
249
+ body << EOL
250
+ end
251
+
252
+ @files.each do |name, handle|
253
+ if handle.respond_to?(:read)
254
+ path = handle.path
255
+ data = io.read
256
+ else
257
+ path = handle
258
+ data = IO.read(path)
259
+ end
260
+
261
+ filename = File.basename(path)
262
+ mime = mime_type(filename)
263
+
264
+ body << separator << EOL
265
+ body << %Q{Content-Disposition: form-data; name="#{name}"; filename="#{filename}"} << EOL
266
+ body << %Q{Content-Type: #{mime}} << EOL
267
+ body << %Q{Content-Transfer-Encoding: binary} << EOL
268
+ body << %Q{Content-Length: #{data.bytesize}} << EOL
269
+ body << EOL
270
+ body << data
271
+ body << EOL
272
+ end
273
+
274
+ body << separator << "--" << EOL
275
+ body
276
+ end
277
+
278
+ private
279
+ def generate_boundary
280
+ SecureRandom.random_bytes(16).unpack('H*').first
281
+ end
282
+
283
+ def mime_type filename
284
+ MIME::Types.type_for(File.extname(filename)).first || DEFAULT_MIME_TYPE
285
+ end
286
+ end # Multipart
287
+
288
+ # Helpers
289
+ class << self
290
+ # Creates a GET request and executes it, returning the response.
291
+ # @see HTTP::Client::Request#initialize
292
+ #
293
+ # @return [HTTP::Client::Response]
294
+ #
295
+ def get *args; Request.new(GET, *args).execute; end
296
+
297
+ # Creates a PUT request and executes it, returning the response.
298
+ # @see HTTP::Client::Request#initialize
299
+ #
300
+ # @return [HTTP::Client::Response]
301
+ #
302
+ def put *args; Request.new(PUT, *args).execute; end
303
+
304
+ # Creates a POST request and executes it, returning the response.
305
+ # @see HTTP::Client::Request#initialize
306
+ #
307
+ # @return [HTTP::Client::Response]
308
+ #
309
+ def post *args; Request.new(POST, *args).execute; end
310
+
311
+ # Creates a DELETE request and executes it, returning the response.
312
+ # @see HTTP::Client::Request#initialize
313
+ #
314
+ # @return [HTTP::Client::Response]
315
+ #
316
+ def delete *args; Request.new(DELETE, *args).execute; end
317
+
318
+ # Creates a OPTIONS request and executes it, returning the response.
319
+ # @see HTTP::Client::Request#initialize
320
+ #
321
+ # @return [HTTP::Client::Response]
322
+ #
323
+ def options *args; Request.new(OPTIONS, *args).execute; end
324
+
325
+ # Creates a TRACE request and executes it, returning the response.
326
+ # @see HTTP::Client::Request#initialize
327
+ #
328
+ # @return [HTTP::Client::Response]
329
+ #
330
+ def trace *args; Request.new(TRACE, *args).execute; end
331
+ end
332
+ end # Client
333
+ end # HTTP
data/test/helper.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'bundler/setup'
2
+
3
+ require 'http/client'
4
+ require 'minitest/spec'
5
+ require 'minitest/autorun'
6
+ require 'minitest/reporters'
7
+
8
+ Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
9
+
@@ -0,0 +1,34 @@
1
+ require_relative 'helper'
2
+
3
+ describe 'HTTP Client Request' do
4
+ it 'should reject invalid arguments' do
5
+ assert_raises(ArgumentError, "invalid verb") {HTTP::Client::Request.new(:foo)}
6
+ assert_raises(URI::InvalidURIError, "invalid uri") {HTTP::Client::Request.new(:get, "http://")}
7
+ assert_raises(ArgumentError, "invalid argument") {HTTP::Client::Request.new(:get, "http://example.org/", foo: 1)}
8
+ end
9
+
10
+ it 'validates body based on request verb' do
11
+ assert_raises(ArgumentError, "get cannot have body") {HTTP::Client::Request.new(:get, "http://a.c", files: {test: __FILE__})}
12
+ end
13
+
14
+ it 'allows creation of valid request object' do
15
+ assert HTTP::Client::Request.new(
16
+ :post,
17
+ "http://example.org/",
18
+ query: {title: "test"},
19
+ files: {test1: __FILE__, test2: __FILE__},
20
+ max_redirects: 2,
21
+ timeout: 10,
22
+ ssl_verify: HTTP::Client::SSL_VERIFY_NONE
23
+ )
24
+ end
25
+
26
+ # TODO: mock http endpoint
27
+ it 'executes a request and returns reponse' do
28
+ assert HTTP::Client.get("http://www.google.com")
29
+ end
30
+
31
+ it 'raises timeout errors' do
32
+ assert_raises(Net::OpenTimeout) {HTTP::Client.get("http://dingus.in:1000/", timeout: 0.2)}
33
+ end
34
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: http-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Bharanee Rathna
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-06-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mime-types
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: http-cookie
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '11.1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '11.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest-reporters
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.1'
69
+ description: Light weight wrapper around Net::HTTP
70
+ email:
71
+ - deepfryed@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - CHANGELOG
77
+ - README.md
78
+ - lib/http-client.rb
79
+ - lib/http/client.rb
80
+ - test/helper.rb
81
+ - test/test_request.rb
82
+ homepage: http://github.com/deepfryed/http-client
83
+ licenses:
84
+ - MIT
85
+ metadata: {}
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubyforge_project:
102
+ rubygems_version: 2.2.2
103
+ signing_key:
104
+ specification_version: 4
105
+ summary: A client wrapper around Net::HTTP
106
+ test_files: []