ruby-requests 0.0.1.a1
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 +7 -0
- data/.gitignore +5 -0
- data/.gitlab-ci.yml +37 -0
- data/.rubocop-disables.yml +96 -0
- data/.rubocop.yml +9 -0
- data/Gemfile +9 -0
- data/LICENSE +21 -0
- data/Makefile +17 -0
- data/README.md +151 -0
- data/lib/requests.rb +95 -0
- data/lib/requests/adapters.rb +124 -0
- data/lib/requests/cookies.rb +98 -0
- data/lib/requests/exceptions.rb +32 -0
- data/lib/requests/http_methods.rb +36 -0
- data/lib/requests/logger.rb +37 -0
- data/lib/requests/request.rb +94 -0
- data/lib/requests/response.rb +93 -0
- data/lib/requests/session.rb +120 -0
- data/lib/requests/utils.rb +162 -0
- data/lib/requests/version.rb +4 -0
- data/ruby-requests.gemspec +23 -0
- data/spec/docker-compose.yml +23 -0
- data/spec/docker_test_entrypoint.sh +12 -0
- data/spec/docker_tests.sh +6 -0
- data/spec/functional/proxies/proxies_spec.rb +84 -0
- data/spec/functional/proxies/squid_conf/passwords +2 -0
- data/spec/functional/proxies/squid_conf/squid.conf +11 -0
- data/spec/functional/requests_spec.rb +326 -0
- data/spec/functional/session_spec.rb +111 -0
- data/spec/spec_helper.rb +44 -0
- data/spec/unit/api_spec.rb +42 -0
- data/spec/unit/utils_spec.rb +112 -0
- metadata +131 -0
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
|
5
|
+
##
|
6
|
+
# Initial implementation here will be very simple and lack features like
|
7
|
+
# connection pooling.
|
8
|
+
|
9
|
+
module Requests
|
10
|
+
class BaseAdapter
|
11
|
+
attr_reader :logger
|
12
|
+
private :logger
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@logger = Requests.logger
|
16
|
+
end
|
17
|
+
|
18
|
+
# :nocov:
|
19
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
20
|
+
def send_request(_request, stream: false, timeout: nil, verify: true,
|
21
|
+
cert: nil, proxies: {})
|
22
|
+
raise(NotImplementedError, "send_request must be implemented by " \
|
23
|
+
"subclasses of #{self.class.name}")
|
24
|
+
end
|
25
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
26
|
+
|
27
|
+
def close
|
28
|
+
raise(NotImplementedError, "close must be implemented by " \
|
29
|
+
"subclasses of #{self.class.name}")
|
30
|
+
end
|
31
|
+
# :nocov:
|
32
|
+
|
33
|
+
def determine_proxy(uri, proxies)
|
34
|
+
scheme_host = "#{uri.scheme}://#{uri.host}"
|
35
|
+
proxy = proxies[scheme_host] || proxies[uri.scheme]
|
36
|
+
proxy ? URI(proxy) : nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class HTTPAdapter < BaseAdapter
|
41
|
+
def send_request(request, stream: false, timeout: nil, verify: true,
|
42
|
+
cert: nil, proxies: {})
|
43
|
+
logger.info("Request URI: #{request.uri.inspect}")
|
44
|
+
|
45
|
+
if stream
|
46
|
+
raise(NotImplementedError,
|
47
|
+
"streaming response body is not yet implemented")
|
48
|
+
end
|
49
|
+
|
50
|
+
proxy = determine_proxy(request.uri, proxies)
|
51
|
+
http = create_http(request.uri, proxy, verify, cert)
|
52
|
+
|
53
|
+
if timeout.is_a?(Array)
|
54
|
+
http.open_timeout, http.read_timeout = timeout
|
55
|
+
else
|
56
|
+
http.open_timeout = timeout
|
57
|
+
http.read_timeout = timeout
|
58
|
+
end
|
59
|
+
|
60
|
+
# :nocov:
|
61
|
+
if ENV['REQUESTS_HTTP_DEBUG'] == 'true'
|
62
|
+
logger.debug("Setting Net::HTTP debug output to stdout")
|
63
|
+
http.set_debug_output($stdout)
|
64
|
+
end
|
65
|
+
# :nocov:
|
66
|
+
|
67
|
+
begin
|
68
|
+
resp = http.start do
|
69
|
+
http.request(request.raw)
|
70
|
+
end
|
71
|
+
rescue Net::OpenTimeout => e
|
72
|
+
raise ConnectionTimeout.new('Connection timed out.',
|
73
|
+
from_error: e, request: request)
|
74
|
+
rescue Net::ReadTimeout => e
|
75
|
+
raise ReadTimeout.new('Read timed out.',
|
76
|
+
from_error: e, request: request)
|
77
|
+
rescue Net::HTTPServerException => e
|
78
|
+
if e.message == '407 "Proxy Authentication Required"'
|
79
|
+
resp = Response.from_net_http_response(e.response,
|
80
|
+
request)
|
81
|
+
raise ProxyAuthError.new(e.message,
|
82
|
+
from_error: e,
|
83
|
+
request: request,
|
84
|
+
response: resp)
|
85
|
+
else
|
86
|
+
raise e
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
body_size = resp.body ? resp.body.bytesize : 0
|
91
|
+
logger.debug("Response body: #{body_size} bytes")
|
92
|
+
|
93
|
+
# TODO: handle redirects
|
94
|
+
|
95
|
+
response = Response.from_net_http_response(resp, request)
|
96
|
+
logger.info("Response: #{response.inspect}")
|
97
|
+
response
|
98
|
+
end
|
99
|
+
|
100
|
+
def create_http(uri, proxy, verify, _cert)
|
101
|
+
if proxy
|
102
|
+
logger.info("Using proxy: #{proxy.scheme}://#{proxy.host}:" \
|
103
|
+
"#{proxy.port}")
|
104
|
+
http = Net::HTTP.new(uri.host, uri.port, proxy.host, proxy.port)
|
105
|
+
http.proxy_user = proxy.user
|
106
|
+
http.proxy_pass = proxy.password
|
107
|
+
else
|
108
|
+
http = Net::HTTP.new(uri.host, uri.port, nil, nil)
|
109
|
+
end
|
110
|
+
|
111
|
+
http.use_ssl = (uri.scheme == 'https')
|
112
|
+
|
113
|
+
if verify == false
|
114
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
115
|
+
elsif verify.is_a?(String)
|
116
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
117
|
+
http.ca_file = verify
|
118
|
+
else
|
119
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
120
|
+
end
|
121
|
+
http
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'http-cookie'
|
2
|
+
|
3
|
+
|
4
|
+
module Requests
|
5
|
+
class Cookie < HTTP::Cookie
|
6
|
+
end
|
7
|
+
|
8
|
+
class CookieJar < HTTP::CookieJar
|
9
|
+
##
|
10
|
+
# Return Hash of cookies from this jar for a given uri.
|
11
|
+
# If provided no argument, it returns a Hash of all cookie
|
12
|
+
# key/value pairs in the jar, but this can lose information
|
13
|
+
# if the same cookie key was stored for different URIs.
|
14
|
+
#
|
15
|
+
# @param uri [URI String] optional
|
16
|
+
|
17
|
+
def to_hash(uri=nil)
|
18
|
+
hash = {}
|
19
|
+
cookies(uri).each do |c|
|
20
|
+
hash[c.name] = c.value
|
21
|
+
end
|
22
|
+
hash
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# Given another CookieJar, copy the cookies with this one
|
27
|
+
|
28
|
+
def merge!(jar)
|
29
|
+
jar.each do |cookie|
|
30
|
+
add(cookie.dup)
|
31
|
+
end
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Given an array of Set-Cookie header values, parse each
|
37
|
+
# and add them to this cookie jar.
|
38
|
+
#
|
39
|
+
# @param header [Array]
|
40
|
+
# @param uri [URI String] URI these cookies belong to
|
41
|
+
|
42
|
+
def set_cookies_from_header(header, uri)
|
43
|
+
header.each do |value|
|
44
|
+
parse(value, uri)
|
45
|
+
end
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.from_hash(hash, domain)
|
50
|
+
jar = new
|
51
|
+
hash.each do |key, value|
|
52
|
+
cookie = Cookie.new(key, value, domain: domain,
|
53
|
+
path: '/', for_domain: true)
|
54
|
+
jar.add(cookie)
|
55
|
+
end
|
56
|
+
jar
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
module CookiesMixin
|
61
|
+
##
|
62
|
+
# The following methods depend on the instance variable
|
63
|
+
# @uri being defined
|
64
|
+
|
65
|
+
def cookies=(cookies)
|
66
|
+
if cookies.is_a?(Hash)
|
67
|
+
domain = @uri.hostname.downcase
|
68
|
+
@cookies = CookieJar.from_hash(cookies, domain)
|
69
|
+
else
|
70
|
+
@cookies = cookies
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def cookies
|
75
|
+
@cookies ||= CookieJar.new
|
76
|
+
@cookies
|
77
|
+
end
|
78
|
+
|
79
|
+
def cookies_hash
|
80
|
+
@cookies.to_hash(@uri)
|
81
|
+
end
|
82
|
+
|
83
|
+
def cookie_header
|
84
|
+
Cookie.cookie_value(@cookies.cookies(@uri))
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Given an array of Set-Cookie header values, add them
|
89
|
+
# to @cookies
|
90
|
+
|
91
|
+
def set_cookies_from_header(header, uri)
|
92
|
+
return if header.nil? || header.empty?
|
93
|
+
@cookies ||= CookieJar.new
|
94
|
+
@cookies.set_cookies_from_header(header, uri)
|
95
|
+
@cookies
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
|
4
|
+
module Requests
|
5
|
+
class RequestException < RuntimeError
|
6
|
+
attr_reader :request, :response, :message, :from_error
|
7
|
+
|
8
|
+
def initialize(msg, from_error: nil, request: nil, response: nil)
|
9
|
+
super(msg)
|
10
|
+
@message = msg
|
11
|
+
@from_error = from_error
|
12
|
+
@request = request
|
13
|
+
@response = response
|
14
|
+
end
|
15
|
+
|
16
|
+
def inspect
|
17
|
+
"#<#{self.class.name} #{@message}>"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class TimeoutError < RequestException
|
22
|
+
end
|
23
|
+
|
24
|
+
class ConnectionTimeout < TimeoutError
|
25
|
+
end
|
26
|
+
|
27
|
+
class ReadTimeout < TimeoutError
|
28
|
+
end
|
29
|
+
|
30
|
+
class ProxyAuthError < RequestException
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Requests
|
2
|
+
module HttpMethods
|
3
|
+
def get(url, params: {}, **kwargs)
|
4
|
+
kwargs[:allow_redirects] = kwargs.fetch(:allow_redirects, true)
|
5
|
+
request('get', url, params: params, **kwargs)
|
6
|
+
end
|
7
|
+
|
8
|
+
def post(url, params: {}, data: nil, json: nil, **kwargs)
|
9
|
+
request('post', url, params: params, data: data,
|
10
|
+
json: json, **kwargs)
|
11
|
+
end
|
12
|
+
|
13
|
+
def put(url, params: {}, data: nil, json: nil, **kwargs)
|
14
|
+
request('put', url, params: params, data: data,
|
15
|
+
json: json, **kwargs)
|
16
|
+
end
|
17
|
+
|
18
|
+
def delete(url, **kwargs)
|
19
|
+
request('delete', url, **kwargs)
|
20
|
+
end
|
21
|
+
|
22
|
+
def head(url, **kwargs)
|
23
|
+
kwargs[:allow_redirects] = kwargs.fetch(:allow_redirects, false)
|
24
|
+
request('head', url, **kwargs)
|
25
|
+
end
|
26
|
+
|
27
|
+
def options(url, **kwargs)
|
28
|
+
kwargs[:allow_redirects] = kwargs.fetch(:allow_redirects, true)
|
29
|
+
request('options', url, **kwargs)
|
30
|
+
end
|
31
|
+
|
32
|
+
def patch(url, **kwargs)
|
33
|
+
request('patch', url, **kwargs)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
|
4
|
+
module Requests
|
5
|
+
class LogFormatter
|
6
|
+
FORMAT = "%<severity>s [%<time>s] %<msg>s\n".freeze
|
7
|
+
|
8
|
+
def call(severity, time, _, msg)
|
9
|
+
FORMAT % {:severity => severity.ljust(5),
|
10
|
+
:time => format_time(time),
|
11
|
+
:msg => msg}
|
12
|
+
end
|
13
|
+
|
14
|
+
def format_time(time)
|
15
|
+
time.iso8601
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Logger < ::Logger
|
20
|
+
DEFAULT_LOG_DEV = STDOUT
|
21
|
+
DEFAULT_FORMATTER = LogFormatter.new
|
22
|
+
|
23
|
+
attr_accessor :enabled
|
24
|
+
|
25
|
+
def initialize(logdev, shift_age=0, shift_size=1048576, level: DEBUG,
|
26
|
+
enabled: true)
|
27
|
+
super(logdev, shift_age, shift_size)
|
28
|
+
@enabled = enabled
|
29
|
+
self.formatter = formatter || DEFAULT_FORMATTER
|
30
|
+
self.level = level
|
31
|
+
end
|
32
|
+
|
33
|
+
def add(severity, message=nil, progname=nil)
|
34
|
+
super(severity, message, progname) if @enabled
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'net/http'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
|
6
|
+
module Requests
|
7
|
+
class Request
|
8
|
+
include CookiesMixin
|
9
|
+
include Utils::HeadersMixin
|
10
|
+
|
11
|
+
attr_accessor :method, :url, :uri, :files, :data, :params, :auth,
|
12
|
+
:json, :body
|
13
|
+
attr_reader :logger, :raw
|
14
|
+
private :logger
|
15
|
+
|
16
|
+
def initialize(method, url, headers: {}, files: nil, data: nil,
|
17
|
+
params: {}, auth: nil, cookies: nil, json: nil)
|
18
|
+
@logger = Requests.logger
|
19
|
+
@method = method
|
20
|
+
@url = url
|
21
|
+
@uri = URI(@url)
|
22
|
+
@headers = headers
|
23
|
+
|
24
|
+
@files = files
|
25
|
+
if @files
|
26
|
+
raise(NotImplementedError,
|
27
|
+
"files keyword param is not yet implemented")
|
28
|
+
end
|
29
|
+
|
30
|
+
@data = data
|
31
|
+
@params = params
|
32
|
+
@auth = auth
|
33
|
+
self.cookies = cookies
|
34
|
+
@json = json
|
35
|
+
end
|
36
|
+
|
37
|
+
def inspect
|
38
|
+
"#<Requests::Request #{@method}>"
|
39
|
+
end
|
40
|
+
|
41
|
+
def prepare
|
42
|
+
if @params && !@params.empty?
|
43
|
+
@uri.query = URI.encode_www_form(@params)
|
44
|
+
end
|
45
|
+
set_body_and_content_type
|
46
|
+
@raw = build_request
|
47
|
+
set_request_auth(@raw)
|
48
|
+
self.headers = @raw.to_hash
|
49
|
+
end
|
50
|
+
|
51
|
+
def set_body_and_content_type
|
52
|
+
if @json
|
53
|
+
@headers['Content-Type'] = 'application/json'
|
54
|
+
@body = @json.to_json
|
55
|
+
elsif @data.is_a?(String)
|
56
|
+
@headers['Content-Type'] ||= 'text/plain'
|
57
|
+
@body = @data
|
58
|
+
elsif @data.is_a?(Array)
|
59
|
+
@headers['Content-Type'] ||= 'application/x-www-form-urlencoded'
|
60
|
+
@body = URI.encode_www_form(Hash[@data])
|
61
|
+
elsif @data.is_a?(Hash)
|
62
|
+
@headers['Content-Type'] ||= 'application/x-www-form-urlencoded'
|
63
|
+
@body = URI.encode_www_form(@data)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def build_request
|
68
|
+
req = http_method_class.new(uri.request_uri)
|
69
|
+
req.body = body
|
70
|
+
@headers['cookie'] = cookie_header if !cookies.empty?
|
71
|
+
|
72
|
+
@headers.each do |key, value|
|
73
|
+
req[key] = value
|
74
|
+
end
|
75
|
+
req
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# @param request: Net::HTTPRequest object to set authentication on
|
80
|
+
# Currently only basic auth is supported
|
81
|
+
|
82
|
+
def set_request_auth(request)
|
83
|
+
if @auth && @auth.is_a?(Array)
|
84
|
+
user, pass = @auth
|
85
|
+
request.basic_auth(user, pass)
|
86
|
+
end
|
87
|
+
request
|
88
|
+
end
|
89
|
+
|
90
|
+
def http_method_class
|
91
|
+
Requests::Utils.http_method_class(@method)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'zlib'
|
3
|
+
|
4
|
+
|
5
|
+
module Requests
|
6
|
+
class Response
|
7
|
+
include CookiesMixin
|
8
|
+
include Utils::HeadersMixin
|
9
|
+
|
10
|
+
attr_accessor :status_code, :body, :request, :reason, :cookie_jar,
|
11
|
+
:uri, :raw
|
12
|
+
|
13
|
+
attr_reader :logger, :raw_headers
|
14
|
+
private :logger
|
15
|
+
|
16
|
+
def initialize(status_code:, reason:, headers:, body:, uri:,
|
17
|
+
request:, raw: nil)
|
18
|
+
@logger = Requests.logger
|
19
|
+
@status_code = status_code
|
20
|
+
@reason = reason
|
21
|
+
@raw_headers = Requests::Utils::InsensitiveDict.new(headers)
|
22
|
+
self.headers = headers
|
23
|
+
@body = body ? decode_content(body) : ''
|
24
|
+
@uri = URI(uri)
|
25
|
+
@request = request
|
26
|
+
@raw = raw
|
27
|
+
set_cookies_from_header(@raw_headers['set-cookie'], @request.uri)
|
28
|
+
fix_string_encoding
|
29
|
+
end
|
30
|
+
|
31
|
+
def inspect
|
32
|
+
"#<Requests::Response #{@status_code} #{@reason}>"
|
33
|
+
end
|
34
|
+
|
35
|
+
def url
|
36
|
+
uri.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
def text
|
40
|
+
body
|
41
|
+
end
|
42
|
+
|
43
|
+
def json
|
44
|
+
JSON.parse(body)
|
45
|
+
end
|
46
|
+
|
47
|
+
def decode_content(body)
|
48
|
+
if content_encoding == 'identity'
|
49
|
+
body
|
50
|
+
elsif content_encoding == 'gzip'
|
51
|
+
str_io = StringIO.new(body)
|
52
|
+
gz = Zlib::GzipReader.new(str_io)
|
53
|
+
gz.read
|
54
|
+
elsif content_encoding == 'deflate'
|
55
|
+
Zlib::Inflate.inflate(body)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def content_encoding
|
60
|
+
@headers['content-encoding'] || 'identity'
|
61
|
+
end
|
62
|
+
|
63
|
+
def fix_string_encoding
|
64
|
+
charset = charset_from_content_type
|
65
|
+
|
66
|
+
if charset
|
67
|
+
begin
|
68
|
+
encoding = Encoding.find(charset)
|
69
|
+
logger.debug("Forcing encoding for response body " \
|
70
|
+
"using charset: #{charset.inspect}")
|
71
|
+
@body.force_encoding(encoding)
|
72
|
+
rescue ArgumentError
|
73
|
+
logger.error("No such encoding: #{charset.inspect}")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def charset_from_content_type
|
79
|
+
Requests::Utils.charset_from_content_type(headers['Content-Type'])
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.from_net_http_response(resp, request)
|
83
|
+
body = resp.body rescue nil
|
84
|
+
self.new(status_code: resp.code.to_i,
|
85
|
+
reason: resp.message,
|
86
|
+
headers: resp.to_hash,
|
87
|
+
body: body,
|
88
|
+
uri: request.uri,
|
89
|
+
request: request,
|
90
|
+
raw: resp)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|