ruby-requests 0.0.1.a1

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