Norman James | 8b2b722 | 2015-10-08 07:01:38 -0500 | [diff] [blame] | 1 | """This module implements all state handling during uploads and downloads, the |
| 2 | main interface to which being the TftpState base class. |
| 3 | |
| 4 | The concept is simple. Each context object represents a single upload or |
| 5 | download, and the state object in the context object represents the current |
| 6 | state of that transfer. The state object has a handle() method that expects |
| 7 | the next packet in the transfer, and returns a state object until the transfer |
| 8 | is complete, at which point it returns None. That is, unless there is a fatal |
| 9 | error, in which case a TftpException is returned instead.""" |
| 10 | |
| 11 | from TftpShared import * |
| 12 | from TftpPacketTypes import * |
| 13 | import os |
| 14 | |
| 15 | ############################################################################### |
| 16 | # State classes |
| 17 | ############################################################################### |
| 18 | |
| 19 | class TftpState(object): |
| 20 | """The base class for the states.""" |
| 21 | |
| 22 | def __init__(self, context): |
| 23 | """Constructor for setting up common instance variables. The involved |
| 24 | file object is required, since in tftp there's always a file |
| 25 | involved.""" |
| 26 | self.context = context |
| 27 | |
| 28 | def handle(self, pkt, raddress, rport): |
| 29 | """An abstract method for handling a packet. It is expected to return |
| 30 | a TftpState object, either itself or a new state.""" |
| 31 | raise NotImplementedError, "Abstract method" |
| 32 | |
| 33 | def handleOACK(self, pkt): |
| 34 | """This method handles an OACK from the server, syncing any accepted |
| 35 | options.""" |
| 36 | if pkt.options.keys() > 0: |
| 37 | if pkt.match_options(self.context.options): |
| 38 | log.info("Successful negotiation of options") |
| 39 | # Set options to OACK options |
| 40 | self.context.options = pkt.options |
| 41 | for key in self.context.options: |
| 42 | log.info(" %s = %s" % (key, self.context.options[key])) |
| 43 | else: |
| 44 | log.error("Failed to negotiate options") |
| 45 | raise TftpException, "Failed to negotiate options" |
| 46 | else: |
| 47 | raise TftpException, "No options found in OACK" |
| 48 | |
| 49 | def returnSupportedOptions(self, options): |
| 50 | """This method takes a requested options list from a client, and |
| 51 | returns the ones that are supported.""" |
| 52 | # We support the options blksize and tsize right now. |
| 53 | # FIXME - put this somewhere else? |
| 54 | accepted_options = {} |
| 55 | for option in options: |
| 56 | if option == 'blksize': |
| 57 | # Make sure it's valid. |
| 58 | if int(options[option]) > MAX_BLKSIZE: |
| 59 | log.info("Client requested blksize greater than %d " |
| 60 | "setting to maximum" % MAX_BLKSIZE) |
| 61 | accepted_options[option] = MAX_BLKSIZE |
| 62 | elif int(options[option]) < MIN_BLKSIZE: |
| 63 | log.info("Client requested blksize less than %d " |
| 64 | "setting to minimum" % MIN_BLKSIZE) |
| 65 | accepted_options[option] = MIN_BLKSIZE |
| 66 | else: |
| 67 | accepted_options[option] = options[option] |
| 68 | elif option == 'tsize': |
| 69 | log.debug("tsize option is set") |
| 70 | accepted_options['tsize'] = 1 |
| 71 | else: |
| 72 | log.info("Dropping unsupported option '%s'" % option) |
| 73 | log.debug("Returning these accepted options: %s", accepted_options) |
| 74 | return accepted_options |
| 75 | |
| 76 | def sendDAT(self): |
| 77 | """This method sends the next DAT packet based on the data in the |
| 78 | context. It returns a boolean indicating whether the transfer is |
| 79 | finished.""" |
| 80 | finished = False |
| 81 | blocknumber = self.context.next_block |
| 82 | # Test hook |
| 83 | if DELAY_BLOCK and DELAY_BLOCK == blocknumber: |
| 84 | import time |
| 85 | log.debug("Deliberately delaying 10 seconds...") |
| 86 | time.sleep(10) |
| 87 | dat = None |
| 88 | blksize = self.context.getBlocksize() |
| 89 | buffer = self.context.fileobj.read(blksize) |
| 90 | log.debug("Read %d bytes into buffer", len(buffer)) |
| 91 | if len(buffer) < blksize: |
| 92 | log.info("Reached EOF on file %s" |
| 93 | % self.context.file_to_transfer) |
| 94 | finished = True |
| 95 | dat = TftpPacketDAT() |
| 96 | dat.data = buffer |
| 97 | dat.blocknumber = blocknumber |
| 98 | self.context.metrics.bytes += len(dat.data) |
| 99 | log.debug("Sending DAT packet %d", dat.blocknumber) |
| 100 | self.context.sock.sendto(dat.encode().buffer, |
| 101 | (self.context.host, self.context.tidport)) |
| 102 | if self.context.packethook: |
| 103 | self.context.packethook(dat) |
| 104 | self.context.last_pkt = dat |
| 105 | return finished |
| 106 | |
| 107 | def sendACK(self, blocknumber=None): |
| 108 | """This method sends an ack packet to the block number specified. If |
| 109 | none is specified, it defaults to the next_block property in the |
| 110 | parent context.""" |
| 111 | log.debug("In sendACK, passed blocknumber is %s", blocknumber) |
| 112 | if blocknumber is None: |
| 113 | blocknumber = self.context.next_block |
| 114 | log.info("Sending ack to block %d" % blocknumber) |
| 115 | ackpkt = TftpPacketACK() |
| 116 | ackpkt.blocknumber = blocknumber |
| 117 | self.context.sock.sendto(ackpkt.encode().buffer, |
| 118 | (self.context.host, |
| 119 | self.context.tidport)) |
| 120 | self.context.last_pkt = ackpkt |
| 121 | |
| 122 | def sendError(self, errorcode): |
| 123 | """This method uses the socket passed, and uses the errorcode to |
| 124 | compose and send an error packet.""" |
| 125 | log.debug("In sendError, being asked to send error %d", errorcode) |
| 126 | errpkt = TftpPacketERR() |
| 127 | errpkt.errorcode = errorcode |
| 128 | self.context.sock.sendto(errpkt.encode().buffer, |
| 129 | (self.context.host, |
| 130 | self.context.tidport)) |
| 131 | self.context.last_pkt = errpkt |
| 132 | |
| 133 | def sendOACK(self): |
| 134 | """This method sends an OACK packet with the options from the current |
| 135 | context.""" |
| 136 | log.debug("In sendOACK with options %s", self.context.options) |
| 137 | pkt = TftpPacketOACK() |
| 138 | pkt.options = self.context.options |
| 139 | self.context.sock.sendto(pkt.encode().buffer, |
| 140 | (self.context.host, |
| 141 | self.context.tidport)) |
| 142 | self.context.last_pkt = pkt |
| 143 | |
| 144 | def resendLast(self): |
| 145 | "Resend the last sent packet due to a timeout." |
| 146 | log.warn("Resending packet %s on sessions %s" |
| 147 | % (self.context.last_pkt, self)) |
| 148 | self.context.metrics.resent_bytes += len(self.context.last_pkt.buffer) |
| 149 | self.context.metrics.add_dup(self.context.last_pkt) |
| 150 | sendto_port = self.context.tidport |
| 151 | if not sendto_port: |
| 152 | # If the tidport wasn't set, then the remote end hasn't even |
| 153 | # started talking to us yet. That's not good. Maybe it's not |
| 154 | # there. |
| 155 | sendto_port = self.context.port |
| 156 | self.context.sock.sendto(self.context.last_pkt.encode().buffer, |
| 157 | (self.context.host, sendto_port)) |
| 158 | if self.context.packethook: |
| 159 | self.context.packethook(self.context.last_pkt) |
| 160 | |
| 161 | def handleDat(self, pkt): |
| 162 | """This method handles a DAT packet during a client download, or a |
| 163 | server upload.""" |
| 164 | log.info("Handling DAT packet - block %d" % pkt.blocknumber) |
| 165 | log.debug("Expecting block %s", self.context.next_block) |
| 166 | if pkt.blocknumber == self.context.next_block: |
| 167 | log.debug("Good, received block %d in sequence", pkt.blocknumber) |
| 168 | |
| 169 | self.sendACK() |
| 170 | self.context.next_block += 1 |
| 171 | |
| 172 | log.debug("Writing %d bytes to output file", len(pkt.data)) |
| 173 | self.context.fileobj.write(pkt.data) |
| 174 | self.context.metrics.bytes += len(pkt.data) |
| 175 | # Check for end-of-file, any less than full data packet. |
| 176 | if len(pkt.data) < self.context.getBlocksize(): |
| 177 | log.info("End of file detected") |
| 178 | return None |
| 179 | |
| 180 | elif pkt.blocknumber < self.context.next_block: |
| 181 | if pkt.blocknumber == 0: |
| 182 | log.warn("There is no block zero!") |
| 183 | self.sendError(TftpErrors.IllegalTftpOp) |
| 184 | raise TftpException, "There is no block zero!" |
| 185 | log.warn("Dropping duplicate block %d" % pkt.blocknumber) |
| 186 | self.context.metrics.add_dup(pkt) |
| 187 | log.debug("ACKing block %d again, just in case", pkt.blocknumber) |
| 188 | self.sendACK(pkt.blocknumber) |
| 189 | |
| 190 | else: |
| 191 | # FIXME: should we be more tolerant and just discard instead? |
| 192 | msg = "Whoa! Received future block %d but expected %d" \ |
| 193 | % (pkt.blocknumber, self.context.next_block) |
| 194 | log.error(msg) |
| 195 | raise TftpException, msg |
| 196 | |
| 197 | # Default is to ack |
| 198 | return TftpStateExpectDAT(self.context) |
| 199 | |
| 200 | class TftpServerState(TftpState): |
| 201 | """The base class for server states.""" |
| 202 | |
| 203 | def __init__(self, context): |
| 204 | TftpState.__init__(self, context) |
| 205 | |
| 206 | # This variable is used to store the absolute path to the file being |
| 207 | # managed. |
| 208 | self.full_path = None |
| 209 | |
| 210 | def serverInitial(self, pkt, raddress, rport): |
| 211 | """This method performs initial setup for a server context transfer, |
| 212 | put here to refactor code out of the TftpStateServerRecvRRQ and |
| 213 | TftpStateServerRecvWRQ classes, since their initial setup is |
| 214 | identical. The method returns a boolean, sendoack, to indicate whether |
| 215 | it is required to send an OACK to the client.""" |
| 216 | options = pkt.options |
| 217 | sendoack = False |
| 218 | if not self.context.tidport: |
| 219 | self.context.tidport = rport |
| 220 | log.info("Setting tidport to %s" % rport) |
| 221 | |
| 222 | log.debug("Setting default options, blksize") |
| 223 | self.context.options = { 'blksize': DEF_BLKSIZE } |
| 224 | |
| 225 | if options: |
| 226 | log.debug("Options requested: %s", options) |
| 227 | supported_options = self.returnSupportedOptions(options) |
| 228 | self.context.options.update(supported_options) |
| 229 | sendoack = True |
| 230 | |
| 231 | # FIXME - only octet mode is supported at this time. |
| 232 | if pkt.mode != 'octet': |
| 233 | self.sendError(TftpErrors.IllegalTftpOp) |
| 234 | raise TftpException, \ |
| 235 | "Only octet transfers are supported at this time." |
| 236 | |
| 237 | # test host/port of client end |
| 238 | if self.context.host != raddress or self.context.port != rport: |
| 239 | self.sendError(TftpErrors.UnknownTID) |
| 240 | log.error("Expected traffic from %s:%s but received it " |
| 241 | "from %s:%s instead." |
| 242 | % (self.context.host, |
| 243 | self.context.port, |
| 244 | raddress, |
| 245 | rport)) |
| 246 | # FIXME: increment an error count? |
| 247 | # Return same state, we're still waiting for valid traffic. |
| 248 | return self |
| 249 | |
| 250 | log.debug("Requested filename is %s", pkt.filename) |
| 251 | |
| 252 | # Build the filename on this server and ensure it is contained |
| 253 | # in the specified root directory. |
| 254 | # |
| 255 | # Filenames that begin with server root are accepted. It's |
| 256 | # assumed the client and server are tightly connected and this |
| 257 | # provides backwards compatibility. |
| 258 | # |
| 259 | # Filenames otherwise are relative to the server root. If they |
| 260 | # begin with a '/' strip it off as otherwise os.path.join will |
| 261 | # treat it as absolute (regardless of whether it is ntpath or |
| 262 | # posixpath module |
| 263 | if pkt.filename.startswith(self.context.root): |
| 264 | full_path = pkt.filename |
| 265 | else: |
| 266 | full_path = os.path.join( |
| 267 | self.context.root, pkt.filename.lstrip('/')) |
| 268 | |
| 269 | # Use abspath to eliminate any remaining relative elements |
| 270 | # (e.g. '..') and ensure that is still within the server's |
| 271 | # root directory |
| 272 | self.full_path = os.path.abspath(full_path) |
| 273 | log.debug("full_path is %s", full_path) |
| 274 | if self.full_path.startswith(self.context.root): |
| 275 | log.info("requested file is in the server root - good") |
| 276 | else: |
| 277 | log.warn("requested file is not within the server root - bad") |
| 278 | self.sendError(TftpErrors.IllegalTftpOp) |
| 279 | raise TftpException, "bad file path" |
| 280 | |
| 281 | self.context.file_to_transfer = pkt.filename |
| 282 | |
| 283 | return sendoack |
| 284 | |
| 285 | |
| 286 | class TftpStateServerRecvRRQ(TftpServerState): |
| 287 | """This class represents the state of the TFTP server when it has just |
| 288 | received an RRQ packet.""" |
| 289 | def handle(self, pkt, raddress, rport): |
| 290 | "Handle an initial RRQ packet as a server." |
| 291 | log.debug("In TftpStateServerRecvRRQ.handle") |
| 292 | sendoack = self.serverInitial(pkt, raddress, rport) |
| 293 | path = self.full_path |
| 294 | log.info("Opening file %s for reading" % path) |
| 295 | if os.path.exists(path): |
| 296 | # Note: Open in binary mode for win32 portability, since win32 |
| 297 | # blows. |
| 298 | self.context.fileobj = open(path, "rb") |
| 299 | elif self.context.dyn_file_func: |
| 300 | log.debug("No such file %s but using dyn_file_func", path) |
| 301 | self.context.fileobj = \ |
| 302 | self.context.dyn_file_func(self.context.file_to_transfer) |
| 303 | |
| 304 | if self.context.fileobj is None: |
| 305 | log.debug("dyn_file_func returned 'None', treating as " |
| 306 | "FileNotFound") |
| 307 | self.sendError(TftpErrors.FileNotFound) |
| 308 | raise TftpException, "File not found: %s" % path |
| 309 | else: |
| 310 | self.sendError(TftpErrors.FileNotFound) |
| 311 | raise TftpException, "File not found: %s" % path |
| 312 | |
| 313 | # Options negotiation. |
| 314 | if sendoack: |
| 315 | # Note, next_block is 0 here since that's the proper |
| 316 | # acknowledgement to an OACK. |
| 317 | # FIXME: perhaps we do need a TftpStateExpectOACK class... |
| 318 | self.sendOACK() |
| 319 | # Note, self.context.next_block is already 0. |
| 320 | else: |
| 321 | self.context.next_block = 1 |
| 322 | log.debug("No requested options, starting send...") |
| 323 | self.context.pending_complete = self.sendDAT() |
| 324 | # Note, we expect an ack regardless of whether we sent a DAT or an |
| 325 | # OACK. |
| 326 | return TftpStateExpectACK(self.context) |
| 327 | |
| 328 | # Note, we don't have to check any other states in this method, that's |
| 329 | # up to the caller. |
| 330 | |
| 331 | class TftpStateServerRecvWRQ(TftpServerState): |
| 332 | """This class represents the state of the TFTP server when it has just |
| 333 | received a WRQ packet.""" |
| 334 | def make_subdirs(self): |
| 335 | """The purpose of this method is to, if necessary, create all of the |
| 336 | subdirectories leading up to the file to the written.""" |
| 337 | # Pull off everything below the root. |
| 338 | subpath = self.full_path[len(self.context.root):] |
| 339 | log.debug("make_subdirs: subpath is %s", subpath) |
| 340 | # Split on directory separators, but drop the last one, as it should |
| 341 | # be the filename. |
| 342 | dirs = subpath.split(os.sep)[:-1] |
| 343 | log.debug("dirs is %s", dirs) |
| 344 | current = self.context.root |
| 345 | for dir in dirs: |
| 346 | if dir: |
| 347 | current = os.path.join(current, dir) |
| 348 | if os.path.isdir(current): |
| 349 | log.debug("%s is already an existing directory", current) |
| 350 | else: |
| 351 | os.mkdir(current, 0700) |
| 352 | |
| 353 | def handle(self, pkt, raddress, rport): |
| 354 | "Handle an initial WRQ packet as a server." |
| 355 | log.debug("In TftpStateServerRecvWRQ.handle") |
| 356 | sendoack = self.serverInitial(pkt, raddress, rport) |
| 357 | path = self.full_path |
| 358 | log.info("Opening file %s for writing" % path) |
| 359 | if os.path.exists(path): |
| 360 | # FIXME: correct behavior? |
| 361 | log.warn("File %s exists already, overwriting..." % self.context.file_to_transfer) |
| 362 | # FIXME: I think we should upload to a temp file and not overwrite the |
| 363 | # existing file until the file is successfully uploaded. |
| 364 | self.make_subdirs() |
| 365 | self.context.fileobj = open(path, "wb") |
| 366 | |
| 367 | # Options negotiation. |
| 368 | if sendoack: |
| 369 | log.debug("Sending OACK to client") |
| 370 | self.sendOACK() |
| 371 | else: |
| 372 | log.debug("No requested options, expecting transfer to begin...") |
| 373 | self.sendACK() |
| 374 | # Whether we're sending an oack or not, we're expecting a DAT for |
| 375 | # block 1 |
| 376 | self.context.next_block = 1 |
| 377 | # We may have sent an OACK, but we're expecting a DAT as the response |
| 378 | # to either the OACK or an ACK, so lets unconditionally use the |
| 379 | # TftpStateExpectDAT state. |
| 380 | return TftpStateExpectDAT(self.context) |
| 381 | |
| 382 | # Note, we don't have to check any other states in this method, that's |
| 383 | # up to the caller. |
| 384 | |
| 385 | class TftpStateServerStart(TftpState): |
| 386 | """The start state for the server. This is a transitory state since at |
| 387 | this point we don't know if we're handling an upload or a download. We |
| 388 | will commit to one of them once we interpret the initial packet.""" |
| 389 | def handle(self, pkt, raddress, rport): |
| 390 | """Handle a packet we just received.""" |
| 391 | log.debug("In TftpStateServerStart.handle") |
| 392 | if isinstance(pkt, TftpPacketRRQ): |
| 393 | log.debug("Handling an RRQ packet") |
| 394 | return TftpStateServerRecvRRQ(self.context).handle(pkt, |
| 395 | raddress, |
| 396 | rport) |
| 397 | elif isinstance(pkt, TftpPacketWRQ): |
| 398 | log.debug("Handling a WRQ packet") |
| 399 | return TftpStateServerRecvWRQ(self.context).handle(pkt, |
| 400 | raddress, |
| 401 | rport) |
| 402 | else: |
| 403 | self.sendError(TftpErrors.IllegalTftpOp) |
| 404 | raise TftpException, \ |
| 405 | "Invalid packet to begin up/download: %s" % pkt |
| 406 | |
| 407 | class TftpStateExpectACK(TftpState): |
| 408 | """This class represents the state of the transfer when a DAT was just |
| 409 | sent, and we are waiting for an ACK from the server. This class is the |
| 410 | same one used by the client during the upload, and the server during the |
| 411 | download.""" |
| 412 | def handle(self, pkt, raddress, rport): |
| 413 | "Handle a packet, hopefully an ACK since we just sent a DAT." |
| 414 | if isinstance(pkt, TftpPacketACK): |
| 415 | log.info("Received ACK for packet %d" % pkt.blocknumber) |
| 416 | # Is this an ack to the one we just sent? |
| 417 | if self.context.next_block == pkt.blocknumber: |
| 418 | if self.context.pending_complete: |
| 419 | log.info("Received ACK to final DAT, we're done.") |
| 420 | return None |
| 421 | else: |
| 422 | log.debug("Good ACK, sending next DAT") |
| 423 | self.context.next_block += 1 |
| 424 | log.debug("Incremented next_block to %d", |
| 425 | self.context.next_block) |
| 426 | self.context.pending_complete = self.sendDAT() |
| 427 | |
| 428 | elif pkt.blocknumber < self.context.next_block: |
| 429 | log.warn("Received duplicate ACK for block %d" |
| 430 | % pkt.blocknumber) |
| 431 | self.context.metrics.add_dup(pkt) |
| 432 | |
| 433 | else: |
| 434 | log.warn("Oooh, time warp. Received ACK to packet we " |
| 435 | "didn't send yet. Discarding.") |
| 436 | self.context.metrics.errors += 1 |
| 437 | return self |
| 438 | elif isinstance(pkt, TftpPacketERR): |
| 439 | log.error("Received ERR packet from peer: %s" % str(pkt)) |
| 440 | raise TftpException, \ |
| 441 | "Received ERR packet from peer: %s" % str(pkt) |
| 442 | else: |
| 443 | log.warn("Discarding unsupported packet: %s" % str(pkt)) |
| 444 | return self |
| 445 | |
| 446 | class TftpStateExpectDAT(TftpState): |
| 447 | """Just sent an ACK packet. Waiting for DAT.""" |
| 448 | def handle(self, pkt, raddress, rport): |
| 449 | """Handle the packet in response to an ACK, which should be a DAT.""" |
| 450 | if isinstance(pkt, TftpPacketDAT): |
| 451 | return self.handleDat(pkt) |
| 452 | |
| 453 | # Every other packet type is a problem. |
| 454 | elif isinstance(pkt, TftpPacketACK): |
| 455 | # Umm, we ACK, you don't. |
| 456 | self.sendError(TftpErrors.IllegalTftpOp) |
| 457 | raise TftpException, "Received ACK from peer when expecting DAT" |
| 458 | |
| 459 | elif isinstance(pkt, TftpPacketWRQ): |
| 460 | self.sendError(TftpErrors.IllegalTftpOp) |
| 461 | raise TftpException, "Received WRQ from peer when expecting DAT" |
| 462 | |
| 463 | elif isinstance(pkt, TftpPacketERR): |
| 464 | self.sendError(TftpErrors.IllegalTftpOp) |
| 465 | raise TftpException, "Received ERR from peer: " + str(pkt) |
| 466 | |
| 467 | else: |
| 468 | self.sendError(TftpErrors.IllegalTftpOp) |
| 469 | raise TftpException, "Received unknown packet type from peer: " + str(pkt) |
| 470 | |
| 471 | class TftpStateSentWRQ(TftpState): |
| 472 | """Just sent an WRQ packet for an upload.""" |
| 473 | def handle(self, pkt, raddress, rport): |
| 474 | """Handle a packet we just received.""" |
| 475 | if not self.context.tidport: |
| 476 | self.context.tidport = rport |
| 477 | log.debug("Set remote port for session to %s", rport) |
| 478 | |
| 479 | # If we're going to successfully transfer the file, then we should see |
| 480 | # either an OACK for accepted options, or an ACK to ignore options. |
| 481 | if isinstance(pkt, TftpPacketOACK): |
| 482 | log.info("Received OACK from server") |
| 483 | try: |
| 484 | self.handleOACK(pkt) |
| 485 | except TftpException: |
| 486 | log.error("Failed to negotiate options") |
| 487 | self.sendError(TftpErrors.FailedNegotiation) |
| 488 | raise |
| 489 | else: |
| 490 | log.debug("Sending first DAT packet") |
| 491 | self.context.pending_complete = self.sendDAT() |
| 492 | log.debug("Changing state to TftpStateExpectACK") |
| 493 | return TftpStateExpectACK(self.context) |
| 494 | |
| 495 | elif isinstance(pkt, TftpPacketACK): |
| 496 | log.info("Received ACK from server") |
| 497 | log.debug("Apparently the server ignored our options") |
| 498 | # The block number should be zero. |
| 499 | if pkt.blocknumber == 0: |
| 500 | log.debug("Ack blocknumber is zero as expected") |
| 501 | log.debug("Sending first DAT packet") |
| 502 | self.context.pending_complete = self.sendDAT() |
| 503 | log.debug("Changing state to TftpStateExpectACK") |
| 504 | return TftpStateExpectACK(self.context) |
| 505 | else: |
| 506 | log.warn("Discarding ACK to block %s" % pkt.blocknumber) |
| 507 | log.debug("Still waiting for valid response from server") |
| 508 | return self |
| 509 | |
| 510 | elif isinstance(pkt, TftpPacketERR): |
| 511 | self.sendError(TftpErrors.IllegalTftpOp) |
| 512 | raise TftpException, "Received ERR from server: " + str(pkt) |
| 513 | |
| 514 | elif isinstance(pkt, TftpPacketRRQ): |
| 515 | self.sendError(TftpErrors.IllegalTftpOp) |
| 516 | raise TftpException, "Received RRQ from server while in upload" |
| 517 | |
| 518 | elif isinstance(pkt, TftpPacketDAT): |
| 519 | self.sendError(TftpErrors.IllegalTftpOp) |
| 520 | raise TftpException, "Received DAT from server while in upload" |
| 521 | |
| 522 | else: |
| 523 | self.sendError(TftpErrors.IllegalTftpOp) |
| 524 | raise TftpException, "Received unknown packet type from server: " + str(pkt) |
| 525 | |
| 526 | # By default, no state change. |
| 527 | return self |
| 528 | |
| 529 | class TftpStateSentRRQ(TftpState): |
| 530 | """Just sent an RRQ packet.""" |
| 531 | def handle(self, pkt, raddress, rport): |
| 532 | """Handle the packet in response to an RRQ to the server.""" |
| 533 | if not self.context.tidport: |
| 534 | self.context.tidport = rport |
| 535 | log.info("Set remote port for session to %s" % rport) |
| 536 | |
| 537 | # Now check the packet type and dispatch it properly. |
| 538 | if isinstance(pkt, TftpPacketOACK): |
| 539 | log.info("Received OACK from server") |
| 540 | try: |
| 541 | self.handleOACK(pkt) |
| 542 | except TftpException, err: |
| 543 | log.error("Failed to negotiate options: %s" % str(err)) |
| 544 | self.sendError(TftpErrors.FailedNegotiation) |
| 545 | raise |
| 546 | else: |
| 547 | log.debug("Sending ACK to OACK") |
| 548 | |
| 549 | self.sendACK(blocknumber=0) |
| 550 | |
| 551 | log.debug("Changing state to TftpStateExpectDAT") |
| 552 | return TftpStateExpectDAT(self.context) |
| 553 | |
| 554 | elif isinstance(pkt, TftpPacketDAT): |
| 555 | # If there are any options set, then the server didn't honour any |
| 556 | # of them. |
| 557 | log.info("Received DAT from server") |
| 558 | if self.context.options: |
| 559 | log.info("Server ignored options, falling back to defaults") |
| 560 | self.context.options = { 'blksize': DEF_BLKSIZE } |
| 561 | return self.handleDat(pkt) |
| 562 | |
| 563 | # Every other packet type is a problem. |
| 564 | elif isinstance(pkt, TftpPacketACK): |
| 565 | # Umm, we ACK, the server doesn't. |
| 566 | self.sendError(TftpErrors.IllegalTftpOp) |
| 567 | raise TftpException, "Received ACK from server while in download" |
| 568 | |
| 569 | elif isinstance(pkt, TftpPacketWRQ): |
| 570 | self.sendError(TftpErrors.IllegalTftpOp) |
| 571 | raise TftpException, "Received WRQ from server while in download" |
| 572 | |
| 573 | elif isinstance(pkt, TftpPacketERR): |
| 574 | self.sendError(TftpErrors.IllegalTftpOp) |
| 575 | raise TftpException, "Received ERR from server: " + str(pkt) |
| 576 | |
| 577 | else: |
| 578 | self.sendError(TftpErrors.IllegalTftpOp) |
| 579 | raise TftpException, "Received unknown packet type from server: " + str(pkt) |
| 580 | |
| 581 | # By default, no state change. |
| 582 | return self |