Skip to content

Commit d16322a

Browse files
committed
Add support for Excon
1 parent 9052df1 commit d16322a

File tree

5 files changed

+292
-0
lines changed

5 files changed

+292
-0
lines changed

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,18 @@ Simply add this configuration to your Flexirest initializer in your app and it w
159159
Flexirest::Base.api_auth_credentials(@access_id, @secret_key)
160160
```
161161

162+
### Excon
163+
164+
ApiAuth can also sign all requests made with [Excon](https://github.com/excon/excon).
165+
166+
``` ruby
167+
require 'api_auth/middleware/excon'
168+
169+
Excon.defaults[:api_auth_access_id] = <access_id>
170+
Excon.defaults[:api_auth_secret_key] = <secret_key>
171+
Excon.defaults[:middlewares] << ApiAuth::Middleware::Excon
172+
```
173+
162174
## Server
163175

164176
ApiAuth provides some built in methods to help you generate API keys for your

lib/api_auth.rb

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
require 'api_auth/request_drivers/rack'
1313
require 'api_auth/request_drivers/httpi'
1414
require 'api_auth/request_drivers/faraday'
15+
require 'api_auth/request_drivers/excon'
1516

1617
require 'api_auth/headers'
1718
require 'api_auth/base'

lib/api_auth/middleware/excon.rb

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
module ApiAuth
2+
module Middleware # :nodoc:
3+
class Excon # :nodoc:
4+
def initialize(stack)
5+
@stack = stack
6+
end
7+
8+
def error_call(datum)
9+
@stack.error_call(datum)
10+
end
11+
12+
def request_call(datum)
13+
request = ExconRequestWrapper.new(datum, @stack.query_string(datum))
14+
ApiAuth.sign!(request, datum[:api_auth_access_id], datum[:api_auth_secret_key])
15+
16+
@stack.request_call(datum)
17+
end
18+
19+
def response_call(datum)
20+
@stack.response_call(datum)
21+
end
22+
end
23+
24+
class ExconRequestWrapper # :nodoc:
25+
attr_reader :datum, :query_string
26+
27+
def initialize(datum, query_string)
28+
@datum = datum
29+
@query_string = query_string
30+
end
31+
32+
def uri
33+
datum[:path] + query_string
34+
end
35+
36+
def method
37+
datum[:method]
38+
end
39+
40+
def headers
41+
datum[:headers]
42+
end
43+
44+
def body
45+
datum[:body]
46+
end
47+
48+
end
49+
end
50+
end

lib/api_auth/request_drivers/excon.rb

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
module ApiAuth
2+
module RequestDrivers # :nodoc:
3+
class ExconRequest # :nodoc:
4+
include ApiAuth::Helpers
5+
6+
def initialize(request)
7+
@request = request
8+
end
9+
10+
def set_auth_header(header)
11+
@request.headers['Authorization'] = header
12+
end
13+
14+
def calculated_md5
15+
md5_base64digest(@request.body || '')
16+
end
17+
18+
def populate_content_md5
19+
return unless @request.body
20+
@request.headers['Content-MD5'] = calculated_md5
21+
end
22+
23+
def md5_mismatch?
24+
if @request.body
25+
calculated_md5 != content_md5
26+
else
27+
false
28+
end
29+
end
30+
31+
def http_method
32+
@request.method
33+
end
34+
35+
def content_type
36+
find_header(%w[CONTENT-TYPE CONTENT_TYPE HTTP_CONTENT_TYPE])
37+
end
38+
39+
def content_md5
40+
find_header(%w[CONTENT-MD5 CONTENT_MD5])
41+
end
42+
43+
def original_uri
44+
find_header(%w[X-ORIGINAL-URI X_ORIGINAL_URI HTTP_X_ORIGINAL_URI])
45+
end
46+
47+
def request_uri
48+
@request.uri
49+
end
50+
51+
def set_date
52+
@request.headers['DATE'] = Time.now.utc.httpdate
53+
end
54+
55+
def timestamp
56+
find_header(%w[DATE HTTP_DATE])
57+
end
58+
59+
def authorization_header
60+
find_header %w[Authorization AUTHORIZATION HTTP_AUTHORIZATION]
61+
end
62+
63+
private
64+
65+
def find_header(keys)
66+
headers = capitalize_keys(@request.headers)
67+
keys.map { |key| headers[key] }.compact.first
68+
end
69+
end
70+
end
71+
end

spec/request_drivers/excon_spec.rb

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
require 'spec_helper'
2+
require 'api_auth/middleware/excon'
3+
4+
describe ApiAuth::RequestDrivers::ExconRequest do
5+
let(:timestamp) { Time.now.utc.httpdate }
6+
let(:body) { "hello\nworld" }
7+
let(:method) {'GET'}
8+
let(:headers) do
9+
{
10+
'Authorization' => 'APIAuth 1044:12345',
11+
'content-md5' => '1B2M2Y8AsgTpgAmY7PhCfg==',
12+
'content-type' => 'text/plain',
13+
'date' => timestamp
14+
}
15+
end
16+
17+
let(:request) do
18+
datum = { path: '/resource.xml',
19+
method: method,
20+
headers: headers,
21+
body: body }
22+
query_string = '?foo=bar&bar=foo'
23+
24+
ApiAuth::Middleware::ExconRequestWrapper.new(datum, query_string)
25+
end
26+
27+
subject(:driven_request) { described_class.new(request) }
28+
29+
describe 'getting headers correctly' do
30+
it 'gets the content_type' do
31+
expect(driven_request.content_type).to eq('text/plain')
32+
end
33+
34+
it 'gets the content_md5' do
35+
expect(driven_request.content_md5).to eq('1B2M2Y8AsgTpgAmY7PhCfg==')
36+
end
37+
38+
it 'gets the request_uri' do
39+
expect(driven_request.request_uri).to eq('/resource.xml?foo=bar&bar=foo')
40+
end
41+
42+
it 'gets the timestamp' do
43+
expect(driven_request.timestamp).to eq(timestamp)
44+
end
45+
46+
it 'gets the authorization_header' do
47+
expect(driven_request.authorization_header).to eq('APIAuth 1044:12345')
48+
end
49+
50+
describe '#calculated_md5' do
51+
it 'calculates md5 from the body' do
52+
expect(driven_request.calculated_md5).to eq('kZXQvrKoieG+Be1rsZVINw==')
53+
end
54+
55+
context 'no body' do
56+
let(:body) { nil }
57+
58+
it 'is treated as empty string' do
59+
expect(driven_request.calculated_md5).to eq('1B2M2Y8AsgTpgAmY7PhCfg==')
60+
end
61+
end
62+
end
63+
64+
describe 'http_method' do
65+
let(:method) { 'PUT' }
66+
67+
it 'is as passed' do
68+
expect(driven_request.http_method).to eq(method)
69+
end
70+
end
71+
end
72+
73+
describe 'setting headers correctly' do
74+
let(:headers) { { 'content-type' => 'text/plain'} }
75+
76+
describe '#populate_content_md5' do
77+
context 'when there is no content body' do
78+
let(:body) { nil }
79+
80+
it "doesn't populate content-md5" do
81+
driven_request.populate_content_md5
82+
expect(request.headers['Content-MD5']).to be_nil
83+
end
84+
end
85+
86+
context 'when there is a content body' do
87+
let(:body) { "hello\nworld" }
88+
89+
it 'populates content-md5' do
90+
driven_request.populate_content_md5
91+
expect(request.headers['Content-MD5']).to eq('kZXQvrKoieG+Be1rsZVINw==')
92+
end
93+
94+
it 'refreshes the cached headers' do
95+
driven_request.populate_content_md5
96+
expect(driven_request.content_md5).to eq('kZXQvrKoieG+Be1rsZVINw==')
97+
end
98+
end
99+
end
100+
101+
describe '#set_date' do
102+
before do
103+
allow(Time).to receive_message_chain(:now, :utc, :httpdate).and_return(timestamp)
104+
end
105+
106+
it 'sets the date header of the request' do
107+
driven_request.set_date
108+
expect(request.headers['DATE']).to eq(timestamp)
109+
end
110+
111+
it 'refreshes the cached headers' do
112+
driven_request.set_date
113+
expect(driven_request.timestamp).to eq(timestamp)
114+
end
115+
end
116+
117+
describe '#set_auth_header' do
118+
it 'sets the auth header' do
119+
driven_request.set_auth_header('APIAuth 1044:54321')
120+
expect(request.headers['Authorization']).to eq('APIAuth 1044:54321')
121+
end
122+
end
123+
end
124+
125+
describe 'md5_mismatch?' do
126+
context 'when there is no content body' do
127+
let(:body) { nil }
128+
129+
it 'is false' do
130+
expect(driven_request.md5_mismatch?).to be false
131+
end
132+
end
133+
134+
context 'when there is a content body' do
135+
let(:body) { "hello\nworld" }
136+
137+
context 'when calculated matches sent' do
138+
before do
139+
request.headers['Content-MD5'] = 'kZXQvrKoieG+Be1rsZVINw=='
140+
end
141+
142+
it 'is false' do
143+
expect(driven_request.md5_mismatch?).to be false
144+
end
145+
end
146+
147+
context "when calculated doesn't match sent" do
148+
before do
149+
request.headers['Content-MD5'] = '3'
150+
end
151+
152+
it 'is true' do
153+
expect(driven_request.md5_mismatch?).to be true
154+
end
155+
end
156+
end
157+
end
158+
end

0 commit comments

Comments
 (0)