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
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 684ae790259d6f77983e1e7564178cdc84783bdf
4
+ data.tar.gz: f0c8d69166831d9fc4fe705ac7a1726273bb9206
5
+ SHA512:
6
+ metadata.gz: fde0ae8fab7e514b187086ce8b2da941a56be9edae08014da3e4458752c2962bd3df11bf0bf834a639b454ef084fe3e4a57ddb99540a0f0f05222edf91d0b52f
7
+ data.tar.gz: b69ff67ec4a30e415d230bde232e91fbf05f11c36720998e65f002bb95b75b2367d0ba0f8b401b020232cb73d81dfe4c15f98aa2bf08e73c7958fcf7bd97dd52
@@ -1,8 +1,6 @@
1
1
  language: ruby
2
2
  rvm:
3
+ - 2.0.0
3
4
  - 1.9.3
4
5
  - 1.9.2
5
- - rbx-18mode
6
6
  - rbx-19mode
7
- - 1.8.7
8
- - ree
data/Gemfile CHANGED
@@ -3,4 +3,4 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in tent-client.gemspec
4
4
  gemspec
5
5
 
6
- gem 'faraday_middleware-multi_json', :git => 'git://github.com/jvatic/faraday_middleware-multi_json.git', :branch => :master
6
+ gem 'hawk-auth', :git => 'git://github.com/tent/hawk-ruby.git', :branch => 'master'
data/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2013 Apollic Software, LLC. All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are
5
+ met:
6
+
7
+ * Redistributions of source code must retain the above copyright
8
+ notice, this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above
10
+ copyright notice, this list of conditions and the following disclaimer
11
+ in the documentation and/or other materials provided with the
12
+ distribution.
13
+ * Neither the name of Apollic Software, LLC nor the names of its
14
+ contributors may be used to endorse or promote products derived from
15
+ this software without specific prior written permission.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md CHANGED
@@ -1,24 +1,13 @@
1
- # Tent Ruby Client ![Build Status](https://magnum.travis-ci.com/tent/tent-client-ruby.png?branch=master&token=YmxLSPgdpsxNUMWJpzRx)
1
+ # Tent Ruby Client [![Build Status](https://travis-ci.org/tent/tent-client-ruby.png?branch=master)](https://travis-ci.org/tent/tent-client-ruby)
2
2
 
3
- TentClient implements a [Tent Protocol](http://tent.io) client library in Ruby.
4
- It is incomplete, currently only the endpoints required by
5
- [tentd](https://github.com/tent/tentd) and
6
- [tentd-admin](https://github.com/tent/tentd-admin) have been implemented.
3
+ TentClient implements a [Tent Protocol](https://tent.io) client library in Ruby.
7
4
 
8
5
  ## Usage
9
6
 
10
- ```ruby
11
- # Tent profile discovery
12
- TentClient.new.discover("http://tent-user.example.org")
7
+ ## Contributing
13
8
 
14
- # Server communication
15
- client = TentClient.new('http://tent-user.example.org',
16
- :mac_key_id => 'be94a6bf',
17
- :mac_key => '974af035',
18
- :mac_algorithm => 'hmac-sha-256')
19
- client.following.create('http://another-tent.example.com')
20
- ```
21
-
22
- ## Contributions
23
-
24
- If you find missing endpoints/actions, please submit a pull request.
9
+ 1. Fork it
10
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
11
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
12
+ 4. Push to the branch (`git push origin my-new-feature`)
13
+ 5. Create new Pull Request
@@ -1,81 +1,141 @@
1
1
  require 'tent-client/version'
2
- require 'oj'
3
2
  require 'faraday'
4
- require 'faraday_middleware'
5
- require 'faraday_middleware/multi_json'
3
+ require 'tent-client/faraday/utils'
4
+ require 'tent-client/multipart-post/parts'
5
+ require 'tent-client/faraday/chunked_adapter'
6
+ require 'tent-client/tent_type'
7
+ require 'tent-client/middleware/content_type_header'
8
+ require 'tent-client/middleware/encode_json'
9
+ require 'tent-client/middleware/authentication'
10
+ require 'tent-client/cycle_http'
11
+ require 'tent-client/discovery'
12
+ require 'tent-client/post'
13
+ require 'tent-client/attachment'
14
+
15
+ ##
16
+ # Ruby 1.8.7 compatibility
17
+ require 'uri'
18
+ unless URI.respond_to?(:encode_www_form_component)
19
+ require 'addressable/uri'
20
+
21
+ URI.class_eval do
22
+ def self.encode_www_form_component(str)
23
+ Addressable::URI.encode_component(
24
+ str.to_s.gsub(/(\r\n|\n|\r)/, "\r\n"),
25
+ Addressable::URI::CharacterClasses::UNRESERVED
26
+ ).gsub("%20", "+")
27
+ end
28
+ end
29
+ end
6
30
 
7
31
  class TentClient
8
- autoload :Discovery, 'tent-client/discovery'
9
- autoload :LinkHeader, 'tent-client/link_header'
10
- autoload :Follower, 'tent-client/follower'
11
- autoload :Following, 'tent-client/following'
12
- autoload :Group, 'tent-client/group'
13
- autoload :Profile, 'tent-client/profile'
14
- autoload :App, 'tent-client/app'
15
- autoload :AppAuthorization, 'tent-client/app_authorization'
16
- autoload :Post, 'tent-client/post'
17
- autoload :PostAttachment, 'tent-client/post_attachment'
18
- autoload :CycleHTTP, 'tent-client/cycle_http'
19
-
20
- require 'tent-client/middleware/accept_header'
21
- require 'tent-client/middleware/mac_auth'
22
- require 'tent-client/middleware/encode_json'
23
-
24
- MEDIA_TYPE = 'application/vnd.tent.v0+json'.freeze
25
- PROFILE_REL = 'https://tent.io/rels/profile'.freeze
26
-
27
- attr_reader :faraday_adapter, :server_urls
28
-
29
- def initialize(server_urls = [], options={})
30
- @server_urls = Array(server_urls)
32
+ POST_MEDIA_TYPE = %(application/vnd.tent.post.v0+json).freeze
33
+ POST_CONTENT_TYPE = %(#{POST_MEDIA_TYPE}; type="%s").freeze
34
+ POST_MENTIONS_CONTENT_TYPE = %(application/vnd.tent.post-mentions.v0+json).freeze
35
+ POST_VERSIONS_CONTENT_TYPE = %(application/vnd.tent.post-versions.v0+json).freeze
36
+ POST_CHILDREN_CONTENT_TYPE = %(application/vnd.tent.post-children.v0+json).freeze
37
+ OAUTH_TOKEN_CONTENT_TYPE = %(application/vnd.tent.oauth.token.v0+json).freeze
38
+ MULTIPART_CONTENT_TYPE = 'multipart/form-data'.freeze
39
+ MULTIPART_BOUNDARY = "-----------TentPart".freeze
40
+
41
+ MalformedServerMeta = Class.new(StandardError)
42
+ ServerNotFound = Class.new(StandardError)
43
+
44
+ attr_reader :entity_uri, :options
45
+ attr_writer :faraday_adapter, :faraday_setup, :server_meta_post
46
+ attr_accessor :ts_skew
47
+ def initialize(entity_uri, options = {})
48
+ @server_meta_post = options.delete(:server_meta)
31
49
  @faraday_adapter = options.delete(:faraday_adapter)
32
- @options = options
50
+ @faraday_setup = options.delete(:faraday_setup)
51
+ @ts_skew = options.delete(:ts_skew)
52
+ @entity_uri, @options = entity_uri, options
33
53
  end
34
54
 
35
- def http
36
- @http ||= CycleHTTP.new(self) do |f|
55
+ def dup
56
+ self.class.new(@entity_uri, @options.merge(
57
+ :server_meta => @server_meta_post,
58
+ :faraday_adapter => @faraday_adapter,
59
+ :faraday_setup => @faraday_setup
60
+ ))
61
+ end
62
+
63
+ def server_meta
64
+ server_meta_post['content'] if server_meta_post
65
+ end
66
+
67
+ def server_meta_post
68
+ @server_meta_post ||= entity_uri ? Discovery.discover(self, entity_uri) : nil
69
+ end
70
+
71
+ def primary_server
72
+ server_meta['servers'].sort_by { |s| s['preference'] }.first
73
+ end
74
+
75
+ def new_http
76
+ authentication_options = {}
77
+ authentication_options[:ts_skew] = @ts_skew if @ts_skew
78
+ authentication_options[:ts_skew_retry_enabled] = @options.has_key?(:ts_skew_retry_enabled) ? @options[:ts_skew_retry_enabled] : true
79
+ authentication_options[:update_ts_skew] = proc do |skew|
80
+ @ts_skew = skew
81
+ end
82
+
83
+ @http = CycleHTTP.new(self) do |f|
84
+ f.use Middleware::ContentTypeHeader
37
85
  f.use Middleware::EncodeJson unless @options[:skip_serialization]
38
- f.response :multi_json, :content_type => /\bjson\Z/ unless @options[:skip_serialization]
39
- f.use Middleware::AcceptHeader
40
- f.use Middleware::MacAuth, @options
86
+ f.use Middleware::Authentication, @options[:credentials], authentication_options if @options[:credentials]
87
+ f.response :multi_json, :content_type => /\bjson\Z/ unless @options[:skip_serialization] || @options[:skip_response_serialization]
88
+ @faraday_setup.call(f) if @faraday_setup
41
89
  f.adapter *Array(faraday_adapter)
42
90
  end
43
91
  end
44
92
 
93
+ def http
94
+ @http || new_http
95
+ end
96
+
45
97
  def faraday_adapter
46
98
  @faraday_adapter || Faraday.default_adapter
47
99
  end
48
100
 
49
- def server_url=(v)
50
- @server_urls = Array(v)
51
- @http = nil # reset Faraday connection
101
+ def faraday_adapter=(adapter)
102
+ @faraday_adapter = adapter
52
103
  end
53
104
 
54
- def discover(url)
55
- Discovery.new(self, url).tap { |d| d.perform }
105
+ def hex_digest(data)
106
+ if data.kind_of?(IO)
107
+ _data = data.read
108
+ data.rewind
109
+ data = _data
110
+ end
111
+ Digest::SHA512.new.update(data).to_s[0...64]
56
112
  end
57
113
 
58
- def follower
59
- Follower.new(self)
114
+ def post
115
+ Post.new(self)
60
116
  end
61
117
 
62
- def following
63
- Following.new(self)
118
+ def attachment
119
+ Attachment.new(self)
64
120
  end
65
121
 
66
- def group
67
- Group.new(self)
68
- end
122
+ def oauth_redirect_uri(params = {})
123
+ uri = URI(primary_server['urls']['oauth_auth'])
69
124
 
70
- def app
71
- App.new(self)
72
- end
125
+ query = params.inject([]) { |m, (k,v)| m << "#{k}=#{URI.encode_www_form_component(v)}"; m }.join('&')
126
+ uri.query ? uri.query += "&#{query}" : uri.query = query
73
127
 
74
- def post
75
- Post.new(self)
128
+ uri
76
129
  end
77
130
 
78
- def profile
79
- Profile.new(self)
131
+ def oauth_token_exchange(data, &block)
132
+ new_block = proc do |request|
133
+ request.headers['Content-Type'] = OAUTH_TOKEN_CONTENT_TYPE
134
+ yield(request) if block_given?
135
+ end
136
+ http.post(:oauth_token, params = {}, {
137
+ :token_type => 'https://tent.io/oauth/hawk-token'
138
+ }.merge(data), &new_block)
80
139
  end
140
+
81
141
  end
@@ -0,0 +1,13 @@
1
+ class TentClient
2
+ class Attachment
3
+ attr_reader :client
4
+ def initialize(client)
5
+ @client = client.dup
6
+ @client.faraday_adapter = :net_http_stream
7
+ end
8
+
9
+ def get(entity, digest, params = {}, &block)
10
+ client.http.get(:attachment, { :entity => entity, :digest => digest }.merge(params), &block)
11
+ end
12
+ end
13
+ end
@@ -1,39 +1,112 @@
1
+ require 'yajl'
2
+
1
3
  class TentClient
2
4
 
3
5
  # Proxies to Faraday and cycles through server urls
4
6
  # until either non left or response status in the 200s or 400s
5
7
  class CycleHTTP
6
- attr_reader :client, :server_urls
8
+ attr_reader :client, :servers
7
9
  def initialize(client, &faraday_block)
8
10
  @faraday_block = faraday_block
9
11
  @client = client
10
- @server_urls = client.server_urls.dup
12
+
13
+ if client.entity_uri
14
+ unless (Hash === client.server_meta) && (Array === client.server_meta['servers'])
15
+ raise MalformedServerMeta.new("Server meta post for Entity(#{client.entity_uri.inspect}) is malformed: #{client.server_meta.inspect}")
16
+ end
17
+
18
+ @servers = client.server_meta['servers'].sort_by { |s| s['preference'] }
19
+ else
20
+ @servers = []
21
+ end
22
+ end
23
+
24
+ def current_server
25
+ @current_server || servers.first
11
26
  end
12
27
 
13
28
  def new_http
14
- @http = Faraday.new(:url => server_urls.shift) do |f|
29
+ @current_server = servers.shift
30
+ @http = Faraday.new do |f|
15
31
  @faraday_block.call(f)
16
32
  end
17
33
  end
18
34
 
19
35
  def http
20
- @http || new_http
36
+ @http ||= new_http
21
37
  end
22
38
 
23
- %w{ head get put post patch delete options }.each do |verb|
24
- define_method verb do |*args, &block|
25
- res = http.send(verb, *args, &block)
26
- return res unless server_urls.any?
27
- case res.status
28
- when 200...300, 400...500
29
- res
30
- else
31
- new_http
32
- send(verb, *args, &block)
39
+ def named_url(name, params = {})
40
+ unless (Hash === current_server) && (Hash === current_server['urls']) && (template = current_server['urls'][name.to_s])
41
+ raise ServerNotFound.new("Failed to match #{name.to_s.inspect} to a url for server: #{Yajl::Encoder.encode(current_server)}")
42
+ end
43
+
44
+ template.to_s.gsub(/\{([^\}]+)\}/) {
45
+ param = (params.delete($1) || params.delete($1.to_sym)).to_s
46
+ URI.encode_www_form_component(param)
47
+ }
48
+ end
49
+
50
+ %w( options get head delete ).map(&:to_sym).each do |verb|
51
+ class_eval(<<-RUBY
52
+ def #{verb}(url, params = {}, headers = {}, &block)
53
+ run_request(#{verb.inspect}, url, params, nil, headers, &block)
54
+ end
55
+ RUBY
56
+ )
57
+ end
58
+
59
+ %w( post put patch ).map(&:to_sym).each do |verb|
60
+ class_eval(<<-RUBY
61
+ def #{verb}(url, params = {}, body = nil, headers = {}, &block)
62
+ run_request(#{verb.inspect}, url, params, body, headers, &block)
33
63
  end
64
+ RUBY
65
+ )
66
+ end
67
+
68
+ def multipart_request(verb, url, params, parts, headers = {}, &block)
69
+ body = multipart_body(parts)
70
+ run_request(verb.to_sym, url, params, body, headers) do |request|
71
+ request.headers['Content-Type'] = "#{MULTIPART_CONTENT_TYPE}; boundary=#{MULTIPART_BOUNDARY}"
72
+ request.headers['Content-Length'] = body.length.to_s
73
+ yield(request) if block_given?
34
74
  end
35
75
  end
36
76
 
77
+ def run_request(verb, url, params, body, headers, &block)
78
+ args = [verb, url, params, body, headers]
79
+ if Symbol === url
80
+ name = url
81
+ url = named_url(url, params || {})
82
+ else
83
+ name = nil
84
+ end
85
+
86
+ res = http.run_request(verb, url, body, headers) do |request|
87
+ request.params.update(params) if params
88
+ yield request if block_given?
89
+ end
90
+
91
+ if name
92
+ res.env[:tent_server] = current_server
93
+ end
94
+
95
+ return res if servers.empty? || !name
96
+
97
+ case res.status
98
+ when 200...300, 400...500
99
+ res
100
+ else
101
+ new_http
102
+ run_request(*args, &block)
103
+ end
104
+ rescue Faraday::Error::TimeoutError, Faraday::Error::ConnectionFailed
105
+ raise if servers.empty?
106
+ new_http
107
+ run_request(*args, &block)
108
+ end
109
+
37
110
  def respond_to_missing?(method_name, include_private = false)
38
111
  http.respond_to?(method_name, include_private)
39
112
  end
@@ -45,5 +118,42 @@ class TentClient
45
118
  super
46
119
  end
47
120
  end
121
+
122
+ private
123
+
124
+ def multipart_body(parts)
125
+ # group by category
126
+ parts = parts.inject(Hash.new) do |memo, part|
127
+ category = part[:category] || part['category']
128
+ memo[category] ||= []
129
+ memo[category] << part
130
+ memo
131
+ end
132
+
133
+ # expend into request parts
134
+ parts = parts.inject(Array.new) do |memo, (category, category_parts)|
135
+ if category_parts.size > 1
136
+ memo.concat category_parts.each_with_index.map { |part, index|
137
+ headers = part[:headers] || part['headers']
138
+ Faraday::Parts::FilePart.new(MULTIPART_BOUNDARY, "#{category}[#{index}]", upload_io(part), headers)
139
+ }
140
+ else
141
+ part = category_parts.first
142
+ headers = part[:headers] || part['headers']
143
+ memo << Faraday::Parts::FilePart.new(MULTIPART_BOUNDARY, category, upload_io(part), :headers => headers)
144
+ end
145
+ end
146
+
147
+ parts << Faraday::Parts::EpiloguePart.new(MULTIPART_BOUNDARY)
148
+ Faraday::CompositeReadIO.new(parts)
149
+ end
150
+
151
+ def upload_io(part)
152
+ Faraday::UploadIO.new(
153
+ (part[:file] || part['file']) || StringIO.new(part[:data] || part['data']),
154
+ part[:content_type] || part['content-type'],
155
+ part[:filename] || part['filename']
156
+ )
157
+ end
48
158
  end
49
159
  end