nelumba 0.0.13

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