| 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 |