Skip to content

Commit 7c115f1

Browse files
authored
Add typhoeus support (#219)
* Add typhoeus support * Document typhoeus * URI guard * Rubocop * fix spec * review feedback * Rubocop
1 parent f7e2b02 commit 7c115f1

File tree

9 files changed

+460
-0
lines changed

9 files changed

+460
-0
lines changed

.rubocop_todo.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ Lint/Void:
3838
Metrics/CyclomaticComplexity:
3939
Max: 16
4040

41+
# Offense count: 1
42+
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
43+
Metrics/ClassLength:
44+
Exclude:
45+
- 'lib/api_auth/request_drivers/typhoeus_request.rb'
46+
4147
# Offense count: 12
4248
Naming/AccessorMethodName:
4349
Exclude:
@@ -53,6 +59,7 @@ Naming/AccessorMethodName:
5359
- 'lib/api_auth/request_drivers/net_http.rb'
5460
- 'lib/api_auth/request_drivers/rack.rb'
5561
- 'lib/api_auth/request_drivers/rest_client.rb'
62+
- 'lib/api_auth/request_drivers/typhoeus_request.rb'
5663

5764
# Offense count: 3
5865
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
## New Features
1010

1111
- Add Excon HTTP client support with middleware (based on contribution by @stiak in PR #154)
12+
- Add Typhoeus HTTP client support (adapted from work by @liaden)
1213

1314
## Improvements
1415

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ added as a request driver.
9898
* **HTTPI** - Common interface for Ruby HTTP clients
9999
* **HTTP** (http.rb) - Fast Ruby HTTP client with a chainable API
100100
* **Excon** - Pure Ruby HTTP client for API interactions (with middleware support)
101+
* **Typhoeus** - Libcurl-powered client supporting hydra batching and streaming
101102
* **Grape** - REST-like API framework for Ruby (via Rack)
102103
* **Rack::Request** - Generic Rack request objects
103104

@@ -324,6 +325,29 @@ ApiAuth.sign!(request, @access_id, @secret_key)
324325
response = connection.request(request_params)
325326
```
326327
328+
#### Typhoeus
329+
330+
Typhoeus requests can be signed directly before being queued or run with Hydra:
331+
332+
```ruby
333+
require 'typhoeus'
334+
require 'api_auth'
335+
336+
request = Typhoeus::Request.new(
337+
'https://api.example.com/resource',
338+
method: :put,
339+
headers: { 'Content-Type' => 'application/json' },
340+
body: '{"key": "value"}'
341+
)
342+
343+
ApiAuth.sign!(request, @access_id, @secret_key)
344+
345+
# Run immediately or add to a Hydra queue
346+
response = request.run
347+
```
348+
349+
When uploading large files you can pass an IO or `File` object as the body. ApiAuth will buffer and rewind the stream while computing the SHA-256 content hash so the upload can continue uninterrupted.
350+
327351
### ActiveResource Clients
328352
329353
ApiAuth can transparently protect your ActiveResource communications with a

api_auth.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Gem::Specification.new do |s|
3434
s.add_development_dependency 'rexml'
3535
s.add_development_dependency 'rspec', '~> 3.13'
3636
s.add_development_dependency 'rubocop', '~> 1.50'
37+
s.add_development_dependency 'typhoeus', '~> 1.4'
3738

3839
s.files = `git ls-files`.split("\n")
3940
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }

lib/api_auth.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
require 'api_auth/request_drivers/faraday_env'
1818
require 'api_auth/request_drivers/http'
1919
require 'api_auth/request_drivers/excon'
20+
require 'api_auth/request_drivers/typhoeus_request'
2021

2122
require 'api_auth/headers'
2223
require 'api_auth/base'

lib/api_auth/headers.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class Headers
1717
[/Faraday::Request/, FaradayRequest],
1818
[/Faraday::Env/, FaradayEnv],
1919
[/HTTP::Request/, HttpRequest],
20+
[/Typhoeus::Request/, TyphoeusRequest],
2021
[/ApiAuth::Middleware::ExconRequestWrapper/, ExconRequest],
2122
[/Excon::Request/, ExconRequest]
2223
].freeze
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
module ApiAuth
2+
module RequestDrivers # :nodoc:
3+
class TyphoeusRequest # :nodoc:
4+
include ApiAuth::Helpers
5+
6+
def initialize(request)
7+
@request = request
8+
@headers = fetch_headers
9+
end
10+
11+
def set_auth_header(header)
12+
headers_hash['Authorization'] = header
13+
@headers = fetch_headers
14+
@request
15+
end
16+
17+
def calculated_hash
18+
sha256_base64digest(body)
19+
end
20+
21+
def populate_content_hash
22+
return unless %w[POST PUT].include?(http_method)
23+
24+
headers_hash['X-Authorization-Content-SHA256'] = calculated_hash
25+
@headers = fetch_headers
26+
end
27+
28+
def content_hash_mismatch?
29+
if %w[POST PUT].include?(http_method)
30+
calculated_hash != content_hash
31+
else
32+
false
33+
end
34+
end
35+
36+
def fetch_headers
37+
@headers = capitalize_keys(headers_hash)
38+
end
39+
40+
def http_method
41+
method = @request.options[:method]
42+
method&.to_s&.upcase
43+
end
44+
45+
def content_type
46+
find_header(%w[CONTENT-TYPE CONTENT_TYPE HTTP_CONTENT_TYPE])
47+
end
48+
49+
def content_hash
50+
find_header(%w[X-AUTHORIZATION-CONTENT-SHA256])
51+
end
52+
53+
def original_uri
54+
find_header(%w[X-ORIGINAL-URI X_ORIGINAL_URI HTTP_X_ORIGINAL_URI])
55+
end
56+
57+
def request_uri
58+
url = (@request.base_url || '').to_s
59+
return '/' if url.empty?
60+
61+
uri = URI.parse(url)
62+
merged_query = merge_query(uri.query, params_query)
63+
uri.query = merged_query unless merged_query.nil?
64+
65+
path = uri.request_uri
66+
path.nil? || path.empty? ? '/' : path
67+
rescue URI::InvalidURIError
68+
'/'
69+
end
70+
71+
def set_date
72+
headers_hash['DATE'] = Time.now.utc.httpdate
73+
@headers = fetch_headers
74+
end
75+
76+
def timestamp
77+
find_header(%w[DATE HTTP_DATE])
78+
end
79+
80+
def authorization_header
81+
find_header %w[Authorization AUTHORIZATION HTTP_AUTHORIZATION]
82+
end
83+
84+
private
85+
86+
def body
87+
encoded = @request.respond_to?(:encoded_body) ? @request.encoded_body : nil
88+
return '' if encoded.nil?
89+
return encoded unless encoded.empty?
90+
91+
source = @request.options[:body]
92+
return '' if source.nil?
93+
94+
if source.respond_to?(:read)
95+
contents = source.read
96+
source.rewind if source.respond_to?(:rewind)
97+
contents
98+
else
99+
source.to_s
100+
end
101+
end
102+
103+
def params_query
104+
params = @request.options[:params]
105+
return nil if params.nil?
106+
return params if params.is_a?(String)
107+
return nil if params.respond_to?(:empty?) && params.empty?
108+
109+
Typhoeus::Pool.with_easy do |easy|
110+
query = Ethon::Easy::Params.new(easy, params)
111+
if @request.options.key?(:params_encoding) && query.respond_to?(:params_encoding=)
112+
query.params_encoding = @request.options[:params_encoding]
113+
end
114+
query.escape = true
115+
query.to_s
116+
end
117+
end
118+
119+
def merge_query(existing, additional)
120+
segments = [existing, additional].compact.reject(&:empty?)
121+
segments.empty? ? nil : segments.join('&')
122+
end
123+
124+
def headers_hash
125+
@request.options[:headers] ||= {}
126+
end
127+
128+
def find_header(keys)
129+
keys.map { |key| @headers[key] }.compact.first
130+
end
131+
end
132+
end
133+
end

0 commit comments

Comments
 (0)