lotus 0.0.12
Sign up to get free protection for your applications and to get access to all the features.
- 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
|