nelumba 0.0.13

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.
Files changed (95) hide show
  1. data/.gitignore +6 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +20 -0
  4. data/README.md +242 -0
  5. data/Rakefile +7 -0
  6. data/assets/lotus_logo_purple.png +0 -0
  7. data/assets/lotus_logo_purple.svg +262 -0
  8. data/lib/nelumba.rb +47 -0
  9. data/lib/nelumba/activity.rb +250 -0
  10. data/lib/nelumba/application.rb +11 -0
  11. data/lib/nelumba/article.rb +11 -0
  12. data/lib/nelumba/atom/account.rb +50 -0
  13. data/lib/nelumba/atom/address.rb +56 -0
  14. data/lib/nelumba/atom/author.rb +176 -0
  15. data/lib/nelumba/atom/category.rb +41 -0
  16. data/lib/nelumba/atom/comment.rb +96 -0
  17. data/lib/nelumba/atom/entry.rb +216 -0
  18. data/lib/nelumba/atom/feed.rb +198 -0
  19. data/lib/nelumba/atom/generator.rb +40 -0
  20. data/lib/nelumba/atom/link.rb +79 -0
  21. data/lib/nelumba/atom/name.rb +57 -0
  22. data/lib/nelumba/atom/organization.rb +62 -0
  23. data/lib/nelumba/atom/person.rb +179 -0
  24. data/lib/nelumba/atom/portable_contacts.rb +117 -0
  25. data/lib/nelumba/atom/source.rb +179 -0
  26. data/lib/nelumba/atom/thread.rb +60 -0
  27. data/lib/nelumba/audio.rb +39 -0
  28. data/lib/nelumba/badge.rb +11 -0
  29. data/lib/nelumba/binary.rb +52 -0
  30. data/lib/nelumba/bookmark.rb +30 -0
  31. data/lib/nelumba/category.rb +49 -0
  32. data/lib/nelumba/collection.rb +34 -0
  33. data/lib/nelumba/comment.rb +47 -0
  34. data/lib/nelumba/crypto.rb +144 -0
  35. data/lib/nelumba/device.rb +11 -0
  36. data/lib/nelumba/discover.rb +362 -0
  37. data/lib/nelumba/event.rb +57 -0
  38. data/lib/nelumba/feed.rb +173 -0
  39. data/lib/nelumba/file.rb +43 -0
  40. data/lib/nelumba/generator.rb +53 -0
  41. data/lib/nelumba/group.rb +11 -0
  42. data/lib/nelumba/identity.rb +63 -0
  43. data/lib/nelumba/image.rb +30 -0
  44. data/lib/nelumba/link.rb +56 -0
  45. data/lib/nelumba/note.rb +34 -0
  46. data/lib/nelumba/notification.rb +229 -0
  47. data/lib/nelumba/object.rb +251 -0
  48. data/lib/nelumba/person.rb +306 -0
  49. data/lib/nelumba/place.rb +34 -0
  50. data/lib/nelumba/product.rb +30 -0
  51. data/lib/nelumba/publisher.rb +44 -0
  52. data/lib/nelumba/question.rb +30 -0
  53. data/lib/nelumba/review.rb +30 -0
  54. data/lib/nelumba/service.rb +11 -0
  55. data/lib/nelumba/subscription.rb +117 -0
  56. data/lib/nelumba/version.rb +3 -0
  57. data/lib/nelumba/video.rb +43 -0
  58. data/nelumba.gemspec +28 -0
  59. data/spec/activity_spec.rb +116 -0
  60. data/spec/application_spec.rb +136 -0
  61. data/spec/article_spec.rb +136 -0
  62. data/spec/atom/comment_spec.rb +455 -0
  63. data/spec/atom/feed_spec.rb +684 -0
  64. data/spec/audio_spec.rb +164 -0
  65. data/spec/badge_spec.rb +136 -0
  66. data/spec/binary_spec.rb +218 -0
  67. data/spec/bookmark.rb +150 -0
  68. data/spec/collection_spec.rb +152 -0
  69. data/spec/comment_spec.rb +128 -0
  70. data/spec/crypto_spec.rb +126 -0
  71. data/spec/device_spec.rb +136 -0
  72. data/spec/event_spec.rb +239 -0
  73. data/spec/feed_spec.rb +252 -0
  74. data/spec/file_spec.rb +190 -0
  75. data/spec/group_spec.rb +136 -0
  76. data/spec/helper.rb +10 -0
  77. data/spec/identity_spec.rb +67 -0
  78. data/spec/image_spec.rb +150 -0
  79. data/spec/link_spec.rb +30 -0
  80. data/spec/note_spec.rb +163 -0
  81. data/spec/notification_spec.rb +89 -0
  82. data/spec/person_spec.rb +244 -0
  83. data/spec/place_spec.rb +162 -0
  84. data/spec/product_spec.rb +150 -0
  85. data/spec/question_spec.rb +156 -0
  86. data/spec/review_spec.rb +149 -0
  87. data/spec/service_spec.rb +136 -0
  88. data/spec/video_spec.rb +164 -0
  89. data/test/example_feed.atom +393 -0
  90. data/test/example_feed_empty_author.atom +336 -0
  91. data/test/example_feed_false_connected.atom +359 -0
  92. data/test/example_feed_link_without_href.atom +134 -0
  93. data/test/example_page.html +4 -0
  94. data/test/mime_type_bug_feed.atom +874 -0
  95. metadata +288 -0
