Skip to content

Commit

Permalink
Add CWD/NLST/LIST and remove threading.
Browse files Browse the repository at this point in the history
Threading really isn't necessary and just adds complexity.
  • Loading branch information
Allen Luce committed Mar 18, 2016
1 parent 7060204 commit 3c65db5
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 33 deletions.
67 changes: 34 additions & 33 deletions stubserver/ftpserver.py
Expand Up @@ -12,6 +12,7 @@ def __init__(self, hostname, port, interactions, files):
self.port = port
self.interactions = interactions
self.files = files
self.cwd = '/'

def __call__(self, request, client_address, server):
self.request = request
Expand All @@ -30,10 +31,13 @@ def handle(self):
self.communicating = True
while self.communicating:
cmd = self.request.recv(1024)
if len(cmd) == 0:
break
if cmd:
self.interactions.append(cmd)
cmd = cmd.decode('utf-8')
getattr(self, '_' + cmd[:4])(cmd)
cmd = cmd.decode('utf-8').rstrip()
first = cmd.split(' ', 1)[0]
getattr(self, '_' + first)(cmd)

def _USER(self, cmd):
self.request.send(b'331 Please specify password.\r\n')
Expand All @@ -45,64 +49,58 @@ def _TYPE(self, cmd):
self.request.send(b'200 Switching to ascii mode.\r\n')

def _PASV(self, cmd):
self.parent_event = threading.Event()
self.child_event = threading.Event()
self.data_handler = FTPDataServer(self.interactions, self.files, self.child_event)
self.data_handler = FTPDataServer(self.interactions, self.files)
self.port = self.port + 1
SocketServer.TCPServer.allow_reuse_address = True
self.data_server = SocketServer.TCPServer((self.hostname, self.port + 1), self.data_handler)

def start_data_server():
self.port = self.port + 1
SocketServer.TCPServer.allow_reuse_address = True
data_server = SocketServer.TCPServer((self.hostname, self.port + 1), self.data_handler)
# needs to signal ready here
self.parent_event.set()
data_server.handle_request()
data_server.server_close()
self.parent_event.set()

self.t2 = threading.Thread(target=start_data_server)
self.t2.start()
self.parent_event.wait()

self.request.send(('227 Entering Passive Mode. (127,0,0,1,%s,%s)\r\n' % (
int((self.port + 1) / 256), (self.port + 1) % 256)).encode('utf-8'))

def filename(self):
return self.interactions[-1:][0][5:].strip()

def child_go(self):
self.child_event.set()
self.parent_event.clear()
self.parent_event.wait()
self.data_server.handle_request()
self.data_server.server_close()

def _STOR(self, cmd):
self.request.send(b'150 Okay to send data\r\n')
self.child_go()
self.request.send(b'226 Got the file\r\n')
self.t2.join(1)

def _LIST(self, cmd):
self.request.send(b'150 Accepted data connection\r\n')
self.child_go()
self.request.send(b'226 You got the listings now\r\n')
self.t2.join(1)

def _RETR(self, cmd):
self.request.send(b'150 Accepted data connection\r\n')
self.child_go()
self.request.send(b'226 Enjoy your file\r\n')
self.t2.join(1)

def _CWD(self, cmd):
self.cwd = cmd.split(' ',2)[1]
self.request.send(b'250 OK. Current directory is "%s"\r\n' % self.cwd.encode('utf-8'))

def _PWD(self, cmd):
self.request.send(b'257 "%s" is your current location\r\n' % self.cwd.encode('utf-8'))

def _NLST(self, cmd):
self.request.send(b'150 Accepted data connection\r\n')
self.child_go()
self.request.send(b'226 You got the listings now\r\n')

def _QUIT(self, cmd):
self.request.send(b'221 Goodbye\r\n')
self.communicating = False
time.sleep(0.001)

self.request.send(b'221-Goodbye.\r\n221 Have fun.')

class FTPDataServer(SocketServer.StreamRequestHandler):
def __init__(self, interactions, files, event):
def __init__(self, interactions, files):
self.interactions = interactions
self.files = files
self.command = 'LIST'
self.child_event = event

def __call__(self, request, client_address, server):
self.request = request
Expand All @@ -116,7 +114,6 @@ def __call__(self, request, client_address, server):
self.finish()

def handle(self):
self.child_event.wait()
cmd = self.interactions[-1:][0].decode('utf-8')
if cmd[:4] == 'PASV':
return
Expand All @@ -131,11 +128,15 @@ def _STOR(self):
self.files[self.filename()] = self.rfile.read().strip()

def _LIST(self):
data = '\n'.join([name for name in self.files.keys()])
self.wfile.write(data.encode('utf-8'))
data = b'\n'.join([name for name in self.files.keys()])
self.wfile.write(data)

def _NLST(self):
data = b'\015\012'.join([name for name in self.files.keys()])
self.wfile.write(data)

def _RETR(self):
self.wfile.write(self.files[self.filename().decode('utf-8')].encode('utf-8'))
self.wfile.write(self.files[self.filename()])


class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
Expand Down
42 changes: 42 additions & 0 deletions test.py
Expand Up @@ -143,6 +143,16 @@ def setUp(self):
def tearDown(self):
self.server.stop()

def test_change_directory(self):
ftp = FTP()
ftp.set_debuglevel(0)
ftp.connect('localhost', self.port)
ftp.login('user1', 'passwd')
ftp.cwd('newdir')
self.assertEqual(ftp.pwd(), 'newdir')
ftp.quit()
ftp.close()

def test_put_test_file(self):
self.assertFalse(self.server.files("foo.txt"))
ftp = FTP()
Expand Down Expand Up @@ -170,6 +180,38 @@ def test_put_2_files_associates_the_correct_content_with_the_correct_filename(se
self.server.files("robot.txt").strip())
self.assertEquals("file2 content", self.server.files("monster.txt").strip())

def test_list_2_files(self):
ftp = FTP()
ftp.connect('localhost', self.port)
ftp.set_debuglevel(0)
ftp.login('user2','other_pass')
self.lines = []
def accumulate(line):
self.lines.append(line)

ftp.storlines('STOR palladium.csv', BytesIO(b'data'))
ftp.storlines('STOR vanadiyam.pdf', BytesIO(b'more data'))
ftp.retrlines('LIST', accumulate)
self.assertEqual(self.lines, ['palladium.csv', 'vanadiyam.pdf'])
ftp.quit()
ftp.close()

def test_nlst_2_files(self):
ftp = FTP()
ftp.connect('localhost', self.port)
ftp.set_debuglevel(0)
ftp.login('user2','other_pass')
self.lines = []
def accumulate(line):
self.lines.append(line)

ftp.storlines('STOR palladium.csv', BytesIO(b'data'))
ftp.storlines('STOR vanadiyam.pdf', BytesIO(b'more data'))
ftp.retrlines('NLST', accumulate)
self.assertEqual(self.lines, ['palladium.csv', 'vanadiyam.pdf'])
ftp.quit()
ftp.close()

def test_retrieve_expected_file_returns_file(self):
expected_content = 'content of my file\nis a complete mystery to me.'
self.server.add_file(b'foo.txt', expected_content)
Expand Down

0 comments on commit 3c65db5

Please sign in to comment.