codekingpro/portable-devtools
114k
1# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>2#3# This file is part of paramiko.4#5# Paramiko is free software; you can redistribute it and/or modify it under the6# terms of the GNU Lesser General Public License as published by the Free7# Software Foundation; either version 2.1 of the License, or (at your option)8# any later version.9#10# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY11# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR12# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more13# details.14#15# You should have received a copy of the GNU Lesser General Public License16# along with Paramiko; if not, write to the Free Software Foundation, Inc.,17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.18 19"""20Packet handling21"""22 23import errno24import os25import socket26import struct27import threading28import time29from hmac import HMAC30 31from paramiko import util32from paramiko.common import (33 linefeed_byte,34 cr_byte_value,35 MSG_NAMES,36 DEBUG,37 xffffffff,38 zero_byte,39 byte_ord,40)41from paramiko.util import u42from paramiko.ssh_exception import SSHException, ProxyCommandFailure43from paramiko.message import Message44 45 46def compute_hmac(key, message, digest_class):47 return HMAC(key, message, digest_class).digest()48 49 50class NeedRekeyException(Exception):51 """52 Exception indicating a rekey is needed.53 """54 55 pass56 57 58def first_arg(e):59 arg = None60 if type(e.args) is tuple and len(e.args) > 0:61 arg = e.args[0]62 return arg63 64 65class Packetizer:66 """67 Implementation of the base SSH packet protocol.68 """69 70 # READ the secsh RFC's before raising these values. if anything,71 # they should probably be lower.72 REKEY_PACKETS = pow(2, 29)73 REKEY_BYTES = pow(2, 29)74 75 # Allow receiving this many packets after a re-key request before76 # terminating77 REKEY_PACKETS_OVERFLOW_MAX = pow(2, 29)78 # Allow receiving this many bytes after a re-key request before terminating79 REKEY_BYTES_OVERFLOW_MAX = pow(2, 29)80 81 def __init__(self, socket):82 self.__socket = socket83 self.__logger = None84 self.__closed = False85 self.__dump_packets = False86 self.__need_rekey = False87 self.__init_count = 088 self.__remainder = bytes()89 self._initial_kex_done = False90 91 # used for noticing when to re-key:92 self.__sent_bytes = 093 self.__sent_packets = 094 self.__received_bytes = 095 self.__received_packets = 096 self.__received_bytes_overflow = 097 self.__received_packets_overflow = 098 99 # current inbound/outbound ciphering:100 self.__block_size_out = 8101 self.__block_size_in = 8102 self.__mac_size_out = 0103 self.__mac_size_in = 0104 self.__block_engine_out = None105 self.__block_engine_in = None106 self.__sdctr_out = False107 self.__mac_engine_out = None108 self.__mac_engine_in = None109 self.__mac_key_out = bytes()110 self.__mac_key_in = bytes()111 self.__compress_engine_out = None112 self.__compress_engine_in = None113 self.__sequence_number_out = 0114 self.__sequence_number_in = 0115 self.__etm_out = False116 self.__etm_in = False117 118 # lock around outbound writes (packet computation)119 self.__write_lock = threading.RLock()120 121 # keepalives:122 self.__keepalive_interval = 0123 self.__keepalive_last = time.time()124 self.__keepalive_callback = None125 126 self.__timer = None127 self.__handshake_complete = False128 self.__timer_expired = False129 130 @property131 def closed(self):132 return self.__closed133 134 def reset_seqno_out(self):135 self.__sequence_number_out = 0136 137 def reset_seqno_in(self):138 self.__sequence_number_in = 0139 140 def set_log(self, log):141 """142 Set the Python log object to use for logging.143 """144 self.__logger = log145 146 def set_outbound_cipher(147 self,148 block_engine,149 block_size,150 mac_engine,151 mac_size,152 mac_key,153 sdctr=False,154 etm=False,155 ):156 """157 Switch outbound data cipher.158 :param etm: Set encrypt-then-mac from OpenSSH159 """160 self.__block_engine_out = block_engine161 self.__sdctr_out = sdctr162 self.__block_size_out = block_size163 self.__mac_engine_out = mac_engine164 self.__mac_size_out = mac_size165 self.__mac_key_out = mac_key166 self.__sent_bytes = 0167 self.__sent_packets = 0168 self.__etm_out = etm169 # wait until the reset happens in both directions before clearing170 # rekey flag171 self.__init_count |= 1172 if self.__init_count == 3:173 self.__init_count = 0174 self.__need_rekey = False175 176 def set_inbound_cipher(177 self,178 block_engine,179 block_size,180 mac_engine,181 mac_size,182 mac_key,183 etm=False,184 ):185 """186 Switch inbound data cipher.187 :param etm: Set encrypt-then-mac from OpenSSH188 """189 self.__block_engine_in = block_engine190 self.__block_size_in = block_size191 self.__mac_engine_in = mac_engine192 self.__mac_size_in = mac_size193 self.__mac_key_in = mac_key194 self.__received_bytes = 0195 self.__received_packets = 0196 self.__received_bytes_overflow = 0197 self.__received_packets_overflow = 0198 self.__etm_in = etm199 # wait until the reset happens in both directions before clearing200 # rekey flag201 self.__init_count |= 2202 if self.__init_count == 3:203 self.__init_count = 0204 self.__need_rekey = False205 206 def set_outbound_compressor(self, compressor):207 self.__compress_engine_out = compressor208 209 def set_inbound_compressor(self, compressor):210 self.__compress_engine_in = compressor211 212 def close(self):213 self.__closed = True214 self.__socket.close()215 216 def set_hexdump(self, hexdump):217 self.__dump_packets = hexdump218 219 def get_hexdump(self):220 return self.__dump_packets221 222 def get_mac_size_in(self):223 return self.__mac_size_in224 225 def get_mac_size_out(self):226 return self.__mac_size_out227 228 def need_rekey(self):229 """230 Returns ``True`` if a new set of keys needs to be negotiated. This231 will be triggered during a packet read or write, so it should be232 checked after every read or write, or at least after every few.233 """234 return self.__need_rekey235 236 def set_keepalive(self, interval, callback):237 """238 Turn on/off the callback keepalive. If ``interval`` seconds pass with239 no data read from or written to the socket, the callback will be240 executed and the timer will be reset.241 """242 self.__keepalive_interval = interval243 self.__keepalive_callback = callback244 self.__keepalive_last = time.time()245 246 def read_timer(self):247 self.__timer_expired = True248 249 def start_handshake(self, timeout):250 """251 Tells `Packetizer` that the handshake process started.252 Starts a book keeping timer that can signal a timeout in the253 handshake process.254 255 :param float timeout: amount of seconds to wait before timing out256 """257 if not self.__timer:258 self.__timer = threading.Timer(float(timeout), self.read_timer)259 self.__timer.start()260 261 def handshake_timed_out(self):262 """263 Checks if the handshake has timed out.264 265 If `start_handshake` wasn't called before the call to this function,266 the return value will always be `False`. If the handshake completed267 before a timeout was reached, the return value will be `False`268 269 :return: handshake time out status, as a `bool`270 """271 if not self.__timer:272 return False273 if self.__handshake_complete:274 return False275 return self.__timer_expired276 277 def complete_handshake(self):278 """279 Tells `Packetizer` that the handshake has completed.280 """281 if self.__timer:282 self.__timer.cancel()283 self.__timer_expired = False284 self.__handshake_complete = True285 286 def read_all(self, n, check_rekey=False):287 """288 Read as close to N bytes as possible, blocking as long as necessary.289 290 :param int n: number of bytes to read291 :return: the data read, as a `str`292 293 :raises:294 ``EOFError`` -- if the socket was closed before all the bytes could295 be read296 """297 out = bytes()298 # handle over-reading from reading the banner line299 if len(self.__remainder) > 0:300 out = self.__remainder[:n]301 self.__remainder = self.__remainder[n:]302 n -= len(out)303 while n > 0:304 got_timeout = False305 if self.handshake_timed_out():306 raise EOFError()307 try:308 x = self.__socket.recv(n)309 if len(x) == 0:310 raise EOFError()311 out += x312 n -= len(x)313 except socket.timeout:314 got_timeout = True315 except socket.error as e:316 # on Linux, sometimes instead of socket.timeout, we get317 # EAGAIN. this is a bug in recent (> 2.6.9) kernels but318 # we need to work around it.319 arg = first_arg(e)320 if arg == errno.EAGAIN:321 got_timeout = True322 elif self.__closed:323 raise EOFError()324 else:325 raise326 if got_timeout:327 if self.__closed:328 raise EOFError()329 if check_rekey and (len(out) == 0) and self.__need_rekey:330 raise NeedRekeyException()331 self._check_keepalive()332 return out333 334 def write_all(self, out):335 self.__keepalive_last = time.time()336 iteration_with_zero_as_return_value = 0337 while len(out) > 0:338 retry_write = False339 try:340 n = self.__socket.send(out)341 except socket.timeout:342 retry_write = True343 except socket.error as e:344 arg = first_arg(e)345 if arg == errno.EAGAIN:346 retry_write = True347 else:348 n = -1349 except ProxyCommandFailure:350 raise # so it doesn't get swallowed by the below catchall351 except Exception:352 # could be: (32, 'Broken pipe')353 n = -1354 if retry_write:355 n = 0356 if self.__closed:357 n = -1358 else:359 if n == 0 and iteration_with_zero_as_return_value > 10:360 # We shouldn't retry the write, but we didn't361 # manage to send anything over the socket. This might be an362 # indication that we have lost contact with the remote363 # side, but are yet to receive an EOFError or other socket364 # errors. Let's give it some iteration to try and catch up.365 n = -1366 iteration_with_zero_as_return_value += 1367 if n < 0:368 raise EOFError()369 if n == len(out):370 break371 out = out[n:]372 return373 374 def readline(self, timeout):375 """376 Read a line from the socket. We assume no data is pending after the377 line, so it's okay to attempt large reads.378 """379 buf = self.__remainder380 while linefeed_byte not in buf:381 buf += self._read_timeout(timeout)382 n = buf.index(linefeed_byte)383 self.__remainder = buf[n + 1 :]384 buf = buf[:n]385 if (len(buf) > 0) and (buf[-1] == cr_byte_value):386 buf = buf[:-1]387 return u(buf)388 389 def send_message(self, data):390 """391 Write a block of data using the current cipher, as an SSH block.392 """393 # encrypt this sucka394 data = data.asbytes()395 cmd = byte_ord(data[0])396 if cmd in MSG_NAMES:397 cmd_name = MSG_NAMES[cmd]398 else:399 cmd_name = "${:x}".format(cmd)400 orig_len = len(data)401 self.__write_lock.acquire()402 try:403 if self.__compress_engine_out is not None:404 data = self.__compress_engine_out(data)405 packet = self._build_packet(data)406 if self.__dump_packets:407 self._log(408 DEBUG,409 "Write packet <{}>, length {}".format(cmd_name, orig_len),410 )411 self._log(DEBUG, util.format_binary(packet, "OUT: "))412 if self.__block_engine_out is not None:413 if self.__etm_out:414 # packet length is not encrypted in EtM415 out = packet[0:4] + self.__block_engine_out.update(416 packet[4:]417 )418 else:419 out = self.__block_engine_out.update(packet)420 else:421 out = packet422 # + mac423 if self.__block_engine_out is not None:424 packed = struct.pack(">I", self.__sequence_number_out)425 payload = packed + (out if self.__etm_out else packet)426 out += compute_hmac(427 self.__mac_key_out, payload, self.__mac_engine_out428 )[: self.__mac_size_out]429 next_seq = (self.__sequence_number_out + 1) & xffffffff430 if next_seq == 0 and not self._initial_kex_done:431 raise SSHException(432 "Sequence number rolled over during initial kex!"433 )434 self.__sequence_number_out = next_seq435 self.write_all(out)436 437 self.__sent_bytes += len(out)438 self.__sent_packets += 1439 sent_too_much = (440 self.__sent_packets >= self.REKEY_PACKETS441 or self.__sent_bytes >= self.REKEY_BYTES442 )443 if sent_too_much and not self.__need_rekey:444 # only ask once for rekeying445 msg = "Rekeying (hit {} packets, {} bytes sent)"446 self._log(447 DEBUG, msg.format(self.__sent_packets, self.__sent_bytes)448 )449 self.__received_bytes_overflow = 0450 self.__received_packets_overflow = 0451 self._trigger_rekey()452 finally:453 self.__write_lock.release()454 455 def read_message(self):456 """457 Only one thread should ever be in this function (no other locking is458 done).459 460 :raises: `.SSHException` -- if the packet is mangled461 :raises: `.NeedRekeyException` -- if the transport should rekey462 """463 header = self.read_all(self.__block_size_in, check_rekey=True)464 if self.__etm_in:465 packet_size = struct.unpack(">I", header[:4])[0]466 remaining = packet_size - self.__block_size_in + 4467 packet = header[4:] + self.read_all(remaining, check_rekey=False)468 mac = self.read_all(self.__mac_size_in, check_rekey=False)469 mac_payload = (470 struct.pack(">II", self.__sequence_number_in, packet_size)471 + packet472 )473 my_mac = compute_hmac(474 self.__mac_key_in, mac_payload, self.__mac_engine_in475 )[: self.__mac_size_in]476 if not util.constant_time_bytes_eq(my_mac, mac):477 raise SSHException("Mismatched MAC")478 header = packet479 480 if self.__block_engine_in is not None:481 header = self.__block_engine_in.update(header)482 if self.__dump_packets:483 self._log(DEBUG, util.format_binary(header, "IN: "))484 485 # When ETM is in play, we've already read the packet size & decrypted486 # everything, so just set the packet back to the header we obtained.487 if self.__etm_in:488 packet = header489 # Otherwise, use the older non-ETM logic490 else:491 packet_size = struct.unpack(">I", header[:4])[0]492 493 # leftover contains decrypted bytes from the first block (after the494 # length field)495 leftover = header[4:]496 if (packet_size - len(leftover)) % self.__block_size_in != 0:497 raise SSHException("Invalid packet blocking")498 buf = self.read_all(499 packet_size + self.__mac_size_in - len(leftover)500 )501 packet = buf[: packet_size - len(leftover)]502 post_packet = buf[packet_size - len(leftover) :]503 504 if self.__block_engine_in is not None:505 packet = self.__block_engine_in.update(packet)506 packet = leftover + packet507 508 if self.__dump_packets:509 self._log(DEBUG, util.format_binary(packet, "IN: "))510 511 if self.__mac_size_in > 0 and not self.__etm_in:512 mac = post_packet[: self.__mac_size_in]513 mac_payload = (514 struct.pack(">II", self.__sequence_number_in, packet_size)515 + packet516 )517 my_mac = compute_hmac(518 self.__mac_key_in, mac_payload, self.__mac_engine_in519 )[: self.__mac_size_in]520 if not util.constant_time_bytes_eq(my_mac, mac):521 raise SSHException("Mismatched MAC")522 padding = byte_ord(packet[0])523 payload = packet[1 : packet_size - padding]524 525 if self.__dump_packets:526 self._log(527 DEBUG,528 "Got payload ({} bytes, {} padding)".format(529 packet_size, padding530 ),531 )532 533 if self.__compress_engine_in is not None:534 payload = self.__compress_engine_in(payload)535 536 msg = Message(payload[1:])537 msg.seqno = self.__sequence_number_in538 next_seq = (self.__sequence_number_in + 1) & xffffffff539 if next_seq == 0 and not self._initial_kex_done:540 raise SSHException(541 "Sequence number rolled over during initial kex!"542 )543 self.__sequence_number_in = next_seq544 545 # check for rekey546 raw_packet_size = packet_size + self.__mac_size_in + 4547 self.__received_bytes += raw_packet_size548 self.__received_packets += 1549 if self.__need_rekey:550 # we've asked to rekey -- give them some packets to comply before551 # dropping the connection552 self.__received_bytes_overflow += raw_packet_size553 self.__received_packets_overflow += 1554 if (555 self.__received_packets_overflow556 >= self.REKEY_PACKETS_OVERFLOW_MAX557 ) or (558 self.__received_bytes_overflow >= self.REKEY_BYTES_OVERFLOW_MAX559 ):560 raise SSHException(561 "Remote transport is ignoring rekey requests"562 )563 elif (self.__received_packets >= self.REKEY_PACKETS) or (564 self.__received_bytes >= self.REKEY_BYTES565 ):566 # only ask once for rekeying567 err = "Rekeying (hit {} packets, {} bytes received)"568 self._log(569 DEBUG,570 err.format(self.__received_packets, self.__received_bytes),571 )572 self.__received_bytes_overflow = 0573 self.__received_packets_overflow = 0574 self._trigger_rekey()575 576 cmd = byte_ord(payload[0])577 if cmd in MSG_NAMES:578 cmd_name = MSG_NAMES[cmd]579 else:580 cmd_name = "${:x}".format(cmd)581 if self.__dump_packets:582 self._log(583 DEBUG,584 "Read packet <{}>, length {}".format(cmd_name, len(payload)),585 )586 return cmd, msg587 588 # ...protected...589 590 def _log(self, level, msg):591 if self.__logger is None:592 return593 if issubclass(type(msg), list):594 for m in msg:595 self.__logger.log(level, m)596 else:597 self.__logger.log(level, msg)598 599 def _check_keepalive(self):600 if (601 not self.__keepalive_interval602 or not self.__block_engine_out603 or self.__need_rekey604 ):605 # wait till we're encrypting, and not in the middle of rekeying606 return607 now = time.time()608 if now > self.__keepalive_last + self.__keepalive_interval:609 self.__keepalive_callback()610 self.__keepalive_last = now611 612 def _read_timeout(self, timeout):613 start = time.time()614 while True:615 try:616 x = self.__socket.recv(128)617 if len(x) == 0:618 raise EOFError()619 break620 except socket.timeout:621 pass622 if self.__closed:623 raise EOFError()624 now = time.time()625 if now - start >= timeout:626 raise socket.timeout()627 return x628 629 def _build_packet(self, payload):630 # pad up at least 4 bytes, to nearest block-size (usually 8)631 bsize = self.__block_size_out632 # do not include payload length in computations for padding in EtM mode633 # (payload length won't be encrypted)634 addlen = 4 if self.__etm_out else 8635 padding = 3 + bsize - ((len(payload) + addlen) % bsize)636 packet = struct.pack(">IB", len(payload) + padding + 1, padding)637 packet += payload638 if self.__sdctr_out or self.__block_engine_out is None:639 # cute trick i caught openssh doing: if we're not encrypting or640 # SDCTR mode (RFC4344),641 # don't waste random bytes for the padding642 packet += zero_byte * padding643 else:644 packet += os.urandom(padding)645 return packet646 647 def _trigger_rekey(self):648 # outside code should check for this flag649 self.__need_rekey = True650 