tent-client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color --backtrace --format documentation
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 1.9.2
5
+ - rbx-18mode
6
+ - rbx-19mode
7
+ - 1.8.7
8
+ - ree
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tent-client.gemspec
4
+ gemspec
5
+
6
+ gem 'faraday_middleware-multi_json', :git => 'git://github.com/jvatic/faraday_middleware-multi_json.git', :branch => :master
data/Guardfile ADDED
@@ -0,0 +1,6 @@
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
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
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/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # Tent Ruby Client ![Build Status](https://magnum.travis-ci.com/tent/tent-client-ruby.png?branch=master&token=YmxLSPgdpsxNUMWJpzRx)
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.
7
+
8
+ ## Usage
9
+
10
+ ```ruby
11
+ # Tent profile discovery
12
+ TentClient.new.discover("http://tent-user.example.org")
13
+
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.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler/setup'
2
+ require 'bundler/gem_tasks'
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec) do |spec|
6
+ spec.pattern = 'spec/**/*_spec.rb'
7
+ end
8
+ task :default => :spec
@@ -0,0 +1,81 @@
1
+ require 'tent-client/version'
2
+ require 'oj'
3
+ require 'faraday'
4
+ require 'faraday_middleware'
5
+ require 'faraday_middleware/multi_json'
6
+
7
+ 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)
31
+ @faraday_adapter = options.delete(:faraday_adapter)
32
+ @options = options
33
+ end
34
+
35
+ def http
36
+ @http ||= CycleHTTP.new(self) do |f|
37
+ 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
41
+ f.adapter *Array(faraday_adapter)
42
+ end
43
+ end
44
+
45
+ def faraday_adapter
46
+ @faraday_adapter || Faraday.default_adapter
47
+ end
48
+
49
+ def server_url=(v)
50
+ @server_urls = Array(v)
51
+ @http = nil # reset Faraday connection
52
+ end
53
+
54
+ def discover(url)
55
+ Discovery.new(self, url).tap { |d| d.perform }
56
+ end
57
+
58
+ def follower
59
+ Follower.new(self)
60
+ end
61
+
62
+ def following
63
+ Following.new(self)
64
+ end
65
+
66
+ def group
67
+ Group.new(self)
68
+ end
69
+
70
+ def app
71
+ App.new(self)
72
+ end
73
+
74
+ def post
75
+ Post.new(self)
76
+ end
77
+
78
+ def profile
79
+ Profile.new(self)
80
+ end
81
+ end
@@ -0,0 +1,33 @@
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
@@ -0,0 +1,21 @@
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
@@ -0,0 +1,49 @@
1
+ class TentClient
2
+
3
+ # Proxies to Faraday and cycles through server urls
4
+ # until either non left or response status in the 200s or 400s
5
+ class CycleHTTP
6
+ attr_reader :client, :server_urls
7
+ def initialize(client, &faraday_block)
8
+ @faraday_block = faraday_block
9
+ @client = client
10
+ @server_urls = client.server_urls.dup
11
+ end
12
+
13
+ def new_http
14
+ @http = Faraday.new(:url => server_urls.shift) do |f|
15
+ @faraday_block.call(f)
16
+ end
17
+ end
18
+
19
+ def http
20
+ @http || new_http
21
+ end
22
+
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)
33
+ end
34
+ end
35
+ end
36
+
37
+ def respond_to_missing?(method_name, include_private = false)
38
+ http.respond_to?(method_name, include_private)
39
+ end
40
+
41
+ def method_missing(method_name, *args, &block)
42
+ if http.respond_to?(method_name)
43
+ http.send(method_name, *args, &block)
44
+ else
45
+ super
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,56 @@
1
+ require 'nokogiri'
2
+
3
+ class TentClient
4
+ class Discovery
5
+ attr_accessor :url, :profile_urls, :primary_profile_url, :profile
6
+
7
+ def initialize(client, url)
8
+ @client, @url = client, url
9
+ end
10
+
11
+ def http
12
+ @http ||= Faraday.new do |f|
13
+ f.response :follow_redirects
14
+ f.adapter *Array(@client.faraday_adapter)
15
+ end
16
+ end
17
+
18
+ def perform
19
+ @profile_urls = perform_head_discovery || perform_get_discovery || []
20
+ @profile_urls.map! { |l| l =~ %r{\A/} ? URI.join(url, l).to_s : l }
21
+ end
22
+
23
+ def get_profile
24
+ profile_urls.each do |url|
25
+ res = @client.http.get(url)
26
+ if res['Content-Type'].split(';').first == MEDIA_TYPE
27
+ @profile = res.body
28
+ @primary_profile_url = url
29
+ break
30
+ end
31
+ end
32
+ [@profile, @primary_profile_url.to_s.sub(%r{/profile$}, '')]
33
+ end
34
+
35
+ def perform_head_discovery
36
+ perform_header_discovery http.head(url)
37
+ end
38
+
39
+ def perform_get_discovery
40
+ res = http.get(url)
41
+ perform_header_discovery(res) || perform_html_discovery(res)
42
+ end
43
+
44
+ def perform_header_discovery(res)
45
+ if header = res['Link']
46
+ links = LinkHeader.parse(header).links.select { |l| l[:rel] == PROFILE_REL }.map { |l| l.uri }
47
+ links unless links.empty?
48
+ end
49
+ end
50
+
51
+ def perform_html_discovery(res)
52
+ return unless res['Content-Type'] == 'text/html'
53
+ Nokogiri::HTML(res.body).css(%(link[rel="#{PROFILE_REL}"])).map { |l| l['href'] }
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,37 @@
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
@@ -0,0 +1,31 @@
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
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,72 @@
1
+ require 'uri'
2
+ require 'strscan'
3
+
4
+ class TentClient
5
+ class LinkHeader
6
+ attr_accessor :links
7
+
8
+ def self.parse(header)
9
+ new header.split(',').map { |l| Link.parse(l) }
10
+ end
11
+
12
+ def initialize(links)
13
+ @links = Array(links)
14
+ end
15
+
16
+ def to_s
17
+ links.map(&:to_s).join(', ')
18
+ end
19
+
20
+ class Link
21
+ attr_accessor :uri, :attributes
22
+
23
+ def self.parse(link_text)
24
+ s = StringScanner.new(link_text)
25
+ s.scan(/[^<]+/)
26
+ link = s.scan(/<[^\s]+>/)
27
+ link = link[1..-2]
28
+
29
+ s.scan(/[^a-z]+/)
30
+ attrs = {}
31
+ while attr = s.scan(/[a-z0-9*\-]+=/)
32
+ next if attr =~ /\*/
33
+ val = s.scan(/".+?"|[^\s";]+/).sub(/\A"/, '').sub(/"\Z/, '')
34
+ attrs[attr[0..-2]] = val
35
+ s.scan(/[^a-z]+/)
36
+ end
37
+ new(link, attrs)
38
+ end
39
+
40
+ def initialize(uri, attributes = {})
41
+ @uri = uri
42
+ @attributes = indifferent_hash(attributes)
43
+ end
44
+
45
+ def ==(other)
46
+ false unless is_a?(self.class)
47
+ uri == other.uri && attributes == other.attributes
48
+ end
49
+
50
+ def [](k)
51
+ attributes[k]
52
+ end
53
+
54
+ def []=(k, v)
55
+ attributes[k.to_s] = v.to_s
56
+ end
57
+
58
+ def to_s
59
+ attr_string = "; " + attributes.sort.map { |k,v| "#{k}=#{v.inspect}" }.join('; ') if attributes
60
+ "<#{uri}>#{attr_string}"
61
+ end
62
+
63
+ private
64
+
65
+ def indifferent_hash(old_hash)
66
+ new_hash = Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
67
+ old_hash.each { |k,v| new_hash[k.to_s] = v.to_s }
68
+ new_hash
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,14 @@
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
@@ -0,0 +1,48 @@
1
+ require 'faraday_middleware'
2
+
3
+ class TentClient
4
+ module Middleware
5
+ # FaradayMiddleware::EncodeJson with our media type
6
+ # https://github.com/pengwynn/faraday_middleware/blob/master/lib/faraday_middleware/request/encode_json.rb
7
+ class EncodeJson < Faraday::Middleware
8
+ CONTENT_TYPE = 'Content-Type'.freeze
9
+
10
+ dependency do
11
+ require 'oj' unless defined?(::Oj)
12
+ end
13
+
14
+ def call(env)
15
+ match_content_type(env) do |data|
16
+ env[:body] = encode data
17
+ end
18
+ @app.call env
19
+ end
20
+
21
+ def encode(data)
22
+ ::Oj.dump data
23
+ end
24
+
25
+ def match_content_type(env)
26
+ if process_request?(env)
27
+ env[:request_headers][CONTENT_TYPE] ||= MEDIA_TYPE
28
+ yield env[:body] unless env[:body].respond_to?(:to_str)
29
+ end
30
+ end
31
+
32
+ def process_request?(env)
33
+ type = request_type(env)
34
+ has_body?(env) and (type.empty? or type == MEDIA_TYPE)
35
+ end
36
+
37
+ def has_body?(env)
38
+ body = env[:body] and !(body.respond_to?(:to_str) and body.empty?)
39
+ end
40
+
41
+ def request_type(env)
42
+ type = env[:request_headers][CONTENT_TYPE].to_s
43
+ type = type.split(';', 2).first if type.index(';')
44
+ type
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,50 @@
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
@@ -0,0 +1,66 @@
1
+ class TentClient
2
+ class Post
3
+ MULTIPART_TYPE = 'multipart/form-data'.freeze
4
+ MULTIPART_BOUNDARY = "-----------TentAttachment".freeze
5
+
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ def count(params={})
11
+ @client.http.get('posts/count', params)
12
+ end
13
+
14
+ def list(params = {})
15
+ @client.http.get('posts', params)
16
+ end
17
+
18
+ def create(post, options={})
19
+ if options[:attachments]
20
+ multipart_post(post, options, options.delete(:attachments))
21
+ else
22
+ @client.http.post(options[:url] || 'posts', post)
23
+ end
24
+ end
25
+
26
+ def get(id)
27
+ @client.http.get("posts/#{id}")
28
+ end
29
+
30
+ def delete(id)
31
+ @client.http.delete("posts/#{id}")
32
+ end
33
+
34
+ def attachment
35
+ PostAttachment.new(@client)
36
+ end
37
+
38
+ private
39
+
40
+ def multipart_post(post, options, attachments)
41
+ post_body = { :category => 'post', :filename => 'post.json', :type => MEDIA_TYPE, :data => post.to_json }
42
+ body = multipart_body(attachments.unshift(post_body))
43
+ @client.http.post(options[:url] || 'posts', body, 'Content-Type' => "#{MULTIPART_TYPE};boundary=#{MULTIPART_BOUNDARY}")
44
+ end
45
+
46
+ def multipart_body(attachments)
47
+ parts = attachments.inject({}) { |h,a|
48
+ (h[a[:category]] ||= []) << a; h
49
+ }.inject([]) { |a,(category,attachments)|
50
+ if attachments.size > 1
51
+ a += attachments.each_with_index.map { |attachment,i|
52
+ Faraday::Parts::FilePart.new(MULTIPART_BOUNDARY, "#{category}[#{i}]", attachment_io(attachment))
53
+ }
54
+ else
55
+ a << Faraday::Parts::FilePart.new(MULTIPART_BOUNDARY, category, attachment_io(attachments.first))
56
+ end
57
+ a
58
+ } << Faraday::Parts::EpiloguePart.new(MULTIPART_BOUNDARY)
59
+ Faraday::CompositeReadIO.new(parts)
60
+ end
61
+
62
+ def attachment_io(attachment)
63
+ Faraday::UploadIO.new(attachment[:file] || StringIO.new(attachment[:data]), attachment[:type], attachment[:filename])
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,17 @@
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
@@ -0,0 +1,3 @@
1
+ class TentClient
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,13 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+
4
+ require 'bundler/setup'
5
+ require 'mocha_standalone'
6
+ require 'tent-client'
7
+ require 'oj'
8
+
9
+ Dir["#{File.dirname(__FILE__)}/support/*.rb"].each { |f| require f }
10
+
11
+ RSpec.configure do |config|
12
+ config.mock_with :mocha
13
+ end
@@ -0,0 +1,84 @@
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
@@ -0,0 +1,46 @@
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
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe TentClient::LinkHeader do
4
+ def new_link(link, attrs={})
5
+ TentClient::LinkHeader::Link.new(link, attrs)
6
+ end
7
+ it 'should parse a simple link' do
8
+ link_header = %Q(<http://example.com/TheBook/chapter2>; rel="previous";\n title="previous chapter")
9
+ expected_link = new_link('http://example.com/TheBook/chapter2', :rel => 'previous', :title => 'previous chapter')
10
+ described_class.parse(link_header).links.first.should eq(expected_link)
11
+ end
12
+
13
+ it 'should ignore utf-8 attributes' do
14
+ link_header = %Q(</TheBook/chapter2>;\n rel="previous"; title*=UTF-8'de'letztes%20Kapitel,\n </TheBook/chapter4>;\n rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel)
15
+ expected_links = [new_link('/TheBook/chapter2', :rel => 'previous'), new_link('/TheBook/chapter4', :rel => 'next')]
16
+ described_class.parse(link_header).links.should eq(expected_links)
17
+ end
18
+
19
+ it 'should convert a link header to a string' do
20
+ expected_header = %Q(<https://example.com/tent/profile>; rel="profile"; type="application/vnd.tent.profile+json")
21
+ link = new_link('https://example.com/tent/profile', :rel => 'profile', :type => 'application/vnd.tent.profile+json')
22
+ described_class.new(link).to_s.should eq(expected_header)
23
+ end
24
+ end
@@ -0,0 +1,68 @@
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
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'tent-client/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "tent-client"
8
+ gem.version = TentClient::VERSION
9
+ gem.authors = ["Jonathan Rudenberg", "Jesse Stuart"]
10
+ gem.email = ["jonathan@titanous.com", "jessestuart@gmail.com"]
11
+ gem.description = %q{Tent Protocol client}
12
+ gem.summary = %q{Tent Protocol client}
13
+ gem.homepage = "http://tent.io"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_runtime_dependency 'faraday', '0.8.4'
21
+ gem.add_runtime_dependency 'faraday_middleware', '0.8.8'
22
+ gem.add_runtime_dependency 'nokogiri'
23
+ gem.add_runtime_dependency 'oj'
24
+ gem.add_runtime_dependency 'faraday_middleware-multi_json'
25
+
26
+ gem.add_development_dependency 'rspec'
27
+ gem.add_development_dependency 'bundler'
28
+ gem.add_development_dependency 'rake'
29
+ gem.add_development_dependency 'guard-rspec'
30
+ gem.add_development_dependency 'mocha'
31
+ end
metadata ADDED
@@ -0,0 +1,248 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tent-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jonathan Rudenberg
9
+ - Jesse Stuart
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-09-20 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: faraday
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - '='
21
+ - !ruby/object:Gem::Version
22
+ version: 0.8.4
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - '='
29
+ - !ruby/object:Gem::Version
30
+ version: 0.8.4
31
+ - !ruby/object:Gem::Dependency
32
+ name: faraday_middleware
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - '='
37
+ - !ruby/object:Gem::Version
38
+ version: 0.8.8
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - '='
45
+ - !ruby/object:Gem::Version
46
+ version: 0.8.8
47
+ - !ruby/object:Gem::Dependency
48
+ name: nokogiri
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ - !ruby/object:Gem::Dependency
64
+ name: oj
65
+ requirement: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ type: :runtime
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ - !ruby/object:Gem::Dependency
80
+ name: faraday_middleware-multi_json
81
+ requirement: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ type: :runtime
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ - !ruby/object:Gem::Dependency
96
+ name: rspec
97
+ requirement: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: bundler
113
+ requirement: !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ! '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ! '>='
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ - !ruby/object:Gem::Dependency
128
+ name: rake
129
+ requirement: !ruby/object:Gem::Requirement
130
+ none: false
131
+ requirements:
132
+ - - ! '>='
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ type: :development
136
+ prerelease: false
137
+ version_requirements: !ruby/object:Gem::Requirement
138
+ none: false
139
+ requirements:
140
+ - - ! '>='
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ - !ruby/object:Gem::Dependency
144
+ name: guard-rspec
145
+ requirement: !ruby/object:Gem::Requirement
146
+ none: false
147
+ requirements:
148
+ - - ! '>='
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ type: :development
152
+ prerelease: false
153
+ version_requirements: !ruby/object:Gem::Requirement
154
+ none: false
155
+ requirements:
156
+ - - ! '>='
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ - !ruby/object:Gem::Dependency
160
+ name: mocha
161
+ requirement: !ruby/object:Gem::Requirement
162
+ none: false
163
+ requirements:
164
+ - - ! '>='
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ type: :development
168
+ prerelease: false
169
+ version_requirements: !ruby/object:Gem::Requirement
170
+ none: false
171
+ requirements:
172
+ - - ! '>='
173
+ - !ruby/object:Gem::Version
174
+ version: '0'
175
+ description: Tent Protocol client
176
+ email:
177
+ - jonathan@titanous.com
178
+ - jessestuart@gmail.com
179
+ executables: []
180
+ extensions: []
181
+ extra_rdoc_files: []
182
+ files:
183
+ - .gitignore
184
+ - .rspec
185
+ - .travis.yml
186
+ - Gemfile
187
+ - Guardfile
188
+ - LICENSE.txt
189
+ - README.md
190
+ - Rakefile
191
+ - lib/tent-client.rb
192
+ - lib/tent-client/app.rb
193
+ - lib/tent-client/app_authorization.rb
194
+ - lib/tent-client/cycle_http.rb
195
+ - lib/tent-client/discovery.rb
196
+ - lib/tent-client/follower.rb
197
+ - lib/tent-client/following.rb
198
+ - lib/tent-client/group.rb
199
+ - lib/tent-client/link_header.rb
200
+ - lib/tent-client/middleware/accept_header.rb
201
+ - lib/tent-client/middleware/encode_json.rb
202
+ - lib/tent-client/middleware/mac_auth.rb
203
+ - lib/tent-client/post.rb
204
+ - lib/tent-client/post_attachment.rb
205
+ - lib/tent-client/profile.rb
206
+ - lib/tent-client/version.rb
207
+ - spec/spec_helper.rb
208
+ - spec/unit/cycle_http_spec.rb
209
+ - spec/unit/discovery_spec.rb
210
+ - spec/unit/link_header_spec.rb
211
+ - spec/unit/middleware/mac_auth_spec.rb
212
+ - tent-client.gemspec
213
+ homepage: http://tent.io
214
+ licenses: []
215
+ post_install_message:
216
+ rdoc_options: []
217
+ require_paths:
218
+ - lib
219
+ required_ruby_version: !ruby/object:Gem::Requirement
220
+ none: false
221
+ requirements:
222
+ - - ! '>='
223
+ - !ruby/object:Gem::Version
224
+ version: '0'
225
+ segments:
226
+ - 0
227
+ hash: -2628975409218371797
228
+ required_rubygems_version: !ruby/object:Gem::Requirement
229
+ none: false
230
+ requirements:
231
+ - - ! '>='
232
+ - !ruby/object:Gem::Version
233
+ version: '0'
234
+ segments:
235
+ - 0
236
+ hash: -2628975409218371797
237
+ requirements: []
238
+ rubyforge_project:
239
+ rubygems_version: 1.8.23
240
+ signing_key:
241
+ specification_version: 3
242
+ summary: Tent Protocol client
243
+ test_files:
244
+ - spec/spec_helper.rb
245
+ - spec/unit/cycle_http_spec.rb
246
+ - spec/unit/discovery_spec.rb
247
+ - spec/unit/link_header_spec.rb
248
+ - spec/unit/middleware/mac_auth_spec.rb