"""A sample socket server and client using SSPI authentication and encryption.
You must run with either 'client' or 'server' as arguments. A server must be
running before a client can connect.
To use with Kerberos you should include in the client options
--target-spn=username, where 'username' is the user under which the server is
being run.
Running either the client or server as a different user can be informative.
A command-line such as the following may be useful:
`runas /user:{user} {fqp}\python.exe {fqp}\socket_server.py --wait client|server`
{fqp} should specify the relevant fully-qualified path names.
To use 'runas' with Kerberos, the client program will need to
specify --target-spn with the username under which the *server* is running.
See the SSPI documentation for more details.
"""
import sys
import struct
import SocketServer
import win32api
import httplib
import traceback
import win32security
import sspi, sspicon
import optparse # sorry, this demo needs 2.3+
options = None # set to optparse object.
def GetUserName():
try:
return win32api.GetUserName()
except win32api.error, details:
# Seeing 'access denied' errors here for non-local users (presumably
# without permission to login locally). Get the fully-qualified
# username, although a side-effect of these permission-denied errors
# is a lack of Python codecs - so printing the Unicode value fails.
# So just return the repr(), and avoid codecs completely.
return repr(win32api.GetUserNameEx(win32api.NameSamCompatible))
# Send a simple "message" over a socket - send the number of bytes first,
# then the string. Ditto for receive.
def _send_msg(s, m):
s.send(struct.pack("i", len(m)))
s.send(m)
def _get_msg(s):
size_data = s.recv(struct.calcsize("i"))
if not size_data:
return None
cb = struct.unpack("i", size_data)[0]
return s.recv(cb)
class SSPISocketServer(SocketServer.TCPServer):
def __init__(self, *args, **kw):
SocketServer.TCPServer.__init__(self, *args, **kw)
self.sa = sspi.ServerAuth(options.package)
def verify_request(self, sock, ca):
# Do the sspi auth dance
self.sa.reset()
while 1:
data = _get_msg(sock)
if data is None:
return False
try:
err, sec_buffer = self.sa.authorize(data)
except sspi.error, details:
print "FAILED to authorize client:", details
return False
if err==0:
break
_send_msg(sock, sec_buffer[0].Buffer)
return True
def process_request(self, request, client_address):
# An example using the connection once it is established.
print "The server is running as user", GetUserName()
self.sa.ctxt.ImpersonateSecurityContext()
try:
print "Having conversation with client as user", GetUserName()
while 1:
# we need to grab 2 bits of data - the encrypted data, and the
# 'key'
data = _get_msg(request)
key = _get_msg(request)
if data is None or key is None:
break
data = self.sa.decrypt(data, key)
print "Client sent:", repr(data)
finally:
self.sa.ctxt.RevertSecurityContext()
self.close_request(request)
print "The server is back to user", GetUserName()
def serve():
s = SSPISocketServer(("localhost", options.port), None)
print "Running test server..."
s.serve_forever()
def sspi_client():
c = httplib.HTTPConnection("localhost", options.port)
c.connect()
# Do the auth dance.
ca = sspi.ClientAuth(options.package, targetspn=options.target_spn)
data = None
while 1:
err, out_buf = ca.authorize(data)
_send_msg(c.sock, out_buf[0].Buffer)
if err==0:
break
data = _get_msg(c.sock)
print "Auth dance complete - sending a few encryted messages"
# Assume out data is sensitive - encrypt the message.
for data in "Hello from the client".split():
blob, key = ca.encrypt(data)
_send_msg(c.sock, blob)
_send_msg(c.sock, key)
c.sock.close()
print "Client completed."
if __name__=='__main__':
parser = optparse.OptionParser("%prog [options] client|server",
description=__doc__)
parser.add_option("", "--package", action="store", default="NTLM",
help="The SSPI package to use (eg, Kerberos) - default is NTLM")
parser.add_option("", "--target-spn", action="store",
help="""The target security provider name to use. The
string contents are security-package specific. For
example, 'Kerberos' or 'Negotiate' require the server
principal name (SPN) (ie, the username) of the remote
process. For NTLM this must be blank.""")
parser.add_option("", "--port", action="store", default="8181",
help="The port number to use (default=8181)")
parser.add_option("", "--wait", action="store_true",
help="""Cause the program to wait for input just before
terminating. Useful when using via runas to see
any error messages before termination.
""")
options, args = parser.parse_args()
try:
options.port = int(options.port)
except (ValueError, TypeError):
parser.error("--port must be an integer")
try:
try:
if not args:
args = ['']
if args[0]=="client":
sspi_client()
elif args[0]=="server":
serve()
else:
parser.error("You must supply 'client' or 'server' - " \
"use --help for details")
except KeyboardInterrupt:
pass
except SystemExit:
pass
except:
traceback.print_exc()
finally:
if options.wait:
raw_input("Press enter to continue")
|