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

Instance Method Summary collapse

Instance Attribute Details

#authenticatorObject

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

#logObject

Logger object used for logging server events



65
66
67
# File 'lib/rstyx/server.rb', line 65

def log
  @log
end

#msizeObject

maximum message size supported



62
63
64
# File 'lib/rstyx/server.rb', line 62

def msize
  @msize
end

#myauthObject

Server’s authentication information



78
79
80
# File 'lib/rstyx/server.rb', line 78

def myauth
  @myauth
end

#rootObject

The root of the file tree for this server



68
69
70
# File 'lib/rstyx/server.rb', line 68

def root
  @root
end

#secretObject

Shared secret obtained during Inferno authentication



84
85
86
# File 'lib/rstyx/server.rb', line 84

def secret
  @secret
end

#sessionObject

The session object corresponding to this connection



75
76
77
# File 'lib/rstyx/server.rb', line 75

def session
  @session
end

#userauthObject

Connected peer’s authentication information



81
82
83
# File 'lib/rstyx/server.rb', line 81

def userauth
  @userauth
end

Instance Method Details

#post_initObject



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.message} for #{msg.to_s}")
    reply(Message::Rerror.new(:ename => "Error: #{e.message}"), tag)
  rescue Exception => e
    @log.error("#{@peername} internal error #{e.message} for #{e.to_s} at #{e.backtrace}")
    reply(Message::Rerror.new(:ename => "Internal RStyx Error: #{e.message}"), 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
    message, @msgbuffer = @msgbuffer.unpack("a#{length}a*")
    styxmsg = Message::StyxMessage.from_bytes(message)
    @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:

  1. The client has not done a version negotiation yet.

  2. 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.permissions & 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.permissions & 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:

  1. The client specified more than MAXWELEM path elements in the walk message.

  2. The client tried to walk to a fid that was already previously opened.

  3. The client used a newfid not the same as fid, where newfid is a fid already assigned to some other file on the server.

  4. The client tried to walk to a file which is not a directory.

  5. The client tried to descend the directory tree to a directory to which execute permission is not available.

  6. 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

#unbindObject



108
109
110
# File 'lib/rstyx/server.rb', line 108

def unbind
  @log.info("#{@peername} session closed")
end