@@ -0,0 +1,47 @@
1
+ module Nelumba
2
+ class Comment
3
+ include Nelumba::Object
4
+
5
+ # Holds a collection of Nelumba::Activity's that this comment is in reply to.
6
+ attr_reader :in_reply_to
7
+
8
+ # Create a new Comment activity object.
9
+ #
10
+ # options:
11
+ # :content => The body of the comment in HTML.
12
+ # :author => A Nelumba::Person that created the object.
13
+ # :display_name => A natural-language, human-readable and plain-text name
14
+ # for the comment.
15
+ # :summary => Natural-language summarization of the comment.
16
+ # :url => The canonical url of this comment.
17
+ # :uid => The unique id that identifies this comment.
18
+ # :image =>
19
+ # :published => The Time when this comment was originally published.
20
+ # :updated => The Time when this comment was last modified.
21
+ def initialize(options = {}, &blk)
22
+ init(options, &blk)
23
+ end
24
+
25
+ def init(options = {}, &blk)
26
+ @in_reply_to = options[:in_reply_to] || []
27
+
28
+ super options
29
+ end
30
+
31
+ # Returns a Hash representing this comment.
32
+ def to_hash
33
+ {
34
+ :in_reply_to => @in_reply_to
35
+ }.merge(super)
36
+ end
37
+
38
+ # Returns a Hash representing this comment with JSON ActivityStreams
39
+ # conventions.
40
+ def to_json_hash
41
+ {
42
+ :objectType => "comment",
43
+ :in_reply_to => @in_reply_to
44
+ }.merge(super)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,144 @@
1
+ module Nelumba
2
+ module Crypto
3
+ require 'openssl'
4
+ require 'rsa'
5
+ require 'base64'
6
+
7
+ KeyPair = Struct.new(:public_key, :private_key)
8
+
9
+ # Generate a new RSA keypair with the given bitlength.
10
+ def self.new_keypair(bits = 2048)
11
+ keypair = KeyPair.new
12
+
13
+ key = RSA::KeyPair.generate(bits)
14
+
15
+ public_key = key.public_key
16
+ m = public_key.modulus
17
+ e = public_key.exponent
18
+
19
+ modulus = ""
20
+ until m == 0 do
21
+ modulus << [m % 256].pack("C")
22
+ m >>= 8
23
+ end
24
+ modulus.reverse!
25
+
26
+ exponent = ""
27
+ until e == 0 do
28
+ exponent << [e % 256].pack("C")
29
+ e >>= 8
30
+ end
31
+ exponent.reverse!
32
+
33
+ keypair.public_key = "RSA.#{Base64::urlsafe_encode64(modulus)}.#{Base64::urlsafe_encode64(exponent)}"
34
+
35
+ tmp_private_key = key.private_key
36
+ m = tmp_private_key.modulus
37
+ e = tmp_private_key.exponent
38
+
39
+ modulus = ""
40
+ until m == 0 do
41
+ modulus << [m % 256].pack("C")
42
+ m >>= 8
43
+ end
44
+ modulus.reverse!
45
+
46
+ exponent = ""
47
+ until e == 0 do
48
+ exponent << [e % 256].pack("C")
49
+ e >>= 8
50
+ end
51
+ exponent.reverse!
52
+
53
+ keypair.private_key = "RSA.#{Base64::urlsafe_encode64(modulus)}.#{Base64::urlsafe_encode64(exponent)}"
54
+
55
+ keypair
56
+ end
57
+
58
+ # Creates an EMSA signature for the given plaintext and key.
59
+ def self.emsa_sign(text, private_key)
60
+ private_key = generate_key(private_key) unless private_key.is_a? RSA::Key
61
+ signature = self.emsa_signature(text, private_key)
62
+ self.decrypt(private_key, signature)
63
+ end
64
+
65
+ # Verifies an existing EMSA signature.
66
+ def self.emsa_verify(text, signature, public_key)
67
+ # RSA encryption is needed to compare the signatures
68
+ public_key = generate_key(public_key) unless public_key.is_a? RSA::Key
69
+
70
+ # Get signature to check
71
+ emsa = self.emsa_signature(text, public_key)
72
+
73
+ # Get signature in payload
74
+ emsa_signature = self.encrypt(public_key, signature)
75
+
76
+ # RSA gem drops leading 0s since it does math upon an Integer
77
+ # As a workaround, I check for what I expect the second byte to be (\x01)
78
+ # This workaround will also handle seeing a \x00 first if the RSA gem is
79
+ # fixed.
80
+ if emsa_signature.getbyte(0) == 1
81
+ emsa_signature = "\x00#{emsa_signature}"
82
+ end
83
+
84
+ # Does the signature match?
85
+ # Return the result.
86
+ emsa_signature == emsa
87
+ end
88
+
89
+ # Decrypts the given data with the given private key.
90
+ def self.decrypt(private_key, data)
91
+ private_key = generate_key(private_key) unless private_key.is_a? RSA::Key
92
+ keypair = generate_keypair(nil, private_key)
93
+ keypair.decrypt(data)
94
+ end
95
+
96
+ # Encrypts the given data with the given public key.
97
+ def self.encrypt(public_key, data)
98
+ public_key = generate_key(public_key) unless public_key.is_a? RSA::Key
99
+ keypair = generate_keypair(public_key, nil)
100
+ keypair.encrypt(data)
101
+ end
102
+
103
+ private
104
+
105
+ # :nodoc:
106
+ def self.emsa_signature(text, key)
107
+ modulus_byte_count = key.modulus.size
108
+
109
+ plaintext = Digest::SHA2.new(256).digest(text)
110
+
111
+ prefix = "\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20"
112
+ padding_count = modulus_byte_count - prefix.bytes.count - plaintext.bytes.count - 3
113
+
114
+ padding = ""
115
+ padding_count.times do
116
+ padding = padding + "\xff"
117
+ end
118
+
119
+ "\x00\x01#{padding}\x00#{prefix}#{plaintext}".force_encoding('binary')
120
+ end
121
+
122
+ # :nodoc:
123
+ def self.generate_key(key_string)
124
+ return nil unless key_string
125
+
126
+ key_string.match /^RSA\.(.*?)\.(.*)$/
127
+
128
+ modulus = decode_key($1)
129
+ exponent = decode_key($2)
130
+
131
+ RSA::Key.new(modulus, exponent)
132
+ end
133
+
134
+ def self.generate_keypair(public_key, private_key)
135
+ RSA::KeyPair.new(private_key, public_key)
136
+ end
137
+
138
+ # :nodoc:
139
+ def self.decode_key(encoded_key_part)
140
+ modulus = Base64::urlsafe_decode64(encoded_key_part)
141
+ modulus.bytes.inject(0) {|num, byte| (num << 8) | byte }
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,11 @@
1
+ module Nelumba
2
+ class Device
3
+ include Nelumba::Object
4
+
5
+ def to_json_hash
6
+ {
7
+ :objectType => "device"
8
+ }.merge(super)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,362 @@
1
+ module Nelumba
2
+ module Discover
3
+ require 'nelumba/atom/feed'
4
+ require 'net/http'
5
+ require 'nokogiri'
6
+
7
+ # The order to respect atom links
8
+ MIME_ORDER = ['application/atom+xml',
9
+ 'application/rss+xml',
10
+ 'application/xml']
11
+
12
+ # Will yield an OStatus::Identity for the given fully qualified name
13
+ # (i.e. "user@domain.tld")
14
+ def self.identity(name)
15
+ xrd = nil
16
+
17
+ if name.match /^https?:\/\//
18
+ url = name
19
+ type = 'text/html'
20
+ response = Nelumba::Discover.pull_url(url, type)
21
+
22
+ # Look at HTTP link headers
23
+ if response["Link"]
24
+ link = response["Link"]
25
+
26
+ new_url = link[/^<([^>]+)>/,1]
27
+ rel = link[/;\s*rel\s*=\s*"([^"]+)"/,1]
28
+ type = link[/;\s*type\s*=\s*"([^"]+)"/,1]
29
+
30
+ if new_url.start_with? "/"
31
+ domain = url[/^(http[s]?:\/\/[^\/]+)\//,1]
32
+ new_url = "#{domain}#{new_url}"
33
+ end
34
+
35
+ if rel == "lrdd"
36
+ Nelumba::Discover.identity_from_xml(new_url)
37
+ else
38
+ nil
39
+ end
40
+ end
41
+ elsif name.match /@/
42
+ Nelumba::Discover.identity_from_webfinger(name)
43
+ end
44
+ end
45
+
46
+ # Retrieves a Nelumba::Identity from the given webfinger account.
47
+ def self.identity_from_webfinger(acct)
48
+ # We allow a port, unusually. Some exaxmples:
49
+ #
50
+ # acct: 'acct:wilkie@example.org:9292'
51
+ # or: 'acct:wilkie@example.org'
52
+ # or: 'wilkie@example.org'
53
+
54
+ # Remove acct: prefix if it exists
55
+ acct.gsub!(/^acct\:/, "")
56
+
57
+ # Get domain and port
58
+ matches = acct.match /([^@]+)@([^:]+)(:\d+)?$/
59
+ username = matches[1]
60
+ domain = matches[2]
61
+ port = matches[3] || "" # will include the ':'
62
+
63
+ accept = ['application/xml+xrd',
64
+ 'application/xml',
65
+ 'text/html']
66
+
67
+ # Pull .well-known/host-meta
68
+ scheme = 'https'
69
+ url = "#{scheme}://#{domain}#{port}/.well-known/host-meta"
70
+ host_meta = Nelumba::Discover.pull_url(url, accept)
71
+
72
+ if host_meta.nil?
73
+ # TODO: Should we do this? probably not. ugh. but we must.
74
+ scheme = 'http'
75
+ url = "#{scheme}://#{domain}#{port}/.well-known/host-meta"
76
+ host_meta = Nelumba::Discover.pull_url(url, accept)
77
+ end
78
+
79
+ return nil if host_meta.nil?
80
+
81
+ # Read xrd template location
82
+ host_meta = host_meta.body
83
+ host_meta = Nokogiri::XML(host_meta)
84
+ links = host_meta.xpath("/xmlns:XRD/xmlns:Link")
85
+ link = links.select{|link| link.attr('rel') == 'lrdd' }.first
86
+ lrdd_template = link.attr('template') || link.attr('href')
87
+
88
+ xrd_url = lrdd_template.gsub(/{uri}/, "acct:#{username}")
89
+
90
+ xrd = Nelumba::Discover.pull_url(xrd_url, accept)
91
+ return nil if xrd.nil?
92
+
93
+ xrd = xrd.body
94
+ xrd = Nokogiri::XML(xrd)
95
+
96
+ unless xrd
97
+ # TODO: Error
98
+ return nil
99
+ end
100
+
101
+ # magic-envelope public key
102
+ public_key = find_link(xrd, 'magic-public-key') || ""
103
+ public_key = public_key.split(",")[1] || ""
104
+
105
+ # ostatus notification endpoint
106
+ salmon_url = find_link(xrd, 'salmon')
107
+
108
+ # pump.io authentication endpoint
109
+ dialback_url = find_link(xrd, 'dialback')
110
+
111
+ # pump.io activity endpoints
112
+ activity_inbox_endpoint = find_link(xrd, 'activity-inbox')
113
+ activity_outbox_endpoint = find_link(xrd, 'activity-outbox')
114
+
115
+ # profile page
116
+ profile_page = find_link(xrd, 'http://webfinger.net/rel/profile-page')
117
+
118
+ Identity.new(:public_key => public_key,
119
+ :profile_page => profile_page,
120
+ :salmon_endpoint => salmon_url,
121
+ :dialback_endpoint => dialback_url,
122
+ :activity_inbox_endpoint => activity_inbox_endpoint,
123
+ :activity_outbox_endpoint => activity_outbox_endpoint)
124
+ end
125
+
126
+ # Retrieves a Nelumba::Identity from the given xml. You specify the xml
127
+ # as a url, which will be retrieved and then parsed.
128
+ def self.identity_from_xml(url, content_type = nil)
129
+ content_type ||= ['application/xml+xrd',
130
+ 'application/xml']
131
+
132
+ xml = Nelumba::Discover.pull_url(url, content_type)
133
+ return nil if xml.nil?
134
+
135
+ Nelumba::Discover.identity_from_xml_string(xml)
136
+ end
137
+
138
+ def self.identity_from_xml_string(xml)
139
+ xrd = Nokogiri::XML(xml)
140
+ unless xrd
141
+ # TODO: Error
142
+ return nil
143
+ end
144
+
145
+ # magic-envelope public key
146
+ public_key = find_link(xrd, 'magic-public-key')
147
+ public_key = public_key.split(",")[1] || ""
148
+
149
+ # ostatus notification endpoint
150
+ salmon_url = find_link(xrd, 'salmon')
151
+
152
+ # pump.io authentication endpoint
153
+ dialback_url = find_link(xrd, 'dialback')
154
+
155
+ # pump.io activity endpoints
156
+ activity_inbox_endpoint = find_link(xrd, 'activity-inbox')
157
+ activity_outbox_endpoint = find_link(xrd, 'activity-outbox')
158
+
159
+ # profile page
160
+ profile_page = find_link(xrd, 'http://webfinger.net/rel/profile-page')
161
+
162
+ Identity.new(:public_key => public_key,
163
+ :profile_page => profile_page,
164
+ :salmon_endpoint => salmon_url,
165
+ :dialback_endpoint => dialback_url,
166
+ :activity_inbox_endpoint => activity_inbox_endpoint,
167
+ :activity_outbox_endpoint => activity_outbox_endpoint)
168
+ end
169
+
170
+ # Will yield an Nelumba::Person for the given person.
171
+ #
172
+ # identity: Can be a String containing a fully qualified name (i.e.
173
+ # "user@domain.tld") or a previously resolved Nelumba::Identity.
174
+ def self.person(identity)
175
+ if identity.is_a? String
176
+ identity = Nelumba::Discover.identity(identity)
177
+ end
178
+
179
+ return nil if identity.nil? || identity.profile_page.nil?
180
+
181
+ # Discover Person information
182
+
183
+ # Pull profile page
184
+ # Look for a feed to pull
185
+ feed = Nelumba::Discover.feed(identity.profile_page)
186
+ feed.authors.first
187
+ end
188
+
189
+ # Will yield a Nelumba::Feed object representing the feed at the given url
190
+ # or identity.
191
+ #
192
+ # Usage:
193
+ # feed = Nelumba::Discover.feed("https://rstat.us/users/wilkieii/feed")
194
+ #
195
+ # i = Nelumba::Discover.identity("wilkieii@rstat.us")
196
+ # feed = Nelumba::Discover.feed(i)
197
+ def self.feed(url_or_identity, content_type = nil)
198
+ if url_or_identity =~ /^(?:acct:)?[^@]+@[^@]+\.[^@]+$/
199
+ url_or_identity = Nelumba::Discover.identity(url_or_identity)
200
+ end
201
+
202
+ if url_or_identity.is_a? Nelumba::Identity
203
+ return Nelumba::Discover.feed(url_or_identity.profile_page)
204
+ end
205
+
206
+ # Atom is default type to attempt to retrieve
207
+ content_type ||= ["application/atom+xml", "text/html"]
208
+ accept = content_type
209
+
210
+ url = url_or_identity
211
+
212
+ if url =~ /^http[s]?:\/\//
213
+ # Url is an internet resource
214
+ response = Nelumba::Discover.pull_url(url, accept)
215
+
216
+ return nil unless response.is_a?(Net::HTTPSuccess)
217
+
218
+ content_type = response.content_type
219
+ str = response.body
220
+ else
221
+ str = open(url).read
222
+ end
223
+
224
+ case content_type
225
+ when 'application/atom+xml', 'application/rss+xml', 'application/xml',
226
+ 'xml', 'atom', 'rss', 'atom+xml', 'rss+xml'
227
+ xml_str = str
228
+
229
+ self.feed_from_string(xml_str, content_type)
230
+ when 'text/html'
231
+ html_str = str
232
+
233
+ # Discover the feed
234
+ doc = Nokogiri::HTML::Document.parse(html_str)
235
+ links = doc.xpath("//link[@rel='alternate']").map {|el|
236
+ {:type => el.attributes['type'].to_s,
237
+ :href => el.attributes['href'].to_s}
238
+ }.select{|e|
239
+ MIME_ORDER.include? e[:type]
240
+ }.sort {|a, b|
241
+ MIME_ORDER.index(a[:type]) <=>
242
+ MIME_ORDER.index(b[:type])
243
+ }
244
+
245
+ return nil if links.empty?
246
+
247
+ href = links.first[:href]
248
+ if href.start_with? "/"
249
+ # Append domain from what we know
250
+ uri = URI::parse(url) rescue URI.new
251
+ href = "#{uri.scheme}://#{uri.host}#{uri.port == 80 ? "" : ":#{uri.port}"}#{href}"
252
+ end
253
+
254
+ # Resolve relative links
255
+ link = URI::parse(href) rescue URI.new
256
+
257
+ unless link.scheme
258
+ link.scheme = URI::parse(url).scheme
259
+ end
260
+
261
+ unless link.host
262
+ link.host = URI::parse(url).host rescue nil
263
+ end
264
+
265
+ unless link.absolute?
266
+ link.path = File::dirname(URI::parse(url).path) \
267
+ + '/' + link.path rescue nil
268
+ end
269
+
270
+ url = link.to_s
271
+ Nelumba::Discover.feed(url, links.first[:type])
272
+ end
273
+ end
274
+
275
+ # Yield a Nelumba::Feed from the given string content.
276
+ def self.feed_from_string(string, content_type = nil)
277
+ # Atom is default type to attempt to retrieve
278
+ content_type ||= "application/atom+xml"
279
+
280
+ case content_type
281
+ when 'application/atom+xml', 'application/rss+xml', 'application/xml'
282
+ Nelumba::Atom::Feed.new(XML::Reader.string(string)).to_canonical
283
+ end
284
+ end
285
+
286
+ def self.activity(url)
287
+ self.activity_from_url(url)
288
+ end
289
+
290
+ # Yield a Nelumba::Activity from the given url.
291
+ def self.activity_from_url(url, content_type = nil)
292
+ # Atom is default type to attempt to retrieve
293
+ content_type ||= "application/atom+xml"
294
+
295
+ response = Nelumba::Discover.pull_url(url, content_type)
296
+
297
+ return nil unless response.is_a?(Net::HTTPSuccess)
298
+
299
+ content_type = response.content_type
300
+
301
+ case content_type
302
+ when 'application/atom+xml', 'application/rss+xml', 'application/xml'
303
+ xml_str = response.body
304
+ self.entry_from_string(xml_str, response.content_type)
305
+ end
306
+ end
307
+
308
+ # Yield a Nelumba::Activity from the given string content.
309
+ def self.activity_from_string(string, content_type = "application/atom+xml")
310
+ content_type ||= "application/atom+xml"
311
+
312
+ case content_type
313
+ when 'application/atom+xml', 'application/rss+xml', 'application/xml'
314
+ Nelumba::Atom::Entry.new(XML::Reader.string(string)).to_canonical
315
+ end
316
+ end
317
+
318
+ private
319
+
320
+ # :nodoc:
321
+ def self.pull_url(url, accept = nil, limit = 10)
322
+ # Atom is default type to attempt to retrieve
323
+ accept ||= ["application/atom+xml",
324
+ "application/xml"]
325
+
326
+ if accept.is_a? String
327
+ accept = [accept]
328
+ end
329
+
330
+ uri = URI(url)
331
+ request = Net::HTTP::Get.new(uri.request_uri)
332
+ request['Accept'] = accept.join(',')
333
+ request['User-Agent'] = 'nelumba'
334
+
335
+ http = Net::HTTP.new(uri.hostname, uri.port)
336
+ if uri.scheme == 'https'
337
+ http.use_ssl = true
338
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
339
+ end
340
+
341
+ response = http.request(request)
342
+
343
+ if response.is_a?(Net::HTTPRedirection) && limit > 0
344
+ location = response['location']
345
+ Nelumba::Discover.pull_url(location, accept, limit - 1)
346
+ else
347
+ response
348
+ end
349
+
350
+ rescue OpenSSL::SSL::SSLError
351
+ return nil
352
+ end
353
+
354
+ # :nodoc:
355
+ def self.find_link(xrd, rel)
356
+ links = xrd.xpath("/xmlns:XRD/xmlns:Link")
357
+ link = links.select{|link| link.attr('rel').downcase == rel }.first
358
+ return nil if link.nil?
359
+ link.attr('template') || link.attr('href')
360
+ end
361
+ end
362
+ end