Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout merge commit
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/proxy_integration_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Set up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@v1 # kept unpinned per maintainer guidance — pinning freezes available Ruby versions
with:
bundler-cache: true
ruby-version: ruby
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/push_gem.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
outputs:
publish: ${{ steps.decide.outputs.publish }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
if: github.event_name == 'workflow_run'
with:
ref: main
Expand Down Expand Up @@ -67,7 +67,7 @@ jobs:
id-token: write

steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event_name == 'workflow_run' && 'main' || github.ref }}
persist-credentials: false
Expand All @@ -84,9 +84,9 @@ jobs:
fi

- name: Set up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@v1 # kept unpinned per maintainer guidance — pinning freezes available Ruby versions
with:
bundler-cache: true
ruby-version: ruby

- uses: rubygems/release-gem@v1
- uses: rubygems/release-gem@6317d8d1f7e28c24d28f6eff169ea854948bd9f7 # v1.2.0
29 changes: 29 additions & 0 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Unit tests

on:
pull_request:

permissions:
contents: read

jobs:
rspec:
runs-on: ubuntu-latest

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

steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- 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: true

- name: Run RSpec
run: bundle exec rspec
25 changes: 24 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ GEM
base64 (0.3.0)
bigdecimal (4.1.1)
connection_pool (3.0.2)
crack (1.0.1)
bigdecimal
rexml
csv (3.3.5)
diff-lcs (1.6.2)
domain_name (0.6.20240107)
Expand All @@ -26,6 +29,7 @@ GEM
logger
faraday-net_http (3.4.2)
net-http (~> 0.5)
ffi (1.17.4)
ffi (1.17.4-aarch64-linux-gnu)
ffi (1.17.4-aarch64-linux-musl)
ffi (1.17.4-arm-linux-gnu)
Expand All @@ -34,6 +38,7 @@ GEM
ffi (1.17.4-x86_64-darwin)
ffi (1.17.4-x86_64-linux-gnu)
ffi (1.17.4-x86_64-linux-musl)
hashdiff (1.2.1)
http-cookie (1.1.0)
domain_name (~> 0.5)
httparty (0.24.2)
Expand Down Expand Up @@ -62,6 +67,7 @@ GEM
mime-types-data (~> 3.2025, >= 3.2025.0507)
mime-types-data (3.2026.0331)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
multi_xml (0.8.1)
bigdecimal (>= 3.1, < 5)
net-http (0.9.1)
Expand All @@ -70,6 +76,9 @@ GEM
net-http-persistent (4.0.8)
connection_pool (>= 2.2.4, < 4)
nkf (0.2.0)
nokogiri (1.19.4)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.19.4-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.4-aarch64-linux-musl)
Expand All @@ -96,6 +105,7 @@ GEM
rainbow (3.1.1)
rake (13.4.2)
regexp_parser (2.12.0)
rexml (3.4.4)
rspec (3.13.2)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
Expand Down Expand Up @@ -132,6 +142,10 @@ GEM
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
uri (1.1.1)
webmock (3.26.2)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.2)
webrobots (0.1.2)
yard (0.9.44)
Expand All @@ -142,6 +156,7 @@ PLATFORMS
arm-linux-gnu
arm-linux-musl
arm64-darwin
ruby
x86_64-darwin
x86_64-linux-gnu
x86_64-linux-musl
Expand All @@ -158,6 +173,7 @@ DEPENDENCIES
rubocop (~> 1.50)
ruby-proxy-headers!
typhoeus (~> 1.6)
webmock (~> 3.18)
yard (~> 0.9)

