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.
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.
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
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)
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)