├── .gems ├── .gitignore ├── LICENSE ├── README.markdown ├── lib ├── cacert.pem ├── requests.rb └── requests │ └── sugar.rb ├── makefile ├── requests.gemspec └── tests ├── proxy_test.rb ├── requests_test.rb └── ssl_test.rb /.gems: -------------------------------------------------------------------------------- 1 | cutest -v 1.2.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Cyril David 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # requests 2 | 3 | Requests: HTTP for Humans 4 | 5 | ## Description 6 | 7 | Inspired by the Requests library for Python, this gem provides an 8 | easy way to issue HTTP requests. 9 | 10 | ## Usage 11 | 12 | Here's an example of a GET request: 13 | 14 | ```ruby 15 | require "requests" 16 | 17 | response = Requests.request("GET", "http://example.com") 18 | 19 | # Now you have these methods available 20 | response.status #=> Number with the status code 21 | response.headers #=> Hash with the response headers 22 | response.body #=> String with the response body 23 | ``` 24 | 25 | If instead of calling `Requests.request` you prefer to specify the 26 | HTTP method directly, you can use `requests/sugar` instead: 27 | 28 | ```ruby 29 | require "requests/sugar" 30 | 31 | response = Requests.get("http://example.com") 32 | 33 | # And again you get a response 34 | response.status #=> Number with the status code 35 | response.headers #=> Hash with the response headers 36 | response.body #=> String with the response body 37 | ``` 38 | 39 | You can also pass parameters with a query string: 40 | 41 | ```ruby 42 | # GET http://example.com?foo=bar 43 | Requests.get("http://example.com", params: { foo: "bar" }) 44 | ``` 45 | 46 | If you want to send data with a POST request, you can add a `data` 47 | option with the value. 48 | 49 | ```ruby 50 | Requests.post("http://example.com", data: "hello world") 51 | ``` 52 | 53 | For Basic Authentication, you can provide the option `auth`, which 54 | should contain an array with the username and password: 55 | 56 | ```ruby 57 | Requests.get("http://example.com", auth: ["username", "password"]) 58 | ``` 59 | 60 | ## Installation 61 | 62 | As usual, you can install it using rubygems. 63 | 64 | ``` 65 | $ gem install requests 66 | ``` 67 | -------------------------------------------------------------------------------- /lib/requests.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'net/http' 3 | require 'openssl' 4 | require 'uri' 5 | 6 | module Requests 7 | class Error < StandardError 8 | attr_reader :response 9 | 10 | def initialize(response) 11 | super(response.message) 12 | 13 | @response = response 14 | end 15 | end 16 | 17 | CA_FILE = ENV.fetch('REQUESTS_CA_FILE', 18 | File.expand_path('../cacert.pem', __FILE__)) 19 | 20 | def self.request(method, url, 21 | headers: {}, 22 | data: nil, 23 | params: nil, 24 | auth: nil, 25 | proxy: nil, 26 | options: {}) 27 | 28 | uri = URI.parse(url) 29 | uri.query = encode_www_form(params) if params 30 | 31 | body = process_params(headers: headers, data: data) if data 32 | 33 | basic_auth(headers, *auth) if auth 34 | 35 | proxy = proxy.to_h.values_at(:host, :port, :user, :password) 36 | response = Net::HTTP.start(uri.host, uri.port, *proxy, opts(uri, options)) do |http| 37 | http.send_request(method, uri, body, headers) 38 | end 39 | 40 | if response.is_a?(Net::HTTPSuccess) 41 | Response.new(response.code, response.to_hash, response.body) 42 | else 43 | raise Error, response 44 | end 45 | end 46 | 47 | private 48 | def self.encode_www_form(params) 49 | URI.encode_www_form(params) 50 | end 51 | 52 | def self.opts(uri, options) 53 | if uri.scheme == 'https' 54 | { use_ssl: true, 55 | verify_mode: OpenSSL::SSL::VERIFY_PEER, 56 | ca_file: CA_FILE, 57 | read_timeout: options[:read_timeout], 58 | open_timeout: options[:open_timeout], 59 | } 60 | end 61 | end 62 | 63 | def self.basic_auth(headers, user, pass) 64 | headers['Authorization'] = 'Basic ' + ["#{user}:#{pass}"].pack('m0') 65 | end 66 | 67 | def self.process_params(headers: nil, data: nil) 68 | if not data.kind_of?(Enumerable) 69 | data 70 | else 71 | headers['content-type'] = 'application/x-www-form-urlencoded' 72 | 73 | encode_www_form(data) 74 | end 75 | end 76 | 77 | class Response 78 | attr :status 79 | attr :headers 80 | attr :body 81 | 82 | def initialize(status, headers, body) 83 | @status, @headers, @body = Integer(status), headers, body 84 | end 85 | 86 | # TODO Verify that JSON can parse data without encoding stuff 87 | def json 88 | JSON.parse(@body) 89 | end 90 | 91 | # TODO Verify that this is based on content-type header 92 | def encoding 93 | @body.encoding 94 | end 95 | 96 | # TODO this will probably do something related to encoding if necessary 97 | def text 98 | @body 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/requests/sugar.rb: -------------------------------------------------------------------------------- 1 | require 'requests' 2 | 3 | module Requests 4 | module Sugar 5 | def get(url, **kwargs) 6 | request('GET', url, **kwargs) 7 | end 8 | 9 | def post(url, data: nil, **kwargs) 10 | request('POST', url, data: data, **kwargs) 11 | end 12 | 13 | def put(url, data: nil, **kwargs) 14 | request('PUT', url, data: data, **kwargs) 15 | end 16 | 17 | def delete(url, **kwargs) 18 | request('DELETE', url, **kwargs) 19 | end 20 | 21 | def head(url, **kwargs) 22 | request('HEAD', url, **kwargs) 23 | end 24 | 25 | def options(url, **kwargs) 26 | request('OPTIONS', url, **kwargs) 27 | end 28 | 29 | def patch(url, **kwargs) 30 | request('PATCH', url, **kwargs) 31 | end 32 | 33 | def trace(url, **kwargs) 34 | request('TRACE', url, **kwargs) 35 | end 36 | end 37 | 38 | extend Sugar 39 | end 40 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | test: 2 | RUBYLIB=./lib cutest tests/*_test.rb 3 | -------------------------------------------------------------------------------- /requests.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "requests" 5 | s.version = "1.0.2" 6 | s.summary = "Requests: HTTP for Humans (Ruby port)" 7 | s.description = "Because Requests for Python is awesome" 8 | s.authors = ["Cyril David"] 9 | s.email= ["cyx@cyx.is"] 10 | s.homepage = "http://github.com/cyx/requests" 11 | s.files = Dir[ 12 | "LICENSE", 13 | "README", 14 | "makefile", 15 | "lib/**/*.rb", 16 | "lib/cacert.pem", 17 | "tests/*.rb", 18 | "*.gemspec" 19 | ] 20 | 21 | s.license = "MIT" 22 | s.require_paths = ["lib"] 23 | s.add_development_dependency "cutest" 24 | end 25 | -------------------------------------------------------------------------------- /tests/proxy_test.rb: -------------------------------------------------------------------------------- 1 | require 'requests/sugar' 2 | require 'webrick' 3 | require 'webrick/httpproxy' 4 | require 'thread' 5 | require 'logger' 6 | 7 | port = ENV.fetch("PROXY_PORT", 8000) 8 | 9 | setup do 10 | WEBrick::HTTPProxyServer.new( 11 | ServerName: "0.0.0.0", 12 | Port: port, 13 | Logger: Logger.new("/dev/null"), 14 | AccessLog: [] 15 | ) 16 | end 17 | 18 | test 'request via proxy' do |proxy| 19 | Thread.new { proxy.start } 20 | 21 | r = Requests.get('http://httpbin.org/get', params: { foo: 'bar' }, proxy: { 22 | host: '0.0.0.0', port: port 23 | }) 24 | 25 | assert_equal 200, r.status 26 | assert_equal ['application/json'], r.headers['content-type'] 27 | assert(r.json['args'] && r.json['args']['foo'] == 'bar') 28 | 29 | assert_equal ["1.1 vegur, 1.1 0.0.0.0:#{port}"], r.headers['via'] 30 | 31 | proxy.shutdown 32 | end 33 | -------------------------------------------------------------------------------- /tests/requests_test.rb: -------------------------------------------------------------------------------- 1 | require 'requests/sugar' 2 | 3 | test 'basic auth' do 4 | r = Requests.get('http://httpbin.org/basic-auth/u/p', auth: ['u', 'p']) 5 | 6 | assert_equal r.json['authenticated'], true 7 | assert_equal r.json['user'], 'u' 8 | end 9 | 10 | test 'GET' do 11 | r = Requests.get('http://httpbin.org/get', params: { foo: 'bar' }) 12 | 13 | assert_equal 200, r.status 14 | assert_equal ['application/json'], r.headers['content-type'] 15 | assert_equal 'UTF-8', r.encoding.to_s 16 | 17 | assert(r.json['args'] && r.json['args']['foo'] == 'bar') 18 | end 19 | 20 | test 'POST data' do 21 | r = Requests.post('http://httpbin.org/post', data: { "plan" => "test" }) 22 | 23 | assert_equal 200, r.status 24 | assert_equal ['application/json'], r.headers['content-type'] 25 | assert_equal 'UTF-8', r.encoding.to_s 26 | 27 | assert(r.json['form'] && r.json['form'] == { 'plan' => 'test' }) 28 | end 29 | 30 | test 'PUT data' do 31 | 32 | end 33 | 34 | test 'POST params' do 35 | payload = [ 36 | ['a[]', 'a1'], 37 | ['a[]', 'a2'], 38 | ['b', '3'], 39 | ['c', '4'] 40 | ] 41 | 42 | r = Requests.post('http://httpbin.org/post', data: payload) 43 | 44 | assert_equal 200, r.status 45 | 46 | form = r.json['form'] 47 | 48 | assert_equal form['a[]'], ['a1', 'a2'] 49 | assert_equal form['b'], '3' 50 | assert_equal form['c'], '4' 51 | end 52 | 53 | test 'Error' do 54 | begin 55 | Requests.post('http://httpbin.org/something') 56 | rescue Requests::Error => e 57 | assert_equal Net::HTTPNotFound, e.response.class 58 | end 59 | end 60 | 61 | test 'read timeout' do 62 | begin 63 | Requests.get('http://httpbin.org:10000', options: { read_timeout: 1 }) 64 | rescue => err 65 | assert err.kind_of?(Errno::ECONNREFUSED) 66 | end 67 | end 68 | 69 | test 'read timeout not failing' do 70 | begin 71 | Requests.get('http://httpbin.org/get', options: { read_timeout: 30 }) 72 | rescue => err 73 | flunk(err) 74 | end 75 | end 76 | 77 | test 'open timeout' do 78 | begin 79 | Requests.get('http://httpbin.org:10000', options: { open_timeout: 1 }) 80 | rescue => err 81 | assert err.kind_of?(Errno::ECONNREFUSED) 82 | else 83 | flunk("expected exception") 84 | end 85 | end 86 | 87 | test 'open timeout not failing' do 88 | begin 89 | Requests.get('http://httpbin.org/get', options: { open_timeout: 30 }) 90 | rescue => err 91 | flunk(err) 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /tests/ssl_test.rb: -------------------------------------------------------------------------------- 1 | require 'requests' 2 | 3 | test 'ssl' do 4 | response = Requests.request('GET', 'https://httpbin.org/get') 5 | 6 | assert_equal 200, response.status 7 | end 8 | --------------------------------------------------------------------------------