ruby-safenet 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4557bfe89264b253153460ccc8c69ddc7326bacd
4
+ data.tar.gz: 0fac6e8ff9d6290792e5f78bc93367e5e5d8ed5a
5
+ SHA512:
6
+ metadata.gz: 2eb2c81e7041b0519caf8519915d184c7b1e93e6c3a916c34a93754b1bb22147f9c7bd186b19cc152a3ab5b0520f8642bda9d33f8989d80e4f85f378598bd3a3
7
+ data.tar.gz: 3dcf2664123244491c1adc5c089162172159029d5ed318c13cfa2b7b3a6d8e68a7708a6e2bdc5912e0b301960f61675706ac64870d56a3509be4227283a06aab
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.1
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in safenet.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # Ruby-Safenet
2
+
3
+ A Ruby library for accessing the SAFE network
4
+
5
+ ## Installation
6
+
7
+ $ gem install safenet
8
+
9
+ ## Usage
10
+
11
+ ```ruby
12
+ require "safenet"
13
+
14
+ SafeNet.set_app_info({
15
+ name: "Demo App",
16
+ version: "0.0.1",
17
+ vendor: "maidsafe",
18
+ id: "org.maidsafe.demo",
19
+ })
20
+
21
+ SafeNet.create_directory("/mydir")
22
+ SafeNet.file("/mydir/index.html")
23
+ SafeNet.update_file_content("/mydir/index.html", "Hello world!<br>I'm a webpage :D")
24
+ SafeNet.register_service("mywonderfulapp", "www", "/mydir")
25
+ SafeNet.get_file("/mydir/index.html")
26
+ ```
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "safenet"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,3 @@
1
+ module Safenet
2
+ VERSION = "0.0.1"
3
+ end
data/lib/safenet.rb ADDED
@@ -0,0 +1,423 @@
1
+ require "safenet/version"
2
+
3
+ module SafeNet
4
+ require "net/http"
5
+ require "rbnacl"
6
+ require "base64"
7
+ require "json"
8
+ require "cgi" # CGI.escape method
9
+
10
+ # default values
11
+ @@NAME = "Demo App"
12
+ @@VERSION = "0.0.1"
13
+ @@VENDOR = "maidsafe"
14
+ @@ID = "org.maidsafe.demo"
15
+ @@LAUCHER_SERVER = "http://localhost:8100/"
16
+ @@CONF_PATH = File.join(File.expand_path('.', __FILE__), "conf.json")
17
+
18
+ def self.set_app_info(options)
19
+ @@NAME = options[:name] if options.has_key?(:name)
20
+ @@VERSION = options[:version] if options.has_key?(:version)
21
+ @@VENDOR = options[:vendor] if options.has_key?(:vendor)
22
+ @@ID = options[:id] if options.has_key?(:id)
23
+ @@LAUCHER_SERVER = options[:server] if options.has_key?(:server)
24
+ @@CONF_PATH = options[:conf_file] if options.has_key?(:conf_file)
25
+ end
26
+
27
+ def self.auth
28
+ url = "#{@@LAUCHER_SERVER}auth"
29
+
30
+ private_key = RbNaCl::PrivateKey.generate
31
+ nonce = RbNaCl::Random.random_bytes(24)
32
+
33
+ payload = {
34
+ app: {
35
+ name: @@NAME,
36
+ version: @@VERSION,
37
+ vendor: @@VENDOR,
38
+ id: @@ID
39
+ },
40
+ publicKey: Base64.strict_encode64(private_key.public_key),
41
+ nonce: Base64.strict_encode64(nonce),
42
+ permissions: []
43
+ }
44
+
45
+ uri = URI(url)
46
+ http = Net::HTTP.new(uri.host, uri.port)
47
+ req = Net::HTTP::Post.new(uri.path, {'Content-Type' => 'application/json'})
48
+ req.body = payload.to_json
49
+ res = http.request(req)
50
+
51
+ if res.code == "200"
52
+ response = JSON.parse(res.body)
53
+
54
+ # save it in conf.json
55
+ conf = response.dup
56
+ conf["nonce"] = Base64.strict_encode64(nonce)
57
+ conf["privateKey"] = Base64.strict_encode64(private_key)
58
+ File.open(@@CONF_PATH, "w") { |f| f << JSON.pretty_generate(conf) }
59
+
60
+ else
61
+ # puts "ERROR #{res.code}: #{res.message}"
62
+ response = nil
63
+ end
64
+
65
+ response
66
+ end
67
+
68
+
69
+ def self.is_token_valid
70
+ url = "#{@@LAUCHER_SERVER}auth"
71
+
72
+ uri = URI(url)
73
+ http = Net::HTTP.new(uri.host, uri.port)
74
+ req = Net::HTTP::Get.new(uri.path, {
75
+ 'Authorization' => "Bearer #{self.get_token()}"
76
+ })
77
+ res = http.request(req)
78
+ res.code == "200"
79
+ end
80
+
81
+
82
+ def self.revoke_token
83
+ url = "#{@@LAUCHER_SERVER}auth"
84
+
85
+ uri = URI(url)
86
+ http = Net::HTTP.new(uri.host, uri.port)
87
+ req = Net::HTTP::Delete.new(uri.path, {
88
+ 'Authorization' => "Bearer #{self.get_valid_token()}"
89
+ })
90
+ res = http.request(req)
91
+ res.code == "200"
92
+ end
93
+
94
+ def self.get_directory(dir_path, options = {})
95
+ # default values
96
+ options[:is_path_shared] ||= false
97
+
98
+ # entry point
99
+ url = "#{@@LAUCHER_SERVER}nfs/directory/#{CGI.escape(dir_path)}/#{options[:is_path_shared]}"
100
+
101
+ uri = URI(url)
102
+ http = Net::HTTP.new(uri.host, uri.port)
103
+ req = Net::HTTP::Get.new(uri.path, {
104
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
105
+ })
106
+ res = http.request(req)
107
+ JSON.parse(self.decrypt(res.body))
108
+ end
109
+
110
+
111
+ # options = is_private, metadata, is_versioned, is_path_shared
112
+ def self.file(file_path, options = {})
113
+ url = "#{@@LAUCHER_SERVER}nfs/file"
114
+
115
+ # default values
116
+ options[:is_private] ||= true
117
+ options[:is_versioned] ||= false
118
+ options[:is_path_shared] ||= false
119
+
120
+ # payload
121
+ payload = {
122
+ filePath: file_path,
123
+ isPrivate: options[:is_private],
124
+ isVersioned: options[:is_versioned],
125
+ isPathShared: options[:is_path_shared]
126
+ }
127
+
128
+ # optional
129
+ payload["metadata"] = options[:metadata] if options.has_key?(:metadata)
130
+
131
+ # api call
132
+ uri = URI(url)
133
+ http = Net::HTTP.new(uri.host, uri.port)
134
+ req = Net::HTTP::Post.new(uri.path, {
135
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
136
+ 'Content-Type' => 'text/plain'
137
+ })
138
+ req.body = self.encrypt(payload.to_json)
139
+ res = http.request(req)
140
+ res.code == "200" ? true : JSON.parse(self.decrypt(res.body))
141
+ end
142
+
143
+
144
+ # options = is_private, metadata, is_versioned, is_path_shared
145
+ # ex.: create_directory("/photos")
146
+ def self.create_directory(dir_path, options = {})
147
+ url = "#{@@LAUCHER_SERVER}nfs/directory"
148
+
149
+ # default values
150
+ options[:is_private] ||= true
151
+ options[:is_versioned] ||= false
152
+ options[:is_path_shared] ||= false
153
+
154
+ # payload
155
+ payload = {
156
+ dirPath: dir_path,
157
+ isPrivate: options[:is_private],
158
+ isVersioned: options[:is_versioned],
159
+ isPathShared: options[:is_path_shared]
160
+ }
161
+
162
+ # optional
163
+ payload["metadata"] = options[:metadata] if options.has_key?(:metadata)
164
+
165
+ # api call
166
+ uri = URI(url)
167
+ http = Net::HTTP.new(uri.host, uri.port)
168
+ req = Net::HTTP::Post.new(uri.path, {
169
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
170
+ 'Content-Type' => 'text/plain'
171
+ })
172
+ req.body = self.encrypt(payload.to_json)
173
+ res = http.request(req)
174
+ res.code == "200" ? true : JSON.parse(self.decrypt(res.body))
175
+ end
176
+
177
+
178
+ # ex.: delete_directory("/photos")
179
+ def self.delete_directory(dir_path, options = {})
180
+ # default values
181
+ options[:is_path_shared] ||= false
182
+
183
+ # entry point
184
+ url = "#{@@LAUCHER_SERVER}nfs/directory/#{CGI.escape(dir_path)}/#{options[:is_path_shared]}"
185
+
186
+ # api call
187
+ uri = URI(url)
188
+ http = Net::HTTP.new(uri.host, uri.port)
189
+ req = Net::HTTP::Delete.new(uri.path, {
190
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
191
+ })
192
+ res = http.request(req)
193
+ res.code == "200" ? true : JSON.parse(self.decrypt(res.body))
194
+ end
195
+
196
+ # options: offset, length, is_path_shared
197
+ def self.get_file(file_path, options = {})
198
+ # default values
199
+ options[:offset] ||= 0
200
+ options[:is_path_shared] ||= false
201
+
202
+ # entry point
203
+ url = "#{@@LAUCHER_SERVER}nfs/file/#{CGI.escape(file_path)}/#{options[:is_path_shared]}?"
204
+
205
+ # query params are encrypted
206
+ query = []
207
+ query << "offset=#{options[:offset]}"
208
+ query << "length=#{options[:length]}" if options.has_key?(:length) # length is optional
209
+ url = "#{url}#{self.encrypt(query.join('&'))}"
210
+
211
+ # api call
212
+ uri = URI(url)
213
+ http = Net::HTTP.new(uri.host, uri.port)
214
+ req = Net::HTTP::Get.new(uri.path, {
215
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
216
+ })
217
+ res = http.request(req)
218
+ res.code == "200" ? self.decrypt(res.body) : JSON.parse(self.decrypt(res.body))
219
+ end
220
+
221
+ def self.update_file_content(file_path, contents, options = {})
222
+ # default values
223
+ options[:offset] ||= 0
224
+ options[:is_path_shared] ||= false
225
+
226
+ # entry point
227
+ url = "#{@@LAUCHER_SERVER}nfs/file/#{CGI.escape(file_path)}/#{options[:is_path_shared]}?offset=#{options[:offset]}"
228
+
229
+ # api call
230
+ uri = URI(url)
231
+ http = Net::HTTP.new(uri.host, uri.port)
232
+ req = Net::HTTP::Put.new(uri.path, {
233
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
234
+ 'Content-Type' => 'text/plain'
235
+ })
236
+ req.body = self.encrypt(contents)
237
+ res = http.request(req)
238
+ res.code == "200" ? true : JSON.parse(self.decrypt(res.body))
239
+ end
240
+
241
+ def self.delete_file(file_path, options = {})
242
+ # default values
243
+ options[:is_path_shared] ||= false
244
+
245
+ # entry point
246
+ url = "#{@@LAUCHER_SERVER}nfs/file/#{CGI.escape(file_path)}/#{options[:is_path_shared]}"
247
+
248
+ # api call
249
+ uri = URI(url)
250
+ http = Net::HTTP.new(uri.host, uri.port)
251
+ req = Net::HTTP::Delete.new(uri.path, {
252
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
253
+ })
254
+ res = http.request(req)
255
+ res.code == "200" ? true : JSON.parse(self.decrypt(res.body))
256
+ end
257
+
258
+
259
+ def self.create_long_name(long_name)
260
+ url = "#{@@LAUCHER_SERVER}dns/#{CGI.escape(long_name)}"
261
+
262
+ # api call
263
+ uri = URI(url)
264
+ http = Net::HTTP.new(uri.host, uri.port)
265
+ req = Net::HTTP::Post.new(uri.path, {
266
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
267
+ 'Content-Type' => 'text/plain'
268
+ })
269
+ res = http.request(req)
270
+ res.code == "200" ? true : JSON.parse(self.decrypt(res.body))
271
+ end
272
+
273
+
274
+ # ex.: register_service("thegoogle", "www", "/www")
275
+ def self.register_service(long_name, service_name, service_home_dir_path, options = {})
276
+ url = "#{@@LAUCHER_SERVER}dns"
277
+
278
+ # default values
279
+ options[:is_path_shared] ||= false
280
+
281
+ # payload
282
+ payload = {
283
+ longName: long_name,
284
+ serviceName: service_name,
285
+ serviceHomeDirPath: service_home_dir_path,
286
+ isPathShared: options[:is_path_shared]
287
+ }
288
+
289
+ # optional
290
+ payload["metadata"] = options[:metadata] if options.has_key?(:metadata)
291
+
292
+ # api call
293
+ uri = URI(url)
294
+ http = Net::HTTP.new(uri.host, uri.port)
295
+ req = Net::HTTP::Post.new(uri.path, {
296
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
297
+ 'Content-Type' => 'text/plain'
298
+ })
299
+ req.body = self.encrypt(payload.to_json)
300
+ res = http.request(req)
301
+ res.code == "200" ? true : JSON.parse(self.decrypt(res.body))
302
+ end
303
+
304
+
305
+ def self.list_long_names
306
+ url = "#{@@LAUCHER_SERVER}dns"
307
+
308
+ # api call
309
+ uri = URI(url)
310
+ http = Net::HTTP.new(uri.host, uri.port)
311
+ req = Net::HTTP::Get.new(uri.path, {
312
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
313
+ })
314
+ res = http.request(req)
315
+ JSON.parse(self.decrypt(res.body))
316
+ end
317
+
318
+
319
+ def self.list_services(long_name)
320
+ url = "#{@@LAUCHER_SERVER}dns/#{CGI.escape(long_name)}"
321
+
322
+ # api call
323
+ uri = URI(url)
324
+ http = Net::HTTP.new(uri.host, uri.port)
325
+ req = Net::HTTP::Get.new(uri.path, {
326
+ 'Authorization' => "Bearer #{self.get_valid_token()}",
327
+ })
328
+ res = http.request(req)
329
+ JSON.parse(self.decrypt(res.body))
330
+ end
331
+
332
+
333
+ def self.get_home_dir(long_name, service_name)
334
+ url = "#{@@LAUCHER_SERVER}dns/#{CGI.escape(service_name)}/#{CGI.escape(long_name)}"
335
+
336
+ # api call
337
+ uri = URI(url)
338
+ http = Net::HTTP.new(uri.host, uri.port)
339
+ req = Net::HTTP::Get.new(uri.path)
340
+ res = http.request(req)
341
+ JSON.parse(res.body)
342
+ end
343
+
344
+
345
+ # get_file_unauth("thegoogle", "www", "index.html", offset: 3, length: 5)
346
+ def self.get_file_unauth(long_name, service_name, file_path, options = {})
347
+ # default values
348
+ options[:offset] ||= 0
349
+
350
+ # entry point
351
+ url = "#{@@LAUCHER_SERVER}dns/#{CGI.escape(service_name)}/#{CGI.escape(long_name)}/#{CGI.escape(file_path)}?"
352
+
353
+ # query params are encrypted
354
+ query = []
355
+ query << "offset=#{options[:offset]}"
356
+ query << "length=#{options[:length]}" if options.has_key?(:length) # length is optional
357
+ url = "#{url}#{self.encrypt(query.join('&'))}"
358
+
359
+ # api call
360
+ uri = URI(url)
361
+ http = Net::HTTP.new(uri.host, uri.port)
362
+ req = Net::HTTP::Get.new(uri.path)
363
+ res = http.request(req)
364
+ res.code == "200" ? res.body : JSON.parse(res.body)
365
+ end
366
+
367
+ private
368
+
369
+ def self.get_token
370
+ @@conf = File.exists?(@@CONF_PATH) ? JSON.parse(File.read(@@CONF_PATH)) : (self.auth() || {})
371
+ @@conf["token"]
372
+ end
373
+
374
+ def self.get_valid_token
375
+ @last_conf = File.exists?(@@CONF_PATH) ? JSON.parse(File.read(@@CONF_PATH)) : {}
376
+ self.auth() unless File.exists?(@@CONF_PATH) && self.is_token_valid()
377
+ @@conf = JSON.parse(File.read(@@CONF_PATH))
378
+ @@conf["token"]
379
+ end
380
+
381
+
382
+ def self.get_keys
383
+ @@keys ||= {}
384
+
385
+ # not loaded yet?
386
+ if @@keys.empty?
387
+ @@conf ||= {}
388
+ self.get_valid_token if @@conf.empty?
389
+
390
+ # extract keys
391
+ cipher_text = Base64.strict_decode64(@@conf["encryptedKey"])
392
+ nonce = Base64.strict_decode64(@@conf["nonce"])
393
+ private_key = Base64.strict_decode64(@@conf["privateKey"])
394
+ public_key = Base64.strict_decode64(@@conf["publicKey"])
395
+
396
+ box = RbNaCl::Box.new(public_key, private_key)
397
+ data = box.decrypt(nonce, cipher_text)
398
+
399
+ # The first segment of the data will have the symmetric key
400
+ @@keys["symmetric_key"] = data.slice(0, RbNaCl::SecretBox.key_bytes)
401
+
402
+ # The second segment of the data will have the nonce to be used
403
+ @@keys["symmetric_nonce"] = data.slice(RbNaCl::SecretBox.key_bytes, RbNaCl::SecretBox.key_bytes)
404
+
405
+ # keep the box object in cache
406
+ @@keys["secret_box"] = RbNaCl::SecretBox.new(@@keys["symmetric_key"])
407
+ end
408
+
409
+ @@keys
410
+ end
411
+
412
+ def self.decrypt(message_base64)
413
+ keys = self.get_keys
414
+ keys["secret_box"].decrypt(keys["symmetric_nonce"], Base64.strict_decode64(message_base64))
415
+ end
416
+
417
+ def self.encrypt(message)
418
+ keys = self.get_keys
419
+ res = keys["secret_box"].encrypt(keys["symmetric_nonce"], message)
420
+ Base64.strict_encode64(res)
421
+ end
422
+
423
+ end
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'safenet/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ruby-safenet"
8
+ spec.version = Safenet::VERSION
9
+ spec.authors = ["Daniel Loureiro"]
10
+ spec.email = ["loureirorg@gmail.com"]
11
+
12
+ spec.summary = %q{Ruby library for accessing the SAFE network}
13
+ spec.description = %q{Ruby library for accessing the SAFE network}
14
+ spec.homepage = "https://github.com/loureirorg/ruby-safenet"
15
+
16
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
17
+ # delete this section to allow pushing this gem to any host.
18
+ # if spec.respond_to?(:metadata)
19
+ # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
20
+ # else
21
+ # raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
22
+ # end
23
+
24
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.10"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_runtime_dependency "rbnacl", "~> 3.3"
32
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-safenet
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Loureiro
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-03-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rbnacl
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.3'
55
+ description: Ruby library for accessing the SAFE network
56
+ email:
57
+ - loureirorg@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".travis.yml"
64
+ - Gemfile
65
+ - README.md
66
+ - Rakefile
67
+ - bin/console
68
+ - bin/setup
69
+ - lib/safenet.rb
70
+ - lib/safenet/version.rb
71
+ - ruby-safenet.gemspec
72
+ homepage: https://github.com/loureirorg/ruby-safenet
73
+ licenses: []
74
+ metadata: {}
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubyforge_project:
91
+ rubygems_version: 2.4.8
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: Ruby library for accessing the SAFE network
95
+ test_files: []
96
+ has_rdoc: