Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ pkg
spec/examples.txt
tmp
Gemfile.lock
.idea/
Copy link
Contributor

@tycooon tycooon Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to put stuff like this in your global gitignore file, so that you don't have to add it in every project.

27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ protocol natively and outsources the parsing to native extensions.
- **Performance**: using native parsers and a clean, lightweight implementation,
http.rb achieves high performance while implementing HTTP in Ruby instead of C.

- **Proxy Support**: http.rb supports both HTTP and SOCKS5 proxies, with or without
authentication.


## Installation

Expand Down Expand Up @@ -104,6 +107,30 @@ and call `#readpartial` on it repeatedly until it returns `nil`:
=> nil
```

### Using Proxies

HTTP.rb supports both HTTP and SOCKS5 proxies, with or without authentication.

#### HTTP Proxy

```ruby
# Using an HTTP proxy without authentication
response = HTTP.via("proxy.example.com", 8080).get("https://github.com")

# Using an HTTP proxy with authentication
response = HTTP.via("proxy.example.com", 8080, "username", "password").get("https://github.com")
```

#### SOCKS5 Proxy

```ruby
# Using a SOCKS5 proxy without authentication
response = HTTP.via_socks5("proxy.example.com", 1080).get("https://github.com")

# Using a SOCKS5 proxy with authentication
response = HTTP.via_socks5("proxy.example.com", 1080, "username", "password").get("https://github.com")
```

## Supported Ruby Versions

This library aims to support and is [tested against][build-link]
Expand Down
11 changes: 11 additions & 0 deletions bin/console
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require "bundler/setup"
require "http"

# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.

require "irb"
IRB.start(__FILE__)
10 changes: 5 additions & 5 deletions http.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ Gem::Specification.new do |gem|

gem.required_ruby_version = ">= 3.0"

gem.add_runtime_dependency "addressable", "~> 2.8"
gem.add_runtime_dependency "http-cookie", "~> 1.0"
gem.add_runtime_dependency "http-form_data", "~> 2.2"
gem.add_dependency "addressable", "~> 2.8"
gem.add_dependency "http-cookie", "~> 1.0"
gem.add_dependency "http-form_data", "~> 2.2"

# Use native llhttp for MRI (more performant) and llhttp-ffi for other interpreters (better compatibility)
if RUBY_ENGINE == "ruby"
gem.add_runtime_dependency "llhttp", "~> 0.5.0"
gem.add_dependency "llhttp", "~> 0.5.0"
else
gem.add_runtime_dependency "llhttp-ffi", "~> 0.5.0"
gem.add_dependency "llhttp-ffi", "~> 0.5.0"
end

gem.metadata = {
Expand Down
32 changes: 30 additions & 2 deletions lib/http/chainable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,19 +156,47 @@ def persistent(host, timeout: 5)
# @param [Array] proxy
# @raise [Request::Error] if HTTP proxy is invalid
def via(*proxy)
proxy_hash = build_proxy_hash(*proxy, type: :http)

# Validate that we have at least an address and port
if !proxy_hash[:proxy_address] || !proxy_hash[:proxy_port]
raise(RequestError, "invalid HTTP proxy: must provide both address and port")
end

branch default_options.with_proxy(proxy_hash)
end

# Build a proxy hash from the given arguments
# @param [Array] proxy
# @param [Symbol] type The proxy type (:http or :socks5)
# @return [Hash] The proxy hash
def build_proxy_hash(*proxy, type:)
proxy_hash = {}
proxy_hash[:proxy_address] = proxy[0] if proxy[0].is_a?(String)
proxy_hash[:proxy_port] = proxy[1] if proxy[1].is_a?(Integer)
proxy_hash[:proxy_username] = proxy[2] if proxy[2].is_a?(String)
proxy_hash[:proxy_password] = proxy[3] if proxy[3].is_a?(String)
proxy_hash[:proxy_headers] = proxy[2] if proxy[2].is_a?(Hash)
proxy_hash[:proxy_headers] = proxy[4] if proxy[4].is_a?(Hash)
proxy_hash[:proxy_type] = type

raise(RequestError, "invalid HTTP proxy: #{proxy_hash}") unless (2..5).cover?(proxy_hash.keys.size)
proxy_hash
end
alias through via

# Make a request through a SOCKS5 proxy
# @param [Array] proxy
# @raise [Request::Error] if SOCKS5 proxy is invalid
def via_socks5(*proxy)
proxy_hash = build_proxy_hash(*proxy, type: :socks5)

# Validate that we have at least an address and port
if !proxy_hash[:proxy_address] || !proxy_hash[:proxy_port]
raise(RequestError, "invalid SOCKS5 proxy: must provide both address and port")
end

branch default_options.with_proxy(proxy_hash)
end
alias through via

# Make client follow redirects.
# @param options
Expand Down
23 changes: 22 additions & 1 deletion lib/http/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "forwardable"

require "http/headers"
require "http/socks5_proxy"

module HTTP
# A connection to the HTTP server
Expand Down Expand Up @@ -172,8 +173,17 @@ def start_tls(req, options)

# Open tunnel through proxy
def send_proxy_connect_request(req)
return unless req.uri.https? && req.using_proxy?
return unless req.using_proxy?

if req.using_socks5_proxy?
connect_via_socks5(req)
elsif req.uri.https? && req.using_http_proxy?
connect_via_http_proxy(req)
end
end

# Connect via HTTP proxy
def connect_via_http_proxy(req)
@pending_request = true

req.connect_using_proxy @socket
Expand All @@ -193,6 +203,17 @@ def send_proxy_connect_request(req)
@pending_response = false
end

# Connect via SOCKS5 proxy
def connect_via_socks5(req)
socks5_proxy = SOCKS5Proxy.new(@socket)
begin
socks5_proxy.connect(req)
rescue ConnectionError
@failed_proxy_connect = true
raise
end
end

# Resets expiration of persistent connection.
# @return [void]
def reset_timer
Expand Down
2 changes: 1 addition & 1 deletion lib/http/headers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def to_a
#
# @return [String]
def inspect
"#<#{self.class} #{to_h.inspect}>"
"#<#{self.class} #{to_h.to_json}>"
end

# Returns list of header names.
Expand Down
10 changes: 10 additions & 0 deletions lib/http/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,16 @@ def using_proxy?
proxy && proxy.keys.size >= 2
end

# Is this request using an HTTP proxy?
def using_http_proxy?
using_proxy? && (!proxy.key?(:proxy_type) || proxy[:proxy_type] == :http)
end

# Is this request using a SOCKS5 proxy?
def using_socks5_proxy?
using_proxy? && proxy[:proxy_type] == :socks5
end

# Is this request using an authenticated proxy?
def using_authenticated_proxy?
proxy && proxy.keys.size >= 4
Expand Down
8 changes: 2 additions & 6 deletions lib/http/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,7 @@ def content_length
value = @headers[Headers::CONTENT_LENGTH]
return nil unless value

begin
Integer(value)
rescue ArgumentError
nil
end
Integer(value, exception: false)
end

# Parsed Content-Type header
Expand Down Expand Up @@ -163,7 +159,7 @@ def parse(type = nil)

# Inspect a response
def inspect
"#<#{self.class}/#{@version} #{code} #{reason} #{headers.to_h.inspect}>"
"#<#{self.class}/#{@version} #{code} #{reason} #{headers.to_h.to_json}>"
end

private
Expand Down
Loading
Loading