homefs 0.3.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/homefs/debug.rb +4 -0
- data/lib/homefs/homefs.rb +110 -255
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9aa5fb16b6856552286724fd6fd1e45f46bc5a6f
|
4
|
+
data.tar.gz: ca26c140a4ad789d383edbe43a7040d2fa23b8f4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7294eef3cd9569a078789e8b148de97054584084e792deb5bee24d5c5d234314fc7ea6d0d7dc11bc69d93002ec1e34cf9e455edc54b5af75e9428e1517e87349
|
7
|
+
data.tar.gz: 5b350ef36937e79201cf61c005acb21447ad014f51c96a1dd06f52753402e19d189e083f339fec10e1536e038bc41a257452878bc52f494ba6bbb1166ab75ddd
|
data/lib/homefs/debug.rb
CHANGED
data/lib/homefs/homefs.rb
CHANGED
@@ -22,7 +22,6 @@ class HomeFS
|
|
22
22
|
def initialize(path, options = Hash.new)
|
23
23
|
@relpath = path
|
24
24
|
read_passwd
|
25
|
-
read_group
|
26
25
|
end
|
27
26
|
|
28
27
|
# Read or re-read /etc/passwd to update the internal table of home
|
@@ -55,30 +54,6 @@ class HomeFS
|
|
55
54
|
}
|
56
55
|
end
|
57
56
|
|
58
|
-
# Read or re-read /etc/group to update the internal table of
|
59
|
-
# group membership.
|
60
|
-
# @return [void]
|
61
|
-
def read_group
|
62
|
-
# Try to read from getent
|
63
|
-
group = `getent group`
|
64
|
-
if $? != 0
|
65
|
-
group = File.read('/etc/group')
|
66
|
-
end
|
67
|
-
|
68
|
-
@groups = Hash.new
|
69
|
-
|
70
|
-
group.each_line.map { |line|
|
71
|
-
next if line.strip.empty?
|
72
|
-
_name, _, gid, members = line.split(':')
|
73
|
-
members = members.strip.split(',').map {|member| @usernames[member]}
|
74
|
-
[Integer(gid), members]
|
75
|
-
}.reject { |line|
|
76
|
-
line.nil?
|
77
|
-
}.each { |gid, members|
|
78
|
-
@groups[gid] = members
|
79
|
-
}
|
80
|
-
end
|
81
|
-
|
82
57
|
# Return the path to the root of HomeFS relative to the underlying
|
83
58
|
# filesystem. If _path_ is specified, it is taken to be relative to the
|
84
59
|
# root of HomeFS.
|
@@ -99,162 +74,28 @@ class HomeFS
|
|
99
74
|
end
|
100
75
|
end
|
101
76
|
|
102
|
-
#
|
103
|
-
#
|
104
|
-
#
|
105
|
-
#
|
106
|
-
#
|
107
|
-
#
|
108
|
-
#
|
109
|
-
#
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
# It is recommended to see
|
120
|
-
# man chmod
|
121
|
-
# to get a more detailed description about file modes.
|
122
|
-
#
|
123
|
-
# @example Test if we can write to a file
|
124
|
-
# mode_mask("/example", 0222, true)
|
125
|
-
# mode_mask("/example", 0222)
|
126
|
-
# @example Test if a file is executable by anyone
|
127
|
-
# mode_mask("/binary", 0111, false)
|
128
|
-
# @example Test if a directory has the sticky bit set
|
129
|
-
# mode_mask("/tmp", 01000, false)
|
130
|
-
#
|
131
|
-
# @param [String] file the path to the file to check
|
132
|
-
# @param [Integer] mask the mask against which to check the file's mode.
|
133
|
-
# It is recommended you write this in octal (with a leading 0).
|
134
|
-
# @param [Integer] uid if not nil, only check user and group permissions
|
135
|
-
# if the user it represents is, respectively, the owner of the
|
136
|
-
# file/in the file's group.
|
137
|
-
# @return [Boolean] whether the mode of _file_ matches the given mask
|
138
|
-
def mode_mask(file, mask, uid = nil)
|
139
|
-
stat = File.stat(file)
|
140
|
-
fmode = stat.mode
|
141
|
-
if uid
|
142
|
-
fuid, fgid = stat.uid, stat.gid
|
143
|
-
# Zero out the third digit (in octal).
|
144
|
-
# We could use a constant here, but this works
|
145
|
-
# for a mask of any length
|
146
|
-
if uid != fuid
|
147
|
-
mask &= ~(mask & 0700)
|
148
|
-
end
|
149
|
-
if !@groups[fgid].include?(uid)
|
150
|
-
mask &= ~(mask & 0070)
|
77
|
+
# If HomeFS is running as root, drop_priv sets the effective user ID
|
78
|
+
# and effective group ID to _uid_ and _gid_, respectively, and then
|
79
|
+
# calls the block passed to the method. Before returning, drop_priv
|
80
|
+
# that the EUID and EGID are each set back to 0.
|
81
|
+
# @param [Integer] uid the user ID to set the effective user ID to
|
82
|
+
# @param [Integer] gid the group ID to set the effective group ID to
|
83
|
+
# @yield an environment with the EUID and EGID set to _uid_ and _gid_
|
84
|
+
# @return the result of the block
|
85
|
+
def drop_priv(uid, gid)
|
86
|
+
if Process::Sys.getuid == 0
|
87
|
+
begin
|
88
|
+
Process::Sys.setegid(gid)
|
89
|
+
Process::Sys.seteuid(uid)
|
90
|
+
ret = yield
|
91
|
+
ensure
|
92
|
+
Process::Sys.seteuid(0)
|
93
|
+
Process::Sys.setegid(0)
|
151
94
|
end
|
152
|
-
end
|
153
|
-
fmode & mask != 0
|
154
|
-
end
|
155
|
-
|
156
|
-
# Test whether we can write to the given path. If no file exists at _path_,
|
157
|
-
# we test if the parent directory is either writable or has the sticky bit
|
158
|
-
# set.
|
159
|
-
# @param [String] path the path to test
|
160
|
-
# @param [Integer] uid the UID to check the privilage of. If nil, this
|
161
|
-
# method returns whether the file is writable by anyone.
|
162
|
-
# @return [Boolean] whether the given path is writable
|
163
|
-
def writable?(path, uid = nil)
|
164
|
-
if !File.exist?(path)
|
165
|
-
mode_mask(File.dirname(path), 01222, uid)
|
166
95
|
else
|
167
|
-
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
# Test whether the user described by _uid_ can read from the given
|
172
|
-
# path. If no file exists at the given path, an Errno::ENOENT is
|
173
|
-
# raised.
|
174
|
-
# @param [String] path the path to test
|
175
|
-
# @param [Integer] uid the UID to check the privilage of. If nil, this
|
176
|
-
# method returns whether the file is readable by anyone.
|
177
|
-
# @return [Boolean] whether the given path is readable
|
178
|
-
# @raise [Errno::ENOENT] if no file exists at _path_
|
179
|
-
def readable?(path, uid = nil)
|
180
|
-
mode_mask(path, 0444, uid)
|
181
|
-
end
|
182
|
-
|
183
|
-
# Test whether the user described by _uid_ can execute the given
|
184
|
-
# path. If no file exists at the given path, an Errno::ENOENT is
|
185
|
-
# raised.
|
186
|
-
# @param [String] path the path to test
|
187
|
-
# @param [Integer] uid the UID to check the privilage of. If nil, this
|
188
|
-
# method returns whether the file is executable by anyone.
|
189
|
-
# @return [Boolean] whether the given path is executable
|
190
|
-
# @raise [Errno::ENOENT] if no file exists at _path_
|
191
|
-
def executable?(path, uid = nil)
|
192
|
-
mode_mask(path, 0111, uid)
|
193
|
-
end
|
194
|
-
|
195
|
-
# This method raises an Errno::EACCES if the user given by _uid_ cannot
|
196
|
-
# write to the given path. The check is the same as in {#writable?}.
|
197
|
-
# @param [String] relpath the path to test, relative to the root of
|
198
|
-
# HomeFS (not the actual filesystem)
|
199
|
-
# @param [Integer] uid the UID to check the privilage of
|
200
|
-
# @return [void]
|
201
|
-
# @raise [Errno::EACCES] if the path is not writable by the given user
|
202
|
-
def check_writable(uid, relpath)
|
203
|
-
hpath = homepath(uid, relpath)
|
204
|
-
unless writable?(hpath, uid)
|
205
|
-
fail Errno::EACCES, relpath
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
209
|
-
# This method raises an Errno::EACCES if the user given by _uid_ cannot
|
210
|
-
# read from the given path.
|
211
|
-
# @param [String] relpath the path to test, relative to the root of
|
212
|
-
# HomeFS (not the actual filesystem)
|
213
|
-
# @param [Integer] uid the UID to check the privilage of
|
214
|
-
# @return [void]
|
215
|
-
# @raise [Errno::EACCES] if the path is not readable by the given user
|
216
|
-
# @raise [Errno::ENOENT] if no file or directory exists at the given
|
217
|
-
# path
|
218
|
-
def check_readable(uid, relpath)
|
219
|
-
hpath = homepath(uid, relpath)
|
220
|
-
unless readable?(hpath, uid)
|
221
|
-
fail Errno::EACCES, relpath
|
222
|
-
end
|
223
|
-
end
|
224
|
-
|
225
|
-
# This method raises an Errno::EACCES if the user given by _uid_ cannot
|
226
|
-
# list the given directory, i.e., if the user does not have the execute
|
227
|
-
# and read permissions on the given directory. If the path given is not
|
228
|
-
# a directory, the dirname of the given path is tested.
|
229
|
-
# @param [String] relpath the path to test, relative to the root of
|
230
|
-
# HomeFS (not the actual filesystem)
|
231
|
-
# @param [Integer] uid the UID to check the privilage of
|
232
|
-
# @return [void]
|
233
|
-
# @raise [Errno::EACCES] if the path is not readable by the given user
|
234
|
-
def check_listable(uid, relpath)
|
235
|
-
hpath = homepath(uid, relpath)
|
236
|
-
unless File.directory?(hpath)
|
237
|
-
hpath = File.dirname(hpath)
|
238
|
-
end
|
239
|
-
unless readable?(hpath) && executable?(hpath)
|
240
|
-
fail Errno::EACCES, relpath
|
241
|
-
end
|
242
|
-
end
|
243
|
-
|
244
|
-
# This method raises an Errno::EACCES if the user given by _uid_ is not
|
245
|
-
# the owner of the file described by _relpath_
|
246
|
-
# @param [String] relpath the path to test, relative to the root of
|
247
|
-
# HomeFS (not the actual filesystem)
|
248
|
-
# @param [Integer] uid the UID to check ownership of
|
249
|
-
# @return [void]
|
250
|
-
# @raise [Errno::EACCES] if the path is not owned by the given user
|
251
|
-
# @raise [Errno::ENOENT] if no file or directory exists at the given
|
252
|
-
# path
|
253
|
-
def check_owner(uid, relpath)
|
254
|
-
hpath = File.dirname(homepath(uid, relpath))
|
255
|
-
unless File.stat(hpath).uid == uid
|
256
|
-
fail Errno::EACCES, relpath
|
96
|
+
ret = yield
|
257
97
|
end
|
98
|
+
return ret
|
258
99
|
end
|
259
100
|
|
260
101
|
# Check access permissions
|
@@ -269,8 +110,16 @@ class HomeFS
|
|
269
110
|
# called directly
|
270
111
|
def access(context, path, mode)
|
271
112
|
uid = context.uid
|
272
|
-
|
273
|
-
|
113
|
+
hpath = homepath(uid, path)
|
114
|
+
drop_priv(uid, context.gid) do
|
115
|
+
access = true
|
116
|
+
check_read = (mode & 4 != 0)
|
117
|
+
check_write = (mode & 2 != 0)
|
118
|
+
check_execute = (mode & 1 != 0)
|
119
|
+
access &&= File.readable?(hpath) if check_read
|
120
|
+
access &&= File.writable?(hpath) if check_write
|
121
|
+
access &&= File.executable?(hpath) if check_execute
|
122
|
+
access
|
274
123
|
end
|
275
124
|
end
|
276
125
|
|
@@ -285,9 +134,11 @@ class HomeFS
|
|
285
134
|
# @note This method should usually only be called by FUSE, and not
|
286
135
|
# called directly
|
287
136
|
def chmod(context, path, mode)
|
288
|
-
|
289
|
-
|
290
|
-
|
137
|
+
uid = context.uid
|
138
|
+
hpath = homepath(uid, path)
|
139
|
+
drop_priv(uid, context.gid) do
|
140
|
+
File.chmod(mode, hpath)
|
141
|
+
end
|
291
142
|
end
|
292
143
|
|
293
144
|
# Change file ownership
|
@@ -302,9 +153,11 @@ class HomeFS
|
|
302
153
|
# @note This method should usually only be called by FUSE, and not
|
303
154
|
# called directly
|
304
155
|
def chown(context, path, uid, gid)
|
305
|
-
|
306
|
-
hpath = homepath(
|
307
|
-
|
156
|
+
uid = context.uid
|
157
|
+
hpath = homepath(uid, path)
|
158
|
+
drop_priv(uid, context.gid) do
|
159
|
+
File.chown(mode, hpath)
|
160
|
+
end
|
308
161
|
end
|
309
162
|
|
310
163
|
# Create and open a file
|
@@ -321,23 +174,11 @@ class HomeFS
|
|
321
174
|
# @note This method should usually only be called by FUSE, and not
|
322
175
|
# called directly
|
323
176
|
def create(context, path, mode, ffi)
|
324
|
-
|
325
|
-
hpath = homepath(
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
# race-condition. To be more specific, if this filesystem is
|
330
|
-
# running as root (which it usually is), the file will be created
|
331
|
-
# owned by root. If the mode were set when the file is created,
|
332
|
-
# then an attacker could create a setuid file writable by anyone,
|
333
|
-
# and then, in the 'real' filesystem (not HomeFS), quickly write a
|
334
|
-
# small program that just executes /bin/bash. If the write
|
335
|
-
# completed before handle.chown was called, the attacker could have
|
336
|
-
# a setuid shell.
|
337
|
-
handle = File.new(hpath, File::CREAT | File::WRONLY, 0600)
|
338
|
-
handle.chown(context.uid, context.gid)
|
339
|
-
handle.chmod(mode)
|
340
|
-
ffi.fh = handle
|
177
|
+
uid = context.uid
|
178
|
+
hpath = homepath(uid, path)
|
179
|
+
ffi.fh = drop_priv(uid, context.gid) do
|
180
|
+
File.new(hpath, File::CREAT | File::WRONLY, mode)
|
181
|
+
end
|
341
182
|
end
|
342
183
|
|
343
184
|
# Get attributes of an open file
|
@@ -431,8 +272,11 @@ class HomeFS
|
|
431
272
|
# @note This method should usually only be called by FUSE, and not
|
432
273
|
# called directly
|
433
274
|
def getattr(context, path)
|
434
|
-
|
435
|
-
|
275
|
+
uid = context.uid
|
276
|
+
hpath = homepath(uid, path)
|
277
|
+
drop_priv(uid, context.gid) do
|
278
|
+
File.lstat(hpath)
|
279
|
+
end
|
436
280
|
end
|
437
281
|
|
438
282
|
# Called when filesystem is initialised
|
@@ -457,20 +301,22 @@ class HomeFS
|
|
457
301
|
# @note This method should usually only be called by FUSE, and not
|
458
302
|
# called directly
|
459
303
|
def link(context, from, to)
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
304
|
+
uid = context.uid
|
305
|
+
hfrom = homepath(uid, from)
|
306
|
+
drop_priv(uid, context.gid) do
|
307
|
+
File.link(hfrom, to)
|
308
|
+
end
|
464
309
|
end
|
465
310
|
|
466
311
|
# (see RFuse::Fuse#mkdir)
|
467
312
|
# @note This method should usually only be called by FUSE, and not
|
468
313
|
# called directly
|
469
314
|
def mkdir(context, path, mode)
|
470
|
-
|
471
|
-
hpath = homepath(
|
472
|
-
|
473
|
-
|
315
|
+
uid = context.uid
|
316
|
+
hpath = homepath(uid, path)
|
317
|
+
drop_priv(uid, context.gid) do
|
318
|
+
Dir.mkdir(hpath, mode)
|
319
|
+
end
|
474
320
|
end
|
475
321
|
|
476
322
|
# File open operation
|
@@ -488,24 +334,17 @@ class HomeFS
|
|
488
334
|
# @note This method should usually only be called by FUSE, and not
|
489
335
|
# called directly
|
490
336
|
def open(context, path, ffi)
|
491
|
-
|
492
|
-
|
493
|
-
elsif ffi.flags & File::WRONLY != 0
|
494
|
-
mask = 0222
|
495
|
-
else
|
496
|
-
mask = 0444
|
497
|
-
end
|
498
|
-
hpath = homepath(context.uid, path)
|
499
|
-
unless mode_mask(hpath, mask, context.uid)
|
500
|
-
fail Errno::EACCES, path
|
501
|
-
end
|
337
|
+
uid = context.uid
|
338
|
+
hpath = homepath(uid, path)
|
502
339
|
|
503
340
|
# We pass the flags straight into File.open because the constants
|
504
341
|
# in RFuse::Fcntl and File::Constants have the same values, because
|
505
342
|
# they both have the same values as the open syscall. If this were
|
506
343
|
# not the case, we'd have the map the values of RFuse::Fcntl to
|
507
344
|
# their equivalents in File::Constants
|
508
|
-
|
345
|
+
drop_priv(uid, context.gid) do
|
346
|
+
ffi.fh = File.open(hpath, ffi.flags)
|
347
|
+
end
|
509
348
|
end
|
510
349
|
|
511
350
|
# Open directory
|
@@ -524,8 +363,11 @@ class HomeFS
|
|
524
363
|
# @note This method should usually only be called by FUSE, and not
|
525
364
|
# called directly
|
526
365
|
def opendir(context, path, ffi)
|
527
|
-
|
528
|
-
|
366
|
+
uid = context.uid
|
367
|
+
hpath = homepath(uid, path)
|
368
|
+
ffi.fh = drop_priv(uid, context.gid) do
|
369
|
+
Dir.new(hpath)
|
370
|
+
end
|
529
371
|
end
|
530
372
|
|
531
373
|
# Read data from an open file
|
@@ -547,7 +389,7 @@ class HomeFS
|
|
547
389
|
else
|
548
390
|
ffi.fh.seek(offset, :SET)
|
549
391
|
end
|
550
|
-
ffi.fh.read(size)
|
392
|
+
ffi.fh.read(size) || ''
|
551
393
|
end
|
552
394
|
|
553
395
|
# (see RFuse::Fuse#readdir)
|
@@ -603,12 +445,12 @@ class HomeFS
|
|
603
445
|
# @note This method should usually only be called by FUSE, and not
|
604
446
|
# called directly
|
605
447
|
def rename(context, from, to)
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
448
|
+
uid = context.uid
|
449
|
+
hfrom = homepath(uid, from)
|
450
|
+
hto = homepath(uid, to)
|
451
|
+
drop_priv(uid, context.gid) do
|
452
|
+
FileUtils.mv(hfrom, hto, :force => true)
|
453
|
+
end
|
612
454
|
end
|
613
455
|
|
614
456
|
# Create a symbolic link
|
@@ -624,12 +466,10 @@ class HomeFS
|
|
624
466
|
# @note This method should usually only be called by FUSE, and not
|
625
467
|
# called directly
|
626
468
|
def symlink(context, to, from)
|
627
|
-
|
628
|
-
hfrom = homepath(
|
629
|
-
|
630
|
-
|
631
|
-
File.lchown(context.uid, context.gid, hfrom)
|
632
|
-
rescue
|
469
|
+
uid = context.uid
|
470
|
+
hfrom = homepath(uid, from)
|
471
|
+
drop_priv(uid, context.gid) do
|
472
|
+
File.symlink(to, hfrom)
|
633
473
|
end
|
634
474
|
end
|
635
475
|
|
@@ -644,8 +484,11 @@ class HomeFS
|
|
644
484
|
# @note This method should usually only be called by FUSE, and not
|
645
485
|
# called directly
|
646
486
|
def truncate(context, path, offset)
|
647
|
-
|
648
|
-
|
487
|
+
uid = context.uid
|
488
|
+
hpath = homepath(uid, path)
|
489
|
+
drop_priv(uid, context.gid) do
|
490
|
+
File.truncate(hpath, offset)
|
491
|
+
end
|
649
492
|
end
|
650
493
|
|
651
494
|
# Remove a file
|
@@ -658,8 +501,11 @@ class HomeFS
|
|
658
501
|
# @note This method should usually only be called by FUSE, and not
|
659
502
|
# called directly
|
660
503
|
def unlink(context, path)
|
661
|
-
|
662
|
-
|
504
|
+
uid = context.uid
|
505
|
+
hpath = homepath(uid, path)
|
506
|
+
drop_priv(uid, context.gid) do
|
507
|
+
File.unlink(hpath)
|
508
|
+
end
|
663
509
|
end
|
664
510
|
|
665
511
|
# Change access/modification times of a file
|
@@ -674,8 +520,11 @@ class HomeFS
|
|
674
520
|
# @note This method should usually only be called by FUSE, and not
|
675
521
|
# called directly
|
676
522
|
def utimens(context, path, actime, modtime)
|
677
|
-
|
678
|
-
|
523
|
+
uid = context.uid
|
524
|
+
hpath = homepath(uid, path)
|
525
|
+
drop_priv(uid, context.gid) do
|
526
|
+
File.utime(actime, modtime, hpath)
|
527
|
+
end
|
679
528
|
end
|
680
529
|
|
681
530
|
# Write data to an open file
|
@@ -717,9 +566,11 @@ begin
|
|
717
566
|
# @note This method is only defined if the gem 'ffi-xattr' is
|
718
567
|
# available
|
719
568
|
def getxattr(context, path, name)
|
720
|
-
|
721
|
-
hpath = homepath(
|
722
|
-
|
569
|
+
uid = context.uid
|
570
|
+
hpath = homepath(uid, path)
|
571
|
+
drop_priv(uid, context.gid) do
|
572
|
+
check_nodata Xattr::Lib.get(hpath, false, name)
|
573
|
+
end
|
723
574
|
end
|
724
575
|
|
725
576
|
# Set extended attributes
|
@@ -737,9 +588,11 @@ begin
|
|
737
588
|
# @note This method is only defined if the gem 'ffi-xattr' is
|
738
589
|
# available
|
739
590
|
def setxattr(context, path, name, data, flags)
|
740
|
-
|
741
|
-
hpath = homepath(
|
742
|
-
|
591
|
+
uid = context.uid
|
592
|
+
hpath = homepath(uid, path)
|
593
|
+
drop_priv(uid, context.gid) do
|
594
|
+
Xattr::Lib.set(hpath, false, name, data)
|
595
|
+
end
|
743
596
|
end
|
744
597
|
|
745
598
|
# List extended attributes
|
@@ -754,9 +607,11 @@ begin
|
|
754
607
|
# @note This method is only defined if the gem 'ffi-xattr' is
|
755
608
|
# available
|
756
609
|
def listxattr(context, path)
|
757
|
-
|
758
|
-
hpath = homepath(
|
759
|
-
|
610
|
+
uid = context.uid
|
611
|
+
hpath = homepath(uid, path)
|
612
|
+
drop_priv(uid, context.gid) do
|
613
|
+
check_nodata Xattr::Lib.list(hpath, false)
|
614
|
+
end
|
760
615
|
end
|
761
616
|
|
762
617
|
# Remove extended attribute
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: homefs
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dylan Frese
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-10-
|
11
|
+
date: 2015-10-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rfuse
|