Skip to content

Commit aa04d92

Browse files
author
Cursor
committed
Add Net::HTTP CONNECT patch, tests, and implementation roadmap
Implement RubyProxyHeaders::NetHTTP.patch! to send optional proxy CONNECT headers and capture CONNECT response headers (X-ProxyMesh-IP). Add integration harness matching python-proxy-headers env vars. Document library research and phased plan for Faraday, HTTParty, Typhoeus, Excon, and Mechanize. Made-with: Cursor
1 parent 6cb78dc commit aa04d92

9 files changed

Lines changed: 419 additions & 1160 deletions

File tree

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/.bundle/
2+
/vendor/bundle
3+
*.gem
4+
/pkg/
5+
/coverage/
6+
*.swp
7+
.env
8+
.ruby-version

IMPLEMENTATION_PRIORITY.md

Lines changed: 89 additions & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -1,216 +1,115 @@
1-
# Implementation Priority for ruby-proxy-headers
1+
# ruby-proxy-headers — implementation plan
22

3-
This document outlines the implementation plan for the ruby-proxy-headers gem.
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).
44

5-
## Architecture Overview
5+
## Architecture
66

77
```
8-
┌─────────────────────────────────────────────────────────────────┐
9-
│ ruby-proxy-headers gem │
10-
├─────────────────────────────────────────────────────────────────┤
11-
│ ┌─────────────────────────────────────────────────────────┐ │
12-
│ │ Core: ProxyHeadersConnection │ │
13-
│ │ - Direct socket handling for CONNECT with headers │ │
14-
│ │ - TLS upgrade after CONNECT │ │
15-
│ │ - Response header capture │ │
16-
│ └─────────────────────────────────────────────────────────┘ │
17-
│ ▲ │
18-
│ │ │
19-
│ ┌─────────────────────────────────────────────────────────┐ │
20-
│ │ Net::HTTP Extension │ │
21-
│ │ - Prepend module to override #connect │ │
22-
│ │ - Store proxy headers on Net::HTTPResponse │ │
23-
│ └─────────────────────────────────────────────────────────┘ │
24-
│ ▲ │
25-
│ ┌───────────────┼───────────────┐ │
26-
│ │ │ │ │
27-
│ ┌───────────┴───┐ ┌───────┴───────┐ ┌───┴───────────┐ │
28-
│ │ Faraday │ │ HTTParty │ │ RestClient │ │
29-
│ │ Middleware │ │ Extension │ │ Extension │ │
30-
│ └───────────────┘ └───────────────┘ └───────────────┘ │
31-
│ │
32-
│ ┌─────────────────────────────────────────────────────────┐ │
33-
│ │ Typhoeus Extension (via Ethon FFI) │ │
34-
│ │ - Direct libcurl CURLOPT_PROXYHEADER support │ │
35-
│ └─────────────────────────────────────────────────────────┘ │
36-
└─────────────────────────────────────────────────────────────────┘
8+
┌─────────────────────────────────────────────────────────────┐
9+
│ ruby-proxy-headers │
10+
├─────────────────────────────────────────────────────────────┤
11+
│ Library wrappers (Faraday, HTTParty, Mechanize, …) │
12+
│ │ │
13+
│ ▼ │
14+
│ Net::HTTP patch (Phase 1) — CONNECT send + capture │
15+
│ │ │
16+
│ ├──► Typhoeus / Ethon — libcurl options (Phase 4) │
17+
│ └──► Excon — custom tunnel (Phase 5) │
18+
└─────────────────────────────────────────────────────────────┘
3719
```
3820

39-
## Phase 1: Core Implementation
21+
## Phase 1 — Net::HTTP core (**done in v0.1**)
4022

41-
### 1.1 ProxyHeadersConnection (lib/ruby_proxy_headers/connection.rb)
42-
- Pure Ruby socket handling
43-
- Build CONNECT request with custom headers
44-
- Parse CONNECT response and capture headers
45-
- TLS upgrade handling
46-
- Thread-safe header storage
23+
**Goal:** `RubyProxyHeaders::NetHTTP.patch!` extends `Net::HTTP#connect` for `use_ssl? && proxy?` to:
4724

48-
### 1.2 Net::HTTP Extension (lib/ruby_proxy_headers/net_http.rb)
49-
- Prepend module to `Net::HTTP`
50-
- Override `connect` method to use ProxyHeadersConnection
51-
- Add `proxy_response_headers` accessor to responses
52-
- Configuration via class-level or instance-level options
25+
- Send optional `proxy_connect_request_headers` on `CONNECT`
26+
- Store `last_proxy_connect_response_headers` from the `CONNECT` response
5327

54-
## Phase 2: Popular Library Integration
28+
**Files:**
5529

