Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .agents/skills/ruby-proxy-headers/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: ruby-proxy-headers
description: >-
Send and receive custom headers during HTTPS CONNECT tunneling in Ruby.
Use when integrating proxy headers with Net::HTTP, Faraday, HTTParty, or Excon.
Use when integrating proxy headers with Net::HTTP, Faraday, HTTParty, Excon, HTTP.rb, Typhoeus, or RestClient.
---

# ruby-proxy-headers
Expand Down Expand Up @@ -31,6 +31,9 @@ Install faraday, httparty, or excon as needed.
| Faraday | `ruby_proxy_headers/faraday` | `FaradayIntegration.connection` |
| HTTParty | `ruby_proxy_headers/httparty` | `ProxyHeadersConnectionAdapter` |
| Excon | `ruby_proxy_headers/excon` | `ExconIntegration.get` (send-only) |
| HTTP.rb | `ruby_proxy_headers/http_gem` | `HTTPGem.get` |
| Typhoeus | `ruby_proxy_headers/typhoeus` | `Typhoeus.get` (HTTPS via Net::HTTP) |
| RestClient | `ruby_proxy_headers/rest_client` | `RestClient.get` |

## Usage Patterns

Expand Down
43 changes: 43 additions & 0 deletions .github/workflows/latest_client_gems.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Weekly check that the adapters still work with the newest client gems allowed
# by the gemspec. No lockfile: bundle update resolves the latest versions.
# Local mock-proxy specs do not need the PROXY_URL secret.

name: Latest client gems

on:
schedule:
- cron: '0 6 * * 1'
workflow_dispatch:

permissions:
contents: read

jobs:
latest:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
ruby-version: ['3.2', '3.3', '3.4']

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install libcurl and Python
run: sudo apt-get update && sudo apt-get install -y libcurl4 python3

- name: Set up Ruby ${{ matrix.ruby-version }}
uses: ruby/setup-ruby@v1 # kept unpinned per maintainer guidance — pinning freezes available Ruby versions
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: false

- name: Install latest client gems
run: |
rm -f Gemfile.lock
bundle update

- name: Run specs and mock-proxy integration tests
run: bundle exec rspec
3 changes: 3 additions & 0 deletions .github/workflows/proxy_integration_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ jobs:
with:
persist-credentials: false

- name: Install libcurl
run: sudo apt-get update && sudo apt-get install -y libcurl4

- name: Set up Ruby
uses: ruby/setup-ruby@v1 # kept unpinned per maintainer guidance — pinning freezes available Ruby versions
with:
Expand Down
51 changes: 50 additions & 1 deletion .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ jobs:

strategy:
matrix:
ruby-version: ['3.2', '3.3']
ruby-version: ['3.2', '3.3', '3.4']

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install libcurl and Python
run: sudo apt-get update && sudo apt-get install -y libcurl4 python3

- name: Set up Ruby ${{ matrix.ruby-version }}
uses: ruby/setup-ruby@v1 # kept unpinned per maintainer guidance — pinning freezes available Ruby versions
with:
Expand All @@ -27,3 +30,49 @@ jobs:

- name: Run RSpec
run: bundle exec rspec

# Plaintext CONNECT on every supported net-http, plus HTTPS-proxy / timeouted_connect
# where that release has them. Kept off the main Gemfile so faraday-net_http's
# net-http ~> 0.5 constraint does not hide older default gems.
net-http:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- ruby-version: '3.2'
net-http-version: '0.3.2'
- ruby-version: '3.3'
net-http-version: '0.4.1'
- ruby-version: '3.3'
net-http-version: '0.5.0'
- ruby-version: '3.4'
net-http-version: '0.6.0'
- ruby-version: '3.3'
net-http-version: '0.9.1'
- ruby-version: '3.4'
net-http-version: '0.10.0'

env:
BUNDLE_GEMFILE: gemfiles/net_http_matrix.gemfile
NET_HTTP_VERSION: ${{ matrix.net-http-version }}

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install Python
run: sudo apt-get update && sudo apt-get install -y python3

- name: Set up Ruby ${{ matrix.ruby-version }}
uses: ruby/setup-ruby@v1 # kept unpinned per maintainer guidance — pinning freezes available Ruby versions
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: false

- name: Install net-http ${{ matrix.net-http-version }}
run: bundle install

- name: Run net-http compatibility specs
run: bundle exec rspec spec/net_http_compat_spec.rb spec/connect_header_injection_spec.rb
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@
*.swp
.env
.ruby-version
spec/examples.txt
gemfiles/*.lock
16 changes: 14 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- HTTP.rb, RestClient, and Typhoeus no longer raise `NoMethodError` on HTTPS requests. They send custom CONNECT headers through `RubyProxyHeaders::NetHTTP.request` and expose the proxy's CONNECT response headers.
- The Net::HTTP patch no longer replaces `Net::HTTP#connect` with a stale copy. Socket setup, `timeouted_connect`, and HTTPS proxies (`proxy_use_ssl`) stay in the installed net-http.
- The Faraday adapter no longer overrides faraday-net_http's private `request_with_wrapped_block`. CONNECT response headers are merged from the public adapter hooks.

### Changed

- Gem metadata now includes `homepage_uri`, `changelog_uri`, and `bug_tracker_uri`.
- The published gem no longer ships `IMPLEMENTATION_PRIORITY.md`, `LIBRARY_RESEARCH.md`, or `DEFERRED.md`. Per-library guides under `docs/` are included instead.
- Faraday, HTTParty, and Excon guides describe `FaradayIntegration`, `ProxyHeadersConnectionAdapter`, and `ExconIntegration` instead of middleware and helpers that were never implemented.

## [0.1.0] - 2024-02-28

### Added
Expand All @@ -23,5 +35,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Comprehensive documentation
- Test suite with RSpec

[Unreleased]: https://github.com/proxymeshai/ruby-proxy-headers/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/proxymeshai/ruby-proxy-headers/releases/tag/v0.1.0
[Unreleased]: https://github.com/proxymesh/ruby-proxy-headers/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/proxymesh/ruby-proxy-headers/releases/tag/v0.1.0
8 changes: 3 additions & 5 deletions DEFERRED.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
# Deferred / not fully supported

## Typhoeus / Ethon
## Typhoeus / Ethon native proxy headers

**Status:** Not implemented in this gem.
**Status:** HTTPS works by delegating to patched Net::HTTP. Native libcurl is not used for CONNECT headers.

[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.

**Workaround:** Use `Net::HTTP` + {RubyProxyHeaders::NetHTTP.patch!} or Faraday with `ruby_proxy_headers_net_http` adapter.
[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. `RubyProxyHeaders::Typhoeus` therefore sends HTTPS requests that need proxy headers through `RubyProxyHeaders::NetHTTP.request`, which can both set CONNECT headers and read the proxy's CONNECT response. A future version may call libcurl directly.

---

Expand Down
18 changes: 18 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ GEM
ffi (1.17.4-x86_64-linux-gnu)
ffi (1.17.4-x86_64-linux-musl)
hashdiff (1.2.1)
http (6.0.4)
http-cookie (~> 1.0)
llhttp (~> 0.6.1)
http-accept (1.7.0)
http-cookie (1.1.0)
domain_name (~> 0.5)
httparty (0.24.3)
Expand All @@ -48,6 +52,7 @@ GEM
json (3.1.0)
language_server-protocol (3.17.0.6)
lint_roller (1.1.0)
llhttp (0.6.2)
logger (1.7.0)
mechanize (2.14.1)
addressable (~> 2.8)
Expand Down Expand Up @@ -77,6 +82,7 @@ GEM
connection_pool (>= 2.2.4, < 4)
net-protocol (0.4.0)
timeout
netrc (0.11.0)
nkf (0.2.0)
nokogiri (1.19.4)
mini_portile2 (~> 2.8.2)
Expand Down Expand Up @@ -107,6 +113,11 @@ GEM
rainbow (3.1.1)
rake (13.4.2)
regexp_parser (2.13.1)
rest-client (2.1.0)
http-accept (>= 1.7.0, < 2.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rexml (3.4.4)
rspec (3.13.2)
rspec-core (~> 3.13.0)
Expand Down Expand Up @@ -169,9 +180,11 @@ DEPENDENCIES
excon (~> 1.4)
faraday (~> 2.14)
faraday-net_http (~> 3.4)
http (~> 6.0)
httparty (~> 0.24)
mechanize (~> 2.14)
rake (~> 13.0)
rest-client (~> 2.1)
rspec (~> 3.12)
rubocop (~> 1.50)
ruby-proxy-headers!
Expand Down Expand Up @@ -203,11 +216,14 @@ CHECKSUMS
ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e
hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1
http (6.0.4) sha256=3768f902489ea8309424f1881c668af8d3ca73675648a755dfe2cb0030d42d26
http-accept (1.7.0) sha256=c626860682bfbb3b46462f8c39cd470fd7b0584f61b3cc9df5b2e9eb9972a126
http-cookie (1.1.0) sha256=38a5e60d1527eebc396831b8c4b9455440509881219273a6c99943d29eadbb19
httparty (0.24.3) sha256=5c95cf29ca70d5bfd834e56140ddfca6e94166c3b870cde9c25d157cdb5a00d2
json (3.1.0) sha256=3ce7ccf8fde58960177a3a756c31f292a4c1e1dedee884547096a5b0604e3bd8
language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
llhttp (0.6.2) sha256=3c3c59aafb1e1594ab2f45293478f697458f5a91eb0f0db4b27f61a064024982
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
mechanize (2.14.1) sha256=b66205133a01d3dc3bc0cbc5e99888e5476818398d8d315193429f21488abafd
mime-types (3.7.0) sha256=dcebf61c246f08e15a4de34e386ebe8233791e868564a470c3fe77c00eed5e56
Expand All @@ -219,6 +235,7 @@ CHECKSUMS
net-http-digest_auth (1.4.1) sha256=4b8ad50ed8d180a58db5d6c49449b987dd0466fe01e24037945bc007562a08db
net-http-persistent (4.0.8) sha256=ef3de8319d691537b329053fae3a33195f8b070bbbfae8bf1a58c796081960e6
net-protocol (0.4.0) sha256=f9f90c8c6f64794a45160bf422177a336db12809de3beeeaa26b88fcd2e8c853
netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f
nkf (0.2.0) sha256=fbc151bda025451f627fafdfcb3f4f13d0b22ae11f58c6d3a2939c76c5f5f126
nokogiri (1.19.4) sha256=50c951611c92bca05c51411aef45f1cbc50f2821c4802758c5c6d34696533ab5
nokogiri (1.19.4-aarch64-linux-gnu) sha256=1269fb644a6de405057a53dd5c762b1209b43ca7424f839454d3dbc677c31a8f
Expand All @@ -237,6 +254,7 @@ CHECKSUMS
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
regexp_parser (2.13.1) sha256=5aedb6b7c35688f51e86eef17a15374dfd287c83f4ca644b2c5f3ec50a33b44b
rest-client (2.1.0) sha256=35a6400bdb14fae28596618e312776c158f7ebbb0ccad752ff4fa142bf2747e3
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
Expand Down
2 changes: 1 addition & 1 deletion MARKETING_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Keep the messaging accurate: this project should market implemented integrations

## 1) Package and Registry Presence

- Publish and maintain on RubyGems (`ruby_proxy_headers`).
- Publish and maintain on RubyGems (`ruby-proxy-headers`).
- Ensure gem metadata is complete: homepage, source code URI, changelog URI, bug tracker URI.
- Keep release cadence visible with changelog updates and GitHub releases.

Expand Down
60 changes: 55 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[![Gem Version](https://badge.fury.io/rb/ruby-proxy-headers.svg)](https://badge.fury.io/rb/ruby-proxy-headers)
# Ruby Proxy Headers

[![Gem Version](https://badge.fury.io/rb/ruby-proxy-headers.svg)](https://badge.fury.io/rb/ruby-proxy-headers)

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`).

## The problem
Expand All @@ -10,7 +11,7 @@ Most Ruby HTTP clients use `Net::HTTP`, Faraday, or libcurl without exposing:
1. Extra headers on the `CONNECT` request to the proxy.
2. Headers from the proxy’s `CONNECT` response (often discarded after the tunnel is established).

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.
This library adds opt-in support for **Net::HTTP**, **Faraday** (2.x via `faraday-net_http`), **HTTParty**, **HTTP.rb**, **Typhoeus**, and **RestClient**, and documents **Excon**’s built-in `:ssl_proxy_headers` for sends.

## Why teams use this

Expand All @@ -30,7 +31,7 @@ Or add to your `Gemfile`:
gem 'ruby-proxy-headers'
```

The `Net::HTTP` patch is pure Ruby. Install **faraday**, **faraday-net_http**, **httparty**, and/or **excon** when you use those integrations.
The `Net::HTTP` patch is pure Ruby. Install **faraday**, **faraday-net_http**, **httparty**, **http**, **typhoeus**, **rest-client**, and/or **excon** when you use those integrations.

## Net::HTTP

Expand Down Expand Up @@ -94,9 +95,55 @@ HTTParty.get(
puts RubyProxyHeaders.proxy_connect_response_headers['X-ProxyMesh-IP']
```

## HTTP.rb

HTTPS requests go through patched `Net::HTTP`, because HTTP.rb does not expose CONNECT headers. Plain HTTP keeps using the `http` gem.

```ruby
require 'ruby_proxy_headers/http_gem'

response = RubyProxyHeaders::HTTPGem.get(
'https://api.ipify.org?format=json',
proxy: ENV.fetch('PROXY_URL'),
proxy_headers: { 'X-ProxyMesh-Country' => 'US' }
)
puts response.body
puts response.proxy_response_headers['X-ProxyMesh-IP']
```

## Typhoeus

Ethon does not expose libcurl's `CURLOPT_PROXYHEADER`. HTTPS requests that send proxy headers use patched `Net::HTTP`, so the proxy's CONNECT response headers are readable.

```ruby
require 'ruby_proxy_headers/typhoeus'

response = RubyProxyHeaders::Typhoeus.get(
'https://api.ipify.org?format=json',
proxy: ENV.fetch('PROXY_URL'),
proxy_headers: { 'X-ProxyMesh-Country' => 'US' }
)
puts response.body
puts response.proxy_response_headers['X-ProxyMesh-IP']
```

## RestClient

```ruby
require 'ruby_proxy_headers/rest_client'

response = RubyProxyHeaders::RestClient.get(
'https://api.ipify.org?format=json',
proxy: ENV.fetch('PROXY_URL'),
proxy_headers: { 'X-ProxyMesh-Country' => 'US' }
)
puts response.body
puts response.proxy_response_headers['X-ProxyMesh-IP']
```

## Excon (send-only; CONNECT response headers)

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).
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](https://github.com/proxymesh/ruby-proxy-headers/blob/main/DEFERRED.md).

```ruby
require 'ruby_proxy_headers/excon'
Expand Down Expand Up @@ -127,8 +174,11 @@ export PROXY_URL=http://user:pass@proxyhost:port
bundle exec ruby test/test_proxy_headers.rb -v
```

## Related
`bundle exec rspec` does not need a live proxy. It includes specs that start [test/mock_connect_proxy.py](https://github.com/proxymesh/ruby-proxy-headers/blob/main/test/mock_connect_proxy.py) on `127.0.0.1` and check that the custom header arrives on `CONNECT` and that the proxy response headers can be read.

## Other languages

- [python-proxy-headers](https://github.com/proxymesh/python-proxy-headers)
- [javascript-proxy-headers](https://github.com/proxymesh/javascript-proxy-headers)
- [scrapy-proxy-headers](https://github.com/proxymesh/scrapy-proxy-headers)
- [proxy-examples (Ruby)](https://github.com/proxymesh/proxy-examples/tree/main/ruby)
Loading