Norman James | 8b2b722 | 2015-10-08 07:01:38 -0500 | [diff] [blame] | 1 | """This module implements the packet types of TFTP itself, and the |
| 2 | corresponding encode and decode methods for them.""" |
| 3 | |
| 4 | import struct |
| 5 | from TftpShared import * |
| 6 | |
| 7 | class TftpSession(object): |
| 8 | """This class is the base class for the tftp client and server. Any shared |
| 9 | code should be in this class.""" |
| 10 | # FIXME: do we need this anymore? |
| 11 | pass |
| 12 | |
| 13 | class TftpPacketWithOptions(object): |
| 14 | """This class exists to permit some TftpPacket subclasses to share code |
| 15 | regarding options handling. It does not inherit from TftpPacket, as the |
| 16 | goal is just to share code here, and not cause diamond inheritance.""" |
| 17 | |
| 18 | def __init__(self): |
| 19 | self.options = {} |
| 20 | |
| 21 | def setoptions(self, options): |
| 22 | log.debug("in TftpPacketWithOptions.setoptions") |
| 23 | log.debug("options: %s", str(options)) |
| 24 | myoptions = {} |
| 25 | for key in options: |
| 26 | newkey = str(key) |
| 27 | myoptions[newkey] = str(options[key]) |
| 28 | log.debug("populated myoptions with %s = %s", |
| 29 | newkey, myoptions[newkey]) |
| 30 | |
| 31 | log.debug("setting options hash to: %s", str(myoptions)) |
| 32 | self._options = myoptions |
| 33 | |
| 34 | def getoptions(self): |
| 35 | log.debug("in TftpPacketWithOptions.getoptions") |
| 36 | return self._options |
| 37 | |
| 38 | # Set up getter and setter on options to ensure that they are the proper |
| 39 | # type. They should always be strings, but we don't need to force the |
| 40 | # client to necessarily enter strings if we can avoid it. |
| 41 | options = property(getoptions, setoptions) |
| 42 | |
| 43 | def decode_options(self, buffer): |
| 44 | """This method decodes the section of the buffer that contains an |
| 45 | unknown number of options. It returns a dictionary of option names and |
| 46 | values.""" |
| 47 | format = "!" |
| 48 | options = {} |
| 49 | |
| 50 | log.debug("decode_options: buffer is: %s", repr(buffer)) |
| 51 | log.debug("size of buffer is %d bytes", len(buffer)) |
| 52 | if len(buffer) == 0: |
| 53 | log.debug("size of buffer is zero, returning empty hash") |
| 54 | return {} |
| 55 | |
| 56 | # Count the nulls in the buffer. Each one terminates a string. |
| 57 | log.debug("about to iterate options buffer counting nulls") |
| 58 | length = 0 |
| 59 | for c in buffer: |
| 60 | if ord(c) == 0: |
| 61 | log.debug("found a null at length %d", length) |
| 62 | if length > 0: |
| 63 | format += "%dsx" % length |
| 64 | length = -1 |
| 65 | else: |
| 66 | raise TftpException, "Invalid options in buffer" |
| 67 | length += 1 |
| 68 | |
| 69 | log.debug("about to unpack, format is: %s", format) |
| 70 | mystruct = struct.unpack(format, buffer) |
| 71 | |
| 72 | tftpassert(len(mystruct) % 2 == 0, |
| 73 | "packet with odd number of option/value pairs") |
| 74 | |
| 75 | for i in range(0, len(mystruct), 2): |
| 76 | log.debug("setting option %s to %s", mystruct[i], mystruct[i+1]) |
| 77 | options[mystruct[i]] = mystruct[i+1] |
| 78 | |
| 79 | return options |
| 80 | |
| 81 | class TftpPacket(object): |
| 82 | """This class is the parent class of all tftp packet classes. It is an |
| 83 | abstract class, providing an interface, and should not be instantiated |
| 84 | directly.""" |
| 85 | def __init__(self): |
| 86 | self.opcode = 0 |
| 87 | self.buffer = None |
| 88 | |
| 89 | def encode(self): |
| 90 | """The encode method of a TftpPacket takes keyword arguments specific |
| 91 | to the type of packet, and packs an appropriate buffer in network-byte |
| 92 | order suitable for sending over the wire. |
| 93 | |
| 94 | This is an abstract method.""" |
| 95 | raise NotImplementedError, "Abstract method" |
| 96 | |
| 97 | def decode(self): |
| 98 | """The decode method of a TftpPacket takes a buffer off of the wire in |
| 99 | network-byte order, and decodes it, populating internal properties as |
| 100 | appropriate. This can only be done once the first 2-byte opcode has |
| 101 | already been decoded, but the data section does include the entire |
| 102 | datagram. |
| 103 | |
| 104 | This is an abstract method.""" |
| 105 | raise NotImplementedError, "Abstract method" |
| 106 | |
| 107 | class TftpPacketInitial(TftpPacket, TftpPacketWithOptions): |
| 108 | """This class is a common parent class for the RRQ and WRQ packets, as |
| 109 | they share quite a bit of code.""" |
| 110 | def __init__(self): |
| 111 | TftpPacket.__init__(self) |
| 112 | TftpPacketWithOptions.__init__(self) |
| 113 | self.filename = None |
| 114 | self.mode = None |
| 115 | |
| 116 | def encode(self): |
| 117 | """Encode the packet's buffer from the instance variables.""" |
| 118 | tftpassert(self.filename, "filename required in initial packet") |
| 119 | tftpassert(self.mode, "mode required in initial packet") |
| 120 | |
| 121 | ptype = None |
| 122 | if self.opcode == 1: ptype = "RRQ" |
| 123 | else: ptype = "WRQ" |
| 124 | log.debug("Encoding %s packet, filename = %s, mode = %s", |
| 125 | ptype, self.filename, self.mode) |
| 126 | for key in self.options: |
| 127 | log.debug(" Option %s = %s", key, self.options[key]) |
| 128 | |
| 129 | format = "!H" |
| 130 | format += "%dsx" % len(self.filename) |
| 131 | if self.mode == "octet": |
| 132 | format += "5sx" |
| 133 | else: |
| 134 | raise AssertionError, "Unsupported mode: %s" % mode |
| 135 | # Add options. |
| 136 | options_list = [] |
| 137 | if self.options.keys() > 0: |
| 138 | log.debug("there are options to encode") |
| 139 | for key in self.options: |
| 140 | # Populate the option name |
| 141 | format += "%dsx" % len(key) |
| 142 | options_list.append(key) |
| 143 | # Populate the option value |
| 144 | format += "%dsx" % len(str(self.options[key])) |
| 145 | options_list.append(str(self.options[key])) |
| 146 | |
| 147 | log.debug("format is %s", format) |
| 148 | log.debug("options_list is %s", options_list) |
| 149 | log.debug("size of struct is %d", struct.calcsize(format)) |
| 150 | |
| 151 | self.buffer = struct.pack(format, |
| 152 | self.opcode, |
| 153 | self.filename, |
| 154 | self.mode, |
| 155 | *options_list) |
| 156 | |
| 157 | log.debug("buffer is %s", repr(self.buffer)) |
| 158 | return self |
| 159 | |
| 160 | def decode(self): |
| 161 | tftpassert(self.buffer, "Can't decode, buffer is empty") |
| 162 | |
| 163 | # FIXME - this shares a lot of code with decode_options |
| 164 | nulls = 0 |
| 165 | format = "" |
| 166 | nulls = length = tlength = 0 |
| 167 | log.debug("in decode: about to iterate buffer counting nulls") |
| 168 | subbuf = self.buffer[2:] |
| 169 | for c in subbuf: |
| 170 | if ord(c) == 0: |
| 171 | nulls += 1 |
| 172 | log.debug("found a null at length %d, now have %d", length, nulls) |
| 173 | format += "%dsx" % length |
| 174 | length = -1 |
| 175 | # At 2 nulls, we want to mark that position for decoding. |
| 176 | if nulls == 2: |
| 177 | break |
| 178 | length += 1 |
| 179 | tlength += 1 |
| 180 | |
| 181 | log.debug("hopefully found end of mode at length %d", tlength) |
| 182 | # length should now be the end of the mode. |
| 183 | tftpassert(nulls == 2, "malformed packet") |
| 184 | shortbuf = subbuf[:tlength+1] |
| 185 | log.debug("about to unpack buffer with format: %s", format) |
| 186 | log.debug("unpacking buffer: %s", repr(shortbuf)) |
| 187 | mystruct = struct.unpack(format, shortbuf) |
| 188 | |
| 189 | tftpassert(len(mystruct) == 2, "malformed packet") |
| 190 | self.filename = mystruct[0] |
| 191 | self.mode = mystruct[1].lower() # force lc - bug 17 |
| 192 | log.debug("set filename to %s", self.filename) |
| 193 | log.debug("set mode to %s", self.mode) |
| 194 | |
| 195 | self.options = self.decode_options(subbuf[tlength+1:]) |
| 196 | return self |
| 197 | |
| 198 | class TftpPacketRRQ(TftpPacketInitial): |
| 199 | """ |
| 200 | :: |
| 201 | |
| 202 | 2 bytes string 1 byte string 1 byte |
| 203 | ----------------------------------------------- |
| 204 | RRQ/ | 01/02 | Filename | 0 | Mode | 0 | |
| 205 | WRQ ----------------------------------------------- |
| 206 | """ |
| 207 | def __init__(self): |
| 208 | TftpPacketInitial.__init__(self) |
| 209 | self.opcode = 1 |
| 210 | |
| 211 | def __str__(self): |
| 212 | s = 'RRQ packet: filename = %s' % self.filename |
| 213 | s += ' mode = %s' % self.mode |
| 214 | if self.options: |
| 215 | s += '\n options = %s' % self.options |
| 216 | return s |
| 217 | |
| 218 | class TftpPacketWRQ(TftpPacketInitial): |
| 219 | """ |
| 220 | :: |
| 221 | |
| 222 | 2 bytes string 1 byte string 1 byte |
| 223 | ----------------------------------------------- |
| 224 | RRQ/ | 01/02 | Filename | 0 | Mode | 0 | |
| 225 | WRQ ----------------------------------------------- |
| 226 | """ |
| 227 | def __init__(self): |
| 228 | TftpPacketInitial.__init__(self) |
| 229 | self.opcode = 2 |
| 230 | |
| 231 | def __str__(self): |
| 232 | s = 'WRQ packet: filename = %s' % self.filename |
| 233 | s += ' mode = %s' % self.mode |
| 234 | if self.options: |
| 235 | s += '\n options = %s' % self.options |
| 236 | return s |
| 237 | |
| 238 | class TftpPacketDAT(TftpPacket): |
| 239 | """ |
| 240 | :: |
| 241 | |
| 242 | 2 bytes 2 bytes n bytes |
| 243 | --------------------------------- |
| 244 | DATA | 03 | Block # | Data | |
| 245 | --------------------------------- |
| 246 | """ |
| 247 | def __init__(self): |
| 248 | TftpPacket.__init__(self) |
| 249 | self.opcode = 3 |
| 250 | self.blocknumber = 0 |
| 251 | self.data = None |
| 252 | |
| 253 | def __str__(self): |
| 254 | s = 'DAT packet: block %s' % self.blocknumber |
| 255 | if self.data: |
| 256 | s += '\n data: %d bytes' % len(self.data) |
| 257 | return s |
| 258 | |
| 259 | def encode(self): |
| 260 | """Encode the DAT packet. This method populates self.buffer, and |
| 261 | returns self for easy method chaining.""" |
| 262 | if len(self.data) == 0: |
| 263 | log.debug("Encoding an empty DAT packet") |
| 264 | format = "!HH%ds" % len(self.data) |
| 265 | self.buffer = struct.pack(format, |
| 266 | self.opcode, |
| 267 | self.blocknumber, |
| 268 | self.data) |
| 269 | return self |
| 270 | |
| 271 | def decode(self): |
| 272 | """Decode self.buffer into instance variables. It returns self for |
| 273 | easy method chaining.""" |
| 274 | # We know the first 2 bytes are the opcode. The second two are the |
| 275 | # block number. |
| 276 | (self.blocknumber,) = struct.unpack("!H", self.buffer[2:4]) |
| 277 | log.debug("decoding DAT packet, block number %d", self.blocknumber) |
| 278 | log.debug("should be %d bytes in the packet total", len(self.buffer)) |
| 279 | # Everything else is data. |
| 280 | self.data = self.buffer[4:] |
| 281 | log.debug("found %d bytes of data", len(self.data)) |
| 282 | return self |
| 283 | |
| 284 | class TftpPacketACK(TftpPacket): |
| 285 | """ |
| 286 | :: |
| 287 | |
| 288 | 2 bytes 2 bytes |
| 289 | ------------------- |
| 290 | ACK | 04 | Block # | |
| 291 | -------------------- |
| 292 | """ |
| 293 | def __init__(self): |
| 294 | TftpPacket.__init__(self) |
| 295 | self.opcode = 4 |
| 296 | self.blocknumber = 0 |
| 297 | |
| 298 | def __str__(self): |
| 299 | return 'ACK packet: block %d' % self.blocknumber |
| 300 | |
| 301 | def encode(self): |
| 302 | log.debug("encoding ACK: opcode = %d, block = %d", |
| 303 | self.opcode, self.blocknumber) |
| 304 | self.buffer = struct.pack("!HH", self.opcode, self.blocknumber) |
| 305 | return self |
| 306 | |
| 307 | def decode(self): |
| 308 | if len(self.buffer) > 4: |
| 309 | log.debug("detected TFTP ACK but request is too large, will truncate") |
| 310 | log.debug("buffer was: %s", repr(self.buffer)) |
| 311 | self.buffer = self.buffer[0:4] |
| 312 | self.opcode, self.blocknumber = struct.unpack("!HH", self.buffer) |
| 313 | log.debug("decoded ACK packet: opcode = %d, block = %d", |
| 314 | self.opcode, self.blocknumber) |
| 315 | return self |
| 316 | |
| 317 | class TftpPacketERR(TftpPacket): |
| 318 | """ |
| 319 | :: |
| 320 | |
| 321 | 2 bytes 2 bytes string 1 byte |
| 322 | ---------------------------------------- |
| 323 | ERROR | 05 | ErrorCode | ErrMsg | 0 | |
| 324 | ---------------------------------------- |
| 325 | |
| 326 | Error Codes |
| 327 | |
| 328 | Value Meaning |
| 329 | |
| 330 | 0 Not defined, see error message (if any). |
| 331 | 1 File not found. |
| 332 | 2 Access violation. |
| 333 | 3 Disk full or allocation exceeded. |
| 334 | 4 Illegal TFTP operation. |
| 335 | 5 Unknown transfer ID. |
| 336 | 6 File already exists. |
| 337 | 7 No such user. |
| 338 | 8 Failed to negotiate options |
| 339 | """ |
| 340 | def __init__(self): |
| 341 | TftpPacket.__init__(self) |
| 342 | self.opcode = 5 |
| 343 | self.errorcode = 0 |
| 344 | # FIXME: We don't encode the errmsg... |
| 345 | self.errmsg = None |
| 346 | # FIXME - integrate in TftpErrors references? |
| 347 | self.errmsgs = { |
| 348 | 1: "File not found", |
| 349 | 2: "Access violation", |
| 350 | 3: "Disk full or allocation exceeded", |
| 351 | 4: "Illegal TFTP operation", |
| 352 | 5: "Unknown transfer ID", |
| 353 | 6: "File already exists", |
| 354 | 7: "No such user", |
| 355 | 8: "Failed to negotiate options" |
| 356 | } |
| 357 | |
| 358 | def __str__(self): |
| 359 | s = 'ERR packet: errorcode = %d' % self.errorcode |
| 360 | s += '\n msg = %s' % self.errmsgs.get(self.errorcode, '') |
| 361 | return s |
| 362 | |
| 363 | def encode(self): |
| 364 | """Encode the DAT packet based on instance variables, populating |
| 365 | self.buffer, returning self.""" |
| 366 | format = "!HH%dsx" % len(self.errmsgs[self.errorcode]) |
| 367 | log.debug("encoding ERR packet with format %s", format) |
| 368 | self.buffer = struct.pack(format, |
| 369 | self.opcode, |
| 370 | self.errorcode, |
| 371 | self.errmsgs[self.errorcode]) |
| 372 | return self |
| 373 | |
| 374 | def decode(self): |
| 375 | "Decode self.buffer, populating instance variables and return self." |
| 376 | buflen = len(self.buffer) |
| 377 | tftpassert(buflen >= 4, "malformed ERR packet, too short") |
| 378 | log.debug("Decoding ERR packet, length %s bytes", buflen) |
| 379 | if buflen == 4: |
| 380 | log.debug("Allowing this affront to the RFC of a 4-byte packet") |
| 381 | format = "!HH" |
| 382 | log.debug("Decoding ERR packet with format: %s", format) |
| 383 | self.opcode, self.errorcode = struct.unpack(format, |
| 384 | self.buffer) |
| 385 | else: |
| 386 | log.debug("Good ERR packet > 4 bytes") |
| 387 | format = "!HH%dsx" % (len(self.buffer) - 5) |
| 388 | log.debug("Decoding ERR packet with format: %s", format) |
| 389 | self.opcode, self.errorcode, self.errmsg = struct.unpack(format, |
| 390 | self.buffer) |
| 391 | log.error("ERR packet - errorcode: %d, message: %s" |
| 392 | % (self.errorcode, self.errmsg)) |
| 393 | return self |
| 394 | |
| 395 | class TftpPacketOACK(TftpPacket, TftpPacketWithOptions): |
| 396 | """ |
| 397 | :: |
| 398 | |
| 399 | +-------+---~~---+---+---~~---+---+---~~---+---+---~~---+---+ |
| 400 | | opc | opt1 | 0 | value1 | 0 | optN | 0 | valueN | 0 | |
| 401 | +-------+---~~---+---+---~~---+---+---~~---+---+---~~---+---+ |
| 402 | """ |
| 403 | def __init__(self): |
| 404 | TftpPacket.__init__(self) |
| 405 | TftpPacketWithOptions.__init__(self) |
| 406 | self.opcode = 6 |
| 407 | |
| 408 | def __str__(self): |
| 409 | return 'OACK packet:\n options = %s' % self.options |
| 410 | |
| 411 | def encode(self): |
| 412 | format = "!H" # opcode |
| 413 | options_list = [] |
| 414 | log.debug("in TftpPacketOACK.encode") |
| 415 | for key in self.options: |
| 416 | log.debug("looping on option key %s", key) |
| 417 | log.debug("value is %s", self.options[key]) |
| 418 | format += "%dsx" % len(key) |
| 419 | format += "%dsx" % len(self.options[key]) |
| 420 | options_list.append(key) |
| 421 | options_list.append(self.options[key]) |
| 422 | self.buffer = struct.pack(format, self.opcode, *options_list) |
| 423 | return self |
| 424 | |
| 425 | def decode(self): |
| 426 | self.options = self.decode_options(self.buffer[2:]) |
| 427 | return self |
| 428 | |
| 429 | def match_options(self, options): |
| 430 | """This method takes a set of options, and tries to match them with |
| 431 | its own. It can accept some changes in those options from the server as |
| 432 | part of a negotiation. Changed or unchanged, it will return a dict of |
| 433 | the options so that the session can update itself to the negotiated |
| 434 | options.""" |
| 435 | for name in self.options: |
| 436 | if options.has_key(name): |
| 437 | if name == 'blksize': |
| 438 | # We can accept anything between the min and max values. |
| 439 | size = self.options[name] |
| 440 | if size >= MIN_BLKSIZE and size <= MAX_BLKSIZE: |
| 441 | log.debug("negotiated blksize of %d bytes", size) |
| 442 | options[blksize] = size |
| 443 | else: |
| 444 | raise TftpException, "Unsupported option: %s" % name |
| 445 | return True |