4
from tftp.datagram import (ACKDatagram, ERRORDatagram, ERR_TID_UNKNOWN,
5
TFTPDatagramFactory, split_opcode, OP_OACK, OP_ERROR, OACKDatagram, OP_ACK,
7
from tftp.session import WriteSession, MAX_BLOCK_SIZE, ReadSession
8
from tftp.util import SequentialCall
9
from twisted.internet import reactor
10
from twisted.internet.protocol import DatagramProtocol
11
from twisted.python import log
12
from twisted.python.util import OrderedDict
14
class TFTPBootstrap(DatagramProtocol):
15
"""Base class for TFTP Bootstrap classes, classes, that handle initial datagram
16
exchange (option negotiation, etc), before the actual transfer is started.
18
Why OrderedDict and not the regular one? As per
19
U{RFC2347<http://tools.ietf.org/html/rfc2347>}, the order of options is, indeed,
20
not important, but having them in an arbitrary order would complicate testing.
22
@cvar supported_options: lists options, that we know how to handle
24
@ivar session: A L{WriteSession} or L{ReadSession} object, that will handle
25
the actual tranfer, after the initial handshake and option negotiation is
27
@type session: L{WriteSession} or L{ReadSession}
29
@ivar options: a mapping of options, that this protocol instance was
30
initialized with. If it is empty and we are the server, usual logic (that
31
doesn't involve OACK datagrams) is used.
32
Default: L{OrderedDict<twisted.python.util.OrderedDict>}.
33
@type options: L{OrderedDict<twisted.python.util.OrderedDict>}
35
@ivar resultant_options: stores the last options mapping value, that was passed
37
@type resultant_options: L{OrderedDict<twisted.python.util.OrderedDict>}
39
@ivar remote: remote peer address
40
@type remote: C{(str, int)}
42
@ivar timeout_watchdog: an object, that is responsible for timing the protocol
43
out. If we are initiating the transfer, it is provided by the parent protocol
45
@ivar backend: L{IReader} or L{IWriter} provider, that is used for this transfer
46
@type backend: L{IReader} or L{IWriter} provider
49
supported_options = ('blksize', 'timeout', 'tsize')
51
def __init__(self, remote, backend, options=None, _clock=None):
53
self.options = OrderedDict()
55
self.options = options
56
self.resultant_options = OrderedDict()
58
self.timeout_watchdog = None
59
self.backend = backend
60
if _clock is not None:
65
def processOptions(self, options):
66
"""Process options mapping, discarding malformed or unknown options.
68
@param options: options mapping to process
69
@type options: L{OrderedDict<twisted.python.util.OrderedDict>}
71
@return: a mapping of processed options. Invalid options are discarded.
72
Whether or not the values of options may be changed is decided on a per-
73
option basis, according to the standard
74
@rtype L{OrderedDict<twisted.python.util.OrderedDict>}
77
accepted_options = OrderedDict()
78
for name, val in options.iteritems():
79
norm_name = name.lower()
80
if norm_name in self.supported_options:
81
actual_value = getattr(self, 'option_' + norm_name)(val)
82
if actual_value is not None:
83
accepted_options[name] = actual_value
84
return accepted_options
86
def option_blksize(self, val):
87
"""Process the block size option. Valid range is between 8 and 65464,
88
inclusive. If the value is more, than L{MAX_BLOCK_SIZE}, L{MAX_BLOCK_SIZE}
91
@param val: value of the option
94
@return: accepted option value or C{None}, if it is invalid
95
@rtype: C{str} or C{None}
99
int_blksize = int(val)
102
if int_blksize < 8 or int_blksize > 65464:
104
int_blksize = min((int_blksize, MAX_BLOCK_SIZE))
105
return str(int_blksize)
107
def option_timeout(self, val):
108
"""Process timeout interval option
109
(U{RFC2349<http://tools.ietf.org/html/rfc2349>}). Valid range is between 1
112
@param val: value of the option
115
@return: accepted option value or C{None}, if it is invalid
116
@rtype: C{str} or C{None}
120
int_timeout = int(val)
123
if int_timeout < 1 or int_timeout > 255:
125
return str(int_timeout)
127
def option_tsize(self, val):
128
"""Process tsize interval option
129
(U{RFC2349<http://tools.ietf.org/html/rfc2349>}). Valid range is 0 and up.
131
@param val: value of the option
134
@return: accepted option value or C{None}, if it is invalid
135
@rtype: C{str} or C{None}
144
return str(int_tsize)
146
def applyOptions(self, session, options):
147
"""Apply given options mapping to the given L{WriteSession} or
148
L{ReadSession} object.
150
@param session: A session object to apply the options to
151
@type session: L{WriteSession} or L{ReadSession}
153
@param options: Options to apply to the session object
154
@type options: L{OrderedDict<twisted.python.util.OrderedDict>}
157
for opt_name, opt_val in options.iteritems():
158
if opt_name == 'blksize':
159
session.block_size = int(opt_val)
160
elif opt_name == 'timeout':
161
timeout = int(opt_val)
162
session.timeout = (timeout,) * 3
163
elif opt_name == 'tsize':
165
session.tsize = tsize
167
def datagramReceived(self, datagram, addr):
168
if self.remote[1] != addr[1]:
169
self.transport.write(ERRORDatagram.from_code(ERR_TID_UNKNOWN).to_wire())
170
return# Does not belong to this transfer
171
datagram = TFTPDatagramFactory(*split_opcode(datagram))
172
if datagram.opcode == OP_ERROR:
173
return self.tftp_ERROR(datagram)
174
return self._datagramReceived(datagram)
176
def tftp_ERROR(self, datagram):
177
"""Handle the L{ERRORDatagram}.
179
@param datagram: An ERROR datagram
180
@type datagram: L{ERRORDatagram}
183
log.msg("Got error: " % datagram)
187
"""Terminate this protocol instance. If the underlying
188
L{ReadSession}/L{WriteSession} is running, delegate the call to it.
191
if self.timeout_watchdog is not None and self.timeout_watchdog.active():
192
self.timeout_watchdog.cancel()
193
if self.session.started:
194
self.session.cancel()
196
self.backend.finish()
197
self.transport.stopListening()
200
"""This protocol instance has timed out during the initial handshake."""
201
log.msg("Timed during option negotiation process")
205
class LocalOriginWriteSession(TFTPBootstrap):
206
"""Bootstraps a L{WriteSession}, that was initiated locally, - we've requested
207
a read from a remote server
210
def __init__(self, remote, writer, options=None, _clock=None):
211
TFTPBootstrap.__init__(self, remote, writer, options, _clock)
212
self.session = WriteSession(writer, self._clock)
214
def startProtocol(self):
215
"""Connect the transport and start the L{timeout_watchdog}"""
216
self.transport.connect(*self.remote)
217
if self.timeout_watchdog is not None:
218
self.timeout_watchdog.start()
220
def tftp_OACK(self, datagram):
221
"""Handle the OACK datagram
223
@param datagram: OACK datagram
224
@type datagram: L{OACKDatagram}
227
if not self.session.started:
228
self.resultant_options = self.processOptions(datagram.options)
229
if self.timeout_watchdog.active():
230
self.timeout_watchdog.cancel()
231
return self.transport.write(ACKDatagram(0).to_wire())
233
log.msg("Duplicate OACK received, send back ACK and ignore")
234
self.transport.write(ACKDatagram(0).to_wire())
236
def _datagramReceived(self, datagram):
237
if datagram.opcode == OP_OACK:
238
return self.tftp_OACK(datagram)
239
elif datagram.opcode == OP_DATA and datagram.blocknum == 1:
240
if self.timeout_watchdog is not None and self.timeout_watchdog.active():
241
self.timeout_watchdog.cancel()
242
if not self.session.started:
243
self.applyOptions(self.session, self.resultant_options)
244
self.session.transport = self.transport
245
self.session.startProtocol()
246
return self.session.datagramReceived(datagram)
247
elif self.session.started:
248
return self.session.datagramReceived(datagram)
251
class RemoteOriginWriteSession(TFTPBootstrap):
252
"""Bootstraps a L{WriteSession}, that was originated remotely, - we've
253
received a WRQ from a client.
258
def __init__(self, remote, writer, options=None, _clock=None):
259
TFTPBootstrap.__init__(self, remote, writer, options, _clock)
260
self.session = WriteSession(writer, self._clock)
262
def startProtocol(self):
263
"""Connect the transport, respond with an initial ACK or OACK (depending on
264
if we were initialized with options or not).
267
self.transport.connect(*self.remote)
269
self.resultant_options = self.processOptions(self.options)
270
bytes = OACKDatagram(self.resultant_options).to_wire()
272
bytes = ACKDatagram(0).to_wire()
273
self.timeout_watchdog = SequentialCall.run(
275
callable=self.transport.write, callable_args=[bytes, ],
276
on_timeout=lambda: self._clock.callLater(self.timeout[-1], self.timedOut),
281
def _datagramReceived(self, datagram):
282
if datagram.opcode == OP_DATA and datagram.blocknum == 1:
283
if self.timeout_watchdog.active():
284
self.timeout_watchdog.cancel()
285
if not self.session.started:
286
self.applyOptions(self.session, self.resultant_options)
287
self.session.transport = self.transport
288
self.session.startProtocol()
289
return self.session.datagramReceived(datagram)
290
elif self.session.started:
291
return self.session.datagramReceived(datagram)
294
class LocalOriginReadSession(TFTPBootstrap):
295
"""Bootstraps a L{ReadSession}, that was originated locally, - we've requested
296
a write to a remote server.
299
def __init__(self, remote, reader, options=None, _clock=None):
300
TFTPBootstrap.__init__(self, remote, reader, options, _clock)
301
self.session = ReadSession(reader, self._clock)
303
def startProtocol(self):
304
"""Connect the transport and start the L{timeout_watchdog}"""
305
self.transport.connect(*self.remote)
306
if self.timeout_watchdog is not None:
307
self.timeout_watchdog.start()
309
def _datagramReceived(self, datagram):
310
if datagram.opcode == OP_OACK:
311
return self.tftp_OACK(datagram)
312
elif (datagram.opcode == OP_ACK and datagram.blocknum == 0
313
and not self.session.started):
314
self.session.transport = self.transport
315
self.session.startProtocol()
316
if self.timeout_watchdog is not None and self.timeout_watchdog.active():
317
self.timeout_watchdog.cancel()
318
return self.session.nextBlock()
319
elif self.session.started:
320
return self.session.datagramReceived(datagram)
322
def tftp_OACK(self, datagram):
323
"""Handle incoming OACK datagram, process and apply options and hand over
324
control to the underlying L{ReadSession}.
326
@param datagram: OACK datagram
327
@type datagram: L{OACKDatagram}
330
if not self.session.started:
331
self.resultant_options = self.processOptions(datagram.options)
332
if self.timeout_watchdog is not None and self.timeout_watchdog.active():
333
self.timeout_watchdog.cancel()
334
self.applyOptions(self.session, self.resultant_options)
335
self.session.transport = self.transport
336
self.session.startProtocol()
337
return self.session.nextBlock()
339
log.msg("Duplicate OACK received, ignored")
341
class RemoteOriginReadSession(TFTPBootstrap):
342
"""Bootstraps a L{ReadSession}, that was started remotely, - we've received
348
def __init__(self, remote, reader, options=None, _clock=None):
349
TFTPBootstrap.__init__(self, remote, reader, options, _clock)
350
self.session = ReadSession(reader, self._clock)
352
def option_tsize(self, val):
353
"""Process tsize option.
355
If tsize is zero, get the size of the file to be read so that it can
356
be returned in the OACK datagram.
358
@see: L{TFTPBootstrap.option_tsize}
361
val = TFTPBootstrap.option_tsize(self, val)
363
val = self.session.reader.size
368
def startProtocol(self):
369
"""Start sending an OACK datagram if we were initialized with options
370
or start the L{ReadSession} immediately.
373
self.transport.connect(*self.remote)
375
self.resultant_options = self.processOptions(self.options)
376
bytes = OACKDatagram(self.resultant_options).to_wire()
377
self.timeout_watchdog = SequentialCall.run(
379
callable=self.transport.write, callable_args=[bytes, ],
380
on_timeout=lambda: self._clock.callLater(self.timeout[-1], self.timedOut),
385
self.session.transport = self.transport
386
self.session.startProtocol()
387
return self.session.nextBlock()
389
def _datagramReceived(self, datagram):
390
if datagram.opcode == OP_ACK and datagram.blocknum == 0:
391
return self.tftp_ACK(datagram)
392
elif self.session.started:
393
return self.session.datagramReceived(datagram)
395
def tftp_ACK(self, datagram):
396
"""Handle incoming ACK datagram. Hand over control to the underlying
399
@param datagram: ACK datagram
400
@type datagram: L{ACKDatagram}
403
if self.timeout_watchdog is not None:
404
self.timeout_watchdog.cancel()
405
if not self.session.started:
406
self.applyOptions(self.session, self.resultant_options)
407
self.session.transport = self.transport
408
self.session.startProtocol()
409
return self.session.nextBlock()