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,120 @@
1
+ module Requests
2
+ class Session
3
+ include HttpMethods
4
+
5
+ attr_accessor :headers, :cookies, :auth, :proxies, :params, :verify,
6
+ :cert, :stream, :max_redirects, :trust_env
7
+ attr_reader :logger
8
+ private :logger
9
+
10
+ DEFAULT_REDIRECT_LIMIT = 30
11
+
12
+ def initialize
13
+ @logger = Requests.logger
14
+ @headers = Utils.default_headers
15
+ @cookies = CookieJar.new
16
+ @proxies = {}
17
+ @params = {}
18
+ @verify = true
19
+ @stream = false
20
+ @trust_env = true
21
+ @max_redirects = DEFAULT_REDIRECT_LIMIT
22
+ end
23
+
24
+ def request(method, url, params: {}, data: nil, headers: {},
25
+ cookies: {}, files: nil, proxies: {}, stream: false,
26
+ auth: nil, timeout: nil, json: nil, allow_redirects: true,
27
+ verify: true)
28
+ headers = @headers.merge(headers)
29
+ req = Request.new(method.upcase,
30
+ url,
31
+ headers: headers,
32
+ files: files,
33
+ params: params,
34
+ data: data,
35
+ json: json,
36
+ cookies: cookies,
37
+ auth: auth)
38
+
39
+ prepare_request(req)
40
+ settings = merge_environment_settings(
41
+ req.url, proxies, stream, verify, cert
42
+ )
43
+ send_kwargs = {
44
+ :timeout => timeout,
45
+ :allow_redirects => allow_redirects,
46
+ }.merge(settings)
47
+ send_request(req, **send_kwargs)
48
+ end
49
+
50
+ def prepare_request(request)
51
+ merged_cookies = CookieJar.new
52
+ merged_cookies.merge!(@cookies)
53
+ merged_cookies.merge!(request.cookies)
54
+
55
+ request.headers = merge_setting(request.headers, @headers,
56
+ Utils::InsensitiveDict)
57
+ request.params = merge_setting(request.params, @params)
58
+ request.auth = merge_setting(request.auth, @auth)
59
+ request.cookies = merged_cookies
60
+ request.prepare
61
+ request
62
+ end
63
+
64
+ def send_request(request, **kwargs)
65
+ kwargs[:stream] = kwargs.fetch(:stream, @stream)
66
+ kwargs[:verify] = kwargs.fetch(:verify, @verify)
67
+ kwargs[:cert] = kwargs.fetch(:cert, @cert)
68
+ kwargs[:proxies] = kwargs.fetch(:proxies, @proxies)
69
+
70
+ ## Use the following line once implementing redirects:
71
+ # allow_redirects = kwargs.delete(:allow_redirects) || true
72
+ kwargs.delete(:allow_redirects)
73
+
74
+ response = adapter.send_request(request, **kwargs)
75
+ @cookies.merge!(response.cookies)
76
+ response
77
+ end
78
+
79
+ def merge_environment_settings(_url, proxies, stream, verify, cert)
80
+ # TODO: Implement actually using environment settings
81
+ proxies = merge_setting(proxies, @proxies)
82
+ stream = merge_setting(stream, @stream)
83
+ verify = merge_setting(verify, @verify)
84
+ cert = merge_setting(cert, @cert)
85
+
86
+ {verify: verify,
87
+ proxies: proxies,
88
+ stream: stream,
89
+ cert: cert}
90
+ end
91
+
92
+ ##
93
+ # Determine appropriate setting for a given request, taking into account
94
+ # the explicit setting on that request, and the setting in the session.
95
+ # If a is a Hash or InsensitiveDict they will be merged together using
96
+ # `dict_class`
97
+
98
+ def merge_setting(request_setting, session_setting, dict_class=Hash)
99
+ return request_setting if session_setting.nil?
100
+ return session_setting if request_setting.nil?
101
+
102
+ map_types = [Hash, Requests::Utils::InsensitiveDict]
103
+ if !(map_types.include?(request_setting.class) &&
104
+ map_types.include?(session_setting.class))
105
+ return request_setting
106
+ end
107
+
108
+ merged_setting = dict_class.new.merge(session_setting)
109
+ merged_setting.merge!(request_setting)
110
+
111
+ nil_keys = merged_setting.select { |_k, v| v.nil? }.map(&:first)
112
+ nil_keys.each { |key| merged_setting.delete(key) }
113
+ merged_setting
114
+ end
115
+
116
+ def adapter
117
+ HTTPAdapter.new
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,162 @@
1
+ require 'forwardable'
2
+
3
+
4
+ module Requests
5
+ module Utils
6
+ ##
7
+ # Dictionary that stores keys as case-insensitive strings.
8
+ # Only Strings can be used as keys in this structure; attempts
9
+ # to use anything else as a key will raise a TypeError.
10
+
11
+ class InsensitiveDict
12
+ attr_accessor :store, :dict
13
+
14
+ extend Forwardable
15
+ include Enumerable
16
+
17
+ def_delegators :@dict, :each
18
+
19
+ def initialize(init_dict={})
20
+ @store = {}
21
+ @dict = {}
22
+ init_dict.each do |k, v|
23
+ self[k] = v
24
+ end
25
+ end
26
+
27
+ ##
28
+ # Can compare to another InsensitiveDict or to a regular Hash.
29
+
30
+ def ==(compare)
31
+ compare.each do |k, v|
32
+ return false if !k.is_a?(String) || self[k.downcase] != v
33
+ end
34
+ true
35
+ end
36
+
37
+ def []=(k, v)
38
+ check_key_type(k)
39
+ @store[k.downcase] = [k, v]
40
+ @dict[k.downcase] = v
41
+ end
42
+
43
+ def [](k)
44
+ check_key_type(k)
45
+ @dict[k.downcase]
46
+ end
47
+
48
+ def merge!(hash)
49
+ hash.each do |k, v|
50
+ self[k] = v
51
+ end
52
+ self
53
+ end
54
+
55
+ def merge(new)
56
+ hash = to_h
57
+ new.each do |k, v|
58
+ hash[k] = v
59
+ end
60
+ hash
61
+ end
62
+
63
+ def to_h
64
+ hash = {}
65
+ @store.each_value do |i|
66
+ orig, value = i
67
+ hash[orig] = value
68
+ end
69
+ hash
70
+ end
71
+
72
+ def inspect
73
+ to_h.inspect
74
+ end
75
+
76
+ def to_s
77
+ to_h.to_s
78
+ end
79
+
80
+ def delete(key)
81
+ if key.is_a?(String)
82
+ key = key.downcase
83
+ @store.delete(key)
84
+ @dict.delete(key)
85
+ end
86
+ end
87
+
88
+ private def check_key_type(key)
89
+ if !key.is_a?(String)
90
+ err = "Key for #{self.class.name} object " \
91
+ "must be String, not #{key.class.name}"
92
+ raise(TypeError, err)
93
+ end
94
+ end
95
+ end
96
+
97
+ def self.default_user_agent
98
+ "ruby-requests/#{Requests::VERSION}"
99
+ end
100
+
101
+ def self.default_accept_encoding
102
+ 'gzip, deflate'
103
+ end
104
+
105
+ def self.default_headers
106
+ headers = {
107
+ 'User-Agent' => default_user_agent,
108
+ 'Accept-Encoding' => default_accept_encoding,
109
+ 'Accept' => '*/*',
110
+ 'Connection' => 'keep-alive',
111
+ }
112
+ InsensitiveDict.new(headers)
113
+ end
114
+
115
+ module HeadersMixin
116
+ ##
117
+ # Set @headers to InsensitiveDict keyed by String with Strings as
118
+ # values. When passed the result of Net::HTTPResponse.to_hash,
119
+ # which is a Hash keyed by String with Arrays as values, it will
120
+ # join the elements in the Array by ', ' as the header value.
121
+ # 'Set-Cookie' is handled specially because a comma is valid in a
122
+ # cookie definition, so it is joined by '; ' instead
123
+
124
+ def headers=(hash)
125
+ @headers = Utils::InsensitiveDict.new
126
+ hash.each do |k, v|
127
+ delimiter = ', '
128
+ @headers[k] = v.is_a?(Array) ? v.join(delimiter) : v
129
+ end
130
+ end
131
+
132
+ def headers
133
+ @headers
134
+ end
135
+ end
136
+
137
+ ##
138
+ # Given a string indicating an HTTP method, return the
139
+ # corresponding Net::HTTP class
140
+
141
+ def self.http_method_class(method)
142
+ {
143
+ 'GET' => Net::HTTP::Get,
144
+ 'POST' => Net::HTTP::Post,
145
+ 'PUT' => Net::HTTP::Put,
146
+ 'DELETE' => Net::HTTP::Delete,
147
+ 'HEAD' => Net::HTTP::Head,
148
+ 'OPTIONS' => Net::HTTP::Options,
149
+ 'PATCH' => Net::HTTP::Patch,
150
+ }[method.upcase]
151
+ end
152
+
153
+ def self.charset_from_content_type(header)
154
+ charset = nil
155
+ if header
156
+ match = header.match('charset=([^\s;]+)')
157
+ charset = match.captures[0] if match
158
+ end
159
+ charset
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,4 @@
1
+ module Requests
2
+ VERSION = '0.0.1.a1'
3
+ VERSION_INFO = Gem::Version.new(VERSION)
4
+ end
@@ -0,0 +1,23 @@
1
+ require File.expand_path('lib/requests/version', __dir__)
2
+
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'ruby-requests'
6
+ s.version = Requests::VERSION
7
+ s.authors = ['Daniel Hones']
8
+ s.description = "A library for HTTP requests that aims to provide " \
9
+ "the same API as the Requests library in Python"
10
+ s.summary = 'A Ruby version of the Requests library for Python'
11
+
12
+ s.license = 'MIT'
13
+ s.homepage = 'https://gitlab.com/danielhones/ruby_requests'
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {spec}/**/*`.split("\n")
17
+ s.required_ruby_version = ">= 2.3.0"
18
+
19
+ s.add_dependency('http-cookie', '~> 1.0')
20
+ s.add_development_dependency('rspec', '~> 3.7')
21
+ s.add_development_dependency('rubocop', '~> 0.54.0')
22
+ s.add_development_dependency('simplecov', '~> 0.15')
23
+ end
@@ -0,0 +1,23 @@
1
+ version: '3'
2
+
3
+ services:
4
+ proxy:
5
+ image: sameersbn/squid
6
+ volumes:
7
+ - ./functional/proxies/squid_conf:/etc/squid3
8
+ - ./functional/proxies/squid_logs:/var/log/squid3
9
+ ports:
10
+ - "3128:3128"
11
+
12
+ ruby:
13
+ image: ruby
14
+ command: /code/spec/docker_test_entrypoint.sh
15
+ environment:
16
+ - REPO_ROOT_DIR=/code
17
+ - RUNNING_IN_DOCKER=true
18
+ - PROXY_HOST=proxy
19
+ - PROXY_PORT=3128
20
+ volumes:
21
+ - ../:/code
22
+ depends_on:
23
+ - "proxy"
@@ -0,0 +1,12 @@
1
+ #!/bin/bash
2
+ #
3
+ # This is the script used as the entrypoint for running proxy
4
+ # tests from within a docker container as part of docker-compose
5
+
6
+ cd $REPO_ROOT_DIR
7
+ gem install -g Gemfile
8
+ rspec spec/
9
+ result=$?
10
+
11
+ echo "ENTRYPOINT: Rspec returned, exiting with code $result"
12
+ exit $result
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+
3
+ this_dir="$(dirname $0)"
4
+ docker_compose_file="$this_dir/docker-compose.yml"
5
+
6
+ docker-compose -f "$docker_compose_file" up --exit-code-from ruby
@@ -0,0 +1,84 @@
1
+ require_relative '../../spec_helper'
2
+
3
+ ##
4
+ # These tests will run from inside the docker container named
5
+ # "ruby", set up by the docker-compose called in spec/docker_tests.sh
6
+
7
+ RSpec.describe 'proxies', :if => running_in_docker do
8
+ Requests.enable_dev_mode
9
+
10
+ let(:proxy) { ENV['PROXY_HOST'] + ":" + ENV['PROXY_PORT'] }
11
+
12
+ # These are hard-coded in the squid_conf/passwords file which gets
13
+ # placed on the squid proxy container:
14
+ let(:proxy_user) { "foobar" }
15
+ let(:proxy_pass) { "SecretPass" }
16
+
17
+ before(:each) do
18
+ # The squid_logs directory is shared in a volume with both
19
+ # the proxy and ruby docker containers:
20
+ @proxy_log_file = File.join(__dir__, 'squid_logs/access.log')
21
+ if File.exist?(@proxy_log_file)
22
+ @proxy_log = File.open(@proxy_log_file)
23
+ @proxy_log.read
24
+ end
25
+ proxy_creds = "http://#{proxy_user}:#{proxy_pass}@#{proxy}"
26
+ proxy_no_creds = "http://#{proxy}"
27
+ @proxies = {'http' => proxy_creds, 'https' => proxy_creds}
28
+ @proxies_no_creds = {'http' => proxy_no_creds,
29
+ 'https' => proxy_no_creds}
30
+ end
31
+
32
+ after(:each) do
33
+ @proxy_log.close if @proxy_log
34
+ end
35
+
36
+ context "HTTP" do
37
+ before(:each) do
38
+ @url = HTTPBIN_URLS['HTTP'] + '/anything'
39
+ end
40
+
41
+ it 'through proxy' do
42
+ r = Requests.get(@url, proxies: @proxies)
43
+ sleep(1) # wait for log to be written to file
44
+ new_log_entries = @proxy_log.readlines
45
+ expect(r.status_code).to eq(200)
46
+ msg = /GET #{@url} #{proxy_user}/
47
+ expect(new_log_entries.any? { |x| x =~ msg }).to eq(true)
48
+ end
49
+
50
+ it "through proxy, wrong credentials returns 407 response" do
51
+ r = Requests.get(@url, proxies: @proxies_no_creds)
52
+ sleep(1) # wait for log to be written to file
53
+ new_log_entries = @proxy_log.readlines
54
+ expect(r.status_code).to eq(407)
55
+ msg = /TCP_DENIED\/407 .* GET #{@url}/
56
+ expect(new_log_entries.any? { |x| x =~ msg }).to eq(true)
57
+ end
58
+ end
59
+
60
+ context "HTTPS" do
61
+ before(:each) do
62
+ @url = HTTPBIN_URLS['HTTPS'] + '/anything'
63
+ end
64
+
65
+ it 'through proxy' do
66
+ r = Requests.get(@url, proxies: @proxies)
67
+ sleep(1) # wait for log to be written to file
68
+ new_log_entries = @proxy_log.readlines
69
+ expect(r.status_code).to eq(200)
70
+ msg = /CONNECT #{URI(@url).host}/
71
+ expect(new_log_entries.any? { |x| x =~ msg }).to eq(true)
72
+ end
73
+
74
+ it "through proxy, wrong credentials raises ProxyAuthError" do
75
+ expect {
76
+ Requests.get(@url, proxies: @proxies_no_creds)
77
+ }.to raise_error(Requests::ProxyAuthError)
78
+ sleep(1) # wait for log to be written to file
79
+ new_log_entries = @proxy_log.readlines
80
+ msg = /TCP_DENIED\/407 .* CONNECT #{URI(@url).host}/
81
+ expect(new_log_entries.any? { |x| x =~ msg }).to eq(true)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,2 @@
1
+ # foobar:SecretPass
2
+ foobar:$apr1$09.iONid$31VssE/7OUnEyu95osiAe0