blob: 1ea6c3160173440e213ba1968b467b8f62de6eda [file] [log] [blame]
Norman James8b2b7222015-10-08 07:01:38 -05001"""This module implements all state handling during uploads and downloads, the
2main interface to which being the TftpState base class.
3
4The concept is simple. Each context object represents a single upload or
5download, and the state object in the context object represents the current
6state of that transfer. The state object has a handle() method that expects
7the next packet in the transfer, and returns a state object until the transfer
8is complete, at which point it returns None. That is, unless there is a fatal
9error, in which case a TftpException is returned instead."""
10
11from TftpShared import *
12from TftpPacketTypes import *
13import os
14
15###############################################################################
16# State classes
17###############################################################################
18
19class 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
200class 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
286class 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
331class 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
385class 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
407class 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
446class 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
471class 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
529class 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