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
checksums.yaml
ADDED
@@ -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
|
data/.travis.yml
CHANGED
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 '
|
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://
|
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](
|
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
|
-
|
11
|
-
# Tent profile discovery
|
12
|
-
TentClient.new.discover("http://tent-user.example.org")
|
7
|
+
## Contributing
|
13
8
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
data/lib/tent-client.rb
CHANGED
@@ -1,81 +1,141 @@
|
|
1
1
|
require 'tent-client/version'
|
2
|
-
require 'oj'
|
3
2
|
require 'faraday'
|
4
|
-
require '
|
5
|
-
require '
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
@
|
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
|
36
|
-
@
|
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.
|
39
|
-
f.
|
40
|
-
f
|
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
|
50
|
-
@
|
51
|
-
@http = nil # reset Faraday connection
|
101
|
+
def faraday_adapter=(adapter)
|
102
|
+
@faraday_adapter = adapter
|
52
103
|
end
|
53
104
|
|
54
|
-
def
|
55
|
-
|
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
|
59
|
-
|
114
|
+
def post
|
115
|
+
Post.new(self)
|
60
116
|
end
|
61
117
|
|
62
|
-
def
|
63
|
-
|
118
|
+
def attachment
|
119
|
+
Attachment.new(self)
|
64
120
|
end
|
65
121
|
|
66
|
-
def
|
67
|
-
|
68
|
-
end
|
122
|
+
def oauth_redirect_uri(params = {})
|
123
|
+
uri = URI(primary_server['urls']['oauth_auth'])
|
69
124
|
|
70
|
-
|
71
|
-
|
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
|
-
|
75
|
-
Post.new(self)
|
128
|
+
uri
|
76
129
|
end
|
77
130
|
|
78
|
-
def
|
79
|
-
|
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, :
|
8
|
+
attr_reader :client, :servers
|
7
9
|
def initialize(client, &faraday_block)
|
8
10
|
@faraday_block = faraday_block
|
9
11
|
@client = client
|
10
|
-
|
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
|
-
@
|
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
|
36
|
+
@http ||= new_http
|
21
37
|
end
|
22
38
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|