lotus 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +6 -0
- data/.travis.yml +9 -0
- data/Gemfile +16 -0
- data/README.md +233 -0
- data/Rakefile +7 -0
- data/lib/lotus.rb +232 -0
- data/lib/lotus/activity.rb +134 -0
- data/lib/lotus/atom/account.rb +50 -0
- data/lib/lotus/atom/address.rb +56 -0
- data/lib/lotus/atom/author.rb +167 -0
- data/lib/lotus/atom/category.rb +41 -0
- data/lib/lotus/atom/entry.rb +159 -0
- data/lib/lotus/atom/feed.rb +174 -0
- data/lib/lotus/atom/generator.rb +40 -0
- data/lib/lotus/atom/link.rb +79 -0
- data/lib/lotus/atom/name.rb +57 -0
- data/lib/lotus/atom/organization.rb +62 -0
- data/lib/lotus/atom/portable_contacts.rb +117 -0
- data/lib/lotus/atom/source.rb +168 -0
- data/lib/lotus/atom/thread.rb +60 -0
- data/lib/lotus/author.rb +177 -0
- data/lib/lotus/category.rb +45 -0
- data/lib/lotus/crypto.rb +146 -0
- data/lib/lotus/feed.rb +190 -0
- data/lib/lotus/generator.rb +53 -0
- data/lib/lotus/identity.rb +59 -0
- data/lib/lotus/link.rb +56 -0
- data/lib/lotus/notification.rb +220 -0
- data/lib/lotus/publisher.rb +40 -0
- data/lib/lotus/subscription.rb +117 -0
- data/lib/lotus/version.rb +3 -0
- data/lotus.gemspec +27 -0
- data/spec/activity_spec.rb +84 -0
- data/spec/atom/feed_spec.rb +681 -0
- data/spec/author_spec.rb +150 -0
- data/spec/crypto_spec.rb +138 -0
- data/spec/feed_spec.rb +252 -0
- data/spec/helper.rb +8 -0
- data/spec/identity_spec.rb +67 -0
- data/spec/link_spec.rb +30 -0
- data/spec/notification_spec.rb +77 -0
- data/test/example_feed.atom +393 -0
- data/test/example_feed_empty_author.atom +336 -0
- data/test/example_feed_false_connected.atom +359 -0
- data/test/example_feed_link_without_href.atom +134 -0
- data/test/example_page.html +4 -0
- data/test/mime_type_bug_feed.atom +874 -0
- metadata +204 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
module Lotus
|
2
|
+
# The generator element identifies the agent used to generate the feed.
|
3
|
+
class Generator
|
4
|
+
# Holds the base URI for relative URIs contained in uri.
|
5
|
+
attr_reader :base
|
6
|
+
|
7
|
+
# Holds the language of the name, when it exists. The language
|
8
|
+
# should be specified as RFC 3066 as either 2 or 3 letter codes.
|
9
|
+
# For example: 'en' for English or more specifically 'en-us'
|
10
|
+
attr_reader :lang
|
11
|
+
|
12
|
+
# Holds the optional uri that SHOULD produce a representation that is
|
13
|
+
# relevant to the agent.
|
14
|
+
attr_reader :uri
|
15
|
+
|
16
|
+
# Holds the optional string identifying the version of the generating
|
17
|
+
# agent.
|
18
|
+
attr_reader :version
|
19
|
+
|
20
|
+
# Holds the string that provides a human-readable name that identifies
|
21
|
+
# the generating agent. The content of this field is language sensitive.
|
22
|
+
attr_reader :name
|
23
|
+
|
24
|
+
# Creates a representation of a generator.
|
25
|
+
#
|
26
|
+
# options:
|
27
|
+
# :base => Optional base URI for use with a relative URI in uri.
|
28
|
+
# :lang => Optional string identifying the language of the name field.
|
29
|
+
# :uri => Optional string identifying the URL that SHOULD produce
|
30
|
+
# a representation that is relevant to the agent.
|
31
|
+
# :version => Optional string indicating the version of the generating
|
32
|
+
# agent.
|
33
|
+
# :name => Optional name of the agent.
|
34
|
+
def initialize(options = {})
|
35
|
+
@base = options[:base]
|
36
|
+
@lang = options[:lang]
|
37
|
+
@uri = options[:uri]
|
38
|
+
@version = options[:version]
|
39
|
+
@name = options[:name]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Yields a hash that represents the generator.
|
43
|
+
def to_hash
|
44
|
+
{
|
45
|
+
:base => self.base,
|
46
|
+
:lang => self.lang,
|
47
|
+
:version => self.version,
|
48
|
+
:uri => self.uri,
|
49
|
+
:name => self.name
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Lotus
|
2
|
+
# Holds information about an Identity. This is more specific to identifying
|
3
|
+
# an Author than that structure. It is generally hosted in one place and
|
4
|
+
# not replicated. It holds identifying information that allow you to
|
5
|
+
# ensure verification of communication with the author.
|
6
|
+
class Identity
|
7
|
+
# Holds the public key for this identity.
|
8
|
+
attr_reader :public_key
|
9
|
+
|
10
|
+
# Holds the salmon endpoint used for direct notifications for this
|
11
|
+
# identity.
|
12
|
+
attr_reader :salmon_endpoint
|
13
|
+
|
14
|
+
# Holds the dialback endpoint used for capability transfer and
|
15
|
+
# authentication for this identity.
|
16
|
+
attr_reader :dialback_endpoint
|
17
|
+
|
18
|
+
# Holds the activity streams inbox endpoint for this identity.
|
19
|
+
attr_reader :activity_inbox_endpoint
|
20
|
+
|
21
|
+
# Holds the activity streams outbox endpoint for this identity.
|
22
|
+
attr_reader :activity_outbox_endpoint
|
23
|
+
|
24
|
+
# Holds the url to this identity's profile.
|
25
|
+
attr_reader :profile_page
|
26
|
+
|
27
|
+
# Create an instance of an Identity.
|
28
|
+
#
|
29
|
+
# options:
|
30
|
+
# :public_key => The identity's public key.
|
31
|
+
# :salmon_endpoint => The salmon endpoint for this identity.
|
32
|
+
# :dialback_endpoint => The dialback endpoint for this identity.
|
33
|
+
# :activity_inbox_endpoint => The activity streams inbox for this
|
34
|
+
# identity.
|
35
|
+
# :activity_outbox_endpoint => The activity streams outbox for this
|
36
|
+
# identity.
|
37
|
+
# :profile_page => The url for this identity's profile page.
|
38
|
+
def initialize(options = {})
|
39
|
+
@public_key = options[:public_key]
|
40
|
+
@salmon_endpoint = options[:salmon_endpoint]
|
41
|
+
@dialback_endpoint = options[:dialback_endpoint]
|
42
|
+
@activity_inbox_endpoint = options[:activity_inbox_endpoint]
|
43
|
+
@activity_outbox_endpoint = options[:activity_outbox_endpoint]
|
44
|
+
@profile_page = options[:profile_page]
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns a hash of the properties of the identity.
|
48
|
+
def to_hash
|
49
|
+
{
|
50
|
+
:public_key => self.public_key,
|
51
|
+
:salmon_endpoint => self.salmon_endpoint,
|
52
|
+
:dialback_endpoint => self.dialback_endpoint,
|
53
|
+
:activity_inbox_endpoint => self.activity_inbox_endpoint,
|
54
|
+
:activity_outbox_endpoint => self.activity_outbox_endpoint,
|
55
|
+
:profile_page => self.profile_page
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/lotus/link.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
module Lotus
|
2
|
+
require 'atom'
|
3
|
+
|
4
|
+
class Link
|
5
|
+
# The URL for the related resource.
|
6
|
+
attr_reader :href
|
7
|
+
|
8
|
+
# A string indicating the relationship type with the current
|
9
|
+
# document.
|
10
|
+
#
|
11
|
+
# Standard:
|
12
|
+
# "alternate" = Signifies that the URL in href identifies an alternative
|
13
|
+
# version of the resource described by the containing
|
14
|
+
# element.
|
15
|
+
# "related" = Signifies that the URL in href identifies a resource that
|
16
|
+
# is related to the contained resource. For example, the
|
17
|
+
# feed for a site that discusses the performance of the
|
18
|
+
# search engine at "http://search.example.com" might
|
19
|
+
# contain, as a link of Feed, a related link to that
|
20
|
+
# "http://search.example.com".
|
21
|
+
# "self" = Signifies that href contains a URL to the containing
|
22
|
+
# resource.
|
23
|
+
# "enclosure" = Signifies that the URL in href identifies a related
|
24
|
+
# resource that is potentially large in size and
|
25
|
+
# require special handling. SHOULD use the length field.
|
26
|
+
# "via" = Signifies that the URL in href identifies a resource that
|
27
|
+
# is the source of the information provided in the
|
28
|
+
# containing element.
|
29
|
+
attr_reader :rel
|
30
|
+
|
31
|
+
# Advises to the content MIME type of the linked resource.
|
32
|
+
attr_reader :type
|
33
|
+
|
34
|
+
# Advises to the language of the linked resource. When used with
|
35
|
+
# rel="alternate" it depicts a translated version of the entry.
|
36
|
+
# Use with a RFC3066 language tag.
|
37
|
+
attr_reader :hreflang
|
38
|
+
|
39
|
+
# Conveys human-readable information about the linked resource.
|
40
|
+
attr_reader :title
|
41
|
+
|
42
|
+
# Advises the length of the linked content in number of bytes. It is simply
|
43
|
+
# a hint about the length based upon prior information. It may change, and
|
44
|
+
# cannot override the actual content length.
|
45
|
+
attr_reader :length
|
46
|
+
|
47
|
+
def initialize(options = {})
|
48
|
+
@href = options[:href]
|
49
|
+
@rel = options[:rel]
|
50
|
+
@type = options[:type]
|
51
|
+
@hreflang = options[:hreflang]
|
52
|
+
@title = options[:title]
|
53
|
+
@length = options[:length]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
module Lotus
|
2
|
+
# This represents a notification that can be sent to a server when you wish
|
3
|
+
# to send information to a server that has not yet subscribed to you. Since
|
4
|
+
# this implies a lack of trust, a notification adds a layer so that the
|
5
|
+
# recipiant can verify the message contents.
|
6
|
+
class Notification
|
7
|
+
require 'xml'
|
8
|
+
require 'digest/sha2'
|
9
|
+
|
10
|
+
attr_reader :activity
|
11
|
+
|
12
|
+
# Create an instance for a particular Lotus::Activity.
|
13
|
+
def initialize activity, signature = nil, plaintext = nil
|
14
|
+
@activity = activity
|
15
|
+
@signature = signature
|
16
|
+
@plaintext = plaintext
|
17
|
+
end
|
18
|
+
|
19
|
+
# Creates an activity for following a particular Author.
|
20
|
+
def self.from_follow(user_author, followed_author)
|
21
|
+
activity = Lotus::Activity.new(
|
22
|
+
:verb => :follow,
|
23
|
+
:object => followed_author,
|
24
|
+
:actor => user_author,
|
25
|
+
:title => "Now following #{followed_author.name}",
|
26
|
+
:content => "Now following #{followed_author.name}",
|
27
|
+
:content_type => "html"
|
28
|
+
)
|
29
|
+
|
30
|
+
self.new(activity)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Creates an activity for unfollowing a particular Author.
|
34
|
+
def self.from_unfollow(user_author, followed_author)
|
35
|
+
activity = Lotus::Activity.new(
|
36
|
+
:verb => "http://ostatus.org/schema/1.0/unfollow",
|
37
|
+
:object => followed_author,
|
38
|
+
:actor => user_author,
|
39
|
+
:title => "Stopped following #{followed_author.name}",
|
40
|
+
:content => "Stopped following #{followed_author.name}",
|
41
|
+
:content_type => "html"
|
42
|
+
)
|
43
|
+
|
44
|
+
self.new(activity)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Creates an activity for a profile update.
|
48
|
+
def self.from_profile_update(user_author)
|
49
|
+
activity = Lotus::Activity.new(
|
50
|
+
:verb => "http://ostatus.org/schema/1.0/update-profile",
|
51
|
+
:actor => user_author,
|
52
|
+
:title => "#{user_author.name} changed their profile information.",
|
53
|
+
:content => "#{user_author.name} changed their profile information.",
|
54
|
+
:content_type => "html"
|
55
|
+
)
|
56
|
+
|
57
|
+
self.new(activity)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Will pull a Lotus::Activity from the given payload and MIME type.
|
61
|
+
def self.from_data(content, content_type)
|
62
|
+
case content_type
|
63
|
+
when 'xml',
|
64
|
+
'magic-envelope+xml',
|
65
|
+
'application/xml',
|
66
|
+
'application/text+xml',
|
67
|
+
'application/magic-envelope+xml'
|
68
|
+
self.from_xml content
|
69
|
+
when 'json',
|
70
|
+
'magic-envelope+json',
|
71
|
+
'application/json',
|
72
|
+
'application/text+json',
|
73
|
+
'application/magic-envelope+json'
|
74
|
+
self.from_json content
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Will pull a Lotus::Activity from a magic envelope described by the JSON.
|
79
|
+
def self.from_json(source)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Will pull a Lotus::Activity from a magic envelope described by the XML.
|
83
|
+
def self.from_xml(source)
|
84
|
+
if source.is_a?(String)
|
85
|
+
if source.length == 0
|
86
|
+
return nil
|
87
|
+
end
|
88
|
+
|
89
|
+
source = XML::Document.string(source,
|
90
|
+
:options => XML::Parser::Options::NOENT)
|
91
|
+
else
|
92
|
+
return nil
|
93
|
+
end
|
94
|
+
|
95
|
+
# Retrieve the envelope
|
96
|
+
envelope = source.find('/me:env',
|
97
|
+
'me:http://salmon-protocol.org/ns/magic-env').first
|
98
|
+
|
99
|
+
return nil unless envelope
|
100
|
+
|
101
|
+
data = envelope.find('me:data',
|
102
|
+
'me:http://salmon-protocol.org/ns/magic-env').first
|
103
|
+
return nil unless data
|
104
|
+
|
105
|
+
data_type = data.attributes["type"]
|
106
|
+
if data_type.nil?
|
107
|
+
data_type = 'application/atom+xml'
|
108
|
+
armored_data_type = ''
|
109
|
+
else
|
110
|
+
armored_data_type = Base64::urlsafe_encode64(data_type)
|
111
|
+
end
|
112
|
+
|
113
|
+
encoding = envelope.find('me:encoding',
|
114
|
+
'me:http://salmon-protocol.org/ns/magic-env').first
|
115
|
+
|
116
|
+
algorithm = envelope.find(
|
117
|
+
'me:alg',
|
118
|
+
'me:http://salmon-protocol.org/ns/magic-env').first
|
119
|
+
|
120
|
+
signature = source.find('me:sig',
|
121
|
+
'me:http://salmon-protocol.org/ns/magic-env').first
|
122
|
+
|
123
|
+
# Parse fields
|
124
|
+
|
125
|
+
# Well, if we cannot verify, we don't accept
|
126
|
+
return nil unless signature
|
127
|
+
|
128
|
+
# XXX: Handle key_id attribute
|
129
|
+
signature = signature.content
|
130
|
+
signature = Base64::urlsafe_decode64(signature)
|
131
|
+
|
132
|
+
if encoding.nil?
|
133
|
+
# When the encoding is omitted, use base64url
|
134
|
+
# Cite: Magic Envelope Draft Spec Section 3.3
|
135
|
+
armored_encoding = ''
|
136
|
+
encoding = 'base64url'
|
137
|
+
else
|
138
|
+
armored_encoding = Base64::urlsafe_encode64(encoding.content)
|
139
|
+
encoding = encoding.content.downcase
|
140
|
+
end
|
141
|
+
|
142
|
+
if algorithm.nil?
|
143
|
+
# When algorithm is omitted, use 'RSA-SHA256'
|
144
|
+
# Cite: Magic Envelope Draft Spec Section 3.3
|
145
|
+
armored_algorithm = ''
|
146
|
+
algorithm = 'rsa-sha256'
|
147
|
+
else
|
148
|
+
armored_algorithm = Base64::urlsafe_encode64(algorithm.content)
|
149
|
+
algorithm = algorithm.content.downcase
|
150
|
+
end
|
151
|
+
|
152
|
+
# Retrieve and decode data payload
|
153
|
+
|
154
|
+
data = data.content
|
155
|
+
armored_data = data
|
156
|
+
|
157
|
+
case encoding
|
158
|
+
when 'base64url'
|
159
|
+
data = Base64::urlsafe_decode64(data)
|
160
|
+
else
|
161
|
+
# Unsupported data encoding
|
162
|
+
return nil
|
163
|
+
end
|
164
|
+
|
165
|
+
# Signature plaintext
|
166
|
+
plaintext = "#{armored_data}.#{armored_data_type}.#{armored_encoding}.#{armored_algorithm}"
|
167
|
+
|
168
|
+
# Interpret data payload
|
169
|
+
payload = XML::Reader.string(data)
|
170
|
+
self.new Lotus::Atom::Entry.new(payload).to_canonical, signature, plaintext
|
171
|
+
end
|
172
|
+
|
173
|
+
# Generate the xml for this notice and sign with the given private key.
|
174
|
+
def to_xml private_key
|
175
|
+
# Generate magic envelope
|
176
|
+
magic_envelope = XML::Document.new
|
177
|
+
|
178
|
+
magic_envelope.root = XML::Node.new 'env'
|
179
|
+
|
180
|
+
me_ns = XML::Namespace.new(magic_envelope.root,
|
181
|
+
'me', 'http://salmon-protocol.org/ns/magic-env')
|
182
|
+
|
183
|
+
magic_envelope.root.namespaces.namespace = me_ns
|
184
|
+
|
185
|
+
# Armored Data <me:data>
|
186
|
+
data = @activity.to_atom
|
187
|
+
@plaintext = data
|
188
|
+
data_armored = Base64::urlsafe_encode64(data)
|
189
|
+
elem = XML::Node.new 'data', data_armored, me_ns
|
190
|
+
elem.attributes['type'] = 'application/atom+xml'
|
191
|
+
data_type_armored = 'YXBwbGljYXRpb24vYXRvbSt4bWw='
|
192
|
+
magic_envelope.root << elem
|
193
|
+
|
194
|
+
# Encoding <me:encoding>
|
195
|
+
magic_envelope.root << XML::Node.new('encoding', 'base64url', me_ns)
|
196
|
+
encoding_armored = 'YmFzZTY0dXJs'
|
197
|
+
|
198
|
+
# Signing Algorithm <me:alg>
|
199
|
+
magic_envelope.root << XML::Node.new('alg', 'RSA-SHA256', me_ns)
|
200
|
+
algorithm_armored = 'UlNBLVNIQTI1Ng=='
|
201
|
+
|
202
|
+
# Signature <me:sig>
|
203
|
+
plaintext =
|
204
|
+
"#{data_armored}.#{data_type_armored}.#{encoding_armored}.#{algorithm_armored}"
|
205
|
+
|
206
|
+
# Assign @signature to the signature generated from the plaintext
|
207
|
+
@signature = Lotus::Crypto.emsa_sign(plaintext, private_key)
|
208
|
+
|
209
|
+
signature_armored = Base64::urlsafe_encode64(@signature)
|
210
|
+
magic_envelope.root << XML::Node.new('sig', signature_armored, me_ns)
|
211
|
+
|
212
|
+
magic_envelope.to_s :indent => true, :encoding => XML::Encoding::UTF_8
|
213
|
+
end
|
214
|
+
|
215
|
+
# Check the origin of this notification.
|
216
|
+
def verified? key
|
217
|
+
Lotus::Crypto.emsa_verify(@plaintext, @signature, key)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Lotus
|
2
|
+
class Publisher
|
3
|
+
require 'net/http'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
# The url of the feed.
|
7
|
+
attr_reader :url
|
8
|
+
|
9
|
+
# The array of feed urls used to push content. Default: []
|
10
|
+
attr_reader :hubs
|
11
|
+
|
12
|
+
# Creates a representation of a Publisher entity.
|
13
|
+
#
|
14
|
+
# options:
|
15
|
+
# :feed => A feed to use to populate the other fields.
|
16
|
+
# :url => The url of the feed that will be published.
|
17
|
+
# :hubs => An array of hub urls that are used to handle load
|
18
|
+
# balancing pushes of new data. Default: []
|
19
|
+
def initialize(options = {})
|
20
|
+
if options[:feed]
|
21
|
+
@url = options[:feed].url
|
22
|
+
@hubs = options[:feed].hubs
|
23
|
+
end
|
24
|
+
|
25
|
+
@url ||= options[:url]
|
26
|
+
@hubs ||= options[:hubs] || []
|
27
|
+
end
|
28
|
+
|
29
|
+
# Will ping PuSH hubs so that they know there is new/updated content. The
|
30
|
+
# hub should respond by pulling the new data and then sending it to
|
31
|
+
# subscribers.
|
32
|
+
def ping_hubs
|
33
|
+
@hubs.each do |hub_url|
|
34
|
+
res = Net::HTTP.post_form(URI.parse(hub_url),
|
35
|
+
{ 'hub.mode' => 'publish',
|
36
|
+
'hub.url' => @topic_url })
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Lotus
|
2
|
+
class Subscription
|
3
|
+
require 'net/http'
|
4
|
+
require 'uri'
|
5
|
+
require 'base64'
|
6
|
+
require 'hmac-sha1'
|
7
|
+
|
8
|
+
# The url that should be used to handle subscription handshakes.
|
9
|
+
attr_reader :callback_url
|
10
|
+
|
11
|
+
# The url of the feed one wishes to subscribe to.
|
12
|
+
attr_reader :topic_url
|
13
|
+
|
14
|
+
# The hub this subscription is made with.
|
15
|
+
attr_reader :hub
|
16
|
+
|
17
|
+
# Creates a representation of a subscription.
|
18
|
+
#
|
19
|
+
# options:
|
20
|
+
# :callback_url => The url that should be used to handle subscription
|
21
|
+
# handshakes.
|
22
|
+
# :topic_url => The url of the feed one wishes to subscribe to.
|
23
|
+
# :secret => A secret that will be passed to the callback to better
|
24
|
+
# verify that communication is not replayed. Default:
|
25
|
+
# A secure random hex.
|
26
|
+
# :hubs => A list of hubs to negotiate the subscription with.
|
27
|
+
# Default: attempts to discover the hubs when it
|
28
|
+
# subscribes for the first time.
|
29
|
+
# :hub => The hub we have a subscription with already.
|
30
|
+
def initialize(options = {})
|
31
|
+
@tokens = []
|
32
|
+
|
33
|
+
secret = options[:secret] || SecureRandom.hex(32)
|
34
|
+
@secret = secret.to_s
|
35
|
+
|
36
|
+
@callback_url = options[:callback_url]
|
37
|
+
@topic_url = options[:topic_url]
|
38
|
+
@tokens << options[:token] if options[:token]
|
39
|
+
|
40
|
+
@hubs = options[:hubs] || []
|
41
|
+
|
42
|
+
@hub = options[:hub]
|
43
|
+
end
|
44
|
+
|
45
|
+
# Actively searches for hubs by talking to publisher directly
|
46
|
+
def discover_hubs_for_topic
|
47
|
+
@hubs = Lotus.feed_from_url(self.topic_url).hubs
|
48
|
+
end
|
49
|
+
|
50
|
+
# Subscribe to the topic through the given hub.
|
51
|
+
def subscribe
|
52
|
+
return unless self.hub.nil?
|
53
|
+
|
54
|
+
# Discover hubs if none exist
|
55
|
+
@hubs = discover_hubs_for_topic(self.topic_url) if self.hubs.empty?
|
56
|
+
@hub = self.hubs.first
|
57
|
+
change_subscription(:subscribe, token)
|
58
|
+
|
59
|
+
# TODO: Check response, if failed, try a different hub
|
60
|
+
end
|
61
|
+
|
62
|
+
# Unsubscribe to the topic.
|
63
|
+
def unsubscribe
|
64
|
+
return if self.hub.nil?
|
65
|
+
|
66
|
+
change_subscription(:unsubscribe)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Change our subscription to this topic at a hub.
|
70
|
+
# mode: Either :subscribe or :unsubscribe
|
71
|
+
# hub_url: The url of the hub to negotiate with
|
72
|
+
# token: A token to verify the response from the hub.
|
73
|
+
def change_subscription(mode)
|
74
|
+
token ||= SecureRandom.hex(32)
|
75
|
+
@tokens << token.to_s
|
76
|
+
|
77
|
+
# TODO: Set up HTTPS foo
|
78
|
+
res = Net::HTTP.post_form(URI.parse(self.hub),
|
79
|
+
{
|
80
|
+
'hub.mode' => mode.to_s,
|
81
|
+
'hub.callback' => @callback_url,
|
82
|
+
'hub.verify' => 'async',
|
83
|
+
'hub.verify_token' => token,
|
84
|
+
'hub.lease_seconds' => '',
|
85
|
+
'hub.secret' => @secret,
|
86
|
+
'hub.topic' => @topic_url
|
87
|
+
})
|
88
|
+
end
|
89
|
+
|
90
|
+
# Verify that a subscription response is valid.
|
91
|
+
def verify_subscription(token)
|
92
|
+
# Is there a token?
|
93
|
+
result = @tokens.include?(token)
|
94
|
+
|
95
|
+
# Ensure we cannot reuse the token
|
96
|
+
@tokens.delete(token)
|
97
|
+
|
98
|
+
result
|
99
|
+
end
|
100
|
+
|
101
|
+
# Determines if the given body matches the signature.
|
102
|
+
def verify_content(body, signature)
|
103
|
+
hmac = HMAC::SHA1.hexdigest(@secret, body)
|
104
|
+
check = "sha1=" + hmac
|
105
|
+
check == signature
|
106
|
+
end
|
107
|
+
|
108
|
+
# Gives the content of a challenge response given the challenge
|
109
|
+
# body.
|
110
|
+
def challenge_response(challenge_code)
|
111
|
+
{
|
112
|
+
:body => challenge_code,
|
113
|
+
:status => 200
|
114
|
+
}
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|