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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +1 -3
  3. data/Gemfile +1 -1
  4. data/LICENSE +27 -0
  5. data/README.md +8 -19
  6. data/lib/tent-client.rb +111 -51
  7. data/lib/tent-client/attachment.rb +13 -0
  8. data/lib/tent-client/cycle_http.rb +124 -14
  9. data/lib/tent-client/discovery.rb +59 -27
  10. data/lib/tent-client/faraday/chunked_adapter.rb +100 -0
  11. data/lib/tent-client/faraday/utils.rb +10 -0
  12. data/lib/tent-client/link_header.rb +5 -0
  13. data/lib/tent-client/middleware/authentication.rb +103 -0
  14. data/lib/tent-client/middleware/content_type_header.rb +24 -0
  15. data/lib/tent-client/middleware/encode_json.rb +5 -6
  16. data/lib/tent-client/multipart-post/parts.rb +100 -0
  17. data/lib/tent-client/post.rb +99 -38
  18. data/lib/tent-client/tent_type.rb +77 -0
  19. data/lib/tent-client/version.rb +1 -1
  20. data/spec/cycle_http_spec.rb +213 -0
  21. data/spec/discovery_spec.rb +114 -0
  22. data/spec/{unit/link_header_spec.rb → link_header_spec.rb} +10 -8
  23. data/spec/spec_helper.rb +7 -3
  24. data/spec/support/discovery_link_behaviour.rb +31 -0
  25. data/spec/timestamp_skew_spec.rb +126 -0
  26. data/tent-client.gemspec +11 -6
  27. metadata +75 -91
  28. data/.rspec +0 -1
  29. data/Guardfile +0 -6
  30. data/LICENSE.txt +0 -22
  31. data/lib/tent-client/app.rb +0 -33
  32. data/lib/tent-client/app_authorization.rb +0 -21
  33. data/lib/tent-client/follower.rb +0 -37
  34. data/lib/tent-client/following.rb +0 -31
  35. data/lib/tent-client/group.rb +0 -19
  36. data/lib/tent-client/middleware/accept_header.rb +0 -14
  37. data/lib/tent-client/middleware/mac_auth.rb +0 -50
  38. data/lib/tent-client/post_attachment.rb +0 -11
  39. data/lib/tent-client/profile.rb +0 -17
  40. data/spec/unit/cycle_http_spec.rb +0 -84
  41. data/spec/unit/discovery_spec.rb +0 -46
  42. data/spec/unit/middleware/mac_auth_spec.rb +0 -68
data/.rspec DELETED
@@ -1 +0,0 @@
1
- --color --backtrace --format documentation
data/Guardfile DELETED
@@ -1,6 +0,0 @@
1
- guard 'rspec', :version => 2 do
2
- watch(%r{^spec/.+_spec\.rb$})
3
- watch(%r{^lib/tent-client/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" }
4
- watch(%r{^lib/[^/]+\.rb$}) { "spec" }
5
- watch(%r{spec/(spec_helper|support/).*\.rb}) { "spec" }
6
- end
@@ -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.
@@ -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
@@ -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
@@ -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,14 +0,0 @@
1
- class TentClient
2
- module Middleware
3
- class AcceptHeader
4
- def initialize(app)
5
- @app = app
6
- end
7
-
8
- def call(env)
9
- env[:request_headers]['Accept'] ||= MEDIA_TYPE
10
- @app.call(env)
11
- end
12
- end
13
- end
14
- 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
@@ -1,11 +0,0 @@
1
- class TentClient
2
- class PostAttachment
3
- def initialize(client)
4
- @client = client
5
- end
6
-
7
- def get(post_id, filename, type)
8
- @client.http.get("posts/#{post_id}/attachments/#{filename}", {}, { 'Accept' => type })
9
- end
10
- end
11
- end
@@ -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
@@ -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