56-
### 2.1 Faraday Middleware (lib/ruby_proxy_headers/faraday.rb)
57-
- Register as Faraday middleware
58-
- Wrap connection with proxy header support
59-
- Store proxy headers in `env[:proxy_response_headers]`
60-
- Access via `response.env[:proxy_response_headers]`
30+
- `lib/ruby_proxy_headers/net_http.rb`
6131

62-
### 2.2 HTTParty Extension (lib/ruby_proxy_headers/httparty.rb)
63-
- Module that can be included in HTTParty classes
64-
- Add `proxy_headers` option to requests
65-
- Store proxy response headers on response object
66-
- Works with existing HTTParty patterns
32+
**Tests:**
6733

68-
### 2.3 HTTP.rb Extension (lib/ruby_proxy_headers/http_gem.rb)
69-
- Extend HTTP::Client or use feature injection
70-
- Add `.with_proxy_headers(headers)` chainable method
71-
- Access via `response.proxy_headers`
34+
- `test/test_proxy_headers.rb` module `net_http`
7235

73-
## Phase 3: Additional Libraries
36+
**Success criteria:**
7437

75-
### 3.1 Typhoeus/Ethon Extension (lib/ruby_proxy_headers/typhoeus.rb)
76-
- Extend Ethon::Easy to expose `CURLOPT_PROXYHEADER`
77-
- Use `CURLOPT_HEADERFUNCTION` for response capture
78-
- Leverage native libcurl support for best performance
38+
- Live test against `PROXY_URL` with `X-ProxyMesh-IP` visible in `last_proxy_connect_response_headers`
7939

80-
### 3.2 Excon Extension (lib/ruby_proxy_headers/excon.rb)
81-
- Middleware/interceptor for Excon
82-
- Wrap connection handling
83-
- Store headers in connection data
40+
---
8441

85-
### 3.3 RestClient Extension (lib/ruby_proxy_headers/rest_client.rb)
86-
- Leverages Net::HTTP extension
87-
- Add accessor for proxy headers on response
42+
## Phase 2 — Faraday
8843

89-
## File Structure
44+
**Goal:** Ergonomic API: `proxy_headers: { ... }` on the connection or per-request, backed by patched `Net::HTTP`.
9045

91-
```
92-
ruby-proxy-headers/
93-
├── lib/
94-
│ ├── ruby_proxy_headers.rb # Main entry point
95-
│ ├── ruby_proxy_headers/
96-
│ │ ├── version.rb # Gem version
97-
│ │ ├── connection.rb # Core proxy connection handler
98-
│ │ ├── net_http.rb # Net::HTTP extension
99-
│ │ ├── faraday.rb # Faraday middleware
100-
│ │ ├── httparty.rb # HTTParty extension
101-
│ │ ├── http_gem.rb # HTTP.rb extension
102-
│ │ ├── typhoeus.rb # Typhoeus/Ethon extension
103-
│ │ ├── excon.rb # Excon extension
104-
│ │ └── rest_client.rb # RestClient extension
105-
├── spec/
106-
│ ├── spec_helper.rb
107-
│ ├── connection_spec.rb
108-
│ ├── net_http_spec.rb
109-
│ ├── faraday_spec.rb
110-
│ ├── httparty_spec.rb
111-
│ └── ...
112-
├── examples/
113-
│ ├── net_http_example.rb
114-
│ ├── faraday_example.rb
115-
│ └── ...
116-
├── ruby_proxy_headers.gemspec
117-
├── Gemfile
118-
├── Rakefile
119-
├── README.md
120-
├── LICENSE
121-
├── CHANGELOG.md
122-
└── docs/ # ReadTheDocs documentation
123-
├── index.md
124-
├── getting-started.md
125-
├── faraday.md
126-
└── ...
127-
```
46+
**Approach:**
12847

129-
## Technical Challenges
130-
131-
### 1. Socket Interception
132-
Ruby's Net::HTTP creates sockets internally. We need to:
133-
- Intercept before TLS upgrade
134-
- Inject CONNECT request with custom headers
135-
- Parse CONNECT response
136-
- Continue with normal TLS handshake
137-
138-
### 2. Thread Safety
139-
Multiple concurrent requests may share connection pools:
140-
- Use thread-local storage for proxy response headers
141-
- Or attach headers to response object directly
142-
143-
### 3. Connection Pooling
144-
Libraries like Faraday may reuse connections:
145-
- Ensure headers are reset per-request
146-
- Don't interfere with keep-alive
147-
148-
### 4. libcurl Integration (Typhoeus)
149-
- Ethon uses FFI, may need to define additional functions
150-
- `CURLOPT_PROXYHEADER` requires curl >= 7.37.0
151-
- Need to handle version detection
152-
153-
## API Design
154-
155-
### Basic Usage
156-
157-
```ruby
158-
require 'ruby_proxy_headers'
159-
160-
# Net::HTTP
161-
response = RubyProxyHeaders::NetHTTP.get(
162-
'https://example.com',
163-
proxy: 'http://user:pass@proxy:8080',
164-
proxy_headers: { 'X-ProxyMesh-Country' => 'US' }
165-
)
166-
puts response.proxy_response_headers['X-ProxyMesh-IP']
167-
168-
# Faraday
169-
conn = Faraday.new(url: 'https://example.com') do |f|
170-
f.use RubyProxyHeaders::Faraday::Middleware,
171-
proxy_headers: { 'X-ProxyMesh-Country' => 'US' }
172-
f.proxy = 'http://user:pass@proxy:8080'
173-
end
174-
response = conn.get('/')
175-
puts response.env[:proxy_response_headers]['X-ProxyMesh-IP']
176-
177-
# HTTParty
178-
class ProxyClient
179-
include HTTParty
180-
include RubyProxyHeaders::HTTParty
181-
182-
http_proxy 'proxy', 8080, 'user', 'pass'
183-
proxy_headers 'X-ProxyMesh-Country' => 'US'
184-
end
185-
response = ProxyClient.get('https://example.com')
186-
puts response.proxy_response_headers['X-ProxyMesh-IP']
187-
```
48+
- `Faraday.new(...) { |f| f.adapter :net_http }` and ensure the adapter’s `Net::HTTP` instance receives `proxy_connect_request_headers`
49+
- Or `Faraday::Connection` subclass / middleware that sets headers on the underlying `Net::HTTP` before `connect`
50+
51+
**Files (planned):**
52+
53+
- `lib/ruby_proxy_headers/faraday.rb`
54+
55+
---
56+
57+
## Phase 3 — HTTParty
58+
59+
**Goal:** Document + optional helper to set `proxy_connect_request_headers` on the internal `Net::HTTP` (or class-level hooks).
60+
61+
**Files (planned):**
62+
63+
- `lib/ruby_proxy_headers/httparty.rb` (thin wrapper or documentation module)
64+
65+
---
66+
67+
## Phase 4 — Typhoeus / Ethon
68+
69+
**Goal:** Map Ruby header hash to libcurl proxy header options; capture CONNECT-related output if feasible.
70+
71+
**Files (planned):**
72+
73+
- `lib/ruby_proxy_headers/typhoeus.rb` or `ethon.rb`
74+
75+
**Risk:** libcurl version differences; may need feature detection.
76+
77+
---
78+
79+
## Phase 5 — Excon
80+
81+
**Goal:** Custom CONNECT path or middleware mirroring the Node `ProxyHeadersAgent` behavior.
82+
83+
**Files (planned):**
84+
85+
- `lib/ruby_proxy_headers/excon.rb`
86+
87+
---
88+
89+
## Phase 6 — Mechanize
90+
91+
**Goal:** Ensure Mechanize sessions use patched `Net::HTTP` behavior and document how to read `last_proxy_connect_response_headers` from the right object.
92+
93+
**Files (planned):**
94+
95+
- `lib/ruby_proxy_headers/mechanize.rb` (wrapper or patches)
96+
97+
---
98+
99+
## Testing
188100

189-
## Dependencies
101+
All phases should plug into `test/test_proxy_headers.rb` with the same env vars as Python/JS:
190102

191-
### Required
192-
- Ruby >= 2.7 (for pattern matching, numbered block params)
193-
- No external gems for core functionality
103+
- `PROXY_URL`, `PROXY_HEADER`, `SEND_PROXY_HEADER`, `SEND_PROXY_VALUE`, `TEST_URL`
194104

195-
### Optional (for specific integrations)
196-
- faraday (>= 1.0) for Faraday middleware
197-
- httparty (>= 0.18) for HTTParty extension
198-
- http (>= 5.0) for HTTP.rb extension
199-
- typhoeus (>= 1.4) for Typhoeus extension
200-
- excon (>= 0.80) for Excon extension
105+
---
201106

202-
## Testing Strategy
107+
## Success criteria (project-wide)
203108

204-
### Unit Tests
205-
- Mock socket connections
206-
- Test CONNECT request building
207-
- Test response parsing
109+
1. At least one production-quality path for **Net::HTTP** (done).
110+
2. Faraday + HTTParty documented with working examples.
111+
3. Optional: Typhoeus, Excon, Mechanize where demand and maintenance cost align.
208112

209-
### Integration Tests
210-
- Use actual proxy (local or test service)
211-
- Verify headers sent and received
212-
- Test with each supported library
113+
---
213114

214-
### Compatibility Tests
215-
- Test with multiple Ruby versions (2.7, 3.0, 3.1, 3.2, 3.3)
216-
- Test with multiple versions of each library
115+
*Plan created: March 2026*

0 commit comments

Comments
 (0)