tent-client 0.0.1 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.travis.yml +1 -3
- data/Gemfile +1 -1
- data/LICENSE +27 -0
- data/README.md +8 -19
- data/lib/tent-client.rb +111 -51
- data/lib/tent-client/attachment.rb +13 -0
- data/lib/tent-client/cycle_http.rb +124 -14
- data/lib/tent-client/discovery.rb +59 -27
- data/lib/tent-client/faraday/chunked_adapter.rb +100 -0
- data/lib/tent-client/faraday/utils.rb +10 -0
- data/lib/tent-client/link_header.rb +5 -0
- data/lib/tent-client/middleware/authentication.rb +103 -0
- data/lib/tent-client/middleware/content_type_header.rb +24 -0
- data/lib/tent-client/middleware/encode_json.rb +5 -6
- data/lib/tent-client/multipart-post/parts.rb +100 -0
- data/lib/tent-client/post.rb +99 -38
- data/lib/tent-client/tent_type.rb +77 -0
- data/lib/tent-client/version.rb +1 -1
- data/spec/cycle_http_spec.rb +213 -0
- data/spec/discovery_spec.rb +114 -0
- data/spec/{unit/link_header_spec.rb → link_header_spec.rb} +10 -8
- data/spec/spec_helper.rb +7 -3
- data/spec/support/discovery_link_behaviour.rb +31 -0
- data/spec/timestamp_skew_spec.rb +126 -0
- data/tent-client.gemspec +11 -6
- metadata +75 -91
- data/.rspec +0 -1
- data/Guardfile +0 -6
- data/LICENSE.txt +0 -22
- data/lib/tent-client/app.rb +0 -33
- data/lib/tent-client/app_authorization.rb +0 -21
- data/lib/tent-client/follower.rb +0 -37
- data/lib/tent-client/following.rb +0 -31
- data/lib/tent-client/group.rb +0 -19
- data/lib/tent-client/middleware/accept_header.rb +0 -14
- data/lib/tent-client/middleware/mac_auth.rb +0 -50
- data/lib/tent-client/post_attachment.rb +0 -11
- data/lib/tent-client/profile.rb +0 -17
- data/spec/unit/cycle_http_spec.rb +0 -84
- data/spec/unit/discovery_spec.rb +0 -46
- data/spec/unit/middleware/mac_auth_spec.rb +0 -68
data/.rspec
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
--color --backtrace --format documentation
|
data/Guardfile
DELETED
data/LICENSE.txt
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
Copyright (c) 2012 Apollic Software, LLC
|
2
|
-
|
3
|
-
MIT License
|
4
|
-
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
-
a copy of this software and associated documentation files (the
|
7
|
-
"Software"), to deal in the Software without restriction, including
|
8
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
-
permit persons to whom the Software is furnished to do so, subject to
|
11
|
-
the following conditions:
|
12
|
-
|
13
|
-
The above copyright notice and this permission notice shall be
|
14
|
-
included in all copies or substantial portions of the Software.
|
15
|
-
|
16
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
-
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
-
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
-
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/lib/tent-client/app.rb
DELETED
@@ -1,33 +0,0 @@
|
|
1
|
-
class TentClient
|
2
|
-
class App
|
3
|
-
attr_accessor :client
|
4
|
-
|
5
|
-
def initialize(client)
|
6
|
-
@client = client
|
7
|
-
end
|
8
|
-
|
9
|
-
def create(data)
|
10
|
-
@client.http.post("apps", data)
|
11
|
-
end
|
12
|
-
|
13
|
-
def get(id)
|
14
|
-
@client.http.get("apps/#{id}")
|
15
|
-
end
|
16
|
-
|
17
|
-
def list(params = {})
|
18
|
-
@client.http.get("apps", params)
|
19
|
-
end
|
20
|
-
|
21
|
-
def delete(id)
|
22
|
-
@client.http.delete("apps/#{id}")
|
23
|
-
end
|
24
|
-
|
25
|
-
def update(id, data)
|
26
|
-
@client.http.put("apps/#{id}", data)
|
27
|
-
end
|
28
|
-
|
29
|
-
def authorization
|
30
|
-
AppAuthorization.new(@client)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
@@ -1,21 +0,0 @@
|
|
1
|
-
class TentClient
|
2
|
-
class AppAuthorization
|
3
|
-
attr_accessor :client
|
4
|
-
|
5
|
-
def initialize(client)
|
6
|
-
@client = client
|
7
|
-
end
|
8
|
-
|
9
|
-
def create(app_id, data)
|
10
|
-
@client.http.post("apps/#{app_id}/authorizations", data)
|
11
|
-
end
|
12
|
-
|
13
|
-
def update(app_id, id, data)
|
14
|
-
@client.http.put("apps/#{app_id}/authorizations/#{id}", data)
|
15
|
-
end
|
16
|
-
|
17
|
-
def delete(app_id, id)
|
18
|
-
@client.http.delete("apps/#{app_id}/authorizations/#{id}")
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
data/lib/tent-client/follower.rb
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
class TentClient
|
2
|
-
class Follower
|
3
|
-
def initialize(client)
|
4
|
-
@client = client
|
5
|
-
end
|
6
|
-
|
7
|
-
def create(data)
|
8
|
-
@client.http.post 'followers', data
|
9
|
-
end
|
10
|
-
|
11
|
-
def count(params={})
|
12
|
-
@client.http.get('followers/count', params)
|
13
|
-
end
|
14
|
-
|
15
|
-
def list(params = {})
|
16
|
-
@client.http.get "followers", params
|
17
|
-
end
|
18
|
-
|
19
|
-
def get(id)
|
20
|
-
@client.http.get "followers/#{id}"
|
21
|
-
end
|
22
|
-
|
23
|
-
def update(id, data)
|
24
|
-
@client.http.put "followers/#{id}", data
|
25
|
-
end
|
26
|
-
|
27
|
-
def delete(id)
|
28
|
-
@client.http.delete "followers/#{id}"
|
29
|
-
end
|
30
|
-
|
31
|
-
def challenge(path)
|
32
|
-
str = SecureRandom.hex(32)
|
33
|
-
res = @client.http.get(path.sub(%r{\A/}, ''), :challenge => str)
|
34
|
-
res.status == 200 && res.body.match(/\A#{str}/)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
@@ -1,31 +0,0 @@
|
|
1
|
-
class TentClient
|
2
|
-
class Following
|
3
|
-
def initialize(client)
|
4
|
-
@client = client
|
5
|
-
end
|
6
|
-
|
7
|
-
def count(params={})
|
8
|
-
@client.http.get('followings/count', params)
|
9
|
-
end
|
10
|
-
|
11
|
-
def list(params = {})
|
12
|
-
@client.http.get 'followings', params
|
13
|
-
end
|
14
|
-
|
15
|
-
def update(id, data)
|
16
|
-
@client.http.put "followings/#{id}", data
|
17
|
-
end
|
18
|
-
|
19
|
-
def create(entity_uri)
|
20
|
-
@client.http.post 'followings', :entity => entity_uri.sub(%r{/$}, '')
|
21
|
-
end
|
22
|
-
|
23
|
-
def get(id)
|
24
|
-
@client.http.get "followings/#{id}"
|
25
|
-
end
|
26
|
-
|
27
|
-
def delete(id)
|
28
|
-
@client.http.delete "followings/#{id}"
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
data/lib/tent-client/group.rb
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
class TentClient
|
2
|
-
class Group
|
3
|
-
def initialize(client)
|
4
|
-
@client = client
|
5
|
-
end
|
6
|
-
|
7
|
-
def count(params={})
|
8
|
-
@client.http.get('groups/count', params)
|
9
|
-
end
|
10
|
-
|
11
|
-
def list(params)
|
12
|
-
@client.http.get('/groups', params)
|
13
|
-
end
|
14
|
-
|
15
|
-
def create(data)
|
16
|
-
@client.http.post('/groups', data)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
@@ -1,50 +0,0 @@
|
|
1
|
-
require 'openssl'
|
2
|
-
require 'base64'
|
3
|
-
require 'securerandom'
|
4
|
-
|
5
|
-
class TentClient
|
6
|
-
module Middleware
|
7
|
-
class MacAuth
|
8
|
-
def initialize(app, options={})
|
9
|
-
@app, @mac_key_id, @mac_key, @mac_algorithm = app, options[:mac_key_id], options[:mac_key], options[:mac_algorithm]
|
10
|
-
end
|
11
|
-
|
12
|
-
def call(env)
|
13
|
-
sign_request(env) if auth_enabled?
|
14
|
-
@app.call(env)
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
18
|
-
|
19
|
-
def auth_enabled?
|
20
|
-
@mac_key_id && @mac_key && @mac_algorithm
|
21
|
-
end
|
22
|
-
|
23
|
-
def sign_request(env)
|
24
|
-
time = Time.now.to_i
|
25
|
-
nonce = SecureRandom.hex(3)
|
26
|
-
request_string = build_request_string(time, nonce, env)
|
27
|
-
signature = Base64.encode64(OpenSSL::HMAC.digest(openssl_digest.new, @mac_key, request_string)).sub("\n", '')
|
28
|
-
env[:request_headers]['Authorization'] = build_auth_header(time, nonce, signature)
|
29
|
-
end
|
30
|
-
|
31
|
-
def build_request_string(time, nonce, env)
|
32
|
-
if env[:body].respond_to?(:read)
|
33
|
-
body = env[:body].read
|
34
|
-
env[:body].rewind
|
35
|
-
else
|
36
|
-
body = env[:body]
|
37
|
-
end
|
38
|
-
[time.to_s, nonce, env[:method].to_s.upcase, env[:url].request_uri, env[:url].host, env[:url].port || env[:url].inferred_port, body, nil].join("\n")
|
39
|
-
end
|
40
|
-
|
41
|
-
def build_auth_header(time, nonce, signature)
|
42
|
-
%Q(MAC id="#{@mac_key_id}", ts="#{time}", nonce="#{nonce}", mac="#{signature}")
|
43
|
-
end
|
44
|
-
|
45
|
-
def openssl_digest
|
46
|
-
@openssl_digest ||= OpenSSL::Digest.const_get(@mac_algorithm.to_s.gsub(/hmac|-/, '').upcase)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
data/lib/tent-client/profile.rb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
class TentClient
|
2
|
-
class Profile
|
3
|
-
attr_accessor :client
|
4
|
-
|
5
|
-
def initialize(client)
|
6
|
-
@client = client
|
7
|
-
end
|
8
|
-
|
9
|
-
def update(type, data)
|
10
|
-
@client.http.put "profile/#{URI.encode_www_form_component(type)}", data
|
11
|
-
end
|
12
|
-
|
13
|
-
def get
|
14
|
-
@client.http.get 'profile'
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
@@ -1,84 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe TentClient::CycleHTTP do
|
4
|
-
|
5
|
-
let(:http_stubs) { Faraday::Adapter::Test::Stubs.new }
|
6
|
-
let(:client_options) { Hash.new }
|
7
|
-
let(:server_urls) { %w{ http://alex.example.org/tent http://alexsmith.example.com/tent http://smith.example.com/tent } }
|
8
|
-
let(:client) { TentClient.new(server_urls, client_options) }
|
9
|
-
|
10
|
-
def expect_server(env, url)
|
11
|
-
expect(env[:url].to_s).to match(url)
|
12
|
-
end
|
13
|
-
|
14
|
-
it 'should proxy http verbs to Faraday' do
|
15
|
-
http_stubs.get('/tent/foo/bar') { |env|
|
16
|
-
expect_server(env, server_urls.first)
|
17
|
-
[200, {}, '']
|
18
|
-
}
|
19
|
-
|
20
|
-
cycle_http = described_class.new(client) do |f|
|
21
|
-
f.adapter :test, http_stubs
|
22
|
-
end
|
23
|
-
|
24
|
-
%w{ head get put patch post delete options }.each { |verb|
|
25
|
-
expect(cycle_http).to respond_to(verb)
|
26
|
-
}
|
27
|
-
|
28
|
-
cycle_http.get('foo/bar')
|
29
|
-
|
30
|
-
http_stubs.verify_stubbed_calls
|
31
|
-
end
|
32
|
-
|
33
|
-
it 'should retry http with next server url' do
|
34
|
-
http_stubs.get('/tent/foo/bar') { |env|
|
35
|
-
expect_server(env, server_urls.first)
|
36
|
-
[500, {}, '']
|
37
|
-
}
|
38
|
-
|
39
|
-
http_stubs.get('/tent/foo/bar') { |env|
|
40
|
-
expect_server(env, server_urls[1])
|
41
|
-
[300, {}, '']
|
42
|
-
}
|
43
|
-
|
44
|
-
http_stubs.get('/tent/foo/bar') { |env|
|
45
|
-
expect_server(env, server_urls.last)
|
46
|
-
[200, {}, '']
|
47
|
-
}
|
48
|
-
|
49
|
-
cycle_http = described_class.new(client) do |f|
|
50
|
-
f.adapter :test, http_stubs
|
51
|
-
end
|
52
|
-
|
53
|
-
cycle_http.get('foo/bar')
|
54
|
-
|
55
|
-
http_stubs.verify_stubbed_calls
|
56
|
-
end
|
57
|
-
|
58
|
-
it 'should return response when on last server url' do
|
59
|
-
http_stubs.get('/tent/foo/bar') { |env|
|
60
|
-
expect_server(env, server_urls.first)
|
61
|
-
[500, {}, '']
|
62
|
-
}
|
63
|
-
|
64
|
-
http_stubs.get('/tent/foo/bar') { |env|
|
65
|
-
expect_server(env, server_urls[1])
|
66
|
-
[500, {}, '']
|
67
|
-
}
|
68
|
-
|
69
|
-
http_stubs.get('/tent/foo/bar') { |env|
|
70
|
-
expect_server(env, server_urls.last)
|
71
|
-
[300, {}, '']
|
72
|
-
}
|
73
|
-
|
74
|
-
http_stubs.get('/tent/foo/bar') { |env|
|
75
|
-
raise StandardError, 'expected stub not be called bus was'
|
76
|
-
}
|
77
|
-
|
78
|
-
cycle_http = described_class.new(client) do |f|
|
79
|
-
f.adapter :test, http_stubs
|
80
|
-
end
|
81
|
-
|
82
|
-
cycle_http.get('foo/bar')
|
83
|
-
end
|
84
|
-
end
|
data/spec/unit/discovery_spec.rb
DELETED
@@ -1,46 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe TentClient::Discovery do
|
4
|
-
LINK_HEADER = %(<https://example.com/tent/profile>; rel="%s") % TentClient::PROFILE_REL
|
5
|
-
LINK_TAG_HTML = %(<html><head><link href="https://example.com/tent/profile" rel="%s" /></head</html>) % TentClient::PROFILE_REL
|
6
|
-
TENT_PROFILE = %({"https://tent.io/types/info/core/v0.1.0":{"licenses":["http://creativecommons.org/licenses/by/3.0/"],"entity":"https://example.com","servers":["https://example.com/tent"]}})
|
7
|
-
|
8
|
-
let(:http_stubs) { Faraday::Adapter::Test::Stubs.new }
|
9
|
-
let(:client) { TentClient.new(nil, :faraday_adapter => [:test, http_stubs]) }
|
10
|
-
|
11
|
-
it 'should discover profile urls via a link header' do
|
12
|
-
http_stubs.head('/') { [200, { 'Link' => LINK_HEADER }, ''] }
|
13
|
-
|
14
|
-
discovery = described_class.new(client, 'http://example.com/')
|
15
|
-
discovery.perform.should eq(['https://example.com/tent/profile'])
|
16
|
-
end
|
17
|
-
|
18
|
-
it 'should discover profile urls via a link html tag' do
|
19
|
-
http_stubs.head('/') { [200, { 'Content-Type' => 'text/html' }, ''] }
|
20
|
-
http_stubs.get('/') { [200, { 'Content-Type' => 'text/html' }, LINK_TAG_HTML] }
|
21
|
-
|
22
|
-
discovery = described_class.new(client, 'http://example.com/')
|
23
|
-
discovery.perform.should eq(['https://example.com/tent/profile'])
|
24
|
-
end
|
25
|
-
|
26
|
-
it 'should work with relative urls' do
|
27
|
-
http_stubs.head('/') { [200, { 'Link' => LINK_HEADER.sub(%r{https://example.com}, '') }, ''] }
|
28
|
-
|
29
|
-
discovery = described_class.new(client, 'http://example.com/')
|
30
|
-
discovery.perform.should eq(['http://example.com/tent/profile'])
|
31
|
-
end
|
32
|
-
|
33
|
-
it 'should fetch a profile' do
|
34
|
-
http_stubs.head('/') { [200, { 'Link' => LINK_HEADER }, ''] }
|
35
|
-
http_stubs.get('/tent/profile') { [200, { 'Content-Type' => TentClient::MEDIA_TYPE }, TENT_PROFILE] }
|
36
|
-
discovery = described_class.new(client, 'http://example.com/')
|
37
|
-
discovery.perform
|
38
|
-
discovery.get_profile.should eq([Oj.load(TENT_PROFILE), "https://example.com/tent"])
|
39
|
-
end
|
40
|
-
|
41
|
-
it 'should delegate TentClient.discover' do
|
42
|
-
instance = mock(:perform => 1)
|
43
|
-
described_class.expects(:new).with(client, 'url').returns(instance)
|
44
|
-
client.discover('url').should eq(instance)
|
45
|
-
end
|
46
|
-
end
|
@@ -1,68 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
|
4
|
-
describe TentClient::Middleware::MacAuth do
|
5
|
-
def auth_header(env)
|
6
|
-
env[:request_headers]['Authorization']
|
7
|
-
end
|
8
|
-
|
9
|
-
def perform(path = '', method = :get, body = nil)
|
10
|
-
make_app.call({
|
11
|
-
:url => URI('http://example.com' + path),
|
12
|
-
:method => method,
|
13
|
-
:request_headers => {},
|
14
|
-
:body => body
|
15
|
-
})
|
16
|
-
end
|
17
|
-
|
18
|
-
def make_app
|
19
|
-
described_class.new(lambda{ |env| env }, options)
|
20
|
-
end
|
21
|
-
|
22
|
-
context 'not configured' do
|
23
|
-
let(:options) { {} }
|
24
|
-
|
25
|
-
it "doesn't add a header" do
|
26
|
-
auth_header(perform).should be_nil
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
context 'configured' do
|
31
|
-
let(:options) { { :mac_key_id => 'h480djs93hd8', :mac_key => '489dks293j39', :mac_algorithm => 'hmac-sha-1' } }
|
32
|
-
let(:expected_header) { 'MAC id="h480djs93hd8", ts="1336363200", nonce="dj83hs9s", mac="%s"' }
|
33
|
-
before do
|
34
|
-
Time.expects(:now).returns(stub(:to_i => 1336363200))
|
35
|
-
SecureRandom.expects(:hex).with(3).returns('dj83hs9s')
|
36
|
-
end
|
37
|
-
|
38
|
-
it 'signs a GET request with no body' do
|
39
|
-
auth_header(perform('/resource/1?b=1&a=2')).should eq(expected_header % '6T3zZzy2Emppni6bzL7kdRxUWL4=')
|
40
|
-
end
|
41
|
-
|
42
|
-
it 'signs POST request with body' do
|
43
|
-
auth_header(perform('/resource/1?b=1&a=2', :post, "asdf\nasdf")).should ==
|
44
|
-
expected_header % 'hqpo01mLJLSYDbxmfRgNMEw38Wg='
|
45
|
-
end
|
46
|
-
|
47
|
-
it 'signs POST request with a readable body' do
|
48
|
-
io = StringIO.new("asdf\nasdf")
|
49
|
-
auth_header(perform('/resource/1?b=1&a=2', :post, io)).should ==
|
50
|
-
expected_header % 'hqpo01mLJLSYDbxmfRgNMEw38Wg='
|
51
|
-
io.eof?.should be_false
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
context 'faraday middleware' do
|
56
|
-
let(:options) { { :mac_key_id => 'h480djs93hd8', :mac_key => '489dks293j39', :mac_algorithm => 'hmac-sha-1' } }
|
57
|
-
let(:http_stub) {
|
58
|
-
Faraday::Adapter::Test::Stubs.new do |s|
|
59
|
-
s.post('/') { [200, {}, ''] }
|
60
|
-
end
|
61
|
-
}
|
62
|
-
let(:client) { TentClient.new('http://example.com', options.merge(:faraday_adapter => [:test, http_stub])) }
|
63
|
-
|
64
|
-
it "should be part of the client middleware stack" do
|
65
|
-
client.http.post('/', :foo => 'bar').env[:request_headers]['Authorization'].should =~ /\AMAC/
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|