Examples

Echo server

The creation of an Echo server is really straightforward. It can be done with the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from pystack.layers.tcp_application import TCPApplication
from pystak.pystack import PyStack

class EchoServer(TCPApplication):
    def packet_received(self, packet, **kwargs):
        self.send_packet(packet, **kwargs) #Just reply the exact same thing to the client

if __name__ =="__main__":
    stack = PyStack()

    echoserver = EchoServer()

    stack.register_tcp_application(echoserver)

    echoserver.bind(8888, echoserver, False)
    echoserver.listen(5)

    stack.run(doreactor=True)

Comments about the code:

  • The EchoServer TCP application is fairly simple, we just override packet_received to reply to the client.
  • Line 15 the bind arguments are very important, there is no need to keep information about the client and the processing is the same for all that’s why all the client will have the same tcp application (echoserver).
  • Default arguments for bind are app=None, newinstance=False so we could have call echoserver.bind(8888) because when no app is provided self is used.
  • Line 18 we start the stack usin reactor, so that it keep the handle and the script wait for Ctrl+C. (If we had use thread the script would have ended up directly)
  • Also line 18 when the user type Ctrl+C the stack.stop() is called automatically.

Client/Server using pysocket

To make socket in a similar way than the official “socket module”, pystack_socket provides an interface to socket. The only thing to do is

import psytack.pystack_socket as socket

Then a client or a server can be done in a similar way than with socket except that, at the end the stack should be stopped. The following sample shows a basic example:

import pystack.pystack_socket as socket
import time

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

if s.connect(("myserver.com", 80)):
    s.sendall('Hello, world\n')
    data = s.recv(1024)
    print('Received '+ repr(data))
    s.close()
else:
    print("Not connected")

time.sleep(4) #Wait a little to avoid to get the stack destroyed before the socket is gently closed.
s.stop() #To stop the stack

Like in socket module a server can be written replacing the connect by:

s.bind(("localhost",PORT))
s.listen(2)
cli, addr = s.accept()

Important

The first parameter sent by bind is ignored by pystack. The server will only listen on the interface on which the stack is listening on. By default the stack use the default interface. For instance if a server is listening on the address 192.168.0.1 on port 80 trying to access the server locally will certainly fail because the system may resolve 192.168.0.1 as 127.0.0.1.

socket module hijacking

Thank’s to pystack_socket it can be interesting to force certain scripts or program to use pystack instead of socket without modifying the source code. As soon as no extra socket functionalities are used this might succeed. This section shows how to do it. This is tricky and it does not work all the time but it might fit in simple cases. Let’s take the following code that use socket.

import socket

class Client():
    def __init__(self):
        pass

    def run(self):
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect(("myserver.com", 5555))
        s.sendall('Hello, world\n')
        data = s.recv(1024)
        s.close()
        print('Received'+ repr(data))

What we will try to do is to make Client to use pystack instead of socket. The following script does it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import pystack.pystack_socket
import sys
sys.modules["socket_original"] = sys.modules["socket"]
sys.modules["socket"] = pystack_socket

from test_client import Client
c =Client()
c.run()

from pystack.pystack import PyStack
s = PyStack() #Retrieve the pystack instance to stop it
s.stop()

sys.modules["socket"] = sys.modules["socket_original"]
sys.modules.pop("socket_original")

Comments about the script:

  • Line 1-5: Replace the socket module in sys by pystack_socket
  • Line 7-9: Import and launch the Client
  • Line 11-13: Import pystack create a PyStack object but because it implement the singleton pattern we retrieve the only instance of PyStack and we stop it.
  • Line 15-16: Put back the genuine socket module in sys.modules

Web server

Making a Web server is a complex tasks. We will tkae advantage here of twisted functionalities. We will only manage to receive and send request. All there serving content part is delegated to twisted. The method used below is inspired from the one used in muXTCP . We will use the class Site taken from twisted.web.server that allow to serve a given static directory as web server. The the trick is located in the Site object which have an attribute called transport which take care of input/output tasks. We will define the transport attribute to be our TCPApplication which implies to implement additional methods. The following class WebServer inherit from both TCPApplication for the pystack part and _ConsumerMixin for the twisted part.

Warning

There is certainly a nicer way to do it, but this not the purpose of this example.

from pystack.layers.tcp_application import TCPApplication
from twisted.web.server import Site
from twisted.web import static
from twisted.internet.abstract import FileDescriptor
from twisted.internet.abstract import _ConsumerMixin
import os

class WebServer(TCPApplication, _ConsumerMixin):
    disconnecting = False #Required by twisted
    connected = True
    disconnected = False

    def __init__(self):
        TCPApplication.__init__(self)
        _ConsumerMixin.__init__(self)
        self.app = Site(static.File(os.path.abspath("./sitetest"))).buildProtocol("test")
        self.app.transport = self #Because we define self as transport we have to implement function normally called by twisted for a transport class

    def packet_received(self, packet, **kwargs): #Override TCPApplication packet_received to call the dataReceived of twisted
        self.lastclient = kwargs["id"]
        try:
            print("Request received")
            self.app.dataReceived(packet)
        except Exception, e:
            print("Something is wrong in the request:"+ str(e))

    def write(self, data):
        print "data to send"
        while len(data) > 0:
            x = data[0:1000]
            data = data[1000:]
            #self.send_data(x)
            self.send_packet(x, **{"id":self.lastclient})

    def getPeer(self):
        class X:
            host = "myHost"
            port = "myPort"
        return X()

    def getHost(self):
        return self.getPeer()

    def writeSequence(self, iovec):
        self.write("".join(iovec))

    def loseConnection(self):
        pass

    def getvalue(self):
        pass

getPeer, getHost, loseConnection and getvalue should be present to work even though we didn’t implemented them. This is for the TCPApplication, the instanciation and registration toward the stack is classic.

if __name__ =="__main__":
    from pystack.pystack import PyStack
    stack = PyStack()

    webserver = WebServer()
    stack.register_tcp_application(webserver)

    webserver.bind(80, app=webserver, newinstance=True)
    webserver.listen(2)

    stack.run(doreactor=True)

Error

A new webserver instance should be instanciated for each new client because twisted does not accept multiples request on the same _ConsumerMixin more than once. (Which is also problematic for a single client)

SSH server

Creating an SSH server is made quite simple thank’s to all the twisted functionalities. It have globaly the same structure than WebServer except that we will create a unix.UnixSSHRealm instead of a Site.

Error

During the test I also had a problem with OpenSSHFactory which failed to read my keys. This problem is independant of pystack and is certainly due to twisted itself. This led me to create my own OpenSSHFactory fixing to problem which was located in getPrivateKeys.

from pystack.layers.tcp_application import TCPApplication
from twisted.internet.abstract import _ConsumerMixin
from twisted.conch import checkers, unix
from twisted.conch.openssh_compat import factory
from twisted.conch.openssh_compat.factory import OpenSSHFactory
from twisted.cred import portal, checkers as chk

class MyFactory(OpenSSHFactory):
    '''I need to create my factory because OpenSSHFactory fail when reading /etc/ssh and all keys
    Because some are not recognised it return None but no test is made
    So I just added "if key:" at the fourth last line of getPrivateKeys'''

    def getPrivateKeys(self):
        from twisted.python import log
        from twisted.python.util import runAsEffectiveUser
        from twisted.conch.ssh import keys
        import os, errno
        privateKeys = {}
        for filename in os.listdir(self.dataRoot):
            if filename[:9] == 'ssh_host_' and filename[-4:]=='_key':
                fullPath = os.path.join(self.dataRoot, filename)
                try:
                    key = keys.Key.fromFile(fullPath)
                except IOError, e:
                    if e.errno == errno.EACCES:
                        # Not allowed, let's switch to root
                        key = runAsEffectiveUser(0, 0, keys.Key.fromFile, fullPath)
                        keyType = keys.objectType(key.keyObject)
                        privateKeys[keyType] = key
                    else:
                        raise
                except Exception, e:
                    log.msg('bad private key file %s: %s' % (filename, e))
                else:
                    if key: #Just to add this fucking Line !
                        keyType = keys.objectType(key.keyObject)
                        privateKeys[keyType] = key
        return privateKeys

class SSHServer(TCPApplication, _ConsumerMixin):
    disconnecting = False #Required by twisted
    connected = True
    disconnected = False

    def __init__(self):
        TCPApplication.__init__(self)
        _ConsumerMixin.__init__(self)

        #t = factory.OpenSSHFactory()
        t = MyFactory() #Use my factory instead of the original one
        t.portal = portal.Portal(unix.UnixSSHRealm())
        t.portal.registerChecker(checkers.UNIXPasswordDatabase())
        t.portal.registerChecker(checkers.SSHPublicKeyDatabase())
        if checkers.pamauth:
            t.portal.registerChecker(chk.PluggableAuthenticationModulesChecker())
        t.dataRoot = '/etc/ssh'
        t.moduliRoot = '/etc/ssh'

        t.startFactory()
        self.app = t.buildProtocol("test")
        self.app.transport = self

    def connection_made(self):
        self.app.connectionMade()

    def packet_received(self, packet, **kwargs): #Override TCPApplication packet_received to call the dataReceived of twisted
        try:
            print("Request received")
            self.app.dataReceived(packet)
        except Exception, e:
            print("Something is wrong in the request:"+ str(e))

    def write(self, data):
        print("Write data")
        while len(data) > 0:
            x = data[0:1000]
            data = data[1000:]
            #self.send_data(x)
            self.send_packet(x)

    def getPeer(self):
        class X:
            host = "myHost"
            port = "myPort"
        return X()

    def getHost(self):
        return self.getPeer()

    def writeSequence(self, iovec):
        self.write("".join(iovec))

    def logPrefix(self):
        return "pystackSSHServer"

    def setTcpNoDelay(self, tog):
        pass

    def loseConnection(self):
        pass

    def getvalue(self):
        pass

Then starting the server works the exact same manner than WebServer

Caution

The close of a session does not always work fine.

if __name__ =="__main__":
    from pystack.pystack import PyStack
    stack = PyStack()

    sshserver = SSHServer()
    stack.register_tcp_application(sshserver)

    sshserver.bind(80, app=sshserver, newinstance=True)
    sshserver.listen(2)

    stack.run(doreactor=True)