ruby-requests 0.0.1.a1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|