Commit 5a025f36 authored by Leigh Stoller's avatar Leigh Stoller

Guts of the new ssl server implemented. The server operates more or less

like this:

* Listen for connections on port 3069. The server requires client
  authentication, and will fail if a certificate is not provided by
  the client.

* Once the certificate is accepted, the server forks a new child.

* The child looks inside the certificate to get the CN field of the
  Distinguished Name (subject). The CN field must hold the uid of the
  user, which is checked against the DB for a matching user. We get
  the groupslist from the DB, and do a setgid,setgroups,setuid to flip
  to the user in the child.

* A instance of the emulabserver class is created, and the request is
  dispatched.

I added an sslxmlrpc_client.py script that mirrors the ssh version of
the client script. I could probably roll these into one, but decided
not to to avoid confusing people who might download it.
parent ae36dc5f
#! /usr/bin/env python
#
# EMULAB-COPYRIGHT
# Copyright (c) 2004 University of Utah and the Flux Group.
# All rights reserved.
#
# Permission to use, copy, modify and distribute this software is hereby
# granted provided that (1) source code retains these copyright, permission,
# and disclaimer notices, and (2) redistributions including binaries
# reproduce the notices in supporting documentation.
#
# THE UNIVERSITY OF UTAH ALLOWS FREE USE OF THIS SOFTWARE IN ITS "AS IS"
# CONDITION. THE UNIVERSITY OF UTAH DISCLAIMS ANY LIABILITY OF ANY KIND
# FOR ANY DAMAGES WHATSOEVER RESULTING FROM THE USE OF THIS SOFTWARE.
#
import sys
sys.path.append("@prefix@/lib")
import getopt
import os
from emulabclient import *
import xmlrpclib
from M2Crypto.m2xmlrpclib import SSL_Transport
from M2Crypto import SSL
##
# The package version number
#
PACKAGE_VERSION = 0.1
# Default server and port
XMLRPC_SERVER = "@BOSSNODE@"
XMLRPC_PORT = 3069
# User supplied server name.
xmlrpc_server = XMLRPC_SERVER
xmlrpc_port = XMLRPC_PORT
# The default RPC module to invoke.
module = "experiment"
# Where to find the default certificate.
certificate = os.environ["HOME"] + "/.ssl/emulab.pem"
# Debugging output.
debug = 0
##
# Print the usage statement to stdout.
#
def usage():
print "Make a request to the Emulab XML-RPC (SSL-based) server."
print ("Usage: " + sys.argv[0]
+ " [-hV] [-l login] [-s server] [-m module] "
+ "<method> [param=value ...]")
print
print "Options:"
print " -h, --help\t\t Display this help message"
print " -V, --version\t\t Show the version number"
print " -s, --server\t\t Set the server hostname"
print " -p, --port\t\t Set the server port"
print " -c, --cert\t\t Set the certificate to use"
print " -m, --module\t\t Set the RPC module (defaults to experiment)"
print
print "Required arguments:"
print " method\t\t The method to execute on the server"
print " params\t\t\t The method arguments in param=value format"
print
print "Example:"
print (" "
+ sys.argv[0]
+ " -s boss.emulab.net echo \"Hello World!\"")
return
#
# Process a single command line
#
def do_method(server, method_and_args):
# Get a pointer to the function we want to invoke.
methodname = method_and_args[0]
if methodname.count(".") == 0:
methodname = module + "." + methodname
pass
meth = getattr(server, methodname)
# Pop off the method, and then convert the rest of the arguments.
# Be sure to add the version.
method_and_args.pop(0)
#
# Convert all params (name=value) into a Dictionary.
#
params = {}
for param in method_and_args:
plist = string.split(param, "=", 1)
if len(plist) != 2:
print ("error: Parameter, '"
+ param
+ "', is not of the form: param=value!")
return -1
value = plist[1]
#
# If the first character of the argument looks like a dictionary,
# try to evaluate it.
#
# if value[0] == '{':
# value = eval(value);
# pass
params[plist[0]] = value
pass
meth_args = [ PACKAGE_VERSION, params ]
#
# Make the call.
#
try:
response = apply(meth, meth_args)
pass
except xmlrpclib.Fault, e:
print e.faultString
return -1
#
# Parse the Response, which is a Dictionary. See EmulabResponse in the
# emulabclient.py module. The XML standard converts classes to a plain
# Dictionary, hence the code below.
#
if len(response["output"]):
print response["output"]
pass
rval = response["code"]
#
# If the code indicates failure, look for a "value". Use that as the
# return value instead of the code.
#
if rval != RESPONSE_SUCCESS:
if response["value"]:
rval = response["value"]
pass
pass
if debug and response["value"]:
print str(response["value"])
pass
return rval
#
# Process program arguments.
#
try:
# Parse the options,
opts, req_args = getopt.getopt(sys.argv[1:],
"dhVs:l:am:zx:p:",
[ "help", "version", "server=", "login=", "module=",
"cert=", "port="])
# ... act on them appropriately, and
for opt, val in opts:
if opt in ("-h", "--help"):
usage()
sys.exit()
pass
elif opt in ("-V", "--version"):
print PACKAGE_VERSION
sys.exit()
pass
elif opt in ("-s", "--server"):
xmlrpc_server = val
pass
elif opt in ("-p", "--port"):
xmlrpc_port = val
pass
elif opt in ("-c", "--cert"):
certificate = val
pass
elif opt in ("-l", "--login"):
login_id = val
pass
elif opt in ("-m", "--module"):
module = val
pass
elif opt in ("-d", "--debug"):
debug = 1
pass
elif opt in ("-a", "--admin"):
admin = 1
pass
elif opt in ("-z", "--devel"):
devel = 1
pass
elif opt in ("-x", "--develuser"):
develuser = val
pass
pass
pass
except getopt.error, e:
print e.args[0]
usage()
sys.exit(2)
pass
#
# Vanilla SSL CTX initialization.
#
if not os.access(certificate, os.R_OK):
print "Certificate cannot be accessed: " + certificate
sys.exit(-1);
pass
ctx = SSL.Context('sslv23')
ctx.load_cert(certificate, certificate)
ctx.set_verify(SSL.verify_none, 16)
ctx.set_allow_unknown_ca(0)
#ctx.set_info_callback()
# This is parsed by the Proxy object.
URI = "https://" + xmlrpc_server + ":" + str(xmlrpc_port)
# Get a handle on the server,
server = xmlrpclib.ServerProxy(URI, SSL_Transport(ctx));
if len(req_args):
# Method and args are on the command line.
sys.exit(do_method(server, req_args))
else:
# Prompt the user for input.
try:
while True:
line = raw_input("$ ")
tokens = line.split(" ")
if len(tokens) >= 1 and len(tokens[0]) > 0:
print str(do_method(server, tokens))
pass
pass
pass
except EOFError:
pass
print
pass
......@@ -6,11 +6,35 @@
#
import sys
import getopt
import os
import traceback
import syslog
import string
# Testbed specific stuff
sys.path.append("@prefix@/lib")
import socket
import sshxmlrpc
from libdb import *
from libtestbed import SENDMAIL, TBOPS
from emulabserver import *
from SimpleXMLRPCServer import SimpleXMLRPCDispatcher
from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler
from M2Crypto import SSL
from M2Crypto.SSL import SSLError
# When debugging, runs in foreground printing to stdout instead of syslog
debug = 0
# The port to listen on. We should get this from configure.
PORT = 3069
# The local address. Using INADDY_ANY for now.
ADDR = "0.0.0.0"
# The server certificate and the server CS.
server_cert = "@prefix@/etc/server.pem"
ca_cert = "@prefix@/etc/emulab.pem"
#
# By default, run a wrapper class that includes all off the modules.
# The client can invoke methods of the form experiment.swapexp when
......@@ -18,32 +42,209 @@ from emulabserver import *
#
DEFAULT_MODULE = "EmulabServer"
module = DEFAULT_MODULE
ReadOnly = 0;
# syslog facility
LOGFACIL = "@TBLOGFACIL@"
#
# Optional argument indicating read-only privs.
#
if len(sys.argv) > 1 and sys.argv[1] == "-ro":
ReadOnly = 1;
sys.argv = sys.argv[1:]
# A simple server based on the forking version SSLServer. We fork cause
# we want to change our uid/gid to that of the person on the other end.
#
class MyServer(SSL.ForkingSSLServer, SimpleXMLRPCDispatcher):
def __init__(self, debug):
self.debug = debug
self.logRequests = 0
self.emulabserver = None;
ctx = SSL.Context('sslv23')
ctx.load_cert(server_cert, server_cert)
ctx.load_verify_info(ca_cert)
ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 16)
ctx.set_allow_unknown_ca(0)
#ctx.set_info_callback()
SimpleXMLRPCDispatcher.__init__(self)
SSL.SSLServer.__init__(self, (ADDR, PORT),
SimpleXMLRPCRequestHandler, ctx)
pass
def logit(self, msg):
if debug:
print msg
pass
else:
syslog.syslog(syslog.LOG_INFO, msg);
pass
return
#
# There might be a better arrangement, but the problem is that we
# do not want to create the server instance until we get a chance
# to look at the certificate and determine the priv level. See
# below in process_request().
#
def _dispatch(self, method, params):
try:
meth = getattr(self.emulabserver, method);
except AttributeError:
raise Exception('method "%s" is not supported' % method)
else:
return apply(meth, params);
pass
#
# Get the unix_uid for the user.
#
def getuserid(self, uid):
userQuery = DBQueryFatal("SELECT unix_uid FROM users AS u "
"WHERE u.uid=%s",
(uid,))
if len(userQuery) == 0:
return 0
return int(userQuery[0][0])
#
# Get the group list for the user.
#
def getusergroups(self, user):
result = []
res = DBQueryFatal("select distinct g.gid,g.unix_gid "
" from group_membership as m "
"left join groups as g on "
" g.pid=m.pid and g.gid=m.gid "
"where m.uid=%s "
"order by date_approved asc ",
(user,))
for group in res:
result.append(int(group[1]));
pass
return result
#
# Flip to the user that is in the certificate.
#
def fliptouser(self, request, client):
subject = request.get_peer_cert().get_subject()
if self.debug:
self.logit(str(subject))
pass
self.user = getattr(subject, "CN");
#
# Must be a valid and non-zero unix_uid from the DB.
#
self.uid = self.getuserid(self.user)
if self.uid == 0:
self.logit('No such user: "%s"' % self.user)
raise Exception('No such user: "%s"' % self.user)
self.glist = self.getusergroups(self.user);
if len(self.glist) == 0:
self.logit('No groups for user: "%s"' % self.user)
raise Exception('No groups for user: "%s"' % self.user)
self.logit("Connect from %s: %s %s" %
(client[0], self.user, str(self.glist)))
try:
os.setgid(self.glist[0])
os.setgroups(self.glist)
os.setuid(self.uid)
os.environ["USER"] = self.user;
os.environ["LOGNAME"] = self.user;
pass
except:
traceback.print_exc()
os._exit(1)
pass
pass
#
# XXX - The builtin process_request() method for ForkingMixIn is
# broken; it closes the "request" in the parent which shuts down
# the ssl connection. So, I have moved the close_request into the
# child where it should be, and in the parent I close the socket
# by reaching into the Connection() class.
#
# In any event, I need to do some other stuff in the child before we
# actually handle the request.
#
def process_request(self, request, client_address):
"""Fork a new subprocess to process the request."""
self.collect_children()
pid = os.fork()
if pid:
# Parent process
if self.active_children is None:
self.active_children = []
self.active_children.append(pid)
request.socket.close()
return
else:
# Child process.
# This must never return, hence os._exit()!
try:
self.fliptouser(request, client_address);
self.emulabserver = EmulabServer(readonly=0)
self.finish_request(request, client_address)
self.close_request(request)
self.logit("request finished");
os._exit(0)
except:
try:
self.handle_error(request, client_address)
finally:
os._exit(1)
def verify_request(self, request, client_address):
return True
pass
#
# Optional argument indicates the specific module the server wants to use.
# This form should be deprecated, I think.
#
if len(sys.argv) > 1:
module = sys.argv[1]
if not debug:
#
# Connect to syslog.
#
syslog.openlog("sslxmlrpc", syslog.LOG_PID,
eval("syslog.LOG_" + string.upper(LOGFACIL)))
syslog.syslog(syslog.LOG_INFO, "SSL XMLRPC server starting up");
#
# Daemonize. We redirect our output into a log file cause I have no
# idea what is going to use plain print.
#
try:
fp = open("@prefix@/log/sslxmlrpc_server.log", "a");
sys.stdout = fp
sys.stderr = fp
sys.stdin.close();
pass
except:
print "Could not open log file for append"
sys.exit(1);
pass
pid = os.fork()
if pid:
os.system("echo " + str(pid) + " > /var/run/sslxmlrpc_server.pid")
sys.exit(0)
pass
os.setsid();
pass
#
# This is invoked inside an SSH, typically from the paperbag shell.
# We use stdin/stdout to read/write the request/response. We handle
# just a single request this way, and then exit.
# Create the server and serve forever. We register the instance above
# when we process the request cause we want to look at the cert before
# we decide on the priv level.
#
# Construct and wrap our object.
server = eval(module + "(readonly=" + str(ReadOnly) + ")")
wrapper = sshxmlrpc.SSHServerWrapper(server)
# Handle the request on stdin and send the response to stdout.
wrapper.serve_forever((sys.stdin, sys.stdout))
sys.exit(0)
server = MyServer(debug);
server.serve_forever()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment