Module: RStyx::Server::StyxServerProtocol
- Defined in:
- lib/rstyx/server.rb
Overview
Message receiving module for the Styx server. The server will assemble all inbound messages
Constant Summary collapse
- DEFAULT_MSIZE =
8216
Instance Attribute Summary collapse
-
#authenticator ⇒ Object
An authenticator which is sent messages received from the client.
-
#log ⇒ Object
Logger object used for logging server events.
-
#msize ⇒ Object
maximum message size supported.
-
#myauth ⇒ Object
Server’s authentication information.
-
#root ⇒ Object
The root of the file tree for this server.
-
#secret ⇒ Object
Shared secret obtained during Inferno authentication.
-
#session ⇒ Object
The session object corresponding to this connection.
-
#userauth ⇒ Object
Connected peer’s authentication information.
Instance Method Summary collapse
- #post_init ⇒ Object
-
#process_styxmsg(msg) ⇒ Object
Process a StyxMessage.
-
#receive_data(data) ⇒ Object
Receive data from the network connection, called by EventMachine.
-
#reply(msg, tag) ⇒ Object
Send a reply back to the peer.
-
#tattach(msg) ⇒ Object
Handle attach messages.
-
#tauth(msg) ⇒ Object
Handle auth messages.
-
#tclunk(msg) ⇒ Object
Handle clunk messages.
-
#tcreate(msg) ⇒ Object
Handle tcreate messages.
-
#tflush(msg) ⇒ Object
Handle flush messages.
-
#topen(msg) ⇒ Object
Handle open messages.
-
#tread(msg) ⇒ Object
Handle reads.
-
#tremove(msg) ⇒ Object
Handle remove messages.
-
#tstat(msg) ⇒ Object
Handle stat messages.
-
#tversion(msg) ⇒ Object
Handle version messages.
-
#twalk(msg) ⇒ Object
Handle walk messages.
-
#twrite(msg) ⇒ Object
Handle writes.
-
#twstat(msg) ⇒ Object
Handle wstat messages.
- #unbind ⇒ Object
Instance Attribute Details
#authenticator ⇒ Object
An authenticator which is sent messages received from the client. Used when doing Inferno authentication.
72 73 74 |
# File 'lib/rstyx/server.rb', line 72 def authenticator @authenticator end |
#log ⇒ Object
Logger object used for logging server events
65 66 67 |
# File 'lib/rstyx/server.rb', line 65 def log @log end |
#msize ⇒ Object
maximum message size supported
62 63 64 |
# File 'lib/rstyx/server.rb', line 62 def msize @msize end |
#myauth ⇒ Object
Server’s authentication information
78 79 80 |
# File 'lib/rstyx/server.rb', line 78 def myauth @myauth end |
#root ⇒ Object
The root of the file tree for this server
68 69 70 |
# File 'lib/rstyx/server.rb', line 68 def root @root end |
#secret ⇒ Object
Shared secret obtained during Inferno authentication
84 85 86 |
# File 'lib/rstyx/server.rb', line 84 def secret @secret end |
#session ⇒ Object
The session object corresponding to this connection
75 76 77 |
# File 'lib/rstyx/server.rb', line 75 def session @session end |
#userauth ⇒ Object
Connected peer’s authentication information
81 82 83 |
# File 'lib/rstyx/server.rb', line 81 def userauth @userauth end |
Instance Method Details
#post_init ⇒ Object
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
# File 'lib/rstyx/server.rb', line 88 def post_init @msize = DEFAULT_MSIZE # Buffer for messages received from the client @msgbuffer = "" # Session object for this session @session = Session.new(self) # Conveniences to allow the logger and root to be # more easily accessible from within the mixin. # Try to get the peername if available pname = get_peername() # XXX - We should be using unpack_sockaddr_un for # Unix domain sockets... if pname.nil? @peername = "(unknown peer)" else port, host = Socket.unpack_sockaddr_in(pname) @peername = "#{host}:#{port}" end end |
#process_styxmsg(msg) ⇒ Object
Process a StyxMessage.
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 |
# File 'lib/rstyx/server.rb', line 583 def process_styxmsg(msg) begin tag = msg.tag @session.add_tag(tag) # call the appropriate handler method based on the name # of the StyxMessage subclass. These methods should either # return a normal response, or raise an exception of # some sort that (usually) gets turned by this block into # an Rerror response based on the exception's message. pname = msg.class.name.split("::")[-1].downcase.intern resp = self.send(pname, msg) if resp.nil? raise StyxException.new("internal error: empty reply") end reply(resp, tag) rescue TagInUseException => e # In this case, we can't reply with an error to the client, # since the tag used was invalid! If debug level is high # enough, simply print out an error. @log.error("#{@peername} #{e.class.to_s} #{msg.to_s}") rescue FidNotFoundException => e @log.error("#{@peername} unknown fid in message #{msg.to_s}") reply(Message::Rerror.new(:ename => "Unknown fid #{e.fid}"), tag) rescue StyxException => e @log.error("#{@peername} styx exception #{e.} for #{msg.to_s}") reply(Message::Rerror.new(:ename => "Error: #{e.}"), tag) rescue Exception => e @log.error("#{@peername} internal error #{e.} for #{e.to_s} at #{e.backtrace}") reply(Message::Rerror.new(:ename => "Internal RStyx Error: #{e.}"), tag) end end |
#receive_data(data) ⇒ Object
Receive data from the network connection, called by EventMachine.
620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 |
# File 'lib/rstyx/server.rb', line 620 def receive_data(data) # If we are in keyring authentication mode, write any data received # into the @auth's buffer, and simply return. unless @authenticator.nil? @authenticator << data return end @msgbuffer << data # self.class.log.debug(" << #{data.unpack("H*").inspect}") while @msgbuffer.length > 4 length = @msgbuffer.unpack("V")[0] # Break out if there is not enough data in the message # buffer to construct a message. if @msgbuffer.length < length break end # Decode the received data , @msgbuffer = @msgbuffer.unpack("a#{length}a*") styxmsg = Message::StyxMessage.from_bytes() @log.debug("#{@peername} >> #{styxmsg.to_s}") process_styxmsg(styxmsg) # after all this is done, there may still be enough data in # the message buffer for more messages so keep looping. end # If we get here, we don't have enough data in the buffer to # build a new message, so we just have to wait until there is # enough. end |
#reply(msg, tag) ⇒ Object
Send a reply back to the peer
569 570 571 572 573 574 575 576 577 578 |
# File 'lib/rstyx/server.rb', line 569 def reply(msg, tag) # Check if the tag is still available. If it has been # flushed, don't send the reply. if @session.has_tag?(tag) msg.tag = tag @log.debug("#{@peername} << #{msg.to_s}") send_data(msg.to_bytes) @session.release_tag(tag) end end |
#tattach(msg) ⇒ Object
Handle attach messages. Internally, this will result in the fid passed by the client being associated with the root of the Styx server’s file system. Possible error conditions here:
-
The client has not done a version negotiation yet.
-
The client has provided a fid which it is already using for something else.
– External methods used:
Session#version_negotiated? * Session#has_fid? * Session#[]= * SFile#qid (root) * ++
167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/rstyx/server.rb', line 167 def tattach(msg) # Do not allow attaches without version negotiation unless @session.version_negotiated? raise StyxException.new("Tversion not seen") end # Check that the supplied fid isn't already used. if @session.has_fid?(msg.fid) raise StyxException.new("fid already in use") end # Associate the fid with the root of the server. @session[msg.fid] = @root return(Message::Rattach.new(:qid => @root.qid)) end |
#tauth(msg) ⇒ Object
Handle auth messages. This should be filled in later, depending on the auth methods that we decide to support.
147 148 149 |
# File 'lib/rstyx/server.rb', line 147 def tauth(msg) return(Message::Rerror.new(:ename => "Authentication methods through auth messages are not supported.")) end |
#tclunk(msg) ⇒ Object
Handle clunk messages.
424 425 426 427 |
# File 'lib/rstyx/server.rb', line 424 def tclunk(msg) @session.clunk(msg.fid) return(Message::Rclunk.new) end |
#tcreate(msg) ⇒ Object
Handle tcreate messages
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 |
# File 'lib/rstyx/server.rb', line 328 def tcreate(msg) dir = @session[msg.fid] unless dir.directory? raise StyxException.new("can't create a file inside another file") end unless @session.writable?(dir) raise StyxException.new("permission denied, no write permissions to parent directory") end # Check the file type perm = msg.perm isdir = (perm & DMDIR) != 0 isapponly = (perm & DMAPPEND) != 0 isexclusive = (perm & DMEXCL) != 0 isauth = (perm & DMAUTH) != 0 if isauth # Auth files cannot be created by Styx messages raise StyxException.new("can't create a file of type DMAUTH") end # Get the low 9 bits of the permission number (these low 9 bits # are the rwxrwxrwx file permissions) operm = msg.perm & 01777 # Get the real permissions of this file. This depends on the # permissions of the parent directory realperm = operm if isdir realperm = operm & (~0777 | (dir. & 0777)) # directories must be opened with OREAD (no other bits set) if msg.mode != OREAD raise StyxException.new("when creating a directory must open with read permission only") end else realperm = operm & (~0666 | (dir. & 0666)) end # Create the file in the directory, add it to the directory tree, # and associate the new file with the given fid new_file = dir.newfile(msg.name, realperm, isdir, isapponly, isexclusive) dir << new_file @session[msg.fid] = new_file new_file.add_client(SFileClient.new(@session, msg.fid, msg.mode)) return(Message::Rcreate.new(:qid => new_file.qid, :iounit => @session.iounit)) end |
#tflush(msg) ⇒ Object
Handle flush messages. The only result of this message is it causes the server to forget about the tag passed: any I/O already in progress when the flush message is received is not actually aborted. This is also the way JStyx handles it. Unfortunately, these semantics are wrong from the Inferno manual, viz. flush(5):
If no response is received before the Rflush, the
flushed transaction is considered to have been cancelled,
and should be treated as though it had never been sent.
XXX - The current implementation doesn’t do this. If a Twrite is flushed, the write will still occur, but no response will be sent back (except for some clients, such as JStyx and RStyx which send the Rflush back to the flushed transaction). Some means, possibly a session-wide global transaction lock on server internal state changes may be necessary to allow flushes of this kind to work. – External methods used:
Session#flush_tag * ++
205 206 207 208 |
# File 'lib/rstyx/server.rb', line 205 def tflush(msg) @session.flush_tag(msg.oldtag) return(Message::Rflush.new) end |
#topen(msg) ⇒ Object
Handle open messages. – External methods used:
Session#[] Session#confirm_open SFile#add_client SFile#set_mtime SFile#qid Session#iounit Session#user ++
315 316 317 318 319 320 321 322 323 324 |
# File 'lib/rstyx/server.rb', line 315 def topen(msg) sf = @session[msg.fid] mode = msg.mode @session.confirm_open(sf, mode) sf.add_client(SFileClient.new(@session, msg.fid, mode)) if mode & OTRUNC == OTRUNC sf.set_mtime(Time.now, @session.user) end return(Message::Ropen.new(:qid => sf.qid, :iounit => @session.iounit)) end |
#tread(msg) ⇒ Object
Handle reads
379 380 381 382 383 384 385 386 387 388 389 390 391 392 |
# File 'lib/rstyx/server.rb', line 379 def tread(msg) sf = @session[msg.fid] # Check if the file is open for reading clnt = sf.client(@session, msg.fid) if clnt.nil? || !clnt.readable? raise StyxException.new("file is not open for reading") end if msg.count > @session.iounit raise StyxException.new("cannot request more than #{@session.iounit} bytes in a single read") end return(sf.read(clnt, msg.offset, msg.count)) end |
#tremove(msg) ⇒ Object
Handle remove messages.
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 |
# File 'lib/rstyx/server.rb', line 432 def tremove(msg) # A remove is just like a clunk with the side effect of # removing the file if the permissions allow. sf = @session[msg.fid] sf.synchronize do @session.clunk(msg.fid) parent = sf.parent unless @session.writable?(parent) raise StyxException.new("permission denied") end if sf.instance_of?(SDirectory) && sf.child_count != 0 raise StyxException.new("directory not empty") end sf.remove parent.set_mtime(Time.now, @session.user) end return(Message::Rremove.new) end |
#tstat(msg) ⇒ Object
Handle stat messages
455 456 457 458 459 |
# File 'lib/rstyx/server.rb', line 455 def tstat(msg) sf = @session[msg.fid] # Stat requests require no special permissions return(Message::Rstat.new(:stat => sf.stat)) end |
#tversion(msg) ⇒ Object
Handle version messages. This handles the version negotiation. At this point, the only version of the protocol supported is 9P2000: all other version strings result in the server returning ‘unknown’ in its Rversion. A successful Tversion/Rversion negotiation results in the protocol_negotiated flag in the current session becoming true, and all other outstanding I/O on the session (e.g. opened fids and the like) all removed. – External methods used:
Session#reset_session * ++
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/rstyx/server.rb', line 125 def tversion(msg) @cversion = msg.version @cmsize = msg.msize if @cversion != "9P2000" # Unsupported protocol version. As per Inferno's version(5): # # If the server does not understand the client's version # string, it should respond with an Rversion message (not # Rerror) with the _version_ string the 7 characters 'unknown'. # return(Message::Rversion.new(:version => "unknown", :msize => 0)) end # Reset the session, which also causes the protocol negotiated # flag in the session to be set to true. @session.reset_session(@cmsize) return(Message::Rversion.new(:version => "9P2000", :msize => @msize)) end |
#twalk(msg) ⇒ Object
Handle walk messages.
Possible error conditions:
-
The client specified more than MAXWELEM path elements in the walk message.
-
The client tried to walk to a fid that was already previously opened.
-
The client used a newfid not the same as fid, where newfid is a fid already assigned to some other file on the server.
-
The client tried to walk to a file which is not a directory.
-
The client tried to descend the directory tree to a directory to which execute permission is not available.
-
The client was unable to walk beyond the root to the file specified.
Note that if several parts of the walk managed to succeed, this method will still return an Rwalk response, but it will NOT associate newfid with anything. – External methods used:
Session#[] * Session#[]= * Session#has_fid? *
SFile#client SFile#directory? SFile#name SFile#atime= SFile#[] SFile#qid ++
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 |
# File 'lib/rstyx/server.rb', line 245 def twalk(msg) if msg.wnames.length > MAXWELEM raise StyxException.new("Too many path elements in Twalk message") end fid = msg.fid # Check that the fid has not already been opened by the client sf = @session[fid] clnt = sf.client(@session, fid) unless clnt.nil? raise StyxException.new("cannot walk to an open fid") end nfid = msg.newfid # if the original and new fids are different, check that # the new fid isn't already in use. if nfid != fid && @session.has_fid?(nfid) raise StyxException.new("fid already in use") end rwalk = Message::Rwalk.new(:qids => []) num = 0 msg.wnames.each do |n| unless sf.directory? raise StyxException.new("#{sf.name} is not a directory") end # Check file permissions if we're descending if n == ".." && !@session.execute?(sf) raise StyxException.new("#{sf.name}: permission denied") end sf.atime = Time.now sf = sf[n] if sf.nil? # Send an error response if the number of walked elements is 0 if num == 0 raise StyxException.new("file does not exist") end break end sf.atime = Time.now # This allows a client to get a fid representing the directory # at the end of the walk, even if the client does not have # execute permissions on that directory. Therefore, in Inferno, # a client could cd into a directory but be unable to read # any of its contents. rwalk.qids << sf.qid sf.refresh num += 1 end if rwalk.qids.length == msg.wnames.length # The whole walk operation was successful. Associate # the new fid with the returned file. @session[nfid] = sf end return(rwalk) end |
#twrite(msg) ⇒ Object
Handle writes
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 |
# File 'lib/rstyx/server.rb', line 397 def twrite(msg) sf = @session[msg.fid] # Check that the file is open for writing clnt = sf.client(@session, msg.fid) if (clnt.nil? || !clnt.writable?) raise StyxException.new("file is not open for writing") end if msg.data.length > @session.iounit raise StyxException.new("cannot write more than #{@session.iounit} bytes in a single operation") end truncate = clnt.truncate? ofs = msg.offset # If this is an append-only file we ignore the specified offset # and just write to the end of the file, without truncation. # This relies on the SFile#length method returning an accurate # value. if sf.appendonly? ofs = sf.length truncate = false end return(sf.write(clnt, ofs, msg.data, truncate)) end |
#twstat(msg) ⇒ Object
Handle wstat messages
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 |
# File 'lib/rstyx/server.rb', line 464 def twstat(msg) nstat = msg.stat sf = @session[msg.fid] sf.synchronize do # Check if we are changing the file's name unless nstat.name.empty? dir = sf.parent unless @session.writable?(dir) raise StyxException.new("write permissions required on parent directory to change file name") end if dir.has_child?(nstat.name) raise StyxException.new("cannot rename file to the name of an existing file") end sf.can_setname?(nstat.name) end # Check if we are changing the length of a file if nstat.size != -1 # Check if we have write permission on the file unless @session.writable?(sf) raise StyxException.new("write permissions required to change file length") end sf.can_setlength?(nstat.size) end # Check if we are changing the mode of a file if nstat.mode != MAXUINT # Must be the file owner to change the file mode if sf.uid != @session.user raise StyxException.new("must be owner to change file mode") end # Can't change the directory bit if ((nstat.mode & DMDIR == DMDIR) && !sf.directory?) raise StyxException.new("can't change a file to a directory") end sf.can_setmode?(nstat.mode) end # Check if we are changing the last modification time of a file if nstat.mtime != MAXUINT # Must be owner if sf.uid != @session.user raise StyxException.new("must be owner to change mtime") end sf.can_setmtime?(nstat.mtime) end # Check if we are changing the gid of a file unless nstat.gid.empty? # Disallowed for now raise StyxException.new("can't change gid on this server") end # No other types are possible for now unless nstat.dtype == 0xffff raise StyxException.new("can't change type") end unless nstat.dev == 0xffffffff raise StyxException.new("can't change dev") end unless nstat.qid == Message::Qid.new(0xff, 0xffffffff, 0xffffffffffffffff) raise StyxException.new("can't change qid") end unless nstat.atime == 0xffffffff raise StyxException.new("can't change atime directly") end unless nstat.uid.empty? raise StyxException.new("can't change uid") end unless nstat.muid.empty? raise StyxException.new("can't change user who last modified file directly") end # Now, all the permissions have been checked, we can actually go # ahead with all the changes unless nstat.name.empty? sf.name = nstat.name end if nstat.size != -1 sf.length = nstat.size end if nstat.mode != MAXUINT sf.mode = nstat.mode end if nstat.mtime != MAXUINT sf.mtime = nstat.mtime end end return(Message::Rwstat.new) end |
#unbind ⇒ Object
108 109 110 |
# File 'lib/rstyx/server.rb', line 108 def unbind @log.info("#{@peername} session closed") end |