-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtftp.py
More file actions
236 lines (188 loc) · 7.81 KB
/
Copy pathtftp.py
File metadata and controls
236 lines (188 loc) · 7.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
'''
Implementing some of:
https://www.ietf.org/rfc/rfc1350.txt
Not implementing:
writing (putting) files
modes other then netascii
returning appropriate errors to the client
'''
import os
import struct
from twisted.internet.protocol import DatagramProtocol
from twisted.internet import reactor
BLOCK_SIZE = 512
opcodeMap = {
'RRQ': '\x00\x01',
'WRQ': '\x00\x02',
'DATA': '\x00\x03',
'ACK': '\x00\x04',
'ERROR': '\x00\x05',
}
def intToBytes(i):
'''
eg 3 -> '\x00\x03'
'''
# TODO what happens if our file is so big the blockno exceeds the size of
# an unsigned short? that would be quite big, 2 bytes is 16 bits is 2^16, so
# file size would be 2^16 * 512 bytes = 2^15 kb = 2^5 MB = 32 MB. Hmm, not
# so big actually . . .
return struct.pack('>H', i)
def bytesToInt(byt):
'''
eg '\x00\x03' -> 3
'''
return struct.unpack('>H', byt)[0]
class BadDataError(Exception):
''' To be raised whenever the data is not as we would prefer.'''
pass
class FileSystem(object):
'''
FileSystem must provide a getFileData method.
'''
def __init__(self, directoryPath='/tmp/test_tftp'):
self.directoryPath = directoryPath
def readBlock(self, path, blockno):
with open(path, 'r') as fin:
fin.seek((blockno - 1) * BLOCK_SIZE)
return fin.read(BLOCK_SIZE)
def getFileData(self, requestedFilename, blockno):
# os.listdir only returns the names of the files in the dir, so compare
# against that just to make sure there's no directory traversal
# possibilities
for filename in os.listdir(self.directoryPath):
if requestedFilename == filename:
return self.readBlock(os.path.join(self.directoryPath,
filename), blockno)
# TODO this is an error we should return to the client (and is it
# really 'bad data'?)
raise BadDataError('File not found.')
class Ack(object):
def __init__(self, data):
if data[:2] != opcodeMap['ACK']:
# TODO return error messages to the client?
raise BadDataError('Message is not an ack.')
self.blockno = bytesToInt(data[2:])
class Download(object):
def __init__(self, readRequest, remoteHost, remotePort, fileSystem=None):
self.filename = readRequest.filename
self.mode = readRequest.mode
self.remoteHost = remoteHost
self.remotePort = remotePort
self.blockno = 0
self.fileSystem = fileSystem
if self.fileSystem is None:
self.fileSystem = FileSystem()
self.done = False
def makeNextDataMessage(self):
blockno, data = self.nextData()
return '%s%s%s' % (
opcodeMap['DATA'],
intToBytes(blockno),
data)
def nextData(self):
self.blockno += 1
data = self.fileSystem.getFileData(self.filename, self.blockno)
if len(data) < BLOCK_SIZE:
self.done = True
return self.blockno, data
def ack(self, message, host, port):
if host != self.remoteHost or port != self.remotePort:
# TODO error handling
raise BadDataError('Message from wrong place!')
ack = Ack(message)
if ack.blockno != self.blockno:
#TODO error handling
raise BadDataError('This ack is out of sequence. ack for blockno %s, current blockno %s' % (ack.blockno, self.blockno))
return self.makeNextDataMessage()
class TftpDownloadHandler(DatagramProtocol):
'''
Subclass of twisted.internet.protocol.DatagramProtocol
(http://twistedmatrix.com/documents/current/api/twisted.internet.protocol.DatagramProtocol.html)
Created when the server gets a read request for a file.
Expects only acks from the remoteHost and remotePort it knows.
As soon as TftpDownloadHandler is created it sends it's first data packet.
A transfer is established by sending a request ([...]
RRQ to read from it), and receiving a
positive reply, [...] the first data packet for read.
Data packets look like:
2 bytes 2 bytes n bytes
----------------------------------
| Opcode | Block # | Data |
----------------------------------
Figure 5-2: DATA packet
Where opcode is \x00\x03 DATA
We send the next block when we get an ack for the last block.
Ack packets look like:
2 bytes 2 bytes
---------------------
| Opcode | Block # |
---------------------
Figure 5-3: ACK packet
Where opcode is \x00\x04 ACK
We stop when:
'the host sending the final
ACK will wait for a while before terminating in order to retransmit
the final ACK if it has been lost. The acknowledger will know that
the ACK has been lost if it receives the final DATA packet again.
The host sending the last DATA must retransmit it until the packet is
acknowledged or the sending host times out.'
in other words: once we've received an ack for our last message, we can hang
up
'''
# timeout period should actually be longer than this I think
timeoutPeriod = 5
def __init__(self, readRequest, remoteHost, remotePort, fileSystem=None):
self.readRequest = readRequest
self.remoteHost = remoteHost
self.remotePort = remotePort
self.fileSystem = fileSystem
self.timeout = reactor.callLater(self.timeoutPeriod, self.timedOut)
def timedOut(self):
if self.transport is not None:
self.transport.stopListening()
def startProtocol(self):
# make download
self.download = Download(self.readRequest, self.remoteHost, self.remotePort, self.fileSystem)
# send first block
self.transport.write(self.download.makeNextDataMessage(), (self.remoteHost, self.remotePort))
def datagramReceived(self, data, (host, port)):
self.timeout.reset(self.timeoutPeriod)
if self.download.done:
self.transport.stopListening()
self.transport.write(self.download.ack(data, host, port), (host, port))
class ReadRequest(object):
'''
Parses a message from a client into a read request, or raises an error.
'''
def __init__(self, data):
if len(data) < 2 or data[:2] != opcodeMap['RRQ']:
# TODO should this be an error message of code 0?
raise BadDataError('Got a message that is not a read request: %s' % data)
filename, mode, nothing = data[2:].split('\x00')
self.filename = filename
self.mode = mode
class TftpReadRequestHandler(DatagramProtocol):
'''
Subclass of twisted.internet.protocol.DatagramProtocol
(http://twistedmatrix.com/documents/current/api/twisted.internet.protocol.DatagramProtocol.html)
Listens on port 69 and expects only read requests.
Read request looks like:
2 bytes string 1 byte string 1 byte
------------------------------------------------
| Opcode | Filename | 0 | Mode | 0 |
------------------------------------------------
Figure 5-1: RRQ/WRQ packet
Where Opcode is \x00\x01
'''
def datagramReceived(self, data, (host, port)):
'''
When we get a read request, start a new port sending data and listening
for acks by calling listenUDP with TftpDownloadHandler.
'''
readRequest = ReadRequest(data)
# Spec says pick a random port.
reactor.listenUDP(0, TftpDownloadHandler(readRequest, host, port))
if __name__ == '__main__':
reactor.listenUDP(69, TftpReadRequestHandler())
print 'serving files'
reactor.run()