CHECKSUMS
Expand All @@ -166,13 +182,15 @@ CHECKSUMS
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
bigdecimal (4.1.1) sha256=1c09efab961da45203c8316b0cdaec0ff391dfadb952dd459584b63ebf8054ca
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933
ethon (0.18.0) sha256=b598afc9f30448cb068b850714b7d6948e941476095d04f90a4ac65b8d6efcb2
excon (1.5.0) sha256=c503ad1d0123bc8ab2a062ff3789dc891ec368cb9e13765ab88a9c58c8bb6d50
faraday (2.14.3) sha256=1882247e6766615c8220b4392bf1d27f6ebb63d8e28267587cef1fb0bf37f278
faraday-net_http (3.4.2) sha256=f147758260d3526939bf57ecf911682f94926a3666502e24c69992765875906c
ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf
ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df
ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39
ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564
Expand All @@ -181,6 +199,7 @@ CHECKSUMS
ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496
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-cookie (1.1.0) sha256=38a5e60d1527eebc396831b8c4b9455440509881219273a6c99943d29eadbb19
httparty (0.24.2) sha256=8fca6a54aa0c4aa4303a0fd33e5e2156175d6a5334f714263b458abd7fda9c38
json (2.21.1) sha256=13a43df75d95641443f5702dff350f237164a9d811ff0f2c2800d4d980220583
Expand All @@ -191,11 +210,13 @@ CHECKSUMS
mime-types (3.7.0) sha256=dcebf61c246f08e15a4de34e386ebe8233791e868564a470c3fe77c00eed5e56
mime-types-data (3.2026.0331) sha256=e9942b1fac72532e2b201b0c32c52e7650ef5ef8ca043a5054674597795c97a5
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
mini_portile2 (2.8.9) sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289
multi_xml (0.8.1) sha256=addba0290bac34e9088bfe73dc4878530297a82a7bbd66cb44dcd0a4b86edf5a
net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996
net-http-digest_auth (1.4.1) sha256=4b8ad50ed8d180a58db5d6c49449b987dd0466fe01e24037945bc007562a08db
net-http-persistent (4.0.8) sha256=ef3de8319d691537b329053fae3a33195f8b070bbbfae8bf1a58c796081960e6
nkf (0.2.0) sha256=fbc151bda025451f627fafdfcb3f4f13d0b22ae11f58c6d3a2939c76c5f5f126
nokogiri (1.19.4) sha256=50c951611c92bca05c51411aef45f1cbc50f2821c4802758c5c6d34696533ab5
nokogiri (1.19.4-aarch64-linux-gnu) sha256=1269fb644a6de405057a53dd5c762b1209b43ca7424f839454d3dbc677c31a8f
nokogiri (1.19.4-aarch64-linux-musl) sha256=35c65b9ce72b3bb03207bdbe7067915019dc18c1b9b59139684bd6690fdd01af
nokogiri (1.19.4-arm-linux-gnu) sha256=a301313e38bb065d68239e79734bcd6f56fb6efaacebde29e9abf2a4735340ca
Expand All @@ -212,6 +233,7 @@ CHECKSUMS
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
Expand All @@ -226,9 +248,10 @@ CHECKSUMS
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
webmock (3.26.2) sha256=774556f2ea6371846cca68c01769b2eac0d134492d21f6d0ab5dd643965a4c90
webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131
webrobots (0.1.2) sha256=ebbcaa2cb4930fa1b83206f432c5cb64746507b2dcf50ea1301569a4d662cda6
yard (0.9.44) sha256=eb087e9b631ccd887b049f303d489963945452d5e2a7eb49a5a74a7cf6887f28

BUNDLED WITH
4.0.8
2.5.0
14 changes: 14 additions & 0 deletions lib/ruby_proxy_headers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,22 @@
require_relative 'ruby_proxy_headers/net_http'

module RubyProxyHeaders
INVALID_HEADER_NAME_RE = /[\r\n\0]/
INVALID_HEADER_VALUE_RE = /[\r\n\0]/

# CONNECT response headers from the last Net::HTTP-based proxied HTTPS request on this thread.
def self.proxy_connect_response_headers
Thread.current[:ruby_proxy_headers_connect_headers]
end

# Raises ArgumentError if +name+ or +value+ contain CR, LF, or NUL bytes
# that would allow HTTP header injection in a raw CONNECT request.
def self.validate_header!(name, value)
if INVALID_HEADER_NAME_RE.match?(name.to_s)
raise ArgumentError, "proxy CONNECT header name contains invalid characters (CR, LF, or NUL): #{name.inspect}"
end
if INVALID_HEADER_VALUE_RE.match?(value.to_s)
raise ArgumentError, "proxy CONNECT header value contains invalid characters (CR, LF, or NUL): #{value.inspect}"
end
end
end
1 change: 1 addition & 0 deletions lib/ruby_proxy_headers/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def build_connect_request(target_host, target_port)

# Add custom proxy headers
@proxy_headers.each do |name, value|
RubyProxyHeaders.validate_header!(name, value)
request_lines << "#{name}: #{value}"
end

Expand Down
1 change: 1 addition & 0 deletions lib/ruby_proxy_headers/net_http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ def connect_with_proxy_tunnel
end
if proxy_connect_request_headers&.any?
proxy_connect_request_headers.each do |k, v|
RubyProxyHeaders.validate_header!(k, v)
buf << "#{k}: #{v}\r\n"
end
end
Expand Down
1 change: 1 addition & 0 deletions ruby-proxy-headers.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ Gem::Specification.new do |s|
s.add_development_dependency 'httparty', '~> 0.24'
s.add_development_dependency 'mechanize', '~> 2.14'
s.add_development_dependency 'typhoeus', '~> 1.6'
s.add_development_dependency 'webmock', '~> 3.18'
end
12 changes: 8 additions & 4 deletions spec/connection_spec.rb
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
# frozen_string_literal: true

require 'spec_helper'
require 'ruby_proxy_headers/connection'

RSpec.describe RubyProxyHeaders::Connection do
describe '#initialize' do
it 'parses proxy URL string' do
pending 'RubyProxyHeaders.parse_proxy_url is not yet implemented'
connection = described_class.new('http://user:pass@proxy.example.com:8080')
expect(connection).to be_a(described_class)
end

it 'accepts proxy hash' do
connection = described_class.new(
host: 'proxy.example.com',
port: 8080,
user: 'user',
password: 'pass'
{ host: 'proxy.example.com', port: 8080, user: 'user', password: 'pass' }
)
expect(connection).to be_a(described_class)
end

it 'accepts proxy headers option' do
pending 'RubyProxyHeaders.parse_proxy_url is not yet implemented'
connection = described_class.new(
'http://proxy:8080',
proxy_headers: { 'X-Custom' => 'value' }
Expand All @@ -30,6 +30,7 @@

describe '.parse_proxy_url' do
it 'parses complete URL' do
pending 'RubyProxyHeaders.parse_proxy_url is not yet implemented'
result = RubyProxyHeaders.parse_proxy_url('http://user:pass@proxy.example.com:8080')

expect(result[:host]).to eq('proxy.example.com')
Expand All @@ -40,6 +41,7 @@
end

it 'handles URL without auth' do
pending 'RubyProxyHeaders.parse_proxy_url is not yet implemented'
result = RubyProxyHeaders.parse_proxy_url('http://proxy.example.com:8080')

expect(result[:host]).to eq('proxy.example.com')
Expand All @@ -49,6 +51,7 @@
end

it 'defaults port to 8080' do
pending 'RubyProxyHeaders.parse_proxy_url is not yet implemented'
result = RubyProxyHeaders.parse_proxy_url('http://proxy.example.com')

expect(result[:port]).to eq(80)
Expand All @@ -57,6 +60,7 @@

describe '.build_auth_header' do
it 'builds Base64 encoded auth header' do
pending 'RubyProxyHeaders.build_auth_header is not yet implemented'
result = RubyProxyHeaders.build_auth_header('user', 'pass')

expect(result).to start_with('Basic ')
Expand Down
48 changes: 48 additions & 0 deletions spec/validate_header_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe 'RubyProxyHeaders.validate_header!' do
it 'accepts clean header name and value' do
expect { RubyProxyHeaders.validate_header!('X-Custom', 'safe-value') }.not_to raise_error
end

it 'rejects header name containing CR' do
expect { RubyProxyHeaders.validate_header!("Bad\rName", 'value') }
.to raise_error(ArgumentError, /header name.*invalid/)
end

it 'rejects header name containing LF' do
expect { RubyProxyHeaders.validate_header!("Bad\nName", 'value') }
.to raise_error(ArgumentError, /header name.*invalid/)
end

it 'rejects header name containing NUL' do
expect { RubyProxyHeaders.validate_header!("Bad\0Name", 'value') }
.to raise_error(ArgumentError, /header name.*invalid/)
end

it 'rejects header value containing CRLF' do
expect { RubyProxyHeaders.validate_header!('X-Good', "value\r\nInjected: evil") }
.to raise_error(ArgumentError, /header value.*invalid/)
end

it 'rejects header value containing bare LF' do
expect { RubyProxyHeaders.validate_header!('X-Good', "value\nInjected: evil") }
.to raise_error(ArgumentError, /header value.*invalid/)
end

it 'rejects header value containing bare CR' do
expect { RubyProxyHeaders.validate_header!('X-Good', "value\rInjected: evil") }
.to raise_error(ArgumentError, /header value.*invalid/)
end

it 'rejects header value containing NUL' do
expect { RubyProxyHeaders.validate_header!('X-Good', "value\0evil") }
.to raise_error(ArgumentError, /header value.*invalid/)
end

it 'converts non-string name and value via to_s' do
expect { RubyProxyHeaders.validate_header!(:'X-Symbol', 42) }.not_to raise_error
end
end