ruby-proxy-headers 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3a39a089e5181641efdaa2920edfd958bc127311e54be9942fdf5ae563ded24a
4
+ data.tar.gz: 0c138a06ea8d8b6dad7ed943b40e67964d05b0daf680e68ac1825a565647596a
5
+ SHA512:
6
+ metadata.gz: 5efd2419ca3cc85309f54823e5a98b59b465cbf98bc32b39503aa9ff11b14732fef71cc6bd5bdb413083b1addeb269bb22b4bdc5093cac29083419823173a234
7
+ data.tar.gz: 4fbaf515b270d7544a70bbfab697fe5594820161733290761367f6daee2d225484f3baaea9283420a1230a088a5dd981d7ca9d6a2c7a44f2c5a07e585cb66aa8
data/DEFERRED.md ADDED
@@ -0,0 +1,29 @@
1
+ # Deferred / not fully supported
2
+
3
+ ## Typhoeus / Ethon
4
+
5
+ **Status:** Not implemented in this gem.
6
+
7
+ [Ethon](https://github.com/typhoeus/ethon) does not expose `CURLOPT_PROXYHEADER` (or equivalent) in its `Ethon::Easy` option mapping as of ethon 0.18.x. Adding CONNECT response header capture would require Ethon/libcurl changes or a custom C extension.
8
+
9
+ **Workaround:** Use `Net::HTTP` + {RubyProxyHeaders::NetHTTP.patch!} or Faraday with `ruby_proxy_headers_net_http` adapter.
10
+
11
+ ---
12
+
13
+ ## Mechanize
14
+
15
+ **Status:** Not implemented.
16
+
17
+ [Mechanize](https://github.com/sparklemotion/mechanize) uses [net-http-persistent](https://github.com/drbrain/net-http-persistent), which maintains its own connection layer on top of `Net::HTTP`. Our prepend patch targets `Net::HTTP#connect` on instances Mechanize creates, but verifying header propagation and lifecycle across the persistent pool needs dedicated work.
18
+
19
+ **Workaround:** Use patched `Net::HTTP` or Faraday for fetches; use Mechanize only when custom CONNECT headers are not required.
20
+
21
+ ---
22
+
23
+ ## Excon — reading CONNECT response headers
24
+
25
+ **Status:** Sending extra CONNECT headers is supported upstream via `:ssl_proxy_headers`.
26
+
27
+ Excon’s public `Excon::Response` for the **origin** request does **not** include headers from the proxy’s `CONNECT` response (only the tunneled HTTPS response headers). Capturing `X-ProxyMesh-IP` from CONNECT would require patching Excon internals or a fork.
28
+
29
+ **Workaround:** Use Net::HTTP / Faraday integrations in this gem, which merge CONNECT headers into the client response where applicable.
@@ -0,0 +1,72 @@
1
+ # ruby-proxy-headers — implementation plan
2
+
3
+ Prioritized roadmap for extension modules, aligned with [javascript-proxy-headers](https://github.com/proxymesh/javascript-proxy-headers) and [python-proxy-headers](https://github.com/proxymesh/python-proxy-headers).
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────┐
9
+ │ ruby-proxy-headers │
10
+ ├─────────────────────────────────────────────────────────────┤
11
+ │ Faraday / HTTParty / Excon helpers │
12
+ │ │ │
13
+ │ ▼ │
14
+ │ Net::HTTP patch — CONNECT send + capture + thread-local │
15
+ └─────────────────────────────────────────────────────────────┘
16
+ ```
17
+
18
+ ## Phase 1 — Net::HTTP core (**done — v0.1+**)
19
+
20
+ - `RubyProxyHeaders::NetHTTP.patch!` extends `Net::HTTP#connect` for HTTPS + proxy.
21
+ - `last_proxy_connect_response_headers` on the `Net::HTTP` instance.
22
+ - `Thread.current[:ruby_proxy_headers_connect_headers]` + `RubyProxyHeaders.proxy_connect_response_headers` for cross-library reads.
23
+
24
+ **File:** `lib/ruby_proxy_headers/net_http.rb`
25
+
26
+ ---
27
+
28
+ ## Phase 2 — Faraday (**done — v0.2+**)
29
+
30
+ - Adapter `ruby_proxy_headers_net_http` (subclass of `Faraday::Adapter::NetHttp`) merges CONNECT response headers into Faraday response headers.
31
+ - Helper `RubyProxyHeaders::FaradayIntegration.connection(...)`.
32
+
33
+ **File:** `lib/ruby_proxy_headers/faraday.rb`
34
+
35
+ ---
36
+
37
+ ## Phase 3 — HTTParty (**done — v0.2+**)
38
+
39
+ - `RubyProxyHeaders::ProxyHeadersConnectionAdapter` — pass `proxy_connect_request_headers` in HTTParty options.
40
+
41
+ **File:** `lib/ruby_proxy_headers/httparty.rb`
42
+
43
+ ---
44
+
45
+ ## Phase 4 — Typhoeus / Ethon (**deferred**)
46
+
47
+ Ethon does not expose libcurl `CURLOPT_PROXYHEADER` in its option layer. See [DEFERRED.md](DEFERRED.md).
48
+
49
+ ---
50
+
51
+ ## Phase 5 — Excon (**partial — v0.2+**)
52
+
53
+ - **Sending** extra CONNECT headers: supported upstream via `:ssl_proxy_headers`; `RubyProxyHeaders::ExconIntegration.get` passes them through.
54
+ - **Reading** CONNECT response headers: not exposed on `Excon::Response` for the tunneled request. See [DEFERRED.md](DEFERRED.md).
55
+
56
+ **File:** `lib/ruby_proxy_headers/excon.rb`
57
+
58
+ ---
59
+
60
+ ## Phase 6 — Mechanize (**deferred**)
61
+
62
+ Uses `net-http-persistent`; needs dedicated integration. See [DEFERRED.md](DEFERRED.md).
63
+
64
+ ---
65
+
66
+ ## Testing
67
+
68
+ `bundle exec ruby test/test_proxy_headers.rb` with `PROXY_URL` set. Modules: `net_http`, `faraday`, `httparty`, `excon` (excon is a smoke test; CONNECT response headers are not asserted).
69
+
70
+ ---
71
+
72
+ *Updated: March 2026*
@@ -0,0 +1,71 @@
1
+ # Ruby library proxy header support (CONNECT tunnel)
2
+
3
+ This document analyzes the Ruby HTTP / scraping stack used in [proxy-examples](https://github.com/proxymesh/proxy-examples/tree/main/ruby) for **custom headers on HTTPS `CONNECT`** and **reading the proxy `CONNECT` response headers**.
4
+
5
+ ## Executive summary
6
+
7
+ | Library | Native CONNECT header API | Extension approach |
8
+ |---------|---------------------------|---------------------|
9
+ | **Net::HTTP** (stdlib) | No — tunnel is hard-coded in `#connect` | **Implemented:** prepend patch; optional `proxy_connect_request_headers`; `last_proxy_connect_response_headers` |
10
+ | **Faraday** | No — delegates to adapters (`net_http`, etc.) | **High:** use patched `Net::HTTP` with `Faraday.new(connection_options)` or custom adapter that sets `proxy_connect_request_headers` |
11
+ | **HTTParty** | No — built on `Net::HTTP` | **High:** global or per-class `Net::HTTP` instances after `patch!`; or subclass `Connection` if needed |
12
+ | **Mechanize** | No — uses `Net::HTTP` / persistent connections internally | **Medium–high:** ensure Mechanize’s internal HTTP object gets the same patched behavior and header accessors |
13
+ | **Excon** | **Send:** yes — `:ssl_proxy_headers` on connection. **Read CONNECT response:** not exposed on origin `Excon::Response` | **Partial:** use `:ssl_proxy_headers` for sends; reading `X-ProxyMesh-IP` from CONNECT needs upstream Excon changes (see [DEFERRED.md](DEFERRED.md)) |
14
+ | **Typhoeus / Ethon** | Partial — libcurl has proxy options | **Medium:** map headers to `CURLOPT_PROXYHEADER` / related options (libcurl version dependent); capture CONNECT response via callbacks or debug hooks where supported |
15
+ | **Nokogiri** | N/A (XML/HTML parser only) | **N/A** — proxying is whatever HTTP client fetches the document (usually `Net::HTTP`) |
16
+
17
+ As with Node and Python stacks, **none** of the high-level Ruby clients expose a first-class “proxy CONNECT request/response headers” API; extensions must hook **below** the HTTP library (stdlib `Net::HTTP`), **inside** Faraday’s adapter, or **at** the libcurl / Excon layer.
18
+
19
+ ## Connection flow (HTTPS over HTTP proxy)
20
+
21
+ ```
22
+ Client -- CONNECT + custom headers --> Proxy
23
+ Client <-- CONNECT response headers --- Proxy
24
+ Client === TLS tunnel =================> Origin
25
+ ```
26
+
27
+ ## Net::HTTP (stdlib)
28
+
29
+ MRI implements the tunnel in `Net::HTTP#connect` (see `net/http.rb`): it writes `CONNECT host:port`, `Host`, optional `Proxy-Authorization`, then reads the response with `Net::HTTPResponse.read_new` and calls `#value` without exposing headers to callers.
30
+
31
+ **Feasibility:** High — same pattern as Python’s `HTTPSConnection._tunnel` override in `python-proxy-headers`.
32
+
33
+ ## Faraday
34
+
35
+ Uses adapters; default `net_http` adapter constructs `Net::HTTP`. If `Net::HTTP` is patched and exposes `proxy_connect_request_headers`, Faraday can pass options through `connection` / `request` options in a thin wrapper.
36
+
37
+ **Feasibility:** High once `Net::HTTP` support is stable.
38
+
39
+ ## HTTParty
40
+
41
+ Uses `Net::HTTP` under the hood for sync requests. After `RubyProxyHeaders::NetHTTP.patch!`, new `Net::HTTP` objects support the new accessors.
42
+
43
+ **Feasibility:** High — may need documented patterns for `default_options` and connection lifecycle.
44
+
45
+ ## Mechanize
46
+
47
+ Builds on `net/http` (often `net-http-persistent`). May require verifying that the patched `connect` runs for its connections and that response/header capture is visible on the right object.
48
+
49
+ **Feasibility:** Medium.
50
+
51
+ ## Excon
52
+
53
+ Own implementation of proxy and TLS; does not use `Net::HTTP#connect`.
54
+
55
+ **Feasibility:** Medium — requires a dedicated Excon middleware or socket layer similar to the JavaScript core agent.
56
+
57
+ ## Typhoeus / Ethon (libcurl)
58
+
59
+ libcurl can send additional headers to the proxy; exact options depend on libcurl version (e.g. proxy header lists). CONNECT response header visibility may require `CURLOPT_DEBUGFUNCTION` or version-specific features.
60
+
61
+ **Feasibility:** Medium — needs research per libcurl version and Ethon API surface.
62
+
63
+ ## Nokogiri
64
+
65
+ Parsing only; no network layer.
66
+
67
+ **Feasibility:** N/A — combine with `Net::HTTP` + patch or another extended client.
68
+
69
+ ---
70
+
71
+ *Last updated: March 2026*
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ProxyMesh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # Ruby Proxy Headers
2
+
3
+ Send and receive custom proxy headers during HTTPS `CONNECT` tunneling in modern Ruby HTTP workflows (for example [ProxyMesh](https://proxymesh.com) `X-ProxyMesh-IP` / `X-ProxyMesh-Country`).
4
+
5
+ ## The problem
6
+
7
+ Most Ruby HTTP clients use `Net::HTTP`, Faraday, or libcurl without exposing:
8
+
9
+ 1. Extra headers on the `CONNECT` request to the proxy.
10
+ 2. Headers from the proxy’s `CONNECT` response (often discarded after the tunnel is established).
11
+
12
+ This library adds opt-in support for **Net::HTTP**, **Faraday** (2.x via `faraday-net_http`), **HTTParty**, and documents **Excon**’s built-in `:ssl_proxy_headers` for sends.
13
+
14
+ ## Why teams use this
15
+
16
+ - **Geo-targeting at tunnel setup**: Send country/session directives on `CONNECT`.
17
+ - **Sticky-session observability**: Read proxy-assigned headers like `X-ProxyMesh-IP`.
18
+ - **Works with common Ruby stacks**: Useful for scraping and API clients using Net::HTTP-based flows.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ gem install ruby-proxy-headers
24
+ ```
25
+
26
+ Or add to your `Gemfile`:
27
+
28
+ ```ruby
29
+ gem 'ruby-proxy-headers'
30
+ ```
31
+
32
+ The `Net::HTTP` patch is pure Ruby. Install **faraday**, **faraday-net_http**, **httparty**, and/or **excon** when you use those integrations.
33
+
34
+ ## Net::HTTP
35
+
36
+ ```ruby
37
+ require 'uri'
38
+ require 'openssl'
39
+ require 'ruby_proxy_headers/net_http'
40
+
41
+ RubyProxyHeaders::NetHTTP.patch!
42
+
43
+ uri = URI('https://api.ipify.org?format=json')
44
+ proxy = URI(ENV.fetch('PROXY_URL'))
45
+
46
+ http = Net::HTTP.new(uri.host, uri.port, proxy.host, proxy.port, proxy.user, proxy.password)
47
+ http.use_ssl = true
48
+
49
+ # Optional: headers to send on CONNECT (e.g. sticky IP)
50
+ http.proxy_connect_request_headers = { 'X-ProxyMesh-IP' => '203.0.113.1' }
51
+
52
+ res = http.request(Net::HTTP::Get.new(uri))
53
+ puts res.body
54
+ puts http.last_proxy_connect_response_headers['X-ProxyMesh-IP']
55
+ ```
56
+
57
+ Call `RubyProxyHeaders::NetHTTP.patch!` once before creating connections. You can also read the last CONNECT response headers on the current thread via `RubyProxyHeaders.proxy_connect_response_headers`.
58
+
59
+ ## Faraday
60
+
61
+ ```ruby
62
+ require 'ruby_proxy_headers/faraday'
63
+
64
+ conn = RubyProxyHeaders::FaradayIntegration.connection(
65
+ proxy: ENV.fetch('PROXY_URL'),
66
+ proxy_connect_headers: { 'X-ProxyMesh-Country' => 'US' } # optional
67
+ )
68
+ res = conn.get('https://api.ipify.org?format=json')
69
+ puts res.headers['X-ProxyMesh-IP']
70
+ ```
71
+
72
+ Uses the registered adapter `:ruby_proxy_headers_net_http`, which merges proxy `CONNECT` response headers into Faraday’s response headers.
73
+
74
+ ## HTTParty
75
+
76
+ ```ruby
77
+ require 'httparty'
78
+ require 'ruby_proxy_headers/httparty'
79
+
80
+ RubyProxyHeaders::NetHTTP.patch!
81
+
82
+ proxy = URI(ENV.fetch('PROXY_URL'))
83
+ HTTParty.get(
84
+ 'https://api.ipify.org?format=json',
85
+ http_proxyaddr: proxy.host,
86
+ http_proxyport: proxy.port,
87
+ http_proxyuser: proxy.user,
88
+ http_proxypass: proxy.password,
89
+ proxy_connect_request_headers: { 'X-ProxyMesh-IP' => '203.0.113.1' }, # optional
90
+ connection_adapter: RubyProxyHeaders::ProxyHeadersConnectionAdapter
91
+ )
92
+
93
+ puts RubyProxyHeaders.proxy_connect_response_headers['X-ProxyMesh-IP']
94
+ ```
95
+
96
+ ## Excon (send-only; CONNECT response headers)
97
+
98
+ Excon supports **sending** extra headers on CONNECT with `:ssl_proxy_headers`. Reading `X-ProxyMesh-IP` from the CONNECT response is **not** exposed on the origin response object — see [DEFERRED.md](DEFERRED.md).
99
+
100
+ ```ruby
101
+ require 'ruby_proxy_headers/excon'
102
+
103
+ RubyProxyHeaders::ExconIntegration.get(
104
+ 'https://api.ipify.org?format=json',
105
+ proxy_url: ENV.fetch('PROXY_URL'),
106
+ proxy_connect_headers: { 'X-ProxyMesh-Country' => 'US' } # optional
107
+ )
108
+ ```
109
+
110
+ ## Testing (live proxy)
111
+
112
+ Same environment variables as [python-proxy-headers](https://github.com/proxymesh/python-proxy-headers):
113
+
114
+ | Variable | Role |
115
+ |----------|------|
116
+ | `PROXY_URL` | Proxy URL (required for tests) |
117
+ | `TEST_URL` | Target HTTPS URL (default `https://api.ipify.org?format=json`) |
118
+ | `PROXY_HEADER` | Header to read from CONNECT response (default `X-ProxyMesh-IP`) |
119
+ | `SEND_PROXY_HEADER` | Optional header name to send on `CONNECT` |
120
+ | `SEND_PROXY_VALUE` | Optional value for that header |
121
+
122
+ ```bash
123
+ cd ruby-proxy-headers
124
+ bundle install
125
+ export PROXY_URL=http://user:pass@proxyhost:port
126
+ bundle exec ruby test/test_proxy_headers.rb -v
127
+ ```
128
+
129
+ ## Related
130
+
131
+ - [python-proxy-headers](https://github.com/proxymesh/python-proxy-headers)
132
+ - [javascript-proxy-headers](https://github.com/proxymesh/javascript-proxy-headers)
133
+ - [proxy-examples (Ruby)](https://github.com/proxymesh/proxy-examples/tree/main/ruby)
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'openssl'
5
+ require 'uri'
6
+
7
+ module RubyProxyHeaders
8
+ # Handles HTTPS CONNECT tunneling with custom proxy headers.
9
+ # This is the core component that manages the proxy connection,
10
+ # sends custom headers, and captures proxy response headers.
11
+ class Connection
12
+ attr_reader :proxy_response_headers, :proxy_response_status, :socket
13
+
14
+ # @param proxy [String, Hash] Proxy URL or hash with :host, :port, :user, :password
15
+ # @param options [Hash] Connection options
16
+ # @option options [Hash] :proxy_headers Custom headers to send during CONNECT
17
+ # @option options [Integer] :connect_timeout Connection timeout in seconds (default: 30)
18
+ # @option options [Boolean] :verify_ssl Verify SSL certificates (default: true)
19
+ def initialize(proxy, options = {})
20
+ @proxy = proxy.is_a?(String) ? RubyProxyHeaders.parse_proxy_url(proxy) : proxy
21
+ @proxy_headers = options[:proxy_headers] || {}
22
+ @connect_timeout = options[:connect_timeout] || 30
23
+ @verify_ssl = options.fetch(:verify_ssl, true)
24
+ @proxy_response_headers = {}
25
+ @proxy_response_status = nil
26
+ @socket = nil
27
+ end
28
+
29
+ # Establish a tunnel through the proxy to the target host.
30
+ # @param target_host [String] Target hostname
31
+ # @param target_port [Integer] Target port (default: 443)
32
+ # @return [OpenSSL::SSL::SSLSocket] TLS-wrapped socket to target
33
+ def connect(target_host, target_port = 443)
34
+ # Connect to proxy
35
+ @socket = TCPSocket.new(@proxy[:host], @proxy[:port])
36
+ @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
37
+
38
+ # Send CONNECT request with custom headers
39
+ connect_request = build_connect_request(target_host, target_port)
40
+ @socket.write(connect_request)
41
+
42
+ # Read and parse CONNECT response
43
+ response = read_connect_response
44
+ parse_connect_response(response)
45
+
46
+ # Check for successful connection
47
+ unless (200..299).cover?(@proxy_response_status)
48
+ @socket.close
49
+ raise_connect_error
50
+ end
51
+
52
+ # Upgrade to TLS
53
+ upgrade_to_tls(target_host)
54
+ end
55
+
56
+ # Close the connection
57
+ def close
58
+ @socket&.close
59
+ end
60
+
61
+ private
62
+
63
+ def build_connect_request(target_host, target_port)
64
+ request_lines = [
65
+ "CONNECT #{target_host}:#{target_port} HTTP/1.1",
66
+ "Host: #{target_host}:#{target_port}"
67
+ ]
68
+
69
+ # Add proxy authentication if provided
70
+ if @proxy[:user]
71
+ auth = RubyProxyHeaders.build_auth_header(@proxy[:user], @proxy[:password])
72
+ request_lines << "Proxy-Authorization: #{auth}"
73
+ end
74
+
75
+ # Add custom proxy headers
76
+ @proxy_headers.each do |name, value|
77
+ request_lines << "#{name}: #{value}"
78
+ end
79
+
80
+ request_lines << 'Proxy-Connection: keep-alive'
81
+ request_lines << ''
82
+ request_lines << ''
83
+
84
+ request_lines.join("\r\n")
85
+ end
86
+
87
+ def read_connect_response
88
+ response = String.new
89
+ loop do
90
+ line = @socket.gets
91
+ break if line.nil?
92
+
93
+ response << line
94
+ break if line == "\r\n" || line == "\n"
95
+ end
96
+ response
97
+ end
98
+
99
+ def parse_connect_response(response)
100
+ lines = response.split(/\r?\n/)
101
+ return if lines.empty?
102
+
103
+ # Parse status line
104
+ status_line = lines.shift
105
+ if (match = status_line.match(%r{HTTP/[\d.]+\s+(\d+)}))
106
+ @proxy_response_status = match[1].to_i
107
+ end
108
+
109
+ # Parse headers
110
+ @proxy_response_headers = {}
111
+ lines.each do |line|
112
+ break if line.empty?
113
+
114
+ if (header_match = line.match(/^([^:]+):\s*(.*)$/))
115
+ name = header_match[1].downcase
116
+ value = header_match[2]
117
+ @proxy_response_headers[name] = value
118
+ end
119
+ end
120
+ end
121
+
122
+ def upgrade_to_tls(target_host)
123
+ ssl_context = OpenSSL::SSL::SSLContext.new
124
+ ssl_context.verify_mode = @verify_ssl ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
125
+
126
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(@socket, ssl_context)
127
+ ssl_socket.hostname = target_host
128
+ ssl_socket.sync_close = true
129
+ ssl_socket.connect
130
+
131
+ @socket = ssl_socket
132
+ end
133
+
134
+ def raise_connect_error
135
+ case @proxy_response_status
136
+ when 407
137
+ raise ProxyAuthenticationError, "Proxy authentication required (407)"
138
+ else
139
+ raise ConnectError, "Proxy CONNECT failed with status #{@proxy_response_status}"
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'excon'
5
+ rescue LoadError => e
6
+ raise LoadError, "excon is required for ruby_proxy_headers/excon (#{e.message})"
7
+ end
8
+
9
+ module RubyProxyHeaders
10
+ # Excon already supports extra CONNECT headers via +:ssl_proxy_headers+ (see
11
+ # Excon::SSLSocket). This module documents the mapping and a small helper.
12
+ #
13
+ # Reading CONNECT response headers is not exposed on Excon's public response
14
+ # object for the origin request; use thread-local {RubyProxyHeaders.proxy_connect_response_headers}
15
+ # only when the underlying transport is patched Net::HTTP, not Excon.
16
+ module ExconIntegration
17
+ module_function
18
+
19
+ # @param proxy_url [String]
20
+ # @param proxy_connect_headers [Hash] sent to the proxy during HTTPS CONNECT (Excon key: :ssl_proxy_headers)
21
+ # @param excon_opts [Hash] merged into Excon.get / Excon.new options
22
+ def get(url, proxy_url:, proxy_connect_headers: nil, **excon_opts)
23
+ opts = {
24
+ proxy: normalize_proxy_url(proxy_url),
25
+ ssl_proxy_headers: proxy_connect_headers,
26
+ ssl_verify_peer: true
27
+ }.merge(excon_opts)
28
+ Excon.get(url, opts)
29
+ end
30
+
31
+ def normalize_proxy_url(proxy_url)
32
+ s = proxy_url.to_s.strip
33
+ return s if s.match?(/\A[a-z][a-z0-9+\-.]*:\/\//i)
34
+
35
+ "http://#{s}"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'net_http'
4
+
5
+ begin
6
+ require 'faraday'
7
+ require 'faraday/net_http'
8
+ rescue LoadError => e
9
+ raise LoadError,
10
+ 'faraday and faraday-net_http are required for ruby_proxy_headers/faraday ' \
11
+ "(#{e.message})"
12
+ end
13
+
14
+ module RubyProxyHeaders
15
+ # Faraday adapter that merges proxy CONNECT response headers into Faraday response headers.
16
+ # Use with {RubyProxyHeaders::NetHTTP.patch!}.
17
+ module FaradayAdapter
18
+ class NetHttp < ::Faraday::Adapter::NetHttp
19
+ def request_with_wrapped_block(http, env, &block)
20
+ res = super
21
+ merge_proxy_headers(env, http)
22
+ res
23
+ end
24
+
25
+ private
26
+
27
+ def merge_proxy_headers(env, http)
28
+ return unless env[:response_headers]
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
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ Faraday::Adapter.register_middleware(
45
+ ruby_proxy_headers_net_http: RubyProxyHeaders::FaradayAdapter::NetHttp
46
+ )
47
+
48
+ module RubyProxyHeaders
49
+ module FaradayIntegration
50
+ module_function
51
+
52
+ def patch!
53
+ RubyProxyHeaders::NetHTTP.patch!
54
+ end
55
+
56
+ # Builds a Faraday connection with the custom Net::HTTP adapter and optional CONNECT headers.
57
+ #
58
+ # @param proxy [String] proxy URL
59
+ # @param proxy_connect_headers [Hash, nil] headers to send on CONNECT (e.g. X-ProxyMesh-IP)
60
+ # @param url [String, nil] optional base URL
61
+ def connection(proxy:, proxy_connect_headers: nil, url: nil, &block)
62
+ patch!
63
+ opts = { proxy: proxy }
64
+ opts[:url] = url if url
65
+ ::Faraday.new(opts) do |f|
66
+ f.adapter :ruby_proxy_headers_net_http do |http|
67
+ http.proxy_connect_request_headers = proxy_connect_headers if proxy_connect_headers&.any?
68
+ end
69
+ yield f if block_given?
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyProxyHeaders
4
+ # HTTP.rb (http gem) integration for proxy headers support.
5
+ #
6
+ # @example
7
+ # client = RubyProxyHeaders::HTTPGem.create_client(
8
+ # proxy: 'http://user:pass@proxy:8080',
9
+ # proxy_headers: { 'X-ProxyMesh-Country' => 'US' }
10
+ # )
11
+ #
12
+ # response = client.get('https://example.com')
13
+ # puts response.proxy_response_headers
14
+ #
15
+ 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
+ def self.create_client(proxy:, proxy_headers: {}, **options)
22
+ ProxyClient.new(proxy: proxy, proxy_headers: proxy_headers, **options)
23
+ end
24
+
25
+ # Make a GET request with proxy headers.
26
+ def self.get(url, proxy:, proxy_headers: {}, **options)
27
+ create_client(proxy: proxy, proxy_headers: proxy_headers).get(url, **options)
28
+ end
29
+
30
+ # Make a POST request with proxy headers.
31
+ def self.post(url, proxy:, proxy_headers: {}, body: nil, **options)
32
+ create_client(proxy: proxy, proxy_headers: proxy_headers).post(url, body: body, **options)
33
+ end
34
+
35
+ # Proxy client wrapper for HTTP.rb
36
+ class ProxyClient
37
+ def initialize(proxy:, proxy_headers: {}, **options)
38
+ @proxy = proxy
39
+ @proxy_headers = proxy_headers
40
+ @options = options
41
+ @last_proxy_response_headers = nil
42
+ end
43
+
44
+ attr_reader :last_proxy_response_headers
45
+
46
+ def get(url, **options)
47
+ request(:get, url, **options)
48
+ end
49
+
50
+ def post(url, body: nil, **options)
51
+ request(:post, url, body: body, **options)
52
+ end
53
+
54
+ def put(url, body: nil, **options)
55
+ request(:put, url, body: body, **options)
56
+ end
57
+
58
+ def delete(url, **options)
59
+ request(:delete, url, **options)
60
+ end
61
+
62
+ private
63
+
64
+ def request(method, url, **options)
65
+ uri = URI.parse(url)
66
+
67
+ # Use our core connection for HTTPS
68
+ if uri.scheme == 'https'
69
+ response = RubyProxyHeaders::NetHTTP.request(
70
+ method,
71
+ url,
72
+ proxy: @proxy,
73
+ proxy_headers: @proxy_headers,
74
+ headers: options[:headers],
75
+ body: options[:body]
76
+ )
77
+ @last_proxy_response_headers = response.proxy_response_headers
78
+ return response
79
+ end
80
+
81
+ # For HTTP, use standard http gem
82
+ require 'http'
83
+
84
+ proxy_uri = URI.parse(@proxy)
85
+ ::HTTP.via(
86
+ proxy_uri.host,
87
+ proxy_uri.port,
88
+ proxy_uri.user,
89
+ proxy_uri.password
90
+ ).send(method, url, **options)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'net_http'
4
+
5
+ begin
6
+ require 'httparty'
7
+ rescue LoadError => e
8
+ raise LoadError, "httparty is required for ruby_proxy_headers/httparty (#{e.message})"
9
+ end
10
+
11
+ module RubyProxyHeaders
12
+ # Drop-in connection adapter: pass +:proxy_connect_request_headers+ in options
13
+ # (along with +http_proxyaddr+ / +http_proxyport+ / etc.).
14
+ #
15
+ # After the request, read CONNECT response headers via
16
+ # {RubyProxyHeaders.proxy_connect_response_headers} (thread-local).
17
+ class ProxyHeadersConnectionAdapter < HTTParty::ConnectionAdapter
18
+ def connection
19
+ http = super
20
+ if http.respond_to?(:proxy_connect_request_headers=)
21
+ h = options[:proxy_connect_request_headers] || options[:proxy_connect_headers]
22
+ http.proxy_connect_request_headers = h if h&.any?
23
+ end
24
+ http
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'net/protocol'
5
+ require 'openssl'
6
+ require 'resolv'
7
+ require 'timeout'
8
+
9
+ module RubyProxyHeaders
10
+ # Patches Net::HTTP#connect for HTTPS-over-proxy so extra headers can be sent on
11
+ # the CONNECT request and response headers can be read (e.g. ProxyMesh
12
+ # X-ProxyMesh-IP). Based on the same tunnel flow as Python's http.client / urllib3
13
+ # extensions in python-proxy-headers.
14
+ #
15
+ # @example
16
+ # require 'ruby_proxy_headers/net_http'
17
+ # RubyProxyHeaders::NetHTTP.patch!
18
+ #
19
+ # http = Net::HTTP.new(uri.host, uri.port, proxy.host, proxy.port, proxy.user, proxy.password)
20
+ # http.use_ssl = true
21
+ # http.proxy_connect_request_headers = { 'X-ProxyMesh-IP' => '203.0.113.1' }
22
+ # res = http.request(Net::HTTP::Get.new(uri))
23
+ # p http.last_proxy_connect_response_headers
24
+ module NetHTTP
25
+ unless const_defined?(:ORIGINAL_CONNECT, false)
26
+ ORIGINAL_CONNECT = ::Net::HTTP.instance_method(:connect)
27
+ end
28
+
29
+ module Extension
30
+ attr_accessor :proxy_connect_request_headers
31
+ attr_reader :last_proxy_connect_response_headers
32
+
33
+ def connect
34
+ if use_ssl? && proxy?
35
+ connect_with_proxy_tunnel
36
+ else
37
+ RubyProxyHeaders::NetHTTP::ORIGINAL_CONNECT.bind_call(self)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # Duplicates Net::HTTP#connect (MRI 3.2) for the HTTPS + proxy branch, with
44
+ # optional extra CONNECT headers and capture of the CONNECT response headers.
45
+ def connect_with_proxy_tunnel
46
+ s = nil
47
+ if use_ssl?
48
+ @ssl_context = OpenSSL::SSL::SSLContext.new
49
+ end
50
+
51
+ if proxy?
52
+ conn_addr = proxy_address
53
+ conn_port = proxy_port
54
+ else
55
+ conn_addr = conn_address
56
+ conn_port = port
57
+ end
58
+
59
+ debug "opening connection to #{conn_addr}:#{conn_port}..."
60
+ s = Timeout.timeout(@open_timeout, Net::OpenTimeout) do
61
+ begin
62
+ TCPSocket.open(conn_addr, conn_port, @local_host, @local_port)
63
+ rescue StandardError => e
64
+ raise e, "Failed to open TCP connection to " \
65
+ "#{conn_addr}:#{conn_port} (#{e.message})"
66
+ end
67
+ end
68
+ s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
69
+ debug 'opened'
70
+
71
+ if use_ssl?
72
+ if proxy?
73
+ plain_sock = Net::BufferedIO.new(s, read_timeout: @read_timeout,
74
+ write_timeout: @write_timeout,
75
+ continue_timeout: @continue_timeout,
76
+ debug_output: @debug_output)
77
+ buf = +"CONNECT #{conn_address}:#{@port} HTTP/#{::Net::HTTP::HTTPVersion}\r\n" \
78
+ "Host: #{@address}:#{@port}\r\n"
79
+ if proxy_user
80
+ credential = ["#{proxy_user}:#{proxy_pass}"].pack('m0')
81
+ buf << "Proxy-Authorization: Basic #{credential}\r\n"
82
+ end
83
+ if proxy_connect_request_headers&.any?
84
+ proxy_connect_request_headers.each do |k, v|
85
+ buf << "#{k}: #{v}\r\n"
86
+ end
87
+ end
88
+ buf << "\r\n"
89
+ plain_sock.write(buf)
90
+
91
+ proxy_res = ::Net::HTTPResponse.read_new(plain_sock)
92
+ @last_proxy_connect_response_headers = {}
93
+ proxy_res.each_header do |k, v|
94
+ @last_proxy_connect_response_headers[k] = v
95
+ end
96
+ Thread.current[:ruby_proxy_headers_connect_headers] = @last_proxy_connect_response_headers.dup
97
+ proxy_res.value
98
+ end
99
+
100
+ ssl_parameters = {}
101
+ iv_list = instance_variables
102
+ Net::HTTP::SSL_IVNAMES.each_with_index do |ivname, i|
103
+ if iv_list.include?(ivname)
104
+ value = instance_variable_get(ivname)
105
+ unless value.nil?
106
+ ssl_parameters[Net::HTTP::SSL_ATTRIBUTES[i]] = value
107
+ end
108
+ end
109
+ end
110
+ @ssl_context.set_params(ssl_parameters)
111
+ unless @ssl_context.session_cache_mode.nil?
112
+ @ssl_context.session_cache_mode =
113
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
114
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
115
+ end
116
+ if @ssl_context.respond_to?(:session_new_cb)
117
+ @ssl_context.session_new_cb = proc { |sock, sess| @ssl_session = sess }
118
+ end
119
+
120
+ verify_hostname = @ssl_context.verify_hostname
121
+
122
+ case @address
123
+ when Resolv::IPv4::Regex, Resolv::IPv6::Regex
124
+ @ssl_context.verify_hostname = false
125
+ else
126
+ ssl_host_address = @address
127
+ end
128
+
129
+ debug "starting SSL for #{conn_addr}:#{conn_port}..."
130
+ s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)
131
+ s.sync_close = true
132
+ s.hostname = ssl_host_address if s.respond_to?(:hostname=) && ssl_host_address
133
+
134
+ if @ssl_session &&
135
+ Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout
136
+ s.session = @ssl_session
137
+ end
138
+ ssl_socket_connect(s, @open_timeout)
139
+ if (@ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE) && verify_hostname
140
+ s.post_connection_check(@address)
141
+ end
142
+ debug "SSL established, protocol: #{s.ssl_version}, cipher: #{s.cipher[0]}"
143
+ end
144
+ @socket = Net::BufferedIO.new(s, read_timeout: @read_timeout,
145
+ write_timeout: @write_timeout,
146
+ continue_timeout: @continue_timeout,
147
+ debug_output: @debug_output)
148
+ @last_communicated = nil
149
+ on_connect
150
+ rescue StandardError => e
151
+ if s
152
+ debug "Conn close because of connect error #{e}"
153
+ s.close
154
+ end
155
+ raise
156
+ end
157
+ end
158
+
159
+ class << self
160
+ def patch!
161
+ return if @patched
162
+
163
+ ::Net::HTTP.prepend(Extension)
164
+ @patched = true
165
+ end
166
+
167
+ def patched?
168
+ @patched == true
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyProxyHeaders
4
+ # RestClient integration for proxy headers support.
5
+ #
6
+ # @example
7
+ # response = RubyProxyHeaders::RestClient.get(
8
+ # 'https://example.com',
9
+ # proxy: 'http://user:pass@proxy:8080',
10
+ # proxy_headers: { 'X-ProxyMesh-Country' => 'US' }
11
+ # )
12
+ # puts response.proxy_response_headers
13
+ #
14
+ module RestClient
15
+ # Make a GET request with proxy headers.
16
+ def self.get(url, proxy:, proxy_headers: {}, **options)
17
+ request(:get, url, proxy: proxy, proxy_headers: proxy_headers, **options)
18
+ end
19
+
20
+ # Make a POST request with proxy headers.
21
+ def self.post(url, proxy:, proxy_headers: {}, payload: nil, **options)
22
+ request(:post, url, proxy: proxy, proxy_headers: proxy_headers, body: payload, **options)
23
+ end
24
+
25
+ # Make a PUT request with proxy headers.
26
+ def self.put(url, proxy:, proxy_headers: {}, payload: nil, **options)
27
+ request(:put, url, proxy: proxy, proxy_headers: proxy_headers, body: payload, **options)
28
+ end
29
+
30
+ # Make a DELETE request with proxy headers.
31
+ def self.delete(url, proxy:, proxy_headers: {}, **options)
32
+ request(:delete, url, proxy: proxy, proxy_headers: proxy_headers, **options)
33
+ end
34
+
35
+ # Make a request with proxy headers.
36
+ def self.request(method, url, proxy:, proxy_headers: {}, **options)
37
+ require 'rest-client'
38
+
39
+ uri = URI.parse(url)
40
+
41
+ # For HTTPS with proxy headers, use our core connection
42
+ if uri.scheme == 'https' && !proxy_headers.empty?
43
+ response = RubyProxyHeaders::NetHTTP.request(
44
+ method,
45
+ url,
46
+ proxy: proxy,
47
+ proxy_headers: proxy_headers,
48
+ headers: options[:headers],
49
+ body: options[:body]
50
+ )
51
+ return response
52
+ end
53
+
54
+ # For HTTP or no proxy headers, use standard RestClient
55
+ ::RestClient.proxy = proxy
56
+ response = ::RestClient.send(method, url, options)
57
+ ProxyResponse.new(response, {})
58
+ end
59
+
60
+ # Response wrapper with proxy headers accessor
61
+ class ProxyResponse
62
+ attr_reader :proxy_response_headers
63
+
64
+ def initialize(response, proxy_headers)
65
+ @response = response
66
+ @proxy_response_headers = proxy_headers
67
+ end
68
+
69
+ def code
70
+ @response.code
71
+ end
72
+
73
+ def body
74
+ @response.body
75
+ end
76
+
77
+ def headers
78
+ @response.headers
79
+ end
80
+
81
+ def method_missing(method, *args, &block)
82
+ @response.send(method, *args, &block)
83
+ end
84
+
85
+ def respond_to_missing?(method, include_private = false)
86
+ @response.respond_to?(method, include_private) || super
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyProxyHeaders
4
+ # Typhoeus/Ethon integration for proxy headers support.
5
+ # Typhoeus wraps libcurl, which has native support for CURLOPT_PROXYHEADER.
6
+ #
7
+ # @example
8
+ # response = RubyProxyHeaders::Typhoeus.get(
9
+ # 'https://example.com',
10
+ # proxy: 'http://user:pass@proxy:8080',
11
+ # proxy_headers: { 'X-ProxyMesh-Country' => 'US' }
12
+ # )
13
+ # puts response.proxy_response_headers
14
+ #
15
+ module Typhoeus
16
+ # Make a GET request with proxy headers.
17
+ # @param url [String] Target URL
18
+ # @param proxy [String] Proxy URL
19
+ # @param proxy_headers [Hash] Custom headers to send to proxy
20
+ # @param options [Hash] Additional Typhoeus options
21
+ # @return [ProxyResponse]
22
+ def self.get(url, proxy:, proxy_headers: {}, **options)
23
+ request(:get, url, proxy: proxy, proxy_headers: proxy_headers, **options)
24
+ end
25
+
26
+ # Make a POST request with proxy headers.
27
+ def self.post(url, proxy:, proxy_headers: {}, body: nil, **options)
28
+ request(:post, url, proxy: proxy, proxy_headers: proxy_headers, body: body, **options)
29
+ end
30
+
31
+ # Make a request with proxy headers.
32
+ # @param method [Symbol] HTTP method
33
+ # @param url [String] Target URL
34
+ # @param proxy [String] Proxy URL
35
+ # @param proxy_headers [Hash] Custom headers to send to proxy
36
+ # @param options [Hash] Additional Typhoeus options
37
+ # @return [ProxyResponse]
38
+ def self.request(method, url, proxy:, proxy_headers: {}, **options)
39
+ require 'typhoeus'
40
+
41
+ uri = URI.parse(url)
42
+
43
+ # For HTTPS, we need custom handling since Typhoeus doesn't expose CURLOPT_PROXYHEADER
44
+ if uri.scheme == 'https' && !proxy_headers.empty?
45
+ # Use our core connection for now
46
+ # TODO: Extend Ethon to expose CURLOPT_PROXYHEADER for native support
47
+ response = RubyProxyHeaders::NetHTTP.request(
48
+ method,
49
+ url,
50
+ proxy: proxy,
51
+ proxy_headers: proxy_headers,
52
+ headers: options[:headers],
53
+ body: options[:body]
54
+ )
55
+ return response
56
+ end
57
+
58
+ # For HTTP or no proxy headers, use standard Typhoeus
59
+ typhoeus_options = options.merge(
60
+ proxy: proxy,
61
+ method: method
62
+ )
63
+
64
+ response = ::Typhoeus::Request.new(url, typhoeus_options).run
65
+ ProxyResponse.new(response, {})
66
+ end
67
+
68
+ # Response wrapper with proxy headers accessor
69
+ class ProxyResponse
70
+ attr_reader :proxy_response_headers
71
+
72
+ def initialize(response, proxy_headers)
73
+ @response = response
74
+ @proxy_response_headers = proxy_headers
75
+ end
76
+
77
+ def code
78
+ @response.code
79
+ end
80
+
81
+ def body
82
+ @response.body
83
+ end
84
+
85
+ def headers
86
+ @response.headers
87
+ end
88
+
89
+ def success?
90
+ @response.success?
91
+ end
92
+
93
+ def method_missing(method, *args, &block)
94
+ @response.send(method, *args, &block)
95
+ end
96
+
97
+ def respond_to_missing?(method, include_private = false)
98
+ @response.respond_to?(method, include_private) || super
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyProxyHeaders
4
+ VERSION = '0.2.0'
5
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ruby_proxy_headers/version'
4
+ require_relative 'ruby_proxy_headers/net_http'
5
+
6
+ module RubyProxyHeaders
7
+ # CONNECT response headers from the last Net::HTTP-based proxied HTTPS request on this thread.
8
+ def self.proxy_connect_response_headers
9
+ Thread.current[:ruby_proxy_headers_connect_headers]
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,158 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-proxy-headers
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - ProxyMesh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-04-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.4'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: excon
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.4'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faraday
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.14'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.14'
55
+ - !ruby/object:Gem::Dependency
56
+ name: faraday-net_http
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.4'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: httparty
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.24'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.24'
83
+ - !ruby/object:Gem::Dependency
84
+ name: mechanize
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.14'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.14'
97
+ - !ruby/object:Gem::Dependency
98
+ name: typhoeus
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.6'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.6'
111
+ description: |
112
+ Extensions for Ruby HTTP stacks to send custom headers on HTTPS CONNECT to a proxy
113
+ and read headers from the proxy CONNECT response (e.g. X-ProxyMesh-IP).
114
+ email: support@proxymesh.com
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - DEFERRED.md
120
+ - IMPLEMENTATION_PRIORITY.md
121
+ - LIBRARY_RESEARCH.md
122
+ - LICENSE
123
+ - README.md
124
+ - lib/ruby_proxy_headers.rb
125
+ - lib/ruby_proxy_headers/connection.rb
126
+ - lib/ruby_proxy_headers/excon.rb
127
+ - lib/ruby_proxy_headers/faraday.rb
128
+ - lib/ruby_proxy_headers/http_gem.rb
129
+ - lib/ruby_proxy_headers/httparty.rb
130
+ - lib/ruby_proxy_headers/net_http.rb
131
+ - lib/ruby_proxy_headers/rest_client.rb
132
+ - lib/ruby_proxy_headers/typhoeus.rb
133
+ - lib/ruby_proxy_headers/version.rb
134
+ homepage: https://github.com/proxymesh/ruby-proxy-headers
135
+ licenses:
136
+ - MIT
137
+ metadata:
138
+ source_code_uri: https://github.com/proxymesh/ruby-proxy-headers
139
+ post_install_message:
140
+ rdoc_options: []
141
+ require_paths:
142
+ - lib
143
+ required_ruby_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '3.1'
148
+ required_rubygems_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ requirements: []
154
+ rubygems_version: 3.4.20
155
+ signing_key:
156
+ specification_version: 4
157
+ summary: Custom proxy CONNECT headers for Ruby HTTP clients (ProxyMesh, etc.)
158
+ test_files: []