From c61858c780c5852f59d921b1ddabcd6dd23a05ee Mon Sep 17 00:00:00 2001 From: Timothy Stack <stack@flux.utah.edu> Date: Fri, 29 Oct 2004 21:48:32 +0000 Subject: [PATCH] Make the hurting stop. Make sshxmlrpc auto-detect things, fails over properly, and dump useful information when it is unable to deal with the peer. * xmlrpc/sshxmlrpc.py: Major update. It now tries to autoconfigure itself by scanning the path for "ssh" and "plink.exe" (although I haven't actually tried it on windows). Environment variables can now be used to turn on debugging and set the command to use for doing the ssh. Before running ssh, it will check for an agent or a passphrase-less key and prints a warning if it finds neither. The last five lines read from the server, as well as the standard error output, are stored so they can be dumped later; helpful for figuring out what is actually being run on the other side. The protocol layer between ssh and xml-rpc will now respond to a "probe" header so that clients can figure out who they are talking too. The server side will now properly detect a closed connection and not write anything, which means no more annoying "Write to stdout failed" messages. You can now pass additional options to ssh and set the identity. The module can be run standalone, with the default action being to probe the peer: $ ./sshxmlrpc.py ssh://boss/xmlrpc Probe results for: ssh://boss/xmlrpc response time=1.49 s Response Headers date: Wed Oct 27 16:10:58 2004 content-length: 0 probe: /usr/testbed/devel/stack/lib/sshxmlrpc.py probe-response: EmulabServer * xmlrpc/sshxmlrpc_server.py.in: Set the value returned by a "probe" to the name of the invoked module. This way, the other side can figure out who they are talking to (e.g. EmulabServer vs. experiment vs. fs vs. osid). * event/sched/event-sched.c, event/sched/rpc.cc, event/sched/rpc.h, xmlrpc/script_wrapper.py.in: Multiple paths (e.g. xmlrpc, $prefix/sbin/sshxmlrpc_server.py) are now probed before giving up. Force the use of the user's default identity and protocol one. For event-sched, a single connection is now made at startup and dropped before going into the event loop. * event/sched/GNUmakefile.in: Add a dependency for the install target and add -I$(OBJDIR) to the CXXFLAGS. * install/ports/ulsshxmlrpcpp/Makefile, install/ports/ulsshxmlrpcpp/distinfo, install/ports/ulsshxmlrpcpp/pkg-descr: Bump version number to 1.1 and tweak the description. * config.h.in, configure, configure.in: Add a "#define TBROOT" that has the install prefix. --- event/sched/GNUmakefile.in | 4 +- event/sched/event-sched.c | 8 +- event/sched/rpc.cc | 140 +++++-- event/sched/rpc.h | 1 + xmlrpc/script_wrapper.py.in | 71 +++- xmlrpc/sshxmlrpc.py | 668 +++++++++++++++++++++++++++++----- xmlrpc/sshxmlrpc_server.py.in | 4 +- 7 files changed, 755 insertions(+), 141 deletions(-) diff --git a/event/sched/GNUmakefile.in b/event/sched/GNUmakefile.in index 36f4260e56..f6cf02dc7a 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 124dbfdfc8..6c099de894 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 4b8ad549b3..1d23448030 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 fd885a5a40..d219dc871b 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 93ee2f3175..5122c9abe8 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 f226d901c8..1198aa82d2 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 05f8d7b5d1..fc8b829326 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) -- GitLab