diff --git a/event/sched/GNUmakefile.in b/event/sched/GNUmakefile.in index 36f4260e564e874d7c8763b18aa600336869713d..f6cf02dc7aa1bb7652b7c8804ea6ccc4708e943d 100644 --- a/event/sched/GNUmakefile.in +++ b/event/sched/GNUmakefile.in @@ -27,7 +27,7 @@ DBLIBS = -L/usr/local/lib/mysql -lmysqlclient -lz LIBS += -levent_r -ltb -lcipher -lz ULXRINC = -I/usr/local/include -I/usr/local/include/ulxmlrpcpp -CXXFLAGS += -pthread -O $(ULXRINC) +CXXFLAGS += -pthread -O $(ULXRINC) -I$(OBJDIR) ULXRLIBS = -L/usr/local/lib -lulsshxmlrpcpp -lulxmlrpcpp -lexpat # @@ -62,7 +62,7 @@ rpc.o: rpc.cc rpc.h rrpc.o: rpc.cc rpc.h $(CXX) $(CXXFLAGS) -DSSLRPC $(ULXRINC) -c -o rrpc.o $< -install: +install: event-sched_rpc -mkdir -p $(INSTALL_DIR)/opsdir/sbin $(INSTALL_PROGRAM) event-sched_rpc $(INSTALL_DIR)/opsdir/sbin/event-sched diff --git a/event/sched/event-sched.c b/event/sched/event-sched.c index 124dbfdfc8fe701024ab6bd3acd42edb156095d6..6c099de894eebb53da44698ad456628824703829 100644 --- a/event/sched/event-sched.c +++ b/event/sched/event-sched.c @@ -154,7 +154,9 @@ main(int argc, char **argv) if (!server) server = "localhost"; #ifdef RPC - RPC_init(NULL, BOSSNODE, 0); + if (RPC_init(NULL, BOSSNODE, 0)) { + fatal("could not connect to rpc server"); + } #endif snprintf(buf, sizeof(buf), "elvin://%s%s%s", @@ -193,6 +195,10 @@ main(int argc, char **argv) fatal("could not get static event list"); } +#ifdef RPC + RPC_kill(); +#endif + /* Dequeue events and process them at the appropriate times: */ dequeue(handle); diff --git a/event/sched/rpc.cc b/event/sched/rpc.cc index 4b8ad549b330910f7b07e22eaeecb9c4ab4d7c9a..1d2344803027592e201851c8dbc9ef85372e2012 100644 --- a/event/sched/rpc.cc +++ b/event/sched/rpc.cc @@ -3,9 +3,14 @@ * Copyright (c) 2004 University of Utah and the Flux Group. * All rights reserved. */ + +#include "config.h" + #include "rpc.h" -#define ULXR_INCLUDE_SSL_STUFF +#include <limits.h> +#include <sys/types.h> +#include <pwd.h> #include <ulxmlrpcpp.h> // always first header #include <iostream> @@ -27,38 +32,111 @@ // This is just a stub that calls the realmain in event-sched.c // extern "C" int realmain(int argc, char **argv); - + +/** + * We cache the connection to the server until all of the RPCs have completed + * so we do not have to reconnect. + */ +static struct { + ulxr::Connection *conn; // The cached connection. + ulxr::Protocol *proto; // The cached protocol layer. +} rpc_data; + int main(int argc, char **argv) { - return realmain(argc, argv); + return realmain(argc, argv); } -/* - * Simply save the stuff we need for making the connections later. - */ -static char *CERTPATH; -static char *PATH; -static char *HOST = "localhost"; -static int PORT = 3069; - int RPC_init(char *certpath, char *host, int port) { - printf("%s %s %d\n", certpath, host, port); + int retval = -1; #ifdef SSHRPC - PATH = "xmlrpc"; + { + char identity_path[PATH_MAX]; + struct passwd *pwd; + + /* Construct the path to the identity and */ + pwd = getpwuid(getuid()); + snprintf(identity_path, + sizeof(identity_path), + "%s/.ssh/identity", + pwd->pw_dir); + /* ... check to make sure it is passphrase-less. */ + if (!ulxr::SSHConnection::has_passphraseless_login(identity_path)) { + /* XXX We should just automatically restore from backup here. */ + fprintf(stderr, " *** ~/.ssh/identity is not a passphrase-less key\n"); + fprintf(stderr, " You will need to regenerate the key manually\n"); + return 1; + } + else { + static char *XMLRPC_PATHS[] = { + "xmlrpc", + TBROOT "/sbin/sshxmlrpc_server.py", + NULL + }; + + int lpc; + + /* + * Probe each path looking for one that has the server on the other side. + */ + for (lpc = 0; XMLRPC_PATHS[lpc]; lpc++) { + try { + ulxr::RFC822Protocol *proto; + ulxr::CppString server_name; + + rpc_data.conn = new ulxr::SSHConnection( + pwd->pw_name, host, XMLRPC_PATHS[lpc], + port ? port : ulxr::SSHConnection::DEFAULT_PORT, + ulxr::SSHConnection::DEFAULT_SSH_OPTS + + ULXR_PCHAR(" -1 -i ") + + ULXR_PCHAR(identity_path)); + rpc_data.proto = proto = new ulxr::RFC822Protocol(rpc_data.conn); + + server_name = proto->probe(); + + if (strcmp(server_name.c_str(), "EmulabServer") == 0) { + break; + } + else { + RPC_kill(); + } + } + catch (ulxr::ConnectionException &ce) { + RPC_kill(); + } + } + if (rpc_data.proto == NULL) { + /* Bah, could not connect to the server. */ + return 1; + } + } + } #else - if (certpath) - CERTPATH = strdup(certpath); - else - CERTPATH = "/usr/testbed/etc/client.pem"; + /* XXX THIS IS OUT OF DATE */ + { + ulxr::SSLConnection conn(false, host, port); + ulxr::HttpProtocol proto(&conn, conn.getHostName()); + if (certpath == NULL) + certpath = "/usr/testbed/etc/client.pem"; + conn.setCryptographyData("", certpath, certpath); + } #endif - HOST = strdup(host); - PORT = port; return 0; } +void RPC_kill(void) +{ + if (rpc_data.proto != NULL) { + delete rpc_data.proto; + rpc_data.proto = NULL; + delete rpc_data.conn; + rpc_data.conn = NULL; + } +} + /* * Contact the server and invoke the method. All of the methods we care * about using return a string, which we return to the caller after @@ -73,25 +151,21 @@ RPC_invoke(char *pid, char *eid, char *method, emulab::EmulabResponse *er) try { #ifdef SSHRPC - const char *user = getenv("USER"); - - ulxr::SSHConnection conn(user, HOST, PATH); - ulxr::RFC822Protocol proto(&conn); - emulab::ServerProxy proxy(&proto); + emulab::ServerProxy proxy(rpc_data.proto); #else - ulxr::SSLConnection conn(false, HOST, PORT); - ulxr::HttpProtocol proto(&conn, conn.getHostName()); - conn.setCryptographyData("", CERTPATH, CERTPATH); - emulab::ServerProxy proxy(&proto, false, "/RPC2"); + /* XXX THIS IS OUT OF DATE */ + emulab::ServerProxy proxy(rpc_data.proto, false, "/RPC2"); #endif *er = proxy.invoke(method, - emulab::SPA_String, "proj", pid, - emulab::SPA_String, "exp", eid, - emulab::SPA_TAG_DONE); - + emulab::SPA_String, "proj", pid, + emulab::SPA_String, "exp", eid, + emulab::SPA_TAG_DONE); + if (! er->isSuccess()){ - ULXR_CERR << "SSL_waitforactive failed: " + ULXR_CERR << "RPC_invoke failed: " + << method + << " " << er->getOutput() << std::endl; return -1; diff --git a/event/sched/rpc.h b/event/sched/rpc.h index fd885a5a40d3ff322bbdac6606acfd2326155b51..d219dc871ba746bdfe67a789e7756c4ea492c1a6 100644 --- a/event/sched/rpc.h +++ b/event/sched/rpc.h @@ -23,6 +23,7 @@ typedef struct address_tuple * address_tuple_t; #endif extern CD int RPC_init(char *certpath, char *host, int port); +extern CD void RPC_kill(void); extern CD int RPC_waitforactive(char *pid, char *eid); extern CD int RPC_agentlist(char *pid, char *eid); extern CD int RPC_grouplist(char *pid, char *eid); diff --git a/xmlrpc/script_wrapper.py.in b/xmlrpc/script_wrapper.py.in index 93ee2f3175161b19af61089f5d2b9cce3d22bc4c..5122c9abe8c7e2fff6c324f79cc5eb7e7849d958 100755 --- a/xmlrpc/script_wrapper.py.in +++ b/xmlrpc/script_wrapper.py.in @@ -60,6 +60,11 @@ admin = 0 devel = 0 needhelp = 0 +# The set of paths to try when connecting to the server. +XMLRPC_PATH = [ "xmlrpc", SERVER_PATH + "/sbin/sshxmlrpc_server.py", ] +# The ssh options that should be added to the default. +SSH_OPTS = { "-1" : "-1" } + API = { "node_admin" : { "func" : "adminmode", "help" : "Boot selected nodes into FreeBSD MFS" }, @@ -133,6 +138,52 @@ def wrapperoptions(): print " --debug Turn on semi-useful debugging" return +## +# Construct an SSHTransport object that is connected to an EmulabServer object +# on the peer. Multiple paths are attempted until one succeeds. +# +# @param user_agent The user-agent identifier. +# @param ssh_identity The ssh identity file to use when connecting. +# @return A pair containing the ssh transport and the path used, in that order. +# +def make_transport(user_agent=None, ssh_identity=None): + if path: + retval = (SSHTransport(user_agent=user_agent, ssh_opts=SSH_OPTS), path) + pass + else: + for xrpath in XMLRPC_PATH: + try: + retval = (SSHTransport(user_agent=user_agent, + ssh_identity=ssh_identity, + ssh_opts=SSH_OPTS), + xrpath) + hdrs = retval[0].probe(xmlrpc_server, + "/" + xrpath, + verbose=debug) + if hdrs["probe-response"] == "EmulabServer": + if debug: + print "make_transport: found path " + xrpath + pass + break + else: + retval = None + pass + pass + except BadResponse, e: + if debug: + print ("make_transport: bad response for " + + xrpath + "; " + str(e)) + pass + pass + pass + pass + + if not retval: + print "error - Unable to connect to RPC server" + pass + + return retval + # # Process a single command line # @@ -143,15 +194,16 @@ def do_method(module, method, params): if impotent: return 0; + transport, fullpath = make_transport(user_agent="sshxmlrpc_wrapper-v0.2") + # Get a handle on the server, server = SSHServerProxy("ssh://" + login_id + "@" + xmlrpc_server + - "/xmlrpc/" + module, - path=path, - user_agent="sshxmlrpc_wrapper-v0.1") - + "/" + fullpath, + transport=transport, + user_agent="sshxmlrpc_wrapper-v0.2") # Get a pointer to the function we want to invoke. - meth = getattr(server, method) + meth = getattr(server, module + "." + method) meth_args = [ PACKAGE_VERSION, params ] # @@ -1558,6 +1610,15 @@ if admin: handler = None; command_argv = None; +if not conf_passphraseless_login(): + sys.stderr.write(sys.argv[0] + + ": error - No agent or passphrase-less key found\n") + sys.stderr.write(sys.argv[0] + + ": You will need to regenerate your passphrase-less " + + "key manually\n") + sys.exit(-1) + pass + if API.has_key(os.path.basename(sys.argv[0])): handler = API[os.path.basename(sys.argv[0])]["func"]; command_argv = sys.argv[len(wrapper_argv) + 1:]; diff --git a/xmlrpc/sshxmlrpc.py b/xmlrpc/sshxmlrpc.py index f226d901c8656c0db234bffc9e7d9ad859eccb6a..1198aa82d261437f4598a24e4f1854fa6851175d 100644 --- a/xmlrpc/sshxmlrpc.py +++ b/xmlrpc/sshxmlrpc.py @@ -1,3 +1,5 @@ +#! /usr/bin/env python + # # EMULAB-COPYRIGHT # Copyright (c) 2004 University of Utah and the Flux Group. @@ -41,22 +43,167 @@ # OF THIS SOFTWARE. # -------------------------------------------------------------------- # -import os import sys import types import urllib import popen2 import rfc822 import xmlrpclib -if os.name != "nt": - import syslog +import os, os.path + +VERSION = 0.5 + +## +## BEGIN Debugging setup +## + +SSHXMLRPC_DEBUG = os.environ.get("SSHXMLRPC_DEBUG", "") + +# SSHXMLRPC_DEBUG = "all" + +if "all" in SSHXMLRPC_DEBUG: + SSHXMLRPC_DEBUG = "config,connect,io,ssh" + pass + +def __sxrdebug__(key, *args): + if key in SSHXMLRPC_DEBUG: + sys.stderr.write("sshxmlrpc.py: " + "".join(args) + "\n") + pass + return + +## +## END Debugging setup +## + + + +__sxrdebug__("config", "OS ", os.name) -# XXX This should come from configure. if os.name != "nt": + import syslog LOG_TESTBED = syslog.LOG_LOCAL5; + pass import traceback + + +## +## BEGIN Self-configuration +## + +## +# Search the user's PATH for the given command. +# +# @param command The command name to search for. +# @return The full path to the command or None if the command was not found. +# +def conf_which(command): + if os.path.exists(command) and os.path.isfile(command): + return command + for path in os.environ.get('PATH', os.defpath).split(os.pathsep): + fullpath = os.path.join(path, command) + if os.path.exists(fullpath) and os.path.isfile(fullpath): + return fullpath + pass + return None + +## +# Search for several commands in the user's PATH and return the first match. +# +# @param possible_commands The list of commands to search for. +# @return The first command that matched, or None if no match was found. +# +def conf_detect(possible_commands): + for cmd in possible_commands: + if len(cmd) > 0: + cmd_no_flags = cmd.split()[0] + retval = conf_which(cmd_no_flags) + if retval is not None: + return cmd + pass + pass + return None + +# The default identity for the user. +DEFAULT_SSH_IDENTITY = os.path.join(os.path.expanduser("~"), + ".ssh", + "identity") + +__sxrdebug__("config", "DEFAULT_SSH_IDENTITY - ", str(DEFAULT_SSH_IDENTITY)) + +## +# Check if the user can perform a passphrase-less login. +# +# @param identity The identity to check. +# @return True if the user can perform a passphraseless login. +# +def conf_passphraseless_login(identity=None): + if os.environ.get("SSH_AUTH_SOCK", "") == "": + # No agent, check for a passphrase-less key and then + if SSHKEYGEN_COMMAND is not None: + if not identity or (identity == ""): + identity = DEFAULT_SSH_IDENTITY + pass + rc = os.system(SSHKEYGEN_COMMAND + + " -p -P \"\" -N \"\" -f " + + identity + + " > /dev/null 2>&1") + if rc != 0: + retval = False + pass + else: + retval = True + pass + pass + # ... complain. + elif os.name != "nt": + retval = False + pass + pass + else: + retval = True + pass + + return retval + +# Find a suitable "ssh" command and +SSH_COMMAND = conf_detect([ + os.environ.get("SSHXMLRPC_SSH", ""), + "ssh -T -x -C -o 'CompressionLevel 5' %(-F)s %(-l)s %(-i)s %(-v)s %(-1)s %(-2)s", + "plink -x -C %(-l)s %(-i)s %(-v)s %(-1)s %(-2)s", + ]) + +__sxrdebug__("config", "SSH_COMMAND - ", str(SSH_COMMAND)) + +# ... error out if we don't. +if SSH_COMMAND is None: + sys.stderr.write("sshxmlrpc.py: Unable to locate a suitable SSH command\n") + if os.environ.has_key("SSHXMLRPC_SSH"): + sys.stderr.write("sshxmlrpc.py: '" + + os.environ["SSHXMLRPC_SSH"] + + "' was not found.\n") + pass + else: + sys.stderr.write("sshxmlrpc.py: Set the SSHXMLRPC_SSH environment " + "variable to a suitable binary (e.g. ssh/plink)\n") + pass + raise ImportError, "suitable ssh not found in path: %(PATH)s" % os.environ + +# Find ssh-keygen so we can do some tests when a connection is made. +SSHKEYGEN_COMMAND = conf_detect([ + os.environ.get("SSHXMLRPC_SSHKEYGEN", ""), + "ssh-keygen", + ]) + +__sxrdebug__("config", "SSHKEYGEN_COMMAND - ", str(SSHKEYGEN_COMMAND)) + +## +## END Self-configuration +## + + + ## # Base class for exceptions in this module. # @@ -113,55 +260,55 @@ class SSHConnection: # @param ssh_config The ssh config file to use when initiating a new # connection. # - def __init__(self, host, handler, streams=None, ssh_config=None): + def __init__(self, host, handler, streams=None, ssh_config=None, + ssh_identity=None, ssh_opts={}): # Store information about the peer and self.handler = handler self.host = host + self.last_lines = [] # ... initialize the read and write file objects. - self.myChild = None if streams: self.rfile = streams[0] self.wfile = streams[1] + self.errfile = None + self.closed = False pass else: - self.user, ssh_host = urllib.splituser(self.host) - # print self.user + " " + self.host + " " + handler + if not conf_passphraseless_login(ssh_identity): + sys.stderr.write("sshxmlrpc.py: warning - No agent or " + "passphrase-less key found, " + "continuing anyways...\n") + pass - # Use ssh unless we're on Windows with no ssh-agent running. - nt = os.name == "nt" - use_ssh = not nt or os.environ.has_key("SSH_AGENT_PID") + self.user, ssh_host = urllib.splituser(self.host) - flags = "" + all_opts = { "-l" : "", "-i" : "", "-F" : "", "-v" : "", + "-1" : "", "-2" : "" } + all_opts.update(ssh_opts) if self.user: - flags = flags + " -l " + self.user + all_opts["-l"] = "-l " + self.user pass - if use_ssh and ssh_config: - flags = flags + " -F " + ssh_config + if ssh_identity: + all_opts["-i"] = "-i " + ssh_identity pass - args = flags + " " + ssh_host + " " + handler - - if use_ssh: - cmd = "ssh -T -x -C -o 'CompressionLevel 5' " + args + if ssh_config: + all_opts["-F"] = "-F " + ssh_config pass - else: - # Use the PyTTY plink, equivalent to the ssh command. - cmd = "plink -x -C " + args + if "ssh" in SSHXMLRPC_DEBUG: + all_opts["-v"] = "-vvv" pass - - if not nt: - # Popen3 objects, and the wait method, are Unix-only. - self.myChild = popen2.Popen3(cmd, 1) - self.rfile = self.myChild.fromchild - self.wfile = self.myChild.tochild - self.errfile = self.myChild.childerr - pass - else: - # Open the pipe in Binary mode so it doesn't mess with CR-LFs. - self.rfile, self.wfile, self.errfile = popen2.popen3(cmd, mode='b') - pass - # print "wfile", self.wfile, "rfile", self.rfile + + args = (SSH_COMMAND % all_opts) + " " + ssh_host + " " + handler + + __sxrdebug__("connect", "open - ", args) + + # Open the pipe in Binary mode so it doesn't mess with CR-LFs. + self.rfile, self.wfile, self.errfile = popen2.popen3( + args, mode='b') + self.closed = False pass + return ## @@ -169,26 +316,44 @@ class SSHConnection: # @return The amount of data read. # def read(self, len=1024): - return self.rfile.read(len) + retval = self.rfile.read(len) + + __sxrdebug__("io", "read - ", retval) + return retval ## # @return A line of data or None if there is no more input. # def readline(self): - return self.rfile.readline() + retval = self.rfile.readline() + if len(retval) > 0: + self.last_lines.append(retval) + if len(self.last_lines) > 5: + self.last_lines.pop(0) + pass + pass + else: + self.closed = True + pass + + __sxrdebug__("io", "readline - ", retval) + return retval ## # @param stuff The data to send to the other side. # @return The amount of data written. # def write(self, stuff): - # print "write", stuff + __sxrdebug__("io", "write - ", stuff) + return self.wfile.write(stuff) ## # Flush any write buffers. # def flush(self): + __sxrdebug__("io", "flush") + self.wfile.flush() return @@ -196,12 +361,10 @@ class SSHConnection: # Close the connection. # def close(self): + __sxrdebug__("connect", "close - ", self.host) + self.wfile.close() self.rfile.close() - if self.myChild: - self.myChild.wait() - self.myChild = None - pass return ## @@ -222,6 +385,46 @@ class SSHConnection: self.flush() return + ## + # Dump the standard error from the peer to the given file pointer with the + # given prefix. + # + # @param fp The file pointer where the output should be written or None if + # you just want to drain the pipe. + # @param prefix Prefix to prepend to every line. (optional) + # + def dump_stderr(self, fp, prefix=""): + if self.errfile: + while True: + line = self.errfile.readline() + if not line: + break + if fp: + fp.write(prefix + line) + pass + pass + pass + return + + ## + # Dump the last five lines of input read from the peer. Helpful for + # debugging connections and what not. + # + # @param fp The file pointer where the output should be written. + # @param prefix Prefix to prepend to every line. (optional) + # + def dump_last_lines(self, fp, prefix=""): + if len(self.last_lines) < 5: + for lpc in range(len(self.last_lines), 5): + if not self.readline(): + break + pass + pass + for line in self.last_lines: + fp.write(prefix + line) + pass + return + def __repr__(self): return "<SSHConnection %s%s>" % (self.host, self.handler) @@ -238,52 +441,63 @@ class SSHTransport: # @param ssh_config The ssh config file to use when making new connections. # @param user_agent Symbolic name for the program acting on behalf of the # user. + # @param ssh_opts List of additional options to pass to SSH_COMMAND. # - def __init__(self, ssh_config=None, user_agent=None): + def __init__(self, ssh_config=None, user_agent=None, ssh_identity=None, + ssh_opts={}): self.connections = {} self.ssh_config = ssh_config - self.user_agent = user_agent + if user_agent: + self.user_agent = user_agent + pass + else: + self.user_agent = sys.argv[0] + pass + self.ssh_identity = ssh_identity + self.ssh_opts = ssh_opts return - def __del__(self): - for key, val in self.connections.items(): - val.close() + ## + # Probe the peer and return their response headers. Useful for making sure + # the other side is what we expect it to be. + # + # @param host The host to contact. + # @param handler The XML-RPC handler. + # @param hdrs A dictionary of additional headers to send to the peer, these + # will be included in their response. + # @return The response headers from the peer. + # @throws BadResponse if there was a problem interpreting the other side's + # response. + # + def probe(self, host, handler, hdrs={}, verbose=False): + handler = self.munge_handler(handler) + + connection = self.get_connection((host, handler)) + connection.putheader("probe", self.user_agent) + for (key, value) in hdrs.items(): + connection.putheader(key, str(value)) pass - return; - + connection.endheaders() + connection.flush() + + return self.parse_headers(connection, verbose=verbose) + ## # Send a request to the destination. # # @param host The host name on which to execute the request # @param handler The python file that will handle the request. # @param request_body The XML-RPC encoded request. - # @param verbose unused. - # @return The value returned + # @return The value returned by the peer method. # - def request(self, host, handler, request_body, verbose=0, path=None): - # Strip the leading slash in the handler, if there is one. - if path: - handler = path + handler - pass - elif handler.startswith('/'): - handler = handler[1:] - pass + def request(self, host, handler, request_body, path=None): + handler = self.munge_handler(handler, path) # Try to get a new connection, - if not self.connections.has_key((host,handler)): - if verbose: - sys.stderr.write("New connection for %s %s\n" % - (host, handler)) - pass - - self.connections[(host,handler)] = SSHConnection(host, handler) - pass - connection = self.connections[(host,handler)] + connection = self.get_connection((host,handler)) # ... send our request, and - if self.user_agent: - connection.putheader("user-agent", self.user_agent) - pass + connection.putheader("user-agent", self.user_agent) connection.putheader("content-length", len(request_body)) connection.putheader("content-type", "text/xml") connection.endheaders() @@ -301,6 +515,43 @@ class SSHTransport: def getparser(self): return xmlrpclib.getparser() + ## + # Munge the handler which means stripping the first slash, if it is there. + # + # @param handler The handler to munge. + # @return The munged handler string. + # + def munge_handler(self, handler, path=None): + # Strip the leading slash in the handler, if there is one. + if path: + retval = path + handler + pass + elif handler.startswith('/'): + retval = handler[1:] + pass + else: + retval = handler + pass + + return retval + + ## + # Get a cached connection or make a new one. + # + # @param pair The host/handler pair that identifies the connection. + # @return An SSHConnection object for the given pair. + # + def get_connection(self, pair): + if not self.connections.has_key(pair): + __sxrdebug__("connect", + "new connection for ", pair[0], " ", pair[1]) + + self.connections[pair] = SSHConnection( + pair[0], pair[1], + ssh_identity=self.ssh_identity, ssh_opts=self.ssh_opts) + pass + return self.connections[pair] + ## # @param connection The connection to drop. # @@ -308,6 +559,29 @@ class SSHTransport: del self.connections[(connection.host,connection.handler)] connection.close() return + + ## + # Parse the headers from the peer. + # + # @param connection The connection to read the headers from. + # @param verbose Be verbose in providing error information. (default: True) + # + def parse_headers(self, connection, verbose=True): + retval = SSHMessage(connection, False) + if retval.status != "": + if verbose: + connection.dump_stderr(sys.stderr, + connection.host + ",stderr: ") + sys.stderr.write("sshxmlrpc.py: Error while reading headers, " + "expected rfc822 headers, received:\n") + connection.dump_last_lines(sys.stderr, ">> ") + pass + self.drop_connection(connection) + raise BadResponse(connection.host, + connection.handler, + retval.status) + + return retval ## # Parse the response from the server. @@ -319,24 +593,22 @@ class SSHTransport: try: # Get the headers, - headers = SSHMessage(connection, False) - if headers.status != "": - self.drop_connection(connection) - raise BadResponse(connection.host, - connection.handler, - headers.status) + headers = self.parse_headers(connection) + # ... the length of the body, and length = int(headers['content-length']) # ... read in the body. response = connection.read(length) - pass except KeyError, e: + connection.dump_stderr(sys.stderr, connection.host + ",stderr: ") + sys.stderr.write("sshxmlrpc.py: Error while reading headers, " + + "expected rfc822 headers, received:\n") + connection.dump_last_lines(sys.stderr, ">> ") # Bad header, drop the connection, and self.drop_connection(connection) # ... tell the user. raise BadResponse(connection.host, connection.handler, e.args[0]) - - # print "response /"+response+"/" + parser.feed(response) return unmarshaller.close() @@ -355,10 +627,17 @@ class SSHServerWrapper: # Initialize this object. # # @param object The object to wrap. + # @param probe_response The value to send back to clients in the + # 'probe-response' header. # - def __init__(self, object): - self.ssh_connection = os.environ['SSH_CONNECTION'].split() + def __init__(self, object, probe_response=None): + self.ssh_connection = os.environ.get( + "SSH_CONNECTION", "stdin 0 stdout 0").split() self.myObject = object + self.probe_response = probe_response + if self.probe_response is None: + self.probe_response = sys.argv[0] + " " + str(VERSION) + pass # # Init syslog @@ -368,6 +647,8 @@ class SSHServerWrapper: syslog.syslog(syslog.LOG_INFO, "Connect by " + os.environ['USER'] + " from " + self.ssh_connection[0]); + pass + return ## @@ -380,22 +661,50 @@ class SSHServerWrapper: def handle_request(self, connection): retval = False try: - # Read the request, + # Read the request headers, hdrs = SSHMessage(connection, False) + + # ... make sure they are sane, if hdrs.status != "": - #sys.stderr.write("server error: Expecting rfc822 headers.\n"); - raise BadRequest(connection.host, hdrs.status) + if not connection.closed: + sys.stderr.write("server error: Expecting rfc822 headers, " + "received:\n"); + connection.dump_last_lines(sys.stderr, "<< ") + sys.stderr.write("conn " + `connection.closed` + "\n") + sys.stderr.write("server error: " + hdrs.status) + raise BadRequest(connection.host, hdrs.status) + else: + return True + pass + + # ... respond to probes immediately, + if hdrs.has_key("probe"): + connection.putheader("probe-response", self.probe_response) + del hdrs["content-length"] + connection.putheader("content-length", 0) + for (key, value) in hdrs.items(): + connection.putheader(key, value) + pass + connection.endheaders() + connection.flush() + return retval + + # ... check for required headers, and if not hdrs.has_key('content-length'): - sys.stderr.write("server error: " - + "expecting content-length header\n") + sys.stderr.write("server error: expecting content-length " + "header, received:\n") + connection.dump_last_lines(sys.stderr, "<< ") raise BadRequest(connection.host, "missing content-length header") + if hdrs.has_key('user-agent'): user_agent = hdrs['user-agent'] pass else: user_agent = "unknown" pass + + # ... start reading the body. length = int(hdrs['content-length']) params, method = xmlrpclib.loads(connection.read(length)) if os.name != "nt": @@ -471,8 +780,12 @@ class SSHServerWrapper: if os.name != "nt": syslog.syslog(syslog.LOG_INFO, "Connection closed"); syslog.closelog() + pass pass return + + def serve_stdio_forever(self): + return self.serve_forever((sys.stdin, sys.stdout)) pass @@ -489,38 +802,36 @@ class SSHServerProxy: # @param transport A python object that implements the Transport interface. # The default is to use a new SSHTransport object. # @param encoding Content encoding. - # @param verbose unused. # @param user_agent Symbolic name for the program acting on behalf of the # user. + # @param ssh_opts List of additional options to pass to SSH_COMMAND. # def __init__(self, uri, transport=None, encoding=None, - verbose=0, path=None, - user_agent=None): + user_agent=None, + ssh_identity=None, + ssh_opts={}): type, uri = urllib.splittype(uri) if type not in ("ssh", ): - raise IOError, "unsupported XML-RPC protocol" + raise IOError, "unsupported XML-RPC protocol: " + `type` self.__host, self.__handler = urllib.splithost(uri) if transport is None: - transport = SSHTransport(user_agent=user_agent) + transport = SSHTransport(user_agent=user_agent, + ssh_identity=ssh_identity, + ssh_opts=ssh_opts) pass self.__transport = transport self.__encoding = encoding - self.__verbose = verbose self.__path = path return - def __del__(self): - del self.__transport - return - ## # Send a request to the server. # @@ -536,7 +847,6 @@ class SSHServerProxy: self.__host, self.__handler, request, - verbose=self.__verbose, path=self.__path ) @@ -564,3 +874,165 @@ class SSHServerProxy: return True pass + +if __name__ == "__main__": + import time + import getopt + + def usage(): + print "SSH-based XML-RPC module/client." + print "Usage: sshxmlrpc.py [-hVq] [-u agent] [-i id] [-s opts] [<URL>]" + print " sshxmlrpc.py [-u agent] [-i id] [-s opts] [<URL> <method>]" + print + print "Options:" + print " -h, --help\t\t Display this help message" + print " -V, --version\t\t Show the version number" + print " -q, --quiet\t\t Be less verbose" + print " -u, --user-agent agent Specify the user agent" + print " -i, --identity id\t Specify the SSH identity to use" + print " -s, --ssh-opts opts\t Specify additional SSH options" + print " \t The format is 'opt=value' (e.g. l=stack)" + print + print "Required arguments:" + print " URL\t\t\t The URL of the server." + print " method\t\t The method name to call." + print + print "Environment Variables:" + print " SSHXMLRPC_DEBUG\t Activate debugging for the listed aspects." + print " \t (e.g. all,config,connect,io,ssh)" + print " SSHXMLRPC_SSH\t\t Specify the ssh command to use." + print " SSHXMLRPC_SSHKEYGEN\t Specify the ssh-keygen command." + print + print "Examples:" + print " $ sshxmlrpc.py ssh://localhost/server.py" + print + print "Configuration:" + print " ssh command\t\t" + SSH_COMMAND + print " ( The '%(-X)s' portions of the command are substituted )" + print " ( with the corresponding flags before the command is run. )" + print " ( Do not include them if your version of SSH does not )" + print " ( fully support them. )" + print " ssh-keygen command\t" + SSHKEYGEN_COMMAND + return + + verbose = True + user_agent = None + ssh_identity = None + ssh_opts = {} + + try: + opts, extra = getopt.getopt(sys.argv[1:], + "hVqu:i:s:", + [ "help", + "version", + "quiet", + "user-agent", + "identity", + "ssh-opts"]) + + for opt, val in opts: + if opt in ("-h", "--help"): + usage() + sys.exit() + pass + elif opt in ("-V", "--version"): + print VERSION + sys.exit() + pass + elif opt in ("-q", "--quiet"): + verbose = False + pass + elif opt in ("-u", "--user-agent"): + user_agent = val + pass + elif opt in ("-i", "--identity"): + ssh_identity = val + pass + elif opt in ("-s", "--ssh-opts"): + so = val.split("=") + if len(so) == 2: + so_key, so_value = so + pass + else: + so_key = so[0] + so_value = None + pass + so_key = "-" + so_key + if so_value: + ssh_opts[so_key] = so_key + " " + so_value + pass + else: + ssh_opts[so_key] = so_key + pass + pass + else: + assert not "unhandled option" + pass + pass + pass + except getopt.error, e: + print e.args[0] + usage() + sys.exit(2) + pass + + # Print usage if there are no arguments, + if len(extra) == 0: + usage() + sys.exit() + pass + # ... check the URL, then + elif not extra[0].startswith("ssh://"): + print "Invalid url: " + extra[0] + usage() + sys.exit(2) + pass + # ... probe the URL or + elif len(extra) == 1: + try: + st = SSHTransport(user_agent=user_agent, + ssh_identity=ssh_identity, + ssh_opts=ssh_opts) + type, uri = urllib.splittype(extra[0]) + host, handler = urllib.splithost(uri) + rc = st.probe(host, handler, + { "date" : time.ctime(time.time()) }, + verbose) + secs = time.mktime(time.strptime(rc["date"])) + + print "Probe results for: " + extra[0] + print " response time=%.2f s" % (time.time() - secs) + print "Response Headers" + for pair in rc.items(): + print " %s: %s" % pair + pass + pass + except BadResponse, e: + print ("sshxmlrpc.py: error - bad response from " + + extra[0] + + "; " + + e[2]) + sys.exit(1) + pass + pass + # ... call a method. + else: + try: + sp = SSHServerProxy(extra[0], + ssh_identity=ssh_identity, + user_agent=user_agent, + ssh_opts=ssh_opts) + method_name = extra[1] + method_args = extra[2:] + method = getattr(sp, method_name) + print str(apply(method, method_args)) + pass + except BadResponse, e: + print ("sshxmlrpc.py: error - bad response from " + + extra[0] + + "; " + + e[2]) + sys.exit(1) + pass + pass + pass diff --git a/xmlrpc/sshxmlrpc_server.py.in b/xmlrpc/sshxmlrpc_server.py.in index 05f8d7b5d1173f61104656444eb5f43552e13603..fc8b829326adb6cdd6b7ac9d4435ca7022a9ba42 100755 --- a/xmlrpc/sshxmlrpc_server.py.in +++ b/xmlrpc/sshxmlrpc_server.py.in @@ -43,7 +43,7 @@ if len(sys.argv) > 1: # # Construct and wrap our object. server = eval(module + "(readonly=" + str(ReadOnly) + ")") -wrapper = sshxmlrpc.SSHServerWrapper(server) +wrapper = sshxmlrpc.SSHServerWrapper(server, module) # Handle the request on stdin and send the response to stdout. -wrapper.serve_forever((sys.stdin, sys.stdout)) +wrapper.serve_stdio_forever() sys.exit(0)