rstyx 0.2.0 → 0.3.0

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.
@@ -0,0 +1,1305 @@
1
+ #!/usr/bin/ruby
2
+ #
3
+ # Copyright (C) 2005-2007 Rafael Sevilla
4
+ # This file is part of RStyx
5
+ #
6
+ # RStyx is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as
8
+ # published by the Free Software Foundation; either version 2.1
9
+ # of the License, or (at your option) any later version.
10
+ #
11
+ # RStyx is distributed in the hope that it will be useful, but
12
+ # WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public
17
+ # License along with RStyx; if not, write to the Free Software
18
+ # Foundation, Inc., 51 Franklin St., Fifth Floor, Boston, MA
19
+ # 02110-1301 USA.
20
+ #
21
+ # Styx Server
22
+ #
23
+ # To create a Styx server, one has to create an SDirectory object that
24
+ # acts as the server root, e.g.:
25
+ #
26
+ # sd = RStyx::Server::SDirectory.new("/")
27
+ # sf = RStyx::Server::InMemoryFile.new("test.file")
28
+ # sf.contents = "hello"
29
+ # sd << sf
30
+ # serv = RStyx::Server::TCPServer.new(:bindaddr => "0.0.0.0",
31
+ # :port => 9876,
32
+ # :root => sd)
33
+ # serv.run.join
34
+ #
35
+ # Author:: Rafael R. Sevilla (mailto:dido@imperium.ph)
36
+ # Copyright:: Copyright (c) 2005-2007 Rafael R. Sevilla
37
+ # License:: GNU Lesser General Public License
38
+ #
39
+ # $Id: server.rb 231 2007-08-10 08:43:57Z dido $
40
+ #
41
+ require 'thread'
42
+ require 'monitor'
43
+ require 'timeout'
44
+ require 'rubygems'
45
+ require 'eventmachine'
46
+ require 'logger'
47
+ require 'socket'
48
+ require 'rstyx/common'
49
+ require 'rstyx/messages'
50
+ require 'rstyx/errors'
51
+
52
+
53
+ module RStyx
54
+ module Server
55
+
56
+ ##
57
+ # Message receiving module for the Styx server. The server will
58
+ # assemble all inbound messages
59
+ #
60
+ module StyxServerProtocol
61
+ attr_accessor :sentmessages, :msize, :log, :root
62
+
63
+ DEFAULT_MSIZE = 8216
64
+
65
+ def post_init
66
+ @msize = DEFAULT_MSIZE
67
+ # Buffer for messages received from the client
68
+ @msgbuffer = ""
69
+ # Session object for this session
70
+ @session = Session.new(self)
71
+ # Conveniences to allow the logger and root to be
72
+ # more easily accessible from within the mixin.
73
+ # Try to get the peername if available
74
+ pname = get_peername()
75
+ # XXX - We should be using unpack_sockaddr_un for
76
+ # Unix domain sockets...
77
+ if pname.nil?
78
+ @peername = "(unknown peer)"
79
+ else
80
+ port, host = Socket.unpack_sockaddr_in(pname)
81
+ @peername = "#{host}:#{port}"
82
+ end
83
+ end
84
+
85
+ def unbind
86
+ @log.info("#{@peername} session closed")
87
+ end
88
+
89
+ ##
90
+ # Handle version messages. This handles the version negotiation.
91
+ # At this point, the only version of the protocol supported is
92
+ # 9P2000: all other version strings result in an error being
93
+ # returned to the client. A successful Tversion/Rversion
94
+ # negotiation results in the protocol_negotiated flag in the
95
+ # current session becoming true, and all other outstanding I/O
96
+ # on the session (e.g. opened fids and the like) all removed.
97
+ #
98
+ # External methods used:
99
+ #
100
+ # Session#reset_session *
101
+ #
102
+ def tversion(msg)
103
+ @cversion = msg.version
104
+ @cmsize = msg.msize
105
+ if @cversion != "9P2000"
106
+ # Unsupported protocol version
107
+ return(Message::Rerror.new(:ename => "Unsupported protocol version #{@cversion} (must be 9P2000)"))
108
+ end
109
+ # Reset the session, which also causes the protocol negotiated
110
+ # flag in the session to be set to true.
111
+ @session.reset_session(@cmsize)
112
+ return(Message::Rversion.new(:version => "9P2000", :msize => @msize))
113
+ end
114
+
115
+ ##
116
+ # Handle auth messages. This should be filled in later,
117
+ # depending on the auth methods that we decide to support.
118
+ #
119
+ def tauth(msg)
120
+ return(Message::Rerror.new(:ename => "Authentication methods through auth messages are not used."))
121
+ end
122
+
123
+ ##
124
+ # Handle attach messages. Internally, this will result in the
125
+ # fid passed by the client being associated with the root of the
126
+ # Styx server's file system. Possible error conditions here:
127
+ #
128
+ # 1. The client has not done a version negotiation yet.
129
+ # 2. The client has provided a fid which it is already using
130
+ # for something else.
131
+ #
132
+ # External methods used:
133
+ #
134
+ # Session#version_negotiated? *
135
+ # Session#has_fid? *
136
+ # Session#[]= *
137
+ # SFile#qid (root) *
138
+ #
139
+ def tattach(msg)
140
+ # Do not allow attaches without version negotiation
141
+ unless @session.version_negotiated?
142
+ raise StyxException.new("Tversion not seen")
143
+ end
144
+ # Check that the supplied fid isn't already used.
145
+ if @session.has_fid?(msg.fid)
146
+ raise StyxException.new("fid already in use")
147
+ end
148
+ # Associate the fid with the root of the server.
149
+ @session[msg.fid] = @root
150
+ return(Message::Rattach.new(:qid => @root.qid))
151
+ end
152
+
153
+ ##
154
+ # Handle flush messages. The only result of this message
155
+ # is it causes the server to forget about the tag passed:
156
+ # any I/O already in progress when the flush message is
157
+ # received is not actually aborted. This is also the way
158
+ # JStyx handles it. Unfortunately, these semantics are wrong
159
+ # from the Inferno manual, viz. flush(5):
160
+ #
161
+ # If no response is received before the Rflush, the
162
+ # flushed transaction is considered to have been cancelled,
163
+ # and should be treated as though it had never been sent.
164
+ #
165
+ # XXX - The current implementation doesn't do this. If a
166
+ # Twrite is flushed, the write will still occur, but no
167
+ # response will be sent back (except for some clients, such
168
+ # as JStyx and RStyx which send the Rflush back to the
169
+ # flushed transaction). Some means, possibly a session-wide
170
+ # global transaction lock on server internal state changes
171
+ # may be necessary to allow flushes of this kind to work.
172
+ #
173
+ # External methods used:
174
+ #
175
+ # Session#flush_tag *
176
+ #
177
+ def tflush(msg)
178
+ @session.flush_tag(msg.oldtag)
179
+ return(Message::Rflush.new)
180
+ end
181
+
182
+ ##
183
+ # Handle walk messages.
184
+ #
185
+ # Possible error conditions:
186
+ #
187
+ # 1. The client specified more than MAXWELEM path elements in the
188
+ # walk message.
189
+ # 2. The client tried to walk to a fid that was already previously
190
+ # opened.
191
+ # 3. The client used a newfid not the same as fid, where newfid
192
+ # is a fid already assigned to some other file on the server.
193
+ # 4. The client tried to walk to a file which is not a directory.
194
+ # 5. The client tried to descend the directory tree to a directory
195
+ # to which execute permission is not available.
196
+ # 6. The client was unable to walk beyond the root to the file
197
+ # specified.
198
+ #
199
+ # Note that if several parts of the walk managed to succeed, this
200
+ # method will still return an Rwalk response, but it will NOT
201
+ # associate newfid with anything.
202
+ #
203
+ # External methods used:
204
+ #
205
+ # Session#[] *
206
+ # Session#[]= *
207
+ # Session#has_fid? *
208
+ #
209
+ # SFile#client
210
+ # SFile#directory?
211
+ # SFile#name
212
+ # SFile#atime=
213
+ # SFile#[]
214
+ # SFile#qid
215
+ #
216
+ #
217
+ def twalk(msg)
218
+ if msg.wnames.length > MAXWELEM
219
+ raise StyxException.new("Too many path elements in Twalk message")
220
+ end
221
+ fid = msg.fid
222
+ # Check that the fid has not already been opened by the client
223
+ sf = @session[fid]
224
+ clnt = sf.client(@session, fid)
225
+ unless clnt.nil?
226
+ raise StyxException.new("cannot walk to an open fid")
227
+ end
228
+ nfid = msg.newfid
229
+ if nfid != fid
230
+ # if the original and new fids are different, check that
231
+ # the new fid isn't already in use.
232
+ if (@session.has_fid?(nfid))
233
+ raise StyxException.new("fid already in use")
234
+ end
235
+ end
236
+ rwalk = Message::Rwalk.new(:qids => [])
237
+ num = 0
238
+ msg.wnames.each do |n|
239
+ unless sf.directory?
240
+ raise StyxException.new("#{sf.name} is not a directory")
241
+ end
242
+ # Check file permissions if we're descending
243
+ if n == ".." && !@session.execute?(sf)
244
+ raise StyxException.new("#{sf.name}: permission denied")
245
+ end
246
+ sf.atime = Time.now
247
+ sf = sf[n]
248
+ if sf.nil?
249
+ # Send an error response if the number of walked elements is 0
250
+ if num == 0
251
+ raise StyxException.new("file does not exist")
252
+ end
253
+ break
254
+ end
255
+ # This allows a client to get a fid representing the directory
256
+ # at the end of the walk, even if the client does not have
257
+ # execute permissions on that directory. Therefore, in Inferno,
258
+ # a client could cd into a directory but be unable to read
259
+ # any of its contents.
260
+ rwalk.qids << sf.qid
261
+ sf.refresh
262
+ num += 1
263
+ end
264
+
265
+ if rwalk.qids.length == msg.wnames.length
266
+ # The whole walk operation was successful. Associate
267
+ # the new fid with the returned file.
268
+ @session[nfid] = sf
269
+ end
270
+
271
+ return(rwalk)
272
+ end
273
+
274
+ ##
275
+ # Handle open messages.
276
+ #
277
+ # External methods used:
278
+ #
279
+ # Session#[]
280
+ # Session#confirm_open
281
+ # SFile#add_client
282
+ # SFile#set_mtime
283
+ # SFile#qid
284
+ # Session#iounit
285
+ # Session#user
286
+ #
287
+ def topen(msg)
288
+ sf = @session[msg.fid]
289
+ mode = msg.mode
290
+ @session.confirm_open(sf, mode)
291
+ sf.add_client(SFileClient.new(@session, msg.fid, mode))
292
+ if mode & OTRUNC == OTRUNC
293
+ sf.set_mtime(Time.now, @session.user)
294
+ end
295
+ return(Message::Ropen.new(:qid => sf.qid, :iounit => @session.iounit))
296
+ end
297
+
298
+ ##
299
+ # Handle tcreate messages
300
+ def tcreate(msg)
301
+ dir = @session[msg.fid]
302
+ unless dir.directory?
303
+ raise StyxException.new("can't create a file inside another file")
304
+ end
305
+
306
+ unless @session.writable?(dir)
307
+ raise StyxException.new("permission denied, no write permissions to parent directory")
308
+ end
309
+
310
+ # Create the file in the directory. Note that SDirectory#newfile
311
+ # has to do all of the permission checking and all that.
312
+ new_file = dir.newfile(msg.name, msg.perm)
313
+ dir << new_file
314
+ @session[msg.fid] = new_file
315
+ new_file.add_client(SFileClient.new(@session, msg.fid, msg.mode))
316
+ return(Message::Rcreate.new(:qid => new_file.qid,
317
+ :iounit => @session.iounit))
318
+ end
319
+
320
+ ##
321
+ # Handle reads
322
+ #
323
+ def tread(msg)
324
+ sf = @session[msg.fid]
325
+ # Check if the file is open for reading
326
+ clnt = sf.client(@session, msg.fid)
327
+ if clnt.nil? || !clnt.readable?
328
+ raise StyxException.new("file is not open for reading")
329
+ end
330
+
331
+ if msg.count > @session.iounit
332
+ raise StyxException.new("cannot request more than #{@session.iounit} bytes in a single read")
333
+ end
334
+
335
+ return(sf.read(clnt, msg.offset, msg.count))
336
+ end
337
+
338
+ ##
339
+ # Handle writes
340
+ #
341
+ def twrite(msg)
342
+ sf = @session[msg.fid]
343
+ # Check that the file is open for writing
344
+ clnt = sf.client(@session, msg.fid)
345
+ if (clnt.nil? || !clnt.writable?)
346
+ raise StyxException.new("file is not open for writing")
347
+ end
348
+ if msg.data.length > @session.iounit
349
+ raise StyxException.new("cannot write more than #{@session.iounit} bytes in a single operation")
350
+ end
351
+ truncate = clnt.truncate?
352
+ ofs = msg.offset
353
+ # If this is an append-only file we ignore the specified offset
354
+ # and just write to the end of the file, without truncation.
355
+ # This relies on the SFile#length method returning an accurate
356
+ # value.
357
+ if sf.appendonly?
358
+ ofs = sf.length
359
+ truncate = false
360
+ end
361
+
362
+ return(sf.write(clnt, ofs, msg.data, truncate))
363
+ end
364
+
365
+ ##
366
+ # Handle clunk messages.
367
+ #
368
+ def tclunk(msg)
369
+ @session.clunk(msg.fid)
370
+ return(Message::Rclunk.new)
371
+ end
372
+
373
+ ##
374
+ # Handle remove messages.
375
+ #
376
+ def tremove(msg)
377
+ # A remove is just like a clunk with the side effect of
378
+ # removing the file if the permissions allow.
379
+ sf = @session[msg.fid]
380
+ sf.lock do
381
+ @session.clunk(msg.fid)
382
+ parent = sf.parent
383
+ if @session.writable?(parent)
384
+ raise StyxException.new("permission denied")
385
+ end
386
+
387
+ if sf.instance_of?(SDirectory) && sf.child_count != 0
388
+ raise StyxException.new("directory not empty")
389
+ end
390
+ sf.remove
391
+ end
392
+ parent.set_mtime(Time.now, @session.user)
393
+ return(Message::Rremove.new)
394
+ end
395
+
396
+ ##
397
+ # Handle stat messages
398
+ #
399
+ def tstat(msg)
400
+ sf = @session[msg.fid]
401
+ # Stat requests require no special permissions
402
+ return(Message::Rstat.new(:stat => sf.stat))
403
+ end
404
+
405
+ ##
406
+ # Handle wstat messages
407
+ #
408
+ def twstat(msg)
409
+ nstat = msg.stat
410
+ sf = @session[msg.fid]
411
+ sf.lock do
412
+ # Check if we are changing the file's name
413
+ unless nstat.name.empty?
414
+ dir = sf.parent
415
+ unless @session.writable?(dir)
416
+ raise StyxException.new("write permissions required on parent directory to change file name")
417
+ end
418
+ unless dir.has_child?(nstat.name)
419
+ raise StyxException.new("cannot rename file to the name of an existing file")
420
+ end
421
+ sf.can_setname?(nstat.name)
422
+ end
423
+
424
+ # Check if we are changing the length of a file
425
+ if nstat.size != -1
426
+ # Check if we have write permission on the file
427
+ if @session.writable?(sf)
428
+ raise StyxException.new("write permissions required to change file length")
429
+ end
430
+ sf.can_setlength?(sf.size)
431
+ end
432
+
433
+ # Check if we are changing the mode of a file
434
+ if nstat.mode != MAXUINT
435
+ # Must be the file owner to change the file mode
436
+ if sf.uid != @session.user
437
+ raise StyxException.new("must be owner to change file mode")
438
+ end
439
+
440
+ # Can't change the directory bit
441
+ if ((nstat.mode & DMDIR == DMDIR) != sf.directory?)
442
+ raise StyxException.new("can't change a file to a directory")
443
+ end
444
+ sf.can_setmode?(nstat.mode)
445
+ end
446
+
447
+ # Check if we are changing the last modification time of a file
448
+ if nstat.mtime != MAXUINT
449
+ # Must be owner
450
+ if sf.uid != @session.user
451
+ raise StyxException.new("must be owner to change mtime")
452
+ end
453
+ sf.can_setmtime?(nstat.mtime)
454
+ end
455
+
456
+ # Check if we are changing the gid of a file
457
+ unless nstat.gid.empty?
458
+ # Disallowed for now
459
+ raise StyxException.new("can't change gid on this server")
460
+ end
461
+
462
+ # No other types are possible for now
463
+ unless nstat.dtype == 0xffff
464
+ raise StyxException.new("can't change type")
465
+ end
466
+
467
+ unless nstat.dev == 0xffffffff
468
+ raise StyxException.new("can't change dev")
469
+ end
470
+
471
+ unless nstat.qid == Message::Qid.new(0xff, 0xffffffff,
472
+ 0xffffffffffffffff)
473
+ raise StyxException.new("can't change qid")
474
+ end
475
+
476
+ unless nstat.atime == 0xffffffff
477
+ raise StyxException.new("can't change atime directly")
478
+ end
479
+
480
+ unless nstat.uid.empty?
481
+ raise StyxException.new("can't change uid")
482
+ end
483
+
484
+ unless nstat.muid.empty?
485
+ raise StyxException.new("can't change user who last modified file directly")
486
+ end
487
+
488
+ # Now, all the permissions have been checked, we can actually go
489
+ # ahead with all the changes
490
+ unless nstat.name.empty?
491
+ sf.name = nstat.name
492
+ end
493
+
494
+ if nstat.size != -1
495
+ sf.length = nstat.length
496
+ end
497
+
498
+ if nstat.mode != MAXUINT
499
+ sf.mode = nstat.mode
500
+ end
501
+
502
+ if nstat.mtime != MAXUINT
503
+ sf.mtime = nstat.mtime
504
+ end
505
+
506
+ end
507
+
508
+ return(Message::Rwstat.new)
509
+ end
510
+
511
+ ##
512
+ # Send a reply back to the peer
513
+ def reply(msg, tag)
514
+ # Check if the tag is still available. If it has been
515
+ # flushed, don't send the reply.
516
+ if @session.has_tag?(tag)
517
+ msg.tag = tag
518
+ @log.debug("#{@peername} << #{msg.to_s}")
519
+ send_data(msg.to_bytes)
520
+ @session.release_tag(tag)
521
+ end
522
+ end
523
+
524
+ ##
525
+ # Process a StyxMessage.
526
+ #
527
+ def process_styxmsg(msg)
528
+ begin
529
+ tag = msg.tag
530
+ @session.add_tag(tag)
531
+ # call the appropriate handler method based on the name
532
+ # of the StyxMessage subclass. These methods should either
533
+ # return a normal response, or raise an exception of
534
+ # some sort that (usually) gets turned by this block into
535
+ # an Rerror response based on the exception's message.
536
+ pname = msg.class.name.split("::")[-1].downcase.intern
537
+ resp = self.send(pname, msg)
538
+ if resp.nil?
539
+ raise StyxException.new("internal error: empty reply")
540
+ end
541
+ reply(resp, tag)
542
+ rescue TagInUseException => e
543
+ # In this case, we can't reply with an error to the client,
544
+ # since the tag used was invalid! If debug level is high
545
+ # enough, simply print out an error.
546
+ @log.error("#{@peername} #{e.to_s} #{msg.to_s}")
547
+ rescue FidNotFoundException => e
548
+ @log.error("#{@peername} unknown fid in message #{msg.to_s}")
549
+ reply(Message::Rerror.new(:ename => "Unknown fid #{e.fid}"), tag)
550
+ rescue StyxException => e
551
+ @log.error("#{@peername} styx exception #{e.message} in #{msg.to_s}")
552
+ reply(Message::Rerror.new(:ename => "Error: #{e.message}"), tag)
553
+ end
554
+
555
+ end
556
+
557
+
558
+ ##
559
+ # Receive data from the network connection, called by EventMachine.
560
+ #
561
+ def receive_data(data)
562
+ @msgbuffer << data
563
+ # self.class.log.debug(" << #{data.unpack("H*").inspect}")
564
+ while @msgbuffer.length > 4
565
+ length = @msgbuffer.unpack("V")[0]
566
+ # Break out if there is not enough data in the message
567
+ # buffer to construct a message.
568
+ if @msgbuffer.length < length
569
+ break
570
+ end
571
+
572
+ # Decode the received data
573
+ message, @msgbuffer = @msgbuffer.unpack("a#{length}a*")
574
+ styxmsg = Message::StyxMessage.from_bytes(message)
575
+ @log.debug("#{@peername} >> #{styxmsg.to_s}")
576
+ process_styxmsg(styxmsg)
577
+
578
+ # after all this is done, there may still be enough data in
579
+ # the message buffer for more messages so keep looping.
580
+ end
581
+ # If we get here, we don't have enough data in the buffer to
582
+ # build a new message, so we just have to wait until there is
583
+ # enough.
584
+ end
585
+
586
+ end
587
+
588
+ ##
589
+ # Session state of a Styx connection.
590
+ #
591
+ class Session < Monitor
592
+
593
+ attr_accessor :msize, :auth, :fids, :tags, :version_negotiated, :user
594
+ attr_accessor :iounit
595
+
596
+ def initialize(conn)
597
+ @conn = conn
598
+ @version_negotiated = false
599
+ @msize = 0
600
+ @user = nil
601
+ @auth = false
602
+ @fids = {}
603
+ @tags = []
604
+ end
605
+
606
+ def version_negotiated?
607
+ return(@version_negotiated)
608
+ end
609
+
610
+ def reset_session(msize)
611
+ # XXX: clunk all outstanding fids and release all outstanding tags
612
+ @version_negotiated = true
613
+ @iounit = msize
614
+ end
615
+
616
+ ##
617
+ # Associates a FID with a file. The FID passed must be checked before
618
+ # using this or the old FID will be forgotten.
619
+ #
620
+ def []=(fid, file)
621
+ @fids.delete(fid)
622
+ @fids[fid] = file
623
+ end
624
+
625
+ ##
626
+ # Gets the file associated with the indexed FID. Raises a
627
+ # FidNotFoundException if the fid is not present.
628
+ #
629
+ def [](fid)
630
+ unless has_fid?(fid)
631
+ raise FidNotFoundException.new(fid)
632
+ end
633
+ return(@fids[fid])
634
+ end
635
+
636
+ def has_fid?(fid)
637
+ return(@fids.has_key?(fid))
638
+ end
639
+
640
+ def clunk(fid)
641
+ unless @fids.has_key?(fid)
642
+ raise FidNotFoundException.new(fid)
643
+ end
644
+ sf = self[fid]
645
+ sf.synchronize do
646
+ # Get the client using this fid, and see whether the file
647
+ # is requested to be deleted on clunk.
648
+ sfc = sf.client(self, fid)
649
+ if (!sfc.nil? && sfc.orclose?)
650
+ begin
651
+ sf.remove
652
+ rescue Exception => e
653
+ # if there was a problem removing the file, ignore it
654
+ end
655
+ sf.remove_client(sfc)
656
+ end
657
+ end
658
+ end
659
+
660
+ def clunk_all
661
+ @fids.each_key do |k|
662
+ begin
663
+ clunk(k)
664
+ rescue FidNotFoundException => e
665
+ # ignore this as we are closing down anyway...
666
+ end
667
+ end
668
+ end
669
+
670
+ def has_tag?(tag)
671
+ return(!@tags.index(tag).nil?)
672
+ end
673
+
674
+ ##
675
+ # Adds the given tag to the list of tags in use, first checking to
676
+ # see if it is already in use. Raises a TagInUseException if the
677
+ # tag is already in use.
678
+ #
679
+ def add_tag(tag)
680
+ if has_tag?(tag)
681
+ raise TagInUseException.new(tag)
682
+ end
683
+ @tags << tag
684
+ end
685
+
686
+ alias << add_tag
687
+
688
+ ##
689
+ # Called when a message is replied to, releasing the tag.
690
+ def release_tag(tag)
691
+ @tags.delete(tag)
692
+ end
693
+
694
+ alias flush_tag release_tag
695
+
696
+ def flush_all
697
+ @tags.each do |f|
698
+ flush_tag(t)
699
+ end
700
+ end
701
+
702
+ ##
703
+ # Check the permissions for a given mode
704
+ # +sf+ the file to check against
705
+ # +mode+ the mode to check (OEXEC, OWRITE, or OREAD)
706
+ #
707
+ # XXX: the permissions are only for anonymous access at the
708
+ # moment, so only the world permissions are ever checked.
709
+ #
710
+ def permission?(sf, mode)
711
+ if mode < 0 || mode > 2
712
+ raise "Internal error: mode should be 0, 1, or 2"
713
+ end
714
+ # We bit shift the permissions value so that the mode is
715
+ # represented by the last bit (all) the fourth to last bit
716
+ # (group), and the seventh-to-last bit (user).
717
+ perms = sf.permissions >> mode
718
+ # Check permissions for 'all' -- the low-order bit
719
+ if perms & 0b000_000_001 == 1
720
+ return(true)
721
+ end
722
+
723
+ # XXX: this has to be something more useful!
724
+ return(false)
725
+ end
726
+
727
+ ##
728
+ # Check for executable permission for the styx file
729
+ #
730
+ def execute?(sf)
731
+ end
732
+
733
+ ##
734
+ # Checks that the given file can be opened with the given mode.
735
+ # Raises a StyxException if this is not possible.
736
+ #
737
+ def confirm_open(sf, mode)
738
+ if sf.exclusive? && sf.num_clients != 0
739
+ raise StyxException.new("can't open locked file")
740
+ end
741
+ openmode = mode & 0x03
742
+ # XXX: Needs to be filled out further...
743
+ end
744
+
745
+ end # class Session
746
+
747
+ class Server
748
+ def initialize(config)
749
+ @root = config[:root]
750
+ @log = config[:log] || Logger.new(STDERR)
751
+ @log.level = config[:debug] || Logger::WARN
752
+ end
753
+
754
+ protected
755
+
756
+ def start_server
757
+ end
758
+
759
+ public
760
+
761
+ ##
762
+ # Start the Styx server, returning the thread of the
763
+ # running Styx server instance.
764
+ def run
765
+ t = Thread.new do
766
+ @log.info("starting")
767
+ start_server
768
+ end
769
+ return(t)
770
+ end
771
+
772
+ end # class Server
773
+
774
+ class TCPServer < Server
775
+ def initialize(config)
776
+ @bindaddr = config[:bindaddr]
777
+ @port = config[:port]
778
+ super(config)
779
+ end
780
+
781
+ protected
782
+ def start_server
783
+ EventMachine::run do
784
+ @log.info("TCP server on #{@bindaddr}:#{@port}")
785
+ EventMachine::start_server(@bindaddr, @port,
786
+ StyxServerProtocol) do |conn|
787
+ conn.root = @root
788
+ conn.log = @log
789
+ end
790
+ end
791
+ end
792
+ end
793
+
794
+
795
+ ##
796
+ # Server's representation of the client of an SFile, created when
797
+ # a client opens a file.
798
+ #
799
+ class SFileClient
800
+ attr_reader :session, :fid, :mode
801
+ attr_accessor :offset, :next_file_to_read
802
+
803
+ ##
804
+ # Create a new SFileClient.
805
+ #
806
+ # +session+:: The session object associated with the client.
807
+ # +fid+:: The client's handle to the file. Note that clients may
808
+ # use many fids opened representing the same file.
809
+ # +mode+:: The mode field as received from the client's Topen
810
+ # message (including the OTRUNC and ORCLOSE bits)
811
+ #
812
+ def initialize(session, fid, mode)
813
+ @session = session
814
+ @fid = fid
815
+ @truncate = ((mode & OTRUNC) == OTRUNC)
816
+ @orclose = ((mode & ORCLOSE) == ORCLOSE)
817
+ @mode = mode & 0x03 # mask off all but the last two bits
818
+ # When a client reads from or writes to file, this records the
819
+ # new offset
820
+ @offset = 0
821
+ # Used when reading a directory: stores the index of the next
822
+ # child of an SFile to include in an RreadMessage.
823
+ @next_file_to_read = 0
824
+ end
825
+
826
+ def truncate?
827
+ return(@truncate)
828
+ end
829
+
830
+ def orclose?
831
+ return(@orclose)
832
+ end
833
+
834
+ alias delete_on_clunk? orclose?
835
+
836
+ ##
837
+ # Check to see if the client can read the file (i.e. the client
838
+ # opened it with read access mode)
839
+ #
840
+ def readable?
841
+ return(mode == OREAD || mode == ORDWR)
842
+ end
843
+
844
+ ##
845
+ # Check to see if the client can write to the file (i.e. the client
846
+ # opened it with write access).
847
+ #
848
+ def writable?
849
+ return(mode == OWRITE || mode == ORDWR)
850
+ end
851
+
852
+ end
853
+
854
+ ##
855
+ # Class representing a file (or directory) on a Styx server. There
856
+ # may be different types of file: a file might map directly to a file
857
+ # on disk, or it may be a synthetic file representing a program
858
+ # interface. This class creates a Styx file which does nothing useful:
859
+ # returning errors when reading from or writing to it. Subclasses
860
+ # should override the SFile#read, SFile#write and SFile#length methods
861
+ # to implement the desired behavior. Each Styx file has exactly one
862
+ # parent, the directory which contains it, thus symbolic links on the
863
+ # underlying operating system cannot be represented.
864
+ #
865
+ class SFile < Monitor
866
+
867
+ attr_reader :name, :uid, :gid, :muid, :mtime
868
+ attr_accessor :permissions, :atime, :parent
869
+
870
+ ##
871
+ # Create a new file object with the given permissions.
872
+ # This accepts a hash with the following keys:
873
+ #
874
+ # name:: the name of the file
875
+ # permissions:: The permissions of the file (e.g. 0755 in octal)
876
+ # apponly:: true if the file is append only
877
+ # excl:: true if the file is for exclusive use, i.e. only one
878
+ # client at a time may open.
879
+ # user:: the username of the owner of the file. If not specified
880
+ # gets the value from an environment variable.
881
+ # group:: the group name of the owner of the file. If not specified
882
+ # gets the value from an environment variable.
883
+ #
884
+ #
885
+ def initialize(name, argv={ :permissions => 0666, :apponly => false,
886
+ :excl => false, :uid => ENV["USER"],
887
+ :gid => ENV["GROUP"] })
888
+ super()
889
+ if name == "" || name == "." || name == ".."
890
+ raise StyxException.new("Illegal file name")
891
+ end
892
+ # The parent directory of the file.
893
+ @parent = nil
894
+ # The name of the file.
895
+ @name = name
896
+ # True if this is a directory
897
+ @directory = false
898
+ # True if this is an append-only file
899
+ @appendonly = argv[:apponly]
900
+ # True if this file may be opened by only one client at a time
901
+ @exclusive = argv[:excl]
902
+ # True if this is a file to be used by the authentication mechanism (normally false)
903
+ @auth = false
904
+ # Permissions represented as a number, e.g. 0755 in octal
905
+ @permissions = argv[:permissions]
906
+ # Version number of the file, incremented whenever the file is
907
+ # modified
908
+ @version = 0
909
+ # Time of creation
910
+ @ctime = Time.now
911
+ # Last access time
912
+ @atime = Time.now
913
+ # Last modification time
914
+ @mtime = Time.now
915
+ # Owner name
916
+ @uid = argv[:user]
917
+ # Group name
918
+ @gid = argv[:group]
919
+ # User who last modified the file
920
+ @muid = ""
921
+ # The clients who have a connection to the file
922
+ @clients = []
923
+ @clients.extend(MonitorMixin)
924
+ end
925
+
926
+ ##
927
+ # Check if the name may be changed. Raises a StyxException
928
+ # if this is not possible.
929
+ #
930
+ def can_setname?(name)
931
+ end
932
+
933
+ ##
934
+ # Check if the File is a directory (should always be the same as
935
+ # Object#instance_of?(Directory).
936
+ #
937
+ def directory?
938
+ return(@directory)
939
+ end
940
+
941
+ ##
942
+ # Check if the file is append-only
943
+ #
944
+ def appendonly?
945
+ return(@appendonly)
946
+ end
947
+
948
+ ##
949
+ # Check if the file is marked as exclusive use
950
+ #
951
+ def exclusive?
952
+ return(@exclusive)
953
+ end
954
+
955
+ ##
956
+ # Check if the file is an authenticator
957
+ #
958
+ def auth?
959
+ return(@auth)
960
+ end
961
+
962
+ ##
963
+ # Get the full path relative to the root of the filesystem.
964
+ #
965
+ def full_path
966
+ if auth? || @parent.nil?
967
+ return(@name)
968
+ end
969
+ return(@parent.full_path + @name)
970
+ end
971
+
972
+ ##
973
+ # Get the length of the file. This default implementation returns
974
+ # zero: subclasses must override this method.
975
+ #
976
+ def length
977
+ return(0)
978
+ end
979
+
980
+ ##
981
+ # Gets the type of the file as a number representing the OR of DMDIR,
982
+ # DMAPPEND, DMEXCL, and DMAUTH as appropriate, used to create the Qid.
983
+ #
984
+ def filetype
985
+ type = 0
986
+ if @directory
987
+ type |= DMDIR
988
+ end
989
+
990
+ if @appendonly
991
+ type |= DMAPPEND
992
+ end
993
+
994
+ if @exclusive
995
+ type |= DMEXCL
996
+ end
997
+
998
+ if @auth
999
+ type |= DMAUTH
1000
+ end
1001
+
1002
+ return(type)
1003
+ end
1004
+
1005
+ ##
1006
+ # Gets the mode of the file (permissions and flags)
1007
+ #
1008
+ def mode
1009
+ return(self.filetype | @permissions)
1010
+ end
1011
+
1012
+ ##
1013
+ # Checks to see if this file allows the mode (permissions and flags)
1014
+ # of the file to be changed. This is called when the server receives
1015
+ # a Twstat message. This default implementation does nothing.
1016
+ #
1017
+ # +newmode+: the new mode of the file (permissions plus any other flags
1018
+ # such as DMDIR, etc.)
1019
+ #
1020
+ def can_setmode?(newmode)
1021
+ end
1022
+
1023
+ ##
1024
+ # Sets the mode of the file (permissions plus other flags). Must check
1025
+ # all the relevant permissions and call SFile#can_setmode? before
1026
+ # calling this method, as the assumption is that this method will
1027
+ # always succeed.
1028
+ def mode=(newmode)
1029
+ @appendonly = (newmode & DMAPPEND == DMAPPEND)
1030
+ @exclusive = (newmode & DMEXCL == DMEXCL)
1031
+ @auth = (newmode & DMAUTH == DMAUTH)
1032
+ @permissions = newmode & 0x03fff
1033
+ return(newmode)
1034
+ end
1035
+
1036
+ def qid
1037
+ t = filetype() >> 24 & 0xff
1038
+ q = Message::Qid.new(t, @version, self.uuid)
1039
+ return(q)
1040
+ end
1041
+
1042
+ def stat
1043
+ s = Message::Stat.new
1044
+ s.dtype = s.dev = 0
1045
+ s.qid = self.qid
1046
+ s.mode = self.mode
1047
+ s.atime = @atime.to_i
1048
+ s.mtime = @mtime.to_i
1049
+ s.length = self.length
1050
+ s.name = @name
1051
+ s.uid = @uid
1052
+ s.gid = @gid
1053
+ s.muid = @muid
1054
+ return(s)
1055
+ end
1056
+
1057
+ ##
1058
+ # Check to see if the length of this file can be changed to the
1059
+ # given value. If this does not throw an exception then
1060
+ # SFile#length= should always succeed. The default implementation
1061
+ # always throws an exception; subclasses should override this method
1062
+ # if they want the length of the file to be changeable.
1063
+ #
1064
+ def can_setlength?(newlength)
1065
+ raise StyxException.new("Cannot change the length of this file directly")
1066
+ end
1067
+
1068
+ ##
1069
+ # Sets the length of the file. The usual disclaimers about permissions
1070
+ # and SFile#can_setlength? apply. Default implementation does nothing
1071
+ # and it should be overriden by subclasses.
1072
+ #
1073
+ def length=(newlength)
1074
+ end
1075
+
1076
+ def can_setmtime?(nmtime)
1077
+ end
1078
+
1079
+ def set_mtime(nmtime, uid)
1080
+ @mtime = nmtime
1081
+ @muid = uid
1082
+ end
1083
+
1084
+ def rename(newname)
1085
+ if @parent == nil
1086
+ raise StyxException.new("Cannot change the name of the root directory")
1087
+ end
1088
+ if @parent.has_child?(newname)
1089
+ raise StyxException.new("A file with name #{newname} already exists in this directory")
1090
+ end
1091
+ @name = newname
1092
+ end
1093
+
1094
+ # Gets the unique numeric ID for the path of this file (generated from
1095
+ # the low-order bytes of the creation time and the hashcode of the full
1096
+ # path). If the file is deleted and re-created the unique ID will
1097
+ # change (except for the extremely unlikely case in which the low-order
1098
+ # bytes of the creation time happen to be the same in the new file and
1099
+ # the old file).
1100
+ def uuid
1101
+ tbytes = @ctime.to_i & 0xffffffff
1102
+ return((self.full_path.hash << 32) | tbytes)
1103
+ end
1104
+
1105
+ ##
1106
+ # Reads data from this file. This method should be overridden by
1107
+ # subclasses and should return an Rread with the data read. This
1108
+ # default implementation simply throws a StyxException, which
1109
+ # results in an Rerror being returned to the client. Subclasses
1110
+ # should override this to provide the desired behavior when the
1111
+ # file is read.
1112
+ #
1113
+ def read(client, offset, count)
1114
+ raise StyxException.new("Cannot read from this file")
1115
+ end
1116
+
1117
+
1118
+ ##
1119
+ # Writes data to this file. This method should be overriden by
1120
+ # subclasses to provide the desired behavior when the file is
1121
+ # written to. It should return the number of bytes actually
1122
+ # "written".
1123
+ #
1124
+ def write(client, offset, count, data, truncate)
1125
+ raise StyxException.new("Cannot write to this file")
1126
+ end
1127
+
1128
+ #
1129
+ # Remove the file from the Styx server
1130
+ def remove
1131
+ self.delete
1132
+ self.parent.remove_child(self)
1133
+ end
1134
+
1135
+ def delete
1136
+ end
1137
+
1138
+ def add_client(cl)
1139
+ @clients.synchronize { @clients << cl }
1140
+ self.client_connected(cl)
1141
+ end
1142
+
1143
+ def client_connected(cl)
1144
+ end
1145
+
1146
+ ##
1147
+ # Get the client connection to this file
1148
+ #
1149
+ def client(sess, fid)
1150
+ @clients.synchronize do
1151
+ @clients.each do |cl|
1152
+ if cl.session == sess && cl.fid == fid
1153
+ return(cl)
1154
+ end
1155
+ end
1156
+ end
1157
+ return(nil)
1158
+ end
1159
+
1160
+ def num_clients
1161
+ @clients.synchronize do
1162
+ remove_dead_clients
1163
+ return(@clients.length)
1164
+ end
1165
+ end
1166
+
1167
+ def remove_dead_clients
1168
+ @clients.synchronize do
1169
+ @clients.each do |clnt|
1170
+ if clnt.session.nil? || !clnt.session.connected?
1171
+ remove_client(clnt)
1172
+ end
1173
+ end
1174
+ end
1175
+ end
1176
+
1177
+ def remove_client(cl)
1178
+ unless cl.nil?
1179
+ @clients.delete(cl)
1180
+ client_disconnected(cl)
1181
+ end
1182
+ end
1183
+
1184
+ def client_disconnected(cl)
1185
+ end
1186
+
1187
+ def contents_changed
1188
+ version_incr
1189
+ end
1190
+
1191
+ def version_incr
1192
+ self.version = ((self.version + 1) & 0xffffffffffffffff)
1193
+ end
1194
+
1195
+ def refresh
1196
+ end
1197
+ end # class SFile
1198
+
1199
+ class SDirectory < SFile
1200
+ def initialize(name, argv={ :permissions => 777, :uid => ENV["USER"],
1201
+ :gid => ENV["GROUP"] })
1202
+ # directories cannot be append-only, exclusive, or auth files
1203
+ argv.merge({:apponly => false, :excl => false})
1204
+ super(name, argv)
1205
+ @directory = true
1206
+ @children = []
1207
+ @children.extend(MonitorMixin)
1208
+ end
1209
+
1210
+ def child_exists?(name)
1211
+ @children.synchronize do
1212
+ @children.each do |c|
1213
+ if c.name == name
1214
+ return(true)
1215
+ end
1216
+ end
1217
+ return(false)
1218
+ end
1219
+ end
1220
+
1221
+ ##
1222
+ # Add a child to this directory. If a file with the same name
1223
+ # already exists, throws a FileExists exception.
1224
+ #
1225
+ def <<(child)
1226
+ @children.synchronize do
1227
+ if child_exists?(child.name)
1228
+ raise FileExists("#{sf.name} already exists")
1229
+ end
1230
+ child.parent = self
1231
+ @children << child
1232
+ end
1233
+ return(child)
1234
+ end
1235
+
1236
+ ##
1237
+ # Get the child with the name +name+, or nil if no such file
1238
+ #
1239
+ def [](name)
1240
+ if name == "."
1241
+ return(self)
1242
+ end
1243
+ @children.synchronize do
1244
+ @children.each do |c|
1245
+ if c.name == name
1246
+ return(c)
1247
+ end
1248
+ end
1249
+ return(nil)
1250
+ end
1251
+ end
1252
+
1253
+ ##
1254
+ # Get the number of children this directory has
1255
+ #
1256
+ def child_count
1257
+ return(@children.length)
1258
+ end
1259
+
1260
+ ##
1261
+ # Read the contents of the directory
1262
+ #
1263
+ def read(client, offset, count)
1264
+ # Check that the offset is valid; zero offsets are always valid,
1265
+ # but non-zero offsets are only valid if this client has read part
1266
+ # of the contents of the directory before.
1267
+ if (offset != 0 && offset != client.offset)
1268
+ raise StyxException.new("invalid offset when reading directory")
1269
+ end
1270
+
1271
+ # Create a string to store the serialized stat representations
1272
+ # of the directory's contents.
1273
+ str = ""
1274
+ nextfile = (offset == 0) ? 0 : client.next_file_to_read
1275
+ while (nextfile < @children.length)
1276
+ sf = @children[nextfile]
1277
+ s = sf.stat.to_bytes
1278
+ if (s.length + str.length) > count
1279
+ break
1280
+ end
1281
+ # Add the serialized stat to the buffer
1282
+ str << s
1283
+ nextfile += 1
1284
+ end
1285
+ client.next_file_to_read = nextfile
1286
+ client.offset += str.length
1287
+ return(Message::Rread.new(:data => str))
1288
+
1289
+ end
1290
+
1291
+ end # class SDirectory
1292
+
1293
+ class InMemoryFile < SFile
1294
+ attr_accessor :contents
1295
+
1296
+ def read(client, offset, count)
1297
+ data = @contents[offset..(offset+count)]
1298
+ data ||= ""
1299
+ return(Message::Rread.new(:data => data))
1300
+ end
1301
+ end
1302
+
1303
+ end # module Server
1304
+
1305
+ end # module RStyx