Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Naming/AccessorMethodName:
- 'lib/api_auth/request_drivers/net_http.rb'
- 'lib/api_auth/request_drivers/rack.rb'
- 'lib/api_auth/request_drivers/rest_client.rb'
- 'lib/api_auth/request_drivers/typhoeus_request.rb'

# Offense count: 3
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
## New Features

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

## Improvements

Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ added as a request driver.
* **HTTPI** - Common interface for Ruby HTTP clients
* **HTTP** (http.rb) - Fast Ruby HTTP client with a chainable API
* **Excon** - Pure Ruby HTTP client for API interactions (with middleware support)
* **Typhoeus** - Libcurl-powered client supporting hydra batching and streaming
* **Grape** - REST-like API framework for Ruby (via Rack)
* **Rack::Request** - Generic Rack request objects

Expand Down Expand Up @@ -324,6 +325,29 @@ ApiAuth.sign!(request, @access_id, @secret_key)
response = connection.request(request_params)
```

#### Typhoeus

Typhoeus requests can be signed directly before being queued or run with Hydra:

```ruby
require 'typhoeus'
require 'api_auth'

request = Typhoeus::Request.new(
'https://api.example.com/resource',
method: :put,
headers: { 'Content-Type' => 'application/json' },
body: '{"key": "value"}'
)

ApiAuth.sign!(request, @access_id, @secret_key)

# Run immediately or add to a Hydra queue
response = request.run
```

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.

### ActiveResource Clients

ApiAuth can transparently protect your ActiveResource communications with a
Expand Down
1 change: 1 addition & 0 deletions api_auth.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Gem::Specification.new do |s|
s.add_development_dependency 'rexml'
s.add_development_dependency 'rspec', '~> 3.13'
s.add_development_dependency 'rubocop', '~> 1.50'
s.add_development_dependency 'typhoeus', '~> 1.4'

s.files = `git ls-files`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
Expand Down
1 change: 1 addition & 0 deletions lib/api_auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
require 'api_auth/request_drivers/faraday_env'
require 'api_auth/request_drivers/http'
require 'api_auth/request_drivers/excon'
require 'api_auth/request_drivers/typhoeus_request'

require 'api_auth/headers'
require 'api_auth/base'
Expand Down
1 change: 1 addition & 0 deletions lib/api_auth/headers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Headers
[/Faraday::Request/, FaradayRequest],
[/Faraday::Env/, FaradayEnv],
[/HTTP::Request/, HttpRequest],
[/Typhoeus::Request/, TyphoeusRequest],
[/ApiAuth::Middleware::ExconRequestWrapper/, ExconRequest],
[/Excon::Request/, ExconRequest]
].freeze
Expand Down
110 changes: 110 additions & 0 deletions lib/api_auth/request_drivers/typhoeus_request.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
module ApiAuth
module RequestDrivers # :nodoc:
class TyphoeusRequest # :nodoc:
include ApiAuth::Helpers

def initialize(request)
@request = request
@headers = fetch_headers
end

def set_auth_header(header)
headers_hash['Authorization'] = header
@headers = fetch_headers
@request
end

def calculated_hash
sha256_base64digest(body)
end

def populate_content_hash
return unless %w[POST PUT].include?(http_method)

headers_hash['X-Authorization-Content-SHA256'] = calculated_hash
@headers = fetch_headers
end

def content_hash_mismatch?
if %w[POST PUT].include?(http_method)
calculated_hash != content_hash
else
false
end
end

def fetch_headers
@headers = capitalize_keys(headers_hash)
end

def http_method
method = @request.options[:method]
method&.to_s&.upcase
end

def content_type
find_header(%w[CONTENT-TYPE CONTENT_TYPE HTTP_CONTENT_TYPE])
end

def content_hash
find_header(%w[X-AUTHORIZATION-CONTENT-SHA256])
end

def original_uri
find_header(%w[X-ORIGINAL-URI X_ORIGINAL_URI HTTP_X_ORIGINAL_URI])
end

def request_uri
url = @request.url.to_s
return '/' if url.empty?

uri = URI.parse(url)
return uri.request_uri if uri.respond_to?(:request_uri)

url
rescue URI::InvalidURIError
'/'
end

def set_date
headers_hash['DATE'] = Time.now.utc.httpdate
@headers = fetch_headers
end

def timestamp
find_header(%w[DATE HTTP_DATE])
end

def authorization_header
find_header %w[Authorization AUTHORIZATION HTTP_AUTHORIZATION]
end

private

def body
encoded = @request.respond_to?(:encoded_body) ? @request.encoded_body : nil
return '' if encoded.nil?
return encoded unless encoded.empty?

source = @request.options[:body]
return '' if source.nil?

if source.respond_to?(:read)
contents = source.read
source.rewind if source.respond_to?(:rewind)
contents
else
source.to_s
end
end

def headers_hash
@request.options[:headers] ||= {}
end

def find_header(keys)
keys.map { |key| @headers[key] }.compact.first
end
end
end
end
Loading
Loading