Skip to content

Commit

Permalink
Add support for Excon
Browse files Browse the repository at this point in the history
  • Loading branch information
stiak committed Sep 20, 2017
1 parent 9052df1 commit 32a978f
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 0 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,18 @@ Simply add this configuration to your Flexirest initializer in your app and it w
Flexirest::Base.api_auth_credentials(@access_id, @secret_key)
```

### Excon

ApiAuth can also sign all requests made with [Excon](https://github.com/excon/excon).

``` ruby
require 'api_auth/middleware/excon'

Excon.defaults[:api_auth_access_id] = <access_id>
Excon.defaults[:api_auth_secret_key] = <secret_key>
Excon.defaults[:middlewares] << ApiAuth::Middleware::Excon
```

## Server

ApiAuth provides some built in methods to help you generate API keys for your
Expand Down
1 change: 1 addition & 0 deletions lib/api_auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
require 'api_auth/request_drivers/rack'
require 'api_auth/request_drivers/httpi'
require 'api_auth/request_drivers/faraday'
require 'api_auth/request_drivers/excon'

require 'api_auth/headers'
require 'api_auth/base'
Expand Down
49 changes: 49 additions & 0 deletions lib/api_auth/middleware/excon.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module ApiAuth
module Middleware # :nodoc:
class Excon # :nodoc:
def initialize(stack)
@stack = stack
end

def error_call(datum)
@stack.error_call(datum)
end

def request_call(datum)
request = ExconRequestWrapper.new(datum, @stack.query_string(datum))
ApiAuth.sign!(request, datum[:api_auth_access_id], datum[:api_auth_secret_key])

@stack.request_call(datum)
end

def response_call(datum)
@stack.response_call(datum)
end
end

class ExconRequestWrapper # :nodoc:
attr_reader :datum, :query_string

def initialize(datum, query_string)
@datum = datum
@query_string = query_string
end

def uri
datum[:path] + query_string
end

def method
datum[:method]
end

def headers
datum[:headers]
end

def body
datum[:body]
end
end
end
end
71 changes: 71 additions & 0 deletions lib/api_auth/request_drivers/excon.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
module ApiAuth
module RequestDrivers # :nodoc:
class ExconRequest # :nodoc:
include ApiAuth::Helpers

def initialize(request)
@request = request
end

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

def calculated_md5
md5_base64digest(@request.body || '')
end

def populate_content_md5
return unless @request.body
@request.headers['Content-MD5'] = calculated_md5
end

def md5_mismatch?
if @request.body
calculated_md5 != content_md5
else
false
end
end

def http_method
@request.method
end

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

def content_md5
find_header(%w[CONTENT-MD5 CONTENT_MD5])
end

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

def request_uri
@request.uri
end

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

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

def authorization_header
find_header %w[Authorization AUTHORIZATION HTTP_AUTHORIZATION]
end

private

def find_header(keys)
headers = capitalize_keys(@request.headers)
keys.map { |key| headers[key] }.compact.first
end
end
end
end
158 changes: 158 additions & 0 deletions spec/request_drivers/excon_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
require 'spec_helper'
require 'api_auth/middleware/excon'

describe ApiAuth::RequestDrivers::ExconRequest do
let(:timestamp) { Time.now.utc.httpdate }
let(:body) { "hello\nworld" }
let(:method) { 'GET' }
let(:headers) do
{
'Authorization' => 'APIAuth 1044:12345',
'content-md5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
'content-type' => 'text/plain',
'date' => timestamp
}
end

let(:request) do
datum = { path: '/resource.xml',
method: method,
headers: headers,
body: body }
query_string = '?foo=bar&bar=foo'

ApiAuth::Middleware::ExconRequestWrapper.new(datum, query_string)
end

subject(:driven_request) { described_class.new(request) }

describe 'getting headers correctly' do
it 'gets the content_type' do
expect(driven_request.content_type).to eq('text/plain')
end

it 'gets the content_md5' do
expect(driven_request.content_md5).to eq('1B2M2Y8AsgTpgAmY7PhCfg==')
end

it 'gets the request_uri' do
expect(driven_request.request_uri).to eq('/resource.xml?foo=bar&bar=foo')
end

it 'gets the timestamp' do
expect(driven_request.timestamp).to eq(timestamp)
end

it 'gets the authorization_header' do
expect(driven_request.authorization_header).to eq('APIAuth 1044:12345')
end

describe '#calculated_md5' do
it 'calculates md5 from the body' do
expect(driven_request.calculated_md5).to eq('kZXQvrKoieG+Be1rsZVINw==')
end

context 'no body' do
let(:body) { nil }

it 'is treated as empty string' do
expect(driven_request.calculated_md5).to eq('1B2M2Y8AsgTpgAmY7PhCfg==')
end
end
end

describe 'http_method' do
let(:method) { 'PUT' }

it 'is as passed' do
expect(driven_request.http_method).to eq(method)
end
end
end

describe 'setting headers correctly' do
let(:headers) { { 'content-type' => 'text/plain' } }

describe '#populate_content_md5' do
context 'when there is no content body' do
let(:body) { nil }

it "doesn't populate content-md5" do
driven_request.populate_content_md5
expect(request.headers['Content-MD5']).to be_nil
end
end

context 'when there is a content body' do
let(:body) { "hello\nworld" }

it 'populates content-md5' do
driven_request.populate_content_md5
expect(request.headers['Content-MD5']).to eq('kZXQvrKoieG+Be1rsZVINw==')
end

it 'refreshes the cached headers' do
driven_request.populate_content_md5
expect(driven_request.content_md5).to eq('kZXQvrKoieG+Be1rsZVINw==')
end
end
end

describe '#set_date' do
before do
allow(Time).to receive_message_chain(:now, :utc, :httpdate).and_return(timestamp)
end

it 'sets the date header of the request' do
driven_request.set_date
expect(request.headers['DATE']).to eq(timestamp)
end

it 'refreshes the cached headers' do
driven_request.set_date
expect(driven_request.timestamp).to eq(timestamp)
end
end

describe '#set_auth_header' do
it 'sets the auth header' do
driven_request.set_auth_header('APIAuth 1044:54321')
expect(request.headers['Authorization']).to eq('APIAuth 1044:54321')
end
end
end

describe 'md5_mismatch?' do
context 'when there is no content body' do
let(:body) { nil }

it 'is false' do
expect(driven_request.md5_mismatch?).to be false
end
end

context 'when there is a content body' do
let(:body) { "hello\nworld" }

context 'when calculated matches sent' do
before do
request.headers['Content-MD5'] = 'kZXQvrKoieG+Be1rsZVINw=='
end

it 'is false' do
expect(driven_request.md5_mismatch?).to be false
end
end

context "when calculated doesn't match sent" do
before do
request.headers['Content-MD5'] = '3'
end

it 'is true' do
expect(driven_request.md5_mismatch?).to be true
end
end
end
end
end

0 comments on commit 32a978f

Please sign in to comment.