# fsh - fast remote execution # Copyright (C) 1999-2001 by Per Cederqvist. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ import os import errno import string import sys import fcntl import fshcompat # The number of characters that a session is always granted to send at # startup. QUOTA = 128 * 1024 class cursor_eof(Exception): """Used by cursor to signal that the list of strings is exhausted. """ pass class cursor: """Helper class for parse_line. Extract strings from a buffer that is implemented as a list of strings. """ def __init__(self, q): """Set up a cursor for the list of strings Q. """ self.q = q self.l = -1 self.__next_line() def incr(self): """Step the cursor forward. May raise cursor_eof. """ if self.l >= len(self.q): raise cursor_eof self.c = self.c + 1 if self.c >= len(self.q[self.l]): self.__next_line() def __next_line(self): self.c = 0 while 1: self.l = self.l + 1 if self.l >= len(self.q): return if len(self.q[self.l]) > 0: return def peek(self): """Return the character pointed to by this cursor. May raise cursor_eof. """ if self.l >= len(self.q): raise cursor_eof return self.q[self.l][self.c] def mark(self): """Remember the current position. See extract(). """ self.mark_l = self.l self.mark_c = self.c def extract(self): """Return the contents between the mark and the current position. The mark is set using mark(). The return value is a string. May raise cursor_eof. """ if self.l >= len(self.q): raise cursor_eof if self.mark_l == self.l: return self.q[self.l][self.mark_c:self.c] elif self.mark_l == self.l - 1: return (self.q[self.mark_l][self.mark_c:] + self.q[self.l][:self.c]) else: return (self.q[self.mark_l][self.mark_c:] + string.join(self.q[self.mark_l + 1 : self.l], "") + self.q[self.l][:self.c]) def incr_many(self, sz): """Moves the current position SZ characters forward. May raise cursor_eof. """ while sz > 0: if self.l >= len(self.q): raise cursor_eof left = len(self.q[self.l]) - self.c if sz < left: self.c = self.c + sz sz = 0 else: self.__next_line() sz = sz - left def discard_handled(self): """Remove everything up to the current position. This destructively alters the list supplied to the constructor of this cursor. The cursor cannot be used once this method has been called. """ del self.q[0:self.l] if self.c != 0: self.q[0] = self.q[0][self.c:] self.q = None self.c = None self.l = None def parse_line(queue, want_session): """Parse one command line from the QUEUE of input. The QUEUE should be a list of strings. Any parsed data will be destructively removed from it. If a syntax error occurs the QUEUE will be cleared. A line is expected to contain: A command (consisting of any character but space) A single space. A session number (if, and only if, want_session is true). An optional space and hollerith-encoded string. A newline. Note that the hollerith-encoded string, if present, may contain any character (including newline). The return value is a list containing: The command (as a string). The session number (if, and only if, want_session is true). The string, or None if no hollerith-encoded string was present. """ if want_session: nothing = [None, None, None] else: nothing = [None, None] try: c = cursor(queue) # Find the command. c.mark() while not c.peek() in " \n": c.incr() cmd = c.extract() # Find the session number, if wanted. if want_session: c.incr() c.mark() while c.peek() not in " \n": c.incr() session = string.atoi(c.extract()) # Find the string length, if any. if c.peek() == " ": c.incr() c.mark() while c.peek() != "H": if c.peek() not in string.digits: del queue[:] return nothing c.incr() sz = string.atoi(c.extract()) c.incr() c.mark() c.incr_many(sz) data = c.extract() else: data = None if c.peek() != "\n": del queue[:] return nothing c.incr() c.discard_handled() if want_session: return [cmd, session, data] else: return [cmd, data] except cursor_eof: return nothing def hollerith(n): """Return a hollerith-encoding of N. N may be a string of integer. """ if type(n) == type(0): s = str(n) else: s = n return "%dH%s" % (len(s), s) def set_nonblocking(fd): """Set file descriptor FD nonblocking. """ oldflags = fcntl.fcntl(fd, fshcompat.F_GETFD) fcntl.fcntl(fd, fshcompat.F_SETFD, oldflags | fshcompat.O_NONBLOCK) def read(fd, queue, sz): """Read at most SZ bytes from FD and store the result in QUEUE. Returns -1 on end of file, or 0 on success. QUEUE should be a list of strings. If anything is read it will be appended to QUEUE. """ try: data = os.read(fd, sz) except os.error, (e, emsg): if e == errno.EINTR or e == errno.EAGAIN or e == errno.EWOULDBLOCK: return 0 else: raise if data == "": return -1 queue.append(data) return 0 def write(fd, queue): """Write the contents of QUEUE to FD. FD is assumed to be in non-blocking mode. This function returns when there is nothing more to write, or when a write would block. Returns the number of bytes written, or -1 if the file descriptor was closed. This function may issue several os.write calls. If a few of them succeeds and the file descriptor is closed -1 will be returned. All data that was written will be destructively removed from QUEUE. """ ret = 0 while 1: # Loop as long as we can write anything. # Make sure there is some data to write at the start of the queue. # Return if the queue is empty. while 1: if len(queue) == 0: return ret if len(queue[0]) == 0: del queue[0] else: break # Avoid writing small chunks. while len(queue) > 1 and len(queue[0]) + len(queue[1]) < 4096: queue[0] = queue[0] + queue[1] del queue[1] # Write a chunk of data. Return if failure. try: sz = os.write(fd, queue[0]) except os.error, (e, emsg): if e == errno.EINTR or e == errno.EAGAIN or e == errno.EWOULDBLOCK: return ret elif e == errno.EPIPE: return -1 else: raise if sz == 0: raise "huh? zero-write?" # Remove the chunk just written. if sz == len(queue[0]): del queue[0] else: queue[0] = queue[0][sz:] ret = ret + sz def print_version(prog): import fshversion print prog, "from fsh version", fshversion.version print "Copyright (C) 1999-2001 Per Cederqvist" sys.exit(0) def fshd_socket(server, method, login): """Return the name of the fshd socket, and the containing directory. """ # The shell-quote encoding is good enough for this purpose as well. # It would be enough to only quote "/" and "\0" here, as long as # we are using a traditional Unix-like filesystem. server = shell_quote(server) method = shell_quote(method) login = shell_quote(login) d = "/tmp/fshd-%d" % (os.getuid(), ) f = "%s.%s.%s" % (server, method, login) return (os.path.join(d, f), d) __safe_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" def shell_quote(s): """Quote s so that it is safe for all common shells. """ res = [] for c in s: if c in __safe_chars: res.append(c) else: res.append("=%02x" % ord(c)) return string.join(res, '') def shell_unquote(s): """Unquote a string quoted by shell_quote. """ if s == "": return "" frags = string.split(s, "=") res = [frags[0]] for f in frags[1:]: res.append(chr(string.atoi(f[:2], 0x10))) res.append(f[2:]) return string.join(res, '')