http-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []