4
from itertools import chain
5
from tftp.errors import (WireProtocolError, InvalidOpcodeError,
6
PayloadDecodeError, InvalidErrorcodeError, OptionsDecodeError)
7
from twisted.python.util import OrderedDict
18
ERR_FILE_NOT_FOUND = 1
19
ERR_ACCESS_VIOLATION = 2
28
ERR_FILE_NOT_FOUND : "File not found",
29
ERR_ACCESS_VIOLATION : "Access violation",
30
ERR_DISK_FULL : "Disk full or allocation exceeded",
31
ERR_ILLEGAL_OP : "Illegal TFTP operation",
32
ERR_TID_UNKNOWN : "Unknown transfer ID",
33
ERR_FILE_EXISTS : "File already exists",
34
ERR_NO_SUCH_USER : "No such user"
38
def split_opcode(datagram):
39
"""Split the raw datagram into opcode and payload.
41
@param datagram: raw datagram
42
@type datagram: C{str}
44
@return: a 2-tuple, the first item is the opcode and the second item is the payload
45
@rtype: (C{int}, C{str})
47
@raise WireProtocolError: if the opcode cannot be extracted
52
return struct.unpack("!H", datagram[:2])[0], datagram[2:]
54
raise WireProtocolError("Failed to extract the opcode")
57
class TFTPDatagram(object):
58
"""Base class for datagrams
60
@cvar opcode: The opcode, corresponding to this datagram
68
def from_wire(cls, payload):
69
"""Parse the payload and return a datagram object
71
@param payload: Binary representation of the payload (without the opcode)
75
raise NotImplementedError("Subclasses must override this")
78
"""Return the wire representation of the datagram.
83
raise NotImplementedError("Subclasses must override this")
86
class RQDatagram(TFTPDatagram):
87
"""Base class for "RQ" (request) datagrams.
89
@ivar filename: File name, that corresponds to this request.
90
@type filename: C{str}
92
@ivar mode: Transfer mode. Valid values are C{netascii} and C{octet}.
96
@ivar options: Any options, that were requested by the client (as per
97
U{RFC2374<http://tools.ietf.org/html/rfc2347>}
98
@type options: C{dict}
103
def from_wire(cls, payload):
104
"""Parse the payload and return a RRQ/WRQ datagram object.
106
@return: datagram object
107
@rtype: L{RRQDatagram} or L{WRQDatagram}
109
@raise OptionsDecodeError: if we failed to decode the options, requested
111
@raise PayloadDecodeError: if there were not enough fields in the payload.
112
Fields are terminated by NUL.
115
parts = payload.split('\x00')
117
filename, mode = parts.pop(0), parts.pop(0)
119
raise PayloadDecodeError("Not enough fields in the payload")
120
if parts and not parts[-1]:
122
options = OrderedDict()
123
# To maintain consistency during testing.
124
# The actual order of options is not important as per RFC2347
126
raise OptionsDecodeError("No value for option %s" % parts[-1])
127
for ind, opt_name in enumerate(parts[::2]):
128
if opt_name in options:
129
raise OptionsDecodeError("Duplicate option specified: %s" % opt_name)
130
options[opt_name] = parts[ind * 2 + 1]
131
return cls(filename, mode, options)
133
def __init__(self, filename, mode, options):
134
self.filename = filename
135
self.mode = mode.lower()
136
self.options = options
140
return ("<%s(filename=%s, mode=%s, options=%s)>" %
141
(self.__class__.__name__, self.filename, self.mode, self.options))
142
return "<%s(filename=%s, mode=%s)>" % (self.__class__.__name__,
143
self.filename, self.mode)
146
opcode = struct.pack("!H", self.opcode)
148
options = '\x00'.join(chain.from_iterable(self.options.iteritems()))
149
return ''.join((opcode, self.filename, '\x00', self.mode, '\x00',
152
return ''.join((opcode, self.filename, '\x00', self.mode, '\x00'))
154
class RRQDatagram(RQDatagram):
157
class WRQDatagram(RQDatagram):
160
class OACKDatagram(TFTPDatagram):
163
@ivar options: Any options, that were requested by the client (as per
164
U{RFC2374<http://tools.ietf.org/html/rfc2347>}
165
@type options: C{dict}
171
def from_wire(cls, payload):
172
"""Parse the payload and return an OACK datagram object.
174
@return: datagram object
175
@rtype: L{OACKDatagram}
177
@raise OptionsDecodeError: if we failed to decode the options
180
parts = payload.split('\x00')
181
#FIXME: Boo, code duplication
182
if parts and not parts[-1]:
184
options = OrderedDict()
186
raise OptionsDecodeError("No value for option %s" % parts[-1])
187
for ind, opt_name in enumerate(parts[::2]):
188
if opt_name in options:
189
raise OptionsDecodeError("Duplicate option specified: %s" % opt_name)
190
options[opt_name] = parts[ind * 2 + 1]
193
def __init__(self, options):
194
self.options = options
197
return ("<%s(options=%s)>" % (self.__class__.__name__, self.options))
200
opcode = struct.pack("!H", self.opcode)
202
options = '\x00'.join(chain.from_iterable(self.options.iteritems()))
203
return ''.join((opcode, options, '\x00'))
207
class DATADatagram(TFTPDatagram):
210
@ivar blocknum: A block number, that this chunk of data is associated with
211
@type blocknum: C{int}
213
@ivar data: binary data
220
def from_wire(cls, payload):
221
"""Parse the payload and return a L{DATADatagram} object.
223
@param payload: Binary representation of the payload (without the opcode)
224
@type payload: C{str}
226
@return: A L{DATADatagram} object
227
@rtype: L{DATADatagram}
229
@raise PayloadDecodeError: if the format of payload is incorrect
233
blocknum, data = struct.unpack('!H', payload[:2])[0], payload[2:]
235
raise PayloadDecodeError()
236
return cls(blocknum, data)
238
def __init__(self, blocknum, data):
239
self.blocknum = blocknum
243
return "<%s(blocknum=%s, %s bytes of data)>" % (self.__class__.__name__,
244
self.blocknum, len(self.data))
247
return ''.join((struct.pack('!HH', self.opcode, self.blocknum), self.data))
249
class ACKDatagram(TFTPDatagram):
252
@ivar blocknum: Block number of the data chunk, which this datagram is supposed to acknowledge
253
@type blocknum: C{int}
259
def from_wire(cls, payload):
260
"""Parse the payload and return a L{ACKDatagram} object.
262
@param payload: Binary representation of the payload (without the opcode)
263
@type payload: C{str}
265
@return: An L{ACKDatagram} object
266
@rtype: L{ACKDatagram}
268
@raise PayloadDecodeError: if the format of payload is incorrect
272
blocknum = struct.unpack('!H', payload)[0]
274
raise PayloadDecodeError("Unable to extract the block number")
277
def __init__(self, blocknum):
278
self.blocknum = blocknum
281
return "<%s(blocknum=%s)>" % (self.__class__.__name__, self.blocknum)
284
return struct.pack('!HH', self.opcode, self.blocknum)
286
class ERRORDatagram(TFTPDatagram):
287
"""An ERROR datagram.
289
@ivar errorcode: A valid TFTP error code
290
@type errorcode: C{int}
292
@ivar errmsg: An error message, describing the error condition in which this
293
datagram was produced
300
def from_wire(cls, payload):
301
"""Parse the payload and return a L{ERRORDatagram} object.
303
This method violates the standard a bit - if the error string was not
304
extracted, a default error string is generated, based on the error code.
306
@param payload: Binary representation of the payload (without the opcode)
307
@type payload: C{str}
309
@return: An L{ERRORDatagram} object
310
@rtype: L{ERRORDatagram}
312
@raise PayloadDecodeError: if the format of payload is incorrect
313
@raise InvalidErrorcodeError: a more specific exception, that is raised
314
if the error code was successfully, extracted, but it does not correspond
315
to any known/standartized error code values.
319
errorcode = struct.unpack('!H', payload[:2])[0]
321
raise PayloadDecodeError("Unable to extract the error code")
322
if not errorcode in errors:
323
raise InvalidErrorcodeError(errorcode)
324
errmsg = payload[2:].split('\x00')[0]
326
errmsg = errors[errorcode]
327
return cls(errorcode, errmsg)
330
def from_code(cls, errorcode, errmsg=None):
331
"""Create an L{ERRORDatagram}, given an error code and, optionally, an
332
error message to go with it. If not provided, default error message for
333
the given error code is used.
335
@param errorcode: An error code (one of L{errors})
336
@type errorcode: C{int}
338
@param errmsg: An error message (optional)
339
@type errmsg: C{str} or C{NoneType}
341
@raise InvalidErrorcodeError: if the error code is not known
343
@return: an L{ERRORDatagram}
344
@rtype: L{ERRORDatagram}
347
if not errorcode in errors:
348
raise InvalidErrorcodeError(errorcode)
350
errmsg = errors[errorcode]
351
return cls(errorcode, errmsg)
354
def __init__(self, errorcode, errmsg):
355
self.errorcode = errorcode
359
return ''.join((struct.pack('!HH', self.opcode, self.errorcode),
360
self.errmsg, '\x00'))
362
class _TFTPDatagramFactory(object):
363
"""Encapsulates the creation of datagrams based on the opcode"""
367
OP_DATA: DATADatagram,
369
OP_ERROR: ERRORDatagram,
370
OP_OACK: OACKDatagram
373
def __call__(self, opcode, payload):
374
"""Create a datagram, given an opcode and payload.
376
Errors, that occur during datagram creation are propagated as-is.
378
@param opcode: opcode
381
@param payload: payload
382
@type payload: C{str}
384
@return: datagram object
385
@rtype: L{TFTPDatagram}
387
@raise InvalidOpcodeError: if the opcode is not recognized
391
datagram_class = self._dgram_classes[opcode]
393
raise InvalidOpcodeError(opcode)
394
return datagram_class.from_wire(payload)
396
TFTPDatagramFactory = _TFTPDatagramFactory()