ruby-proxy-headers 0.2.2 → 0.3.0
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 +4 -4
- data/CHANGELOG.md +49 -0
- data/README.md +48 -1
- data/docs/core-api.md +184 -0
- data/docs/excon.md +29 -0
- data/docs/faraday.md +29 -0
- data/docs/getting-started.md +66 -0
- data/docs/http-gem.md +126 -0
- data/docs/httparty.md +35 -0
- data/docs/index.md +109 -0
- data/docs/net-http.md +29 -0
- data/docs/rest-client.md +96 -0
- data/docs/testing.md +105 -0
- data/docs/typhoeus.md +47 -0
- data/lib/ruby_proxy_headers/connection.rb +1 -8
- data/lib/ruby_proxy_headers/faraday.rb +19 -16
- data/lib/ruby_proxy_headers/http_gem.rb +61 -40
- data/lib/ruby_proxy_headers/net_http.rb +76 -131
- data/lib/ruby_proxy_headers/rest_client.rb +76 -60
- data/lib/ruby_proxy_headers/typhoeus.rb +81 -74
- data/lib/ruby_proxy_headers/version.rb +1 -1
- data/lib/ruby_proxy_headers.rb +45 -0
- metadata +70 -10
- data/DEFERRED.md +0 -29
- data/IMPLEMENTATION_PRIORITY.md +0 -72
- data/LIBRARY_RESEARCH.md +0 -71
data/docs/index.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# ruby-proxy-headers
|
|
2
|
+
|
|
3
|
+
Send and receive custom proxy headers during HTTPS CONNECT tunneling in Ruby.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
When making HTTPS requests through a proxy server, a tunnel is established using the HTTP CONNECT method:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Client → CONNECT example.com:443 HTTP/1.1 → Proxy → Target
|
|
11
|
+
← 200 Connection established ←
|
|
12
|
+
←→ [TLS encrypted traffic] ←→
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Standard Ruby HTTP libraries support basic proxy authentication but **do not** allow:
|
|
16
|
+
|
|
17
|
+
- Sending additional custom headers during CONNECT (e.g., `X-ProxyMesh-Country`)
|
|
18
|
+
- Accessing headers returned by the proxy (e.g., `X-ProxyMesh-IP`)
|
|
19
|
+
|
|
20
|
+
**ruby-proxy-headers** solves both problems.
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- **Send custom headers** to the proxy during HTTPS CONNECT
|
|
25
|
+
- **Receive proxy response headers** from the CONNECT response
|
|
26
|
+
- **Support for 7 popular Ruby HTTP libraries**
|
|
27
|
+
- **Thread-safe** implementation
|
|
28
|
+
- **Pure Ruby** core (no native extensions required)
|
|
29
|
+
|
|
30
|
+
## Supported Libraries
|
|
31
|
+
|
|
32
|
+
| Library | Module | Status |
|
|
33
|
+
|---------|--------|--------|
|
|
34
|
+
| Net::HTTP | `RubyProxyHeaders::NetHTTP` | ✅ |
|
|
35
|
+
| Faraday | `RubyProxyHeaders::FaradayIntegration` | ✅ |
|
|
36
|
+
| HTTParty | `RubyProxyHeaders::ProxyHeadersConnectionAdapter` | ✅ |
|
|
37
|
+
| HTTP.rb | `RubyProxyHeaders::HTTPGem` | ✅, http gem >= 6 |
|
|
38
|
+
| Typhoeus | `RubyProxyHeaders::Typhoeus` | ✅, Typhoeus ~> 1.7 |
|
|
39
|
+
| Excon | `RubyProxyHeaders::ExconIntegration` | Send only |
|
|
40
|
+
| RestClient | `RubyProxyHeaders::RestClient` | ✅ |
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
require 'uri'
|
|
46
|
+
require 'ruby_proxy_headers/net_http'
|
|
47
|
+
|
|
48
|
+
RubyProxyHeaders::NetHTTP.patch!
|
|
49
|
+
|
|
50
|
+
uri = URI('https://api.ipify.org?format=json')
|
|
51
|
+
proxy = URI('http://user:pass@proxy.example.com:31280')
|
|
52
|
+
http = Net::HTTP.new(uri.host, uri.port, proxy.host, proxy.port, proxy.user, proxy.password)
|
|
53
|
+
http.use_ssl = true
|
|
54
|
+
http.proxy_connect_request_headers = { 'X-ProxyMesh-Country' => 'US' }
|
|
55
|
+
response = http.request(Net::HTTP::Get.new(uri))
|
|
56
|
+
|
|
57
|
+
puts response.code
|
|
58
|
+
puts response.body
|
|
59
|
+
puts http.last_proxy_connect_response_headers['X-ProxyMesh-IP']
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Use Cases
|
|
63
|
+
|
|
64
|
+
### ProxyMesh Integration
|
|
65
|
+
|
|
66
|
+
[ProxyMesh](https://docs.proxymesh.com/) uses custom headers for:
|
|
67
|
+
|
|
68
|
+
- **Country selection**: `X-ProxyMesh-Country: US`
|
|
69
|
+
- **Session persistence**: `X-ProxyMesh-Session: abc123`
|
|
70
|
+
- **IP assignment feedback**: Returns `X-ProxyMesh-IP` in response
|
|
71
|
+
|
|
72
|
+
### Proxy Service Headers
|
|
73
|
+
|
|
74
|
+
Many proxy services use custom headers for:
|
|
75
|
+
|
|
76
|
+
- Geolocation targeting
|
|
77
|
+
- Session management
|
|
78
|
+
- Usage tracking
|
|
79
|
+
- Load balancing hints
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
Add to your Gemfile:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
gem 'ruby-proxy-headers'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Then run:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
bundle install
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Or install directly:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
gem install ruby_proxy_headers
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Requirements
|
|
102
|
+
|
|
103
|
+
- Ruby >= 3.2
|
|
104
|
+
- OpenSSL (included in standard Ruby)
|
|
105
|
+
|
|
106
|
+
## Related Projects
|
|
107
|
+
|
|
108
|
+
- [python-proxy-headers](https://github.com/proxymesh/python-proxy-headers) - Python version
|
|
109
|
+
- [javascript-proxy-headers](https://github.com/proxymeshai/javascript-proxy-headers) - JavaScript/Node.js version
|
data/docs/net-http.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Net::HTTP
|
|
2
|
+
|
|
3
|
+
`RubyProxyHeaders::NetHTTP.patch!` prepends a small wrapper around the installed `Net::HTTP#connect`. The gem does not replace that method with its own copy. Call `patch!` once before opening connections.
|
|
4
|
+
|
|
5
|
+
Proxy header names are documented at <https://docs.proxymesh.com/>.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require 'uri'
|
|
11
|
+
require 'ruby_proxy_headers/net_http'
|
|
12
|
+
|
|
13
|
+
RubyProxyHeaders::NetHTTP.patch!
|
|
14
|
+
|
|
15
|
+
uri = URI('https://api.ipify.org?format=json')
|
|
16
|
+
proxy = URI('http://user:pass@proxy.example.com:31280')
|
|
17
|
+
|
|
18
|
+
http = Net::HTTP.new(uri.host, uri.port, proxy.host, proxy.port, proxy.user, proxy.password)
|
|
19
|
+
http.use_ssl = true
|
|
20
|
+
http.proxy_connect_request_headers = { 'X-ProxyMesh-Country' => 'US' }
|
|
21
|
+
|
|
22
|
+
response = http.request(Net::HTTP::Get.new(uri))
|
|
23
|
+
puts response.body
|
|
24
|
+
puts http.last_proxy_connect_response_headers['X-ProxyMesh-IP']
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`last_proxy_connect_response_headers` is case-insensitive. The same headers are on `RubyProxyHeaders.proxy_connect_response_headers` for the current thread.
|
|
28
|
+
|
|
29
|
+
An `https://` proxy needs net-http 0.5 or newer, which is what `proxy_use_ssl` requires. Set `http.proxy_use_ssl = true` for that proxy.
|
data/docs/rest-client.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# RestClient
|
|
2
|
+
|
|
3
|
+
[RestClient](https://github.com/rest-client/rest-client) is a simple REST client. The `RubyProxyHeaders::RestClient` module adds proxy header support.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
gem 'rest-client'
|
|
9
|
+
gem 'ruby-proxy-headers'
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Basic Usage
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
require 'ruby_proxy_headers/rest_client'
|
|
16
|
+
|
|
17
|
+
response = RubyProxyHeaders::RestClient.get(
|
|
18
|
+
'https://api.ipify.org?format=json',
|
|
19
|
+
proxy: 'http://user:pass@proxy.example.com:31280',
|
|
20
|
+
proxy_headers: { 'X-ProxyMesh-Country' => 'US' }
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
puts "Status: #{response.code}"
|
|
24
|
+
puts "Body: #{response.body}"
|
|
25
|
+
puts "Proxy IP: #{response.proxy_response_headers['x-proxymesh-ip']}"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## API Reference
|
|
29
|
+
|
|
30
|
+
### `RubyProxyHeaders::RestClient.get(url, options)`
|
|
31
|
+
|
|
32
|
+
Make a GET request.
|
|
33
|
+
|
|
34
|
+
**Parameters:**
|
|
35
|
+
|
|
36
|
+
- `url` (String) - Target URL
|
|
37
|
+
- `options` (Hash):
|
|
38
|
+
- `:proxy` (String) - Proxy URL (required)
|
|
39
|
+
- `:proxy_headers` (Hash) - Headers to send to proxy
|
|
40
|
+
- `:headers` (Hash) - Request headers
|
|
41
|
+
|
|
42
|
+
**Returns:** `ProxyResponse`
|
|
43
|
+
|
|
44
|
+
### `RubyProxyHeaders::RestClient.post(url, options)`
|
|
45
|
+
|
|
46
|
+
Make a POST request.
|
|
47
|
+
|
|
48
|
+
**Parameters:**
|
|
49
|
+
|
|
50
|
+
- Same as `get`, plus:
|
|
51
|
+
- `:payload` (String/Hash) - Request body
|
|
52
|
+
|
|
53
|
+
### `RubyProxyHeaders::RestClient.put(url, options)`
|
|
54
|
+
|
|
55
|
+
Make a PUT request.
|
|
56
|
+
|
|
57
|
+
### `RubyProxyHeaders::RestClient.delete(url, options)`
|
|
58
|
+
|
|
59
|
+
Make a DELETE request.
|
|
60
|
+
|
|
61
|
+
## Response Object
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
response.code # HTTP status code
|
|
65
|
+
response.body # Response body
|
|
66
|
+
response.headers # Response headers
|
|
67
|
+
response.proxy_response_headers # Proxy CONNECT response headers
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Example: RESTful API
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
require 'ruby_proxy_headers/rest_client'
|
|
74
|
+
require 'json'
|
|
75
|
+
|
|
76
|
+
proxy_opts = {
|
|
77
|
+
proxy: ENV['PROXY_URL'],
|
|
78
|
+
proxy_headers: { 'X-ProxyMesh-Country' => 'US' }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Create
|
|
82
|
+
response = RubyProxyHeaders::RestClient.post(
|
|
83
|
+
'https://jsonplaceholder.typicode.com/posts',
|
|
84
|
+
payload: { title: 'Test', body: 'Content', userId: 1 }.to_json,
|
|
85
|
+
headers: { 'Content-Type' => 'application/json' },
|
|
86
|
+
**proxy_opts
|
|
87
|
+
)
|
|
88
|
+
puts "Created: #{JSON.parse(response.body)['id']}"
|
|
89
|
+
|
|
90
|
+
# Read
|
|
91
|
+
response = RubyProxyHeaders::RestClient.get(
|
|
92
|
+
'https://jsonplaceholder.typicode.com/posts/1',
|
|
93
|
+
**proxy_opts
|
|
94
|
+
)
|
|
95
|
+
puts "Read: #{JSON.parse(response.body)['title']}"
|
|
96
|
+
```
|
data/docs/testing.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
This guide covers running tests for ruby-proxy-headers.
|
|
4
|
+
|
|
5
|
+
## Running Unit Tests
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install dependencies
|
|
9
|
+
bundle install
|
|
10
|
+
|
|
11
|
+
# Run all tests
|
|
12
|
+
rake spec
|
|
13
|
+
|
|
14
|
+
# Run with verbose output
|
|
15
|
+
rake test
|
|
16
|
+
|
|
17
|
+
# Run specific test file
|
|
18
|
+
bundle exec rspec spec/connection_spec.rb
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Running Integration Tests
|
|
22
|
+
|
|
23
|
+
Integration tests require an actual proxy server.
|
|
24
|
+
|
|
25
|
+
### Environment Variables
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Required
|
|
29
|
+
export PROXY_URL='http://user:pass@proxy.example.com:31280'
|
|
30
|
+
|
|
31
|
+
# Optional
|
|
32
|
+
export TEST_URL='https://api.ipify.org?format=json'
|
|
33
|
+
export PROXY_HEADER='X-ProxyMesh-IP'
|
|
34
|
+
export SEND_PROXY_HEADER='X-ProxyMesh-Country'
|
|
35
|
+
export SEND_PROXY_VALUE='US'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Running Tests
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Run all integration tests
|
|
42
|
+
ruby test/test_proxy_headers.rb
|
|
43
|
+
|
|
44
|
+
# Run specific tests
|
|
45
|
+
ruby test/test_proxy_headers.rb net_http faraday
|
|
46
|
+
|
|
47
|
+
# List available tests
|
|
48
|
+
ruby test/test_proxy_headers.rb -l
|
|
49
|
+
|
|
50
|
+
# Verbose output
|
|
51
|
+
ruby test/test_proxy_headers.rb -v
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The harness prints `Testing <module>... OK` or `FAIL`. Module names are `net_http`, `faraday`, `httparty`, `excon`, `typhoeus`, `http_gem`, and `rest_client`. Excon checks that the proxied request succeeds. It does not read CONNECT response headers.
|
|
55
|
+
|
|
56
|
+
## Writing Tests
|
|
57
|
+
|
|
58
|
+
### Unit Test Example
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
# spec/my_module_spec.rb
|
|
62
|
+
require 'spec_helper'
|
|
63
|
+
|
|
64
|
+
RSpec.describe MyModule do
|
|
65
|
+
describe '#my_method' do
|
|
66
|
+
it 'does something' do
|
|
67
|
+
result = MyModule.my_method
|
|
68
|
+
expect(result).to eq(expected_value)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Integration Test Example
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# test/my_integration_test.rb
|
|
78
|
+
require 'uri'
|
|
79
|
+
require_relative '../lib/ruby_proxy_headers'
|
|
80
|
+
|
|
81
|
+
proxy_url = ENV['PROXY_URL']
|
|
82
|
+
raise 'Set PROXY_URL' unless proxy_url
|
|
83
|
+
|
|
84
|
+
RubyProxyHeaders::NetHTTP.patch!
|
|
85
|
+
uri = URI('https://api.ipify.org?format=json')
|
|
86
|
+
proxy = URI(proxy_url)
|
|
87
|
+
http = Net::HTTP.new(uri.host, uri.port, proxy.host, proxy.port, proxy.user, proxy.password)
|
|
88
|
+
http.use_ssl = true
|
|
89
|
+
http.proxy_connect_request_headers = { 'X-Test' => 'value' }
|
|
90
|
+
response = http.request(Net::HTTP::Get.new(uri))
|
|
91
|
+
puts response.code
|
|
92
|
+
puts http.last_proxy_connect_response_headers['X-ProxyMesh-IP']
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## CI
|
|
96
|
+
|
|
97
|
+
`.github/workflows/unit_tests.yml` runs `bundle exec rspec` on Ruby 3.2 and 3.3. `.github/workflows/proxy_integration_tests.yml` runs `test/test_proxy_headers.rb` when the `PROXY_URL` Actions secret is set.
|
|
98
|
+
|
|
99
|
+
## Troubleshooting
|
|
100
|
+
|
|
101
|
+
Set `open_timeout` and `read_timeout` on the client you are using. For a mock origin with a self-signed certificate, disable verification on that client (`verify_mode`, Faraday `ssl.verify`, HTTP.rb `verify_ssl: false`, or RestClient `verify_ssl: false`). Do not do that against a public site.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
bundle install
|
|
105
|
+
```
|
data/docs/typhoeus.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Typhoeus
|
|
2
|
+
|
|
3
|
+
[Typhoeus](https://typhoeus.github.io/) wraps libcurl. `RubyProxyHeaders::Typhoeus` sends custom proxy headers with `CURLOPT_PROXYHEADER` and reads the CONNECT response.
|
|
4
|
+
|
|
5
|
+
Requires Typhoeus 1.7 (Ethon >= 0.19.1) and libcurl with `CURLOPT_PROXYHEADER`. The gem does not depend on Typhoeus at runtime.
|
|
6
|
+
|
|
7
|
+
Proxy header names are documented at <https://docs.proxymesh.com/>.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'typhoeus', '~> 1.7'
|
|
13
|
+
gem 'ruby-proxy-headers'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Basic Usage
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
require 'ruby_proxy_headers/typhoeus'
|
|
20
|
+
|
|
21
|
+
response = RubyProxyHeaders::Typhoeus.get(
|
|
22
|
+
'https://api.ipify.org?format=json',
|
|
23
|
+
proxy: 'http://user:pass@proxy.example.com:31280',
|
|
24
|
+
proxy_headers: { 'X-ProxyMesh-Country' => 'US' }
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
puts response.code
|
|
28
|
+
puts response.body
|
|
29
|
+
puts response.proxy_response_headers['X-ProxyMesh-IP']
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`proxy_headers` are sent only to the proxy (`headeropt: :separate`). On HTTPS that is the CONNECT request. `headers:` is the origin request and is not copied onto CONNECT.
|
|
33
|
+
|
|
34
|
+
`response.headers` stays the origin response. The CONNECT response, including `X-ProxyMesh-IP`, is `response.proxy_response_headers`. Lookup is case-insensitive.
|
|
35
|
+
|
|
36
|
+
`post` takes the same proxy arguments plus `body:`.
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
RubyProxyHeaders::Typhoeus.post(
|
|
40
|
+
'https://api.ipify.org?format=json',
|
|
41
|
+
proxy: 'http://user:pass@proxy.example.com:31280',
|
|
42
|
+
proxy_headers: { 'X-ProxyMesh-Country' => 'US' },
|
|
43
|
+
body: '{"q":"example"}'
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Header names and values are rejected when they contain CR, LF, or NUL.
|
|
@@ -135,14 +135,7 @@ module RubyProxyHeaders
|
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
def validate_connect_target!(host, port)
|
|
138
|
-
|
|
139
|
-
raise ArgumentError,
|
|
140
|
-
"CONNECT target host contains invalid characters (CR, LF, or NUL): #{host.inspect}"
|
|
141
|
-
end
|
|
142
|
-
if RubyProxyHeaders::INVALID_HEADER_VALUE_RE.match?(port.to_s)
|
|
143
|
-
raise ArgumentError,
|
|
144
|
-
"CONNECT target port contains invalid characters (CR, LF, or NUL): #{port.inspect}"
|
|
145
|
-
end
|
|
138
|
+
RubyProxyHeaders.validate_connect_target!(host, port)
|
|
146
139
|
end
|
|
147
140
|
|
|
148
141
|
def raise_connect_error
|
|
@@ -14,28 +14,31 @@ end
|
|
|
14
14
|
module RubyProxyHeaders
|
|
15
15
|
# Faraday adapter that merges proxy CONNECT response headers into Faraday response headers.
|
|
16
16
|
# Use with {RubyProxyHeaders::NetHTTP.patch!}.
|
|
17
|
+
#
|
|
18
|
+
# faraday-net_http 3.4 and 3.5 perform the request in the private method
|
|
19
|
+
# +request_with_wrapped_block+. This adapter does not override that method.
|
|
20
|
+
# It uses the public +build_connection+ and +call+ methods.
|
|
17
21
|
module FaradayAdapter
|
|
22
|
+
# Copies CONNECT response headers onto the Faraday response.
|
|
18
23
|
class NetHttp < ::Faraday::Adapter::NetHttp
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
merge_proxy_headers(env, http)
|
|
22
|
-
res
|
|
24
|
+
def build_connection(env)
|
|
25
|
+
super.tap { |http| Thread.current[:ruby_proxy_headers_faraday_http] = http }
|
|
23
26
|
end
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
def call(env)
|
|
29
|
+
response = super
|
|
30
|
+
http = Thread.current[:ruby_proxy_headers_faraday_http]
|
|
31
|
+
headers = http.last_proxy_connect_response_headers if http
|
|
32
|
+
if env[:response_headers] && headers
|
|
33
|
+
headers.each do |key, value|
|
|
34
|
+
next if value.nil?
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return unless http.respond_to?(:last_proxy_connect_response_headers)
|
|
30
|
-
|
|
31
|
-
ph = http.last_proxy_connect_response_headers
|
|
32
|
-
return unless ph.is_a?(Hash)
|
|
33
|
-
|
|
34
|
-
ph.each do |k, v|
|
|
35
|
-
next if v.nil?
|
|
36
|
-
|
|
37
|
-
env[:response_headers][k] ||= v
|
|
36
|
+
env[:response_headers][key] ||= value
|
|
37
|
+
end
|
|
38
38
|
end
|
|
39
|
+
response
|
|
40
|
+
ensure
|
|
41
|
+
Thread.current[:ruby_proxy_headers_faraday_http] = nil
|
|
39
42
|
end
|
|
40
43
|
end
|
|
41
44
|
end
|
|
@@ -1,48 +1,53 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'delegate'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require_relative '../ruby_proxy_headers'
|
|
7
|
+
|
|
8
|
+
begin
|
|
9
|
+
require 'http'
|
|
10
|
+
rescue LoadError => e
|
|
11
|
+
raise LoadError, "the http gem >= 6 is required for ruby_proxy_headers/http_gem (#{e.message})"
|
|
12
|
+
end
|
|
13
|
+
|
|
3
14
|
module RubyProxyHeaders
|
|
4
|
-
# HTTP.rb (http gem)
|
|
15
|
+
# HTTP.rb (the +http+ gem) sends +proxy_headers+ on CONNECT and returns them
|
|
16
|
+
# as +response.proxy_headers+. This wrapper adds +proxy_response_headers+,
|
|
17
|
+
# matching the other adapters.
|
|
5
18
|
#
|
|
6
19
|
# @example
|
|
7
20
|
# client = RubyProxyHeaders::HTTPGem.create_client(
|
|
8
|
-
# proxy: 'http://user:pass@proxy:
|
|
21
|
+
# proxy: 'http://user:pass@proxy.example.com:31280',
|
|
9
22
|
# proxy_headers: { 'X-ProxyMesh-Country' => 'US' }
|
|
10
23
|
# )
|
|
11
|
-
#
|
|
12
24
|
# response = client.get('https://example.com')
|
|
13
|
-
# puts response.proxy_response_headers
|
|
14
|
-
#
|
|
25
|
+
# puts response.proxy_response_headers['X-ProxyMesh-IP']
|
|
15
26
|
module HTTPGem
|
|
16
|
-
# Create an HTTP client with proxy header support.
|
|
17
|
-
# @param proxy [String] Proxy URL
|
|
18
|
-
# @param proxy_headers [Hash] Custom headers to send to proxy
|
|
19
|
-
# @param options [Hash] Additional HTTP.rb options
|
|
20
|
-
# @return [ProxyClient]
|
|
21
27
|
def self.create_client(proxy:, proxy_headers: {}, **options)
|
|
22
28
|
ProxyClient.new(proxy: proxy, proxy_headers: proxy_headers, **options)
|
|
23
29
|
end
|
|
24
30
|
|
|
25
|
-
# Make a GET request with proxy headers.
|
|
26
31
|
def self.get(url, proxy:, proxy_headers: {}, **options)
|
|
27
32
|
create_client(proxy: proxy, proxy_headers: proxy_headers).get(url, **options)
|
|
28
33
|
end
|
|
29
34
|
|
|
30
|
-
# Make a POST request with proxy headers.
|
|
31
35
|
def self.post(url, proxy:, proxy_headers: {}, body: nil, **options)
|
|
32
36
|
create_client(proxy: proxy, proxy_headers: proxy_headers).post(url, body: body, **options)
|
|
33
37
|
end
|
|
34
38
|
|
|
35
|
-
#
|
|
39
|
+
# HTTP.rb client that sends proxy_headers on CONNECT.
|
|
36
40
|
class ProxyClient
|
|
41
|
+
attr_reader :last_proxy_response_headers
|
|
42
|
+
|
|
43
|
+
REQUEST_OPTIONS = %i[open_timeout read_timeout headers body verify_ssl].freeze
|
|
44
|
+
|
|
37
45
|
def initialize(proxy:, proxy_headers: {}, **options)
|
|
38
46
|
@proxy = proxy
|
|
39
47
|
@proxy_headers = proxy_headers
|
|
40
|
-
@
|
|
41
|
-
@last_proxy_response_headers = nil
|
|
48
|
+
@default_options = options
|
|
42
49
|
end
|
|
43
50
|
|
|
44
|
-
attr_reader :last_proxy_response_headers
|
|
45
|
-
|
|
46
51
|
def get(url, **options)
|
|
47
52
|
request(:get, url, **options)
|
|
48
53
|
end
|
|
@@ -62,32 +67,48 @@ module RubyProxyHeaders
|
|
|
62
67
|
private
|
|
63
68
|
|
|
64
69
|
def request(method, url, **options)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
options = @default_options.merge(options)
|
|
71
|
+
unknown = options.keys - REQUEST_OPTIONS
|
|
72
|
+
raise ArgumentError, "unknown option: #{unknown.first}" unless unknown.empty?
|
|
73
|
+
|
|
74
|
+
@proxy_headers.each { |key, value| RubyProxyHeaders.validate_header!(key, value) }
|
|
75
|
+
proxy_uri = URI.parse(@proxy)
|
|
76
|
+
user = proxy_uri.user && URI.decode_www_form_component(proxy_uri.user)
|
|
77
|
+
password = proxy_uri.password && URI.decode_www_form_component(proxy_uri.password)
|
|
78
|
+
client = ::HTTP.via(proxy_uri.host, proxy_uri.port, user, password, @proxy_headers)
|
|
79
|
+
timeouts = {}
|
|
80
|
+
timeouts[:connect] = options[:open_timeout] if options[:open_timeout]
|
|
81
|
+
timeouts[:read] = options[:read_timeout] if options[:read_timeout]
|
|
82
|
+
client = client.timeout(timeouts) unless timeouts.empty?
|
|
83
|
+
|
|
84
|
+
request_options = {}
|
|
85
|
+
request_options[:headers] = options[:headers] if options[:headers]
|
|
86
|
+
request_options[:body] = options[:body] unless options[:body].nil?
|
|
87
|
+
if options[:verify_ssl] == false
|
|
88
|
+
context = OpenSSL::SSL::SSLContext.new
|
|
89
|
+
context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
90
|
+
request_options[:ssl_context] = context
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
response = client.request(method, url, **request_options)
|
|
94
|
+
# HTTP::Headers is not a Hash. Copy into HeaderMap so callers can
|
|
95
|
+
# read X-ProxyMesh-IP the same way as the other adapters.
|
|
96
|
+
mapped = HeaderMap.new
|
|
97
|
+
response.proxy_headers.each do |name, value|
|
|
98
|
+
mapped[name] = mapped.key?(name) ? "#{mapped[name]}, #{value}" : value
|
|
79
99
|
end
|
|
100
|
+
@last_proxy_response_headers = mapped
|
|
101
|
+
ProxyResponse.new(response, mapped)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
80
104
|
|
|
81
|
-
|
|
82
|
-
|
|
105
|
+
# HTTP::Response plus +proxy_response_headers+.
|
|
106
|
+
class ProxyResponse < SimpleDelegator
|
|
107
|
+
attr_reader :proxy_response_headers
|
|
83
108
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
proxy_uri.port,
|
|
88
|
-
proxy_uri.user,
|
|
89
|
-
proxy_uri.password
|
|
90
|
-
).send(method, url, **options)
|
|
109
|
+
def initialize(response, proxy_headers)
|
|
110
|
+
super(response)
|
|
111
|
+
@proxy_response_headers = proxy_headers
|
|
91
112
|
end
|
|
92
113
|
end
|
|
93
114
|
end
|