From b68489d04dfeb4ed8805428781816edd75f88c84 Mon Sep 17 00:00:00 2001 From: Kingbox <37674310+lopezvg@users.noreply.github.com> Date: Tue, 27 Nov 2018 18:16:07 +0100 Subject: [PATCH 1/3] =?UTF-8?q?SMB=20client:=20versi=C3=B3n=201.1.25=20de?= =?UTF-8?q?=20pysmb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hace posible el funcionamiento de la Videoteca en un servidor Samba (disco contectado a router, NAS, servidor windows,...) --- plugin.video.alfa/core/filetools.py | 21 + plugin.video.alfa/lib/sambatools/libsmb.py | 8 +- .../lib/sambatools/nmb/NetBIOS.py | 293 +- .../lib/sambatools/nmb/NetBIOSProtocol.py | 276 +- plugin.video.alfa/lib/sambatools/nmb/base.py | 360 +- .../lib/sambatools/nmb/nmb_constants.py | 76 +- .../lib/sambatools/nmb/nmb_structs.py | 138 +- plugin.video.alfa/lib/sambatools/nmb/utils.py | 100 +- .../lib/sambatools/smb/SMBConnection.py | 1212 ++-- .../lib/sambatools/smb/SMBHandler.py | 199 +- .../lib/sambatools/smb/SMBProtocol.py | 807 +-- .../lib/sambatools/smb/__init__.py | 2 +- plugin.video.alfa/lib/sambatools/smb/base.py | 5593 +++++++++-------- plugin.video.alfa/lib/sambatools/smb/ntlm.py | 497 +- .../sambatools/smb/security_descriptors.py | 367 ++ .../lib/sambatools/smb/securityblob.py | 272 +- .../lib/sambatools/smb/smb2_constants.py | 216 +- .../lib/sambatools/smb/smb2_structs.py | 1852 +++--- .../lib/sambatools/smb/smb_constants.py | 496 +- .../lib/sambatools/smb/smb_structs.py | 2844 +++++---- .../lib/sambatools/smb/utils/README.txt | 24 +- .../lib/sambatools/smb/utils/__init__.py | 6 +- .../lib/sambatools/smb/utils/md4.py | 508 +- .../lib/sambatools/smb/utils/pyDes.py | 1704 ++--- .../lib/sambatools/smb/utils/sha256.py | 222 +- .../platformcode/platformtools.py | 4 +- .../platformcode/xbmc_videolibrary.py | 4 +- 27 files changed, 9496 insertions(+), 8605 deletions(-) create mode 100644 plugin.video.alfa/lib/sambatools/smb/security_descriptors.py diff --git a/plugin.video.alfa/core/filetools.py b/plugin.video.alfa/core/filetools.py index 4bfc111f..278c9dea 100755 --- a/plugin.video.alfa/core/filetools.py +++ b/plugin.video.alfa/core/filetools.py @@ -576,3 +576,24 @@ def remove_tags(title): return title_without_tags else: return title + + +def remove_smb_credential(path): + """ + devuelve el path sin contraseña/usuario para paths de SMB + @param path: ruta + @type path: str + @return: cadena sin credenciales + @rtype: str + """ + logger.info() + + if not path.startswith("smb://"): + return path + + path_without_credentials = scrapertools.find_single_match(path, '^smb:\/\/(?:[^;\n]+;)?(?:[^:@\n]+[:|@])?(?:[^@\n]+@)?(.*?$)') + + if path_without_credentials: + return ('smb://' + path_without_credentials) + else: + return path diff --git a/plugin.video.alfa/lib/sambatools/libsmb.py b/plugin.video.alfa/lib/sambatools/libsmb.py index 74506e92..64130347 100755 --- a/plugin.video.alfa/lib/sambatools/libsmb.py +++ b/plugin.video.alfa/lib/sambatools/libsmb.py @@ -1,18 +1,21 @@ # -*- coding: utf-8 -*- import os +import re from nmb.NetBIOS import NetBIOS from platformcode import logger from smb.SMBConnection import SMBConnection +GitHub = 'https://github.com/miketeo/pysmb' #buscar aquí de vez en cuando la última versiónde SMB-pysmb, y actualizar en Alfa +vesion_actual_pysmb = '1.1.25' #actualizada el 25/11/2018 + remote = None def parse_url(url): # logger.info("Url: %s" % url) url = url.strip() - import re patron = "^smb://(?:([^;\n]+);)?(?:([^:@\n]+)[:|@])?(?:([^@\n]+)@)?([^/]+)/([^/\n]+)([/]?.*?)$" domain, user, password, server_name, share_name, path = re.compile(patron, re.DOTALL).match(url).groups() @@ -29,8 +32,7 @@ def parse_url(url): def get_server_name_ip(server): - import re - if re.compile("^\d+.\d+.\d+.\d+$").findall(server): + if re.compile("^\d+.\d+.\d+.\d+$").findall(server) or re.compile("^([^\.]+\.(?:[^\.]+\.)?(?:\w+)?)$").findall(server): server_ip = server server_name = None else: diff --git a/plugin.video.alfa/lib/sambatools/nmb/NetBIOS.py b/plugin.video.alfa/lib/sambatools/nmb/NetBIOS.py index 89df49e6..34058054 100755 --- a/plugin.video.alfa/lib/sambatools/nmb/NetBIOS.py +++ b/plugin.video.alfa/lib/sambatools/nmb/NetBIOS.py @@ -1,149 +1,144 @@ -import logging -import random -import select -import socket -import time - -from base import NBNS, NotConnectedError -from nmb_constants import TYPE_SERVER - - -class NetBIOS(NBNS): - - log = logging.getLogger('NMB.NetBIOS') - - def __init__(self, broadcast = True, listen_port = 0): - """ - Instantiate a NetBIOS instance, and creates a IPv4 UDP socket to listen/send NBNS packets. - - :param boolean broadcast: A boolean flag to indicate if we should setup the listening UDP port in broadcast mode - :param integer listen_port: Specifies the UDP port number to bind to for listening. If zero, OS will automatically select a free port number. - """ - self.broadcast = broadcast - self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - if self.broadcast: - self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - if listen_port: - self.sock.bind(( '', listen_port )) - - def close(self): - """ - Close the underlying and free resources. - - The NetBIOS instance should not be used to perform any operations after this method returns. - - :return: None - """ - self.sock.close() - self.sock = None - - def write(self, data, ip, port): - assert self.sock, 'Socket is already closed' - self.sock.sendto(data, ( ip, port )) - - def queryName(self, name, ip = '', port = 137, timeout = 30): - """ - Send a query on the network and hopes that if machine matching the *name* will reply with its IP address. - - :param string ip: If the NBNSProtocol instance was instianted with broadcast=True, then this parameter can be an empty string. We will leave it to the OS to determine an appropriate broadcast address. - If the NBNSProtocol instance was instianted with broadcast=False, then you should provide a target IP to send the query. - :param integer port: The NetBIOS-NS port (IANA standard defines this port to be 137). You should not touch this parameter unless you know what you are doing. - :param integer/float timeout: Number of seconds to wait for a reply, after which the method will return None - :return: A list of IP addresses in dotted notation (aaa.bbb.ccc.ddd). On timeout, returns None. - """ - assert self.sock, 'Socket is already closed' - - trn_id = random.randint(1, 0xFFFF) - data = self.prepareNameQuery(trn_id, name) - if self.broadcast and not ip: - ip = '' - elif not ip: - self.log.warning('queryName: ip parameter is empty. OS might not transmit this query to the network') - - self.write(data, ip, port) - - return self._pollForNetBIOSPacket(trn_id, timeout) - - def queryIPForName(self, ip, port = 137, timeout = 30): - """ - Send a query to the machine with *ip* and hopes that the machine will reply back with its name. - - The implementation of this function is contributed by Jason Anderson. - - :param string ip: If the NBNSProtocol instance was instianted with broadcast=True, then this parameter can be an empty string. We will leave it to the OS to determine an appropriate broadcast address. - If the NBNSProtocol instance was instianted with broadcast=False, then you should provide a target IP to send the query. - :param integer port: The NetBIOS-NS port (IANA standard defines this port to be 137). You should not touch this parameter unless you know what you are doing. - :param integer/float timeout: Number of seconds to wait for a reply, after which the method will return None - :return: A list of string containing the names of the machine at *ip*. On timeout, returns None. - """ - assert self.sock, 'Socket is already closed' - - trn_id = random.randint(1, 0xFFFF) - data = self.prepareNetNameQuery(trn_id, False) - self.write(data, ip, port) - ret = self._pollForQueryPacket(trn_id, timeout) - if ret: - return map(lambda s: s[0], filter(lambda s: s[1] == TYPE_SERVER, ret)) - else: - return None - - # - # Protected Methods - # - - def _pollForNetBIOSPacket(self, wait_trn_id, timeout): - end_time = time.time() + timeout - while True: - try: - _timeout = end_time - time.time() - if _timeout <= 0: - return None - - ready, _, _ = select.select([ self.sock.fileno() ], [ ], [ ], _timeout) - if not ready: - return None - - data, _ = self.sock.recvfrom(0xFFFF) - if len(data) == 0: - raise NotConnectedError - - trn_id, ret = self.decodePacket(data) - - if trn_id == wait_trn_id: - return ret - except select.error, ex: - if type(ex) is types.TupleType: - if ex[0] != errno.EINTR and ex[0] != errno.EAGAIN: - raise ex - else: - raise ex - - # - # Contributed by Jason Anderson - # - def _pollForQueryPacket(self, wait_trn_id, timeout): - end_time = time.time() + timeout - while True: - try: - _timeout = end_time - time.time() - if _timeout <= 0: - return None - - ready, _, _ = select.select([ self.sock.fileno() ], [ ], [ ], _timeout) - if not ready: - return None - - data, _ = self.sock.recvfrom(0xFFFF) - if len(data) == 0: - raise NotConnectedError - - trn_id, ret = self.decodeIPQueryPacket(data) - - if trn_id == wait_trn_id: - return ret - except select.error, ex: - if type(ex) is types.TupleType: - if ex[0] != errno.EINTR and ex[0] != errno.EAGAIN: - raise ex - else: - raise ex + +import os, logging, random, socket, time, select +from base import NBNS, NotConnectedError +from nmb_constants import TYPE_CLIENT, TYPE_SERVER, TYPE_WORKSTATION + +class NetBIOS(NBNS): + + log = logging.getLogger('NMB.NetBIOS') + + def __init__(self, broadcast = True, listen_port = 0): + """ + Instantiate a NetBIOS instance, and creates a IPv4 UDP socket to listen/send NBNS packets. + + :param boolean broadcast: A boolean flag to indicate if we should setup the listening UDP port in broadcast mode + :param integer listen_port: Specifies the UDP port number to bind to for listening. If zero, OS will automatically select a free port number. + """ + self.broadcast = broadcast + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + if self.broadcast: + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + if listen_port: + self.sock.bind(( '', listen_port )) + + def close(self): + """ + Close the underlying and free resources. + + The NetBIOS instance should not be used to perform any operations after this method returns. + + :return: None + """ + self.sock.close() + self.sock = None + + def write(self, data, ip, port): + assert self.sock, 'Socket is already closed' + self.sock.sendto(data, ( ip, port )) + + def queryName(self, name, ip = '', port = 137, timeout = 30): + """ + Send a query on the network and hopes that if machine matching the *name* will reply with its IP address. + + :param string ip: If the NBNSProtocol instance was instianted with broadcast=True, then this parameter can be an empty string. We will leave it to the OS to determine an appropriate broadcast address. + If the NBNSProtocol instance was instianted with broadcast=False, then you should provide a target IP to send the query. + :param integer port: The NetBIOS-NS port (IANA standard defines this port to be 137). You should not touch this parameter unless you know what you are doing. + :param integer/float timeout: Number of seconds to wait for a reply, after which the method will return None + :return: A list of IP addresses in dotted notation (aaa.bbb.ccc.ddd). On timeout, returns None. + """ + assert self.sock, 'Socket is already closed' + + trn_id = random.randint(1, 0xFFFF) + data = self.prepareNameQuery(trn_id, name) + if self.broadcast and not ip: + ip = '' + elif not ip: + self.log.warning('queryName: ip parameter is empty. OS might not transmit this query to the network') + + self.write(data, ip, port) + + return self._pollForNetBIOSPacket(trn_id, timeout) + + def queryIPForName(self, ip, port = 137, timeout = 30): + """ + Send a query to the machine with *ip* and hopes that the machine will reply back with its name. + + The implementation of this function is contributed by Jason Anderson. + + :param string ip: If the NBNSProtocol instance was instianted with broadcast=True, then this parameter can be an empty string. We will leave it to the OS to determine an appropriate broadcast address. + If the NBNSProtocol instance was instianted with broadcast=False, then you should provide a target IP to send the query. + :param integer port: The NetBIOS-NS port (IANA standard defines this port to be 137). You should not touch this parameter unless you know what you are doing. + :param integer/float timeout: Number of seconds to wait for a reply, after which the method will return None + :return: A list of string containing the names of the machine at *ip*. On timeout, returns None. + """ + assert self.sock, 'Socket is already closed' + + trn_id = random.randint(1, 0xFFFF) + data = self.prepareNetNameQuery(trn_id, False) + self.write(data, ip, port) + ret = self._pollForQueryPacket(trn_id, timeout) + if ret: + return map(lambda s: s[0], filter(lambda s: s[1] == TYPE_SERVER, ret)) + else: + return None + + # + # Protected Methods + # + + def _pollForNetBIOSPacket(self, wait_trn_id, timeout): + end_time = time.time() + timeout + while True: + try: + _timeout = end_time - time.time() + if _timeout <= 0: + return None + + ready, _, _ = select.select([ self.sock.fileno() ], [ ], [ ], _timeout) + if not ready: + return None + + data, _ = self.sock.recvfrom(0xFFFF) + if len(data) == 0: + raise NotConnectedError + + trn_id, ret = self.decodePacket(data) + + if trn_id == wait_trn_id: + return ret + except select.error, ex: + if type(ex) is types.TupleType: + if ex[0] != errno.EINTR and ex[0] != errno.EAGAIN: + raise ex + else: + raise ex + + # + # Contributed by Jason Anderson + # + def _pollForQueryPacket(self, wait_trn_id, timeout): + end_time = time.time() + timeout + while True: + try: + _timeout = end_time - time.time() + if _timeout <= 0: + return None + + ready, _, _ = select.select([ self.sock.fileno() ], [ ], [ ], _timeout) + if not ready: + return None + + data, _ = self.sock.recvfrom(0xFFFF) + if len(data) == 0: + raise NotConnectedError + + trn_id, ret = self.decodeIPQueryPacket(data) + + if trn_id == wait_trn_id: + return ret + except select.error, ex: + if type(ex) is types.TupleType: + if ex[0] != errno.EINTR and ex[0] != errno.EAGAIN: + raise ex + else: + raise ex diff --git a/plugin.video.alfa/lib/sambatools/nmb/NetBIOSProtocol.py b/plugin.video.alfa/lib/sambatools/nmb/NetBIOSProtocol.py index 2c8dce2e..3d9b6b87 100755 --- a/plugin.video.alfa/lib/sambatools/nmb/NetBIOSProtocol.py +++ b/plugin.video.alfa/lib/sambatools/nmb/NetBIOSProtocol.py @@ -1,140 +1,136 @@ -import logging -import random -import socket -import time - -from twisted.internet import reactor, defer -from twisted.internet.protocol import DatagramProtocol - -from base import NBNS -from nmb_constants import TYPE_SERVER - -IP_QUERY, NAME_QUERY = range(2) - -class NetBIOSTimeout(Exception): - """Raised in NBNSProtocol via Deferred.errback method when queryName method has timeout waiting for reply""" - pass - -class NBNSProtocol(DatagramProtocol, NBNS): - - log = logging.getLogger('NMB.NBNSProtocol') - - def __init__(self, broadcast = True, listen_port = 0): - """ - Instantiate a NBNSProtocol instance. - - This automatically calls reactor.listenUDP method to start listening for incoming packets, so you **must not** call the listenUDP method again. - - :param boolean broadcast: A boolean flag to indicate if we should setup the listening UDP port in broadcast mode - :param integer listen_port: Specifies the UDP port number to bind to for listening. If zero, OS will automatically select a free port number. - """ - self.broadcast = broadcast - self.pending_trns = { } # TRN ID -> ( expiry_time, name, Deferred instance ) - self.transport = reactor.listenUDP(listen_port, self) - if self.broadcast: - self.transport.getHandle().setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - reactor.callLater(1, self.cleanupPendingTrns) - - def datagramReceived(self, data, from_info): - host, port = from_info - trn_id, ret = self.decodePacket(data) - - # pending transaction exists for trn_id - handle it and remove from queue - if trn_id in self.pending_trns: - _, ip, d = self.pending_trns.pop(trn_id) - if ip is NAME_QUERY: - # decode as query packet - trn_id, ret = self.decodeIPQueryPacket(data) - d.callback(ret) - - def write(self, data, ip, port): - # We don't use the transport.write method directly as it keeps raising DeprecationWarning for ip='' - self.transport.getHandle().sendto(data, ( ip, port )) - - def queryName(self, name, ip = '', port = 137, timeout = 30): - """ - Send a query on the network and hopes that if machine matching the *name* will reply with its IP address. - - :param string ip: If the NBNSProtocol instance was instianted with broadcast=True, then this parameter can be an empty string. We will leave it to the OS to determine an appropriate broadcast address. - If the NBNSProtocol instance was instianted with broadcast=False, then you should provide a target IP to send the query. - :param integer port: The NetBIOS-NS port (IANA standard defines this port to be 137). You should not touch this parameter unless you know what you are doing. - :param integer/float timeout: Number of seconds to wait for a reply, after which the returned Deferred instance will be called with a NetBIOSTimeout exception. - :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a list of IP addresses in dotted notation (aaa.bbb.ccc.ddd). - On timeout, the errback function will be called with a Failure instance wrapping around a NetBIOSTimeout exception - """ - trn_id = random.randint(1, 0xFFFF) - while True: - if not self.pending_trns.has_key(trn_id): - break - else: - trn_id = (trn_id + 1) & 0xFFFF - - data = self.prepareNameQuery(trn_id, name) - if self.broadcast and not ip: - ip = '' - elif not ip: - self.log.warning('queryName: ip parameter is empty. OS might not transmit this query to the network') - - self.write(data, ip, port) - - d = defer.Deferred() - self.pending_trns[trn_id] = ( time.time()+timeout, name, d ) - return d - - def queryIPForName(self, ip, port = 137, timeout = 30): - """ - Send a query to the machine with *ip* and hopes that the machine will reply back with its name. - - The implementation of this function is contributed by Jason Anderson. - - :param string ip: If the NBNSProtocol instance was instianted with broadcast=True, then this parameter can be an empty string. We will leave it to the OS to determine an appropriate broadcast address. - If the NBNSProtocol instance was instianted with broadcast=False, then you should provide a target IP to send the query. - :param integer port: The NetBIOS-NS port (IANA standard defines this port to be 137). You should not touch this parameter unless you know what you are doing. - :param integer/float timeout: Number of seconds to wait for a reply, after which the method will return None - :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a list of names of the machine at *ip*. - On timeout, the errback function will be called with a Failure instance wrapping around a NetBIOSTimeout exception - """ - trn_id = random.randint(1, 0xFFFF) - while True: - if not self.pending_trns.has_key(trn_id): - break - else: - trn_id = (trn_id + 1) & 0xFFFF - - data = self.prepareNetNameQuery(trn_id) - self.write(data, ip, port) - - d = defer.Deferred() - d2 = defer.Deferred() - d2.addErrback(d.errback) - - def stripCode(ret): - if ret is not None: # got valid response. Somehow the callback is also called when there is an error. - d.callback(map(lambda s: s[0], filter(lambda s: s[1] == TYPE_SERVER, ret))) - - d2.addCallback(stripCode) - self.pending_trns[trn_id] = ( time.time()+timeout, NAME_QUERY, d2 ) - return d - - def stopProtocol(self): - DatagramProtocol.stopProtocol(self) - - def cleanupPendingTrns(self): - now = time.time() - - # reply should have been received in the past - expired = filter(lambda (trn_id, (expiry_time, name, d)): expiry_time < now, self.pending_trns.iteritems()) - - # remove expired items from dict + call errback - def expire_item(item): - trn_id, (expiry_time, name, d) = item - - del self.pending_trns[trn_id] - try: - d.errback(NetBIOSTimeout(name)) - except: pass - - map(expire_item, expired) - - if self.transport: - reactor.callLater(1, self.cleanupPendingTrns) + +import os, logging, random, socket, time +from twisted.internet import reactor, defer +from twisted.internet.protocol import DatagramProtocol +from nmb_constants import TYPE_SERVER +from base import NBNS + +IP_QUERY, NAME_QUERY = range(2) + +class NetBIOSTimeout(Exception): + """Raised in NBNSProtocol via Deferred.errback method when queryName method has timeout waiting for reply""" + pass + +class NBNSProtocol(DatagramProtocol, NBNS): + + log = logging.getLogger('NMB.NBNSProtocol') + + def __init__(self, broadcast = True, listen_port = 0): + """ + Instantiate a NBNSProtocol instance. + + This automatically calls reactor.listenUDP method to start listening for incoming packets, so you **must not** call the listenUDP method again. + + :param boolean broadcast: A boolean flag to indicate if we should setup the listening UDP port in broadcast mode + :param integer listen_port: Specifies the UDP port number to bind to for listening. If zero, OS will automatically select a free port number. + """ + self.broadcast = broadcast + self.pending_trns = { } # TRN ID -> ( expiry_time, name, Deferred instance ) + self.transport = reactor.listenUDP(listen_port, self) + if self.broadcast: + self.transport.getHandle().setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + reactor.callLater(1, self.cleanupPendingTrns) + + def datagramReceived(self, data, from_info): + host, port = from_info + trn_id, ret = self.decodePacket(data) + + # pending transaction exists for trn_id - handle it and remove from queue + if trn_id in self.pending_trns: + _, ip, d = self.pending_trns.pop(trn_id) + if ip is NAME_QUERY: + # decode as query packet + trn_id, ret = self.decodeIPQueryPacket(data) + d.callback(ret) + + def write(self, data, ip, port): + # We don't use the transport.write method directly as it keeps raising DeprecationWarning for ip='' + self.transport.getHandle().sendto(data, ( ip, port )) + + def queryName(self, name, ip = '', port = 137, timeout = 30): + """ + Send a query on the network and hopes that if machine matching the *name* will reply with its IP address. + + :param string ip: If the NBNSProtocol instance was instianted with broadcast=True, then this parameter can be an empty string. We will leave it to the OS to determine an appropriate broadcast address. + If the NBNSProtocol instance was instianted with broadcast=False, then you should provide a target IP to send the query. + :param integer port: The NetBIOS-NS port (IANA standard defines this port to be 137). You should not touch this parameter unless you know what you are doing. + :param integer/float timeout: Number of seconds to wait for a reply, after which the returned Deferred instance will be called with a NetBIOSTimeout exception. + :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a list of IP addresses in dotted notation (aaa.bbb.ccc.ddd). + On timeout, the errback function will be called with a Failure instance wrapping around a NetBIOSTimeout exception + """ + trn_id = random.randint(1, 0xFFFF) + while True: + if not self.pending_trns.has_key(trn_id): + break + else: + trn_id = (trn_id + 1) & 0xFFFF + + data = self.prepareNameQuery(trn_id, name) + if self.broadcast and not ip: + ip = '' + elif not ip: + self.log.warning('queryName: ip parameter is empty. OS might not transmit this query to the network') + + self.write(data, ip, port) + + d = defer.Deferred() + self.pending_trns[trn_id] = ( time.time()+timeout, name, d ) + return d + + def queryIPForName(self, ip, port = 137, timeout = 30): + """ + Send a query to the machine with *ip* and hopes that the machine will reply back with its name. + + The implementation of this function is contributed by Jason Anderson. + + :param string ip: If the NBNSProtocol instance was instianted with broadcast=True, then this parameter can be an empty string. We will leave it to the OS to determine an appropriate broadcast address. + If the NBNSProtocol instance was instianted with broadcast=False, then you should provide a target IP to send the query. + :param integer port: The NetBIOS-NS port (IANA standard defines this port to be 137). You should not touch this parameter unless you know what you are doing. + :param integer/float timeout: Number of seconds to wait for a reply, after which the method will return None + :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a list of names of the machine at *ip*. + On timeout, the errback function will be called with a Failure instance wrapping around a NetBIOSTimeout exception + """ + trn_id = random.randint(1, 0xFFFF) + while True: + if not self.pending_trns.has_key(trn_id): + break + else: + trn_id = (trn_id + 1) & 0xFFFF + + data = self.prepareNetNameQuery(trn_id) + self.write(data, ip, port) + + d = defer.Deferred() + d2 = defer.Deferred() + d2.addErrback(d.errback) + + def stripCode(ret): + if ret is not None: # got valid response. Somehow the callback is also called when there is an error. + d.callback(map(lambda s: s[0], filter(lambda s: s[1] == TYPE_SERVER, ret))) + + d2.addCallback(stripCode) + self.pending_trns[trn_id] = ( time.time()+timeout, NAME_QUERY, d2 ) + return d + + def stopProtocol(self): + DatagramProtocol.stopProtocol(self) + + def cleanupPendingTrns(self): + now = time.time() + + # reply should have been received in the past + expired = filter(lambda (trn_id, (expiry_time, name, d)): expiry_time < now, self.pending_trns.iteritems()) + + # remove expired items from dict + call errback + def expire_item(item): + trn_id, (expiry_time, name, d) = item + + del self.pending_trns[trn_id] + try: + d.errback(NetBIOSTimeout(name)) + except: pass + + map(expire_item, expired) + + if self.transport: + reactor.callLater(1, self.cleanupPendingTrns) diff --git a/plugin.video.alfa/lib/sambatools/nmb/base.py b/plugin.video.alfa/lib/sambatools/nmb/base.py index 9526c0a8..0bbef825 100755 --- a/plugin.video.alfa/lib/sambatools/nmb/base.py +++ b/plugin.video.alfa/lib/sambatools/nmb/base.py @@ -1,179 +1,181 @@ -import logging - -from nmb_constants import * -from nmb_structs import * -from utils import encode_name - - -class NMBSession: - - log = logging.getLogger('NMB.NMBSession') - - def __init__(self, my_name, remote_name, host_type = TYPE_SERVER, is_direct_tcp = False): - self.my_name = my_name.upper() - self.remote_name = remote_name.upper() - self.host_type = host_type - self.data_buf = '' - - if is_direct_tcp: - self.data_nmb = DirectTCPSessionMessage() - self.sendNMBPacket = self._sendNMBPacket_DirectTCP - else: - self.data_nmb = NMBSessionMessage() - self.sendNMBPacket = self._sendNMBPacket_NetBIOS - - # - # Overridden Methods - # - - def write(self, data): - raise NotImplementedError - - def onNMBSessionMessage(self, flags, data): - pass - - def onNMBSessionOK(self): - pass - - def onNMBSessionFailed(self): - pass - - # - # Public Methods - # - - def feedData(self, data): - self.data_buf = self.data_buf + data - - offset = 0 - while True: - length = self.data_nmb.decode(self.data_buf, offset) - if length == 0: - break - elif length > 0: - offset += length - self._processNMBSessionPacket(self.data_nmb) - else: - raise NMBError - - if offset > 0: - self.data_buf = self.data_buf[offset:] - - def sendNMBMessage(self, data): - self.sendNMBPacket(SESSION_MESSAGE, data) - - def requestNMBSession(self): - my_name_encoded = encode_name(self.my_name, TYPE_WORKSTATION) - remote_name_encoded = encode_name(self.remote_name, self.host_type) - self.sendNMBPacket(SESSION_REQUEST, remote_name_encoded + my_name_encoded) - - # - # Protected Methods - # - - def _processNMBSessionPacket(self, packet): - if packet.type == SESSION_MESSAGE: - self.onNMBSessionMessage(packet.flags, packet.data) - elif packet.type == POSITIVE_SESSION_RESPONSE: - self.onNMBSessionOK() - elif packet.type == NEGATIVE_SESSION_RESPONSE: - self.onNMBSessionFailed() - else: - self.log.warning('Unrecognized NMB session type: 0x%02x', packet.type) - - def _sendNMBPacket_NetBIOS(self, packet_type, data): - length = len(data) - assert length <= 0x01FFFF - flags = 0 - if length > 0xFFFF: - flags |= 0x01 - length &= 0xFFFF - self.write(struct.pack('>BBH', packet_type, flags, length) + data) - - def _sendNMBPacket_DirectTCP(self, packet_type, data): - length = len(data) - assert length <= 0x00FFFFFF - self.write(struct.pack('>I', length) + data) - - -class NBNS: - - log = logging.getLogger('NMB.NBNS') - - HEADER_STRUCT_FORMAT = '>HHHHHH' - HEADER_STRUCT_SIZE = struct.calcsize(HEADER_STRUCT_FORMAT) - - def write(self, data, ip, port): - raise NotImplementedError - - def decodePacket(self, data): - if len(data) < self.HEADER_STRUCT_SIZE: - raise Exception - - trn_id, code, question_count, answer_count, authority_count, additional_count = struct.unpack(self.HEADER_STRUCT_FORMAT, data[:self.HEADER_STRUCT_SIZE]) - - is_response = bool((code >> 15) & 0x01) - opcode = (code >> 11) & 0x0F - flags = (code >> 4) & 0x7F - rcode = code & 0x0F - - if opcode == 0x0000 and is_response: - name_len = ord(data[self.HEADER_STRUCT_SIZE]) - offset = self.HEADER_STRUCT_SIZE+2+name_len+8 # constant 2 for the padding bytes before/after the Name and constant 8 for the Type, Class and TTL fields in the Answer section after the Name - record_count = (struct.unpack('>H', data[offset:offset+2])[0]) / 6 - - offset += 4 # Constant 4 for the Data Length and Flags field - ret = [ ] - for i in range(0, record_count): - ret.append('%d.%d.%d.%d' % struct.unpack('4B', (data[offset:offset + 4]))) - offset += 6 - return trn_id, ret - else: - return trn_id, None - - - def prepareNameQuery(self, trn_id, name, is_broadcast = True): - header = struct.pack(self.HEADER_STRUCT_FORMAT, - trn_id, (is_broadcast and 0x0110) or 0x0100, 1, 0, 0, 0) - payload = encode_name(name, 0x20) + '\x00\x20\x00\x01' - - return header + payload - - # - # Contributed by Jason Anderson - # - def decodeIPQueryPacket(self, data): - if len(data) < self.HEADER_STRUCT_SIZE: - raise Exception - - trn_id, code, question_count, answer_count, authority_count, additional_count = struct.unpack(self.HEADER_STRUCT_FORMAT, data[:self.HEADER_STRUCT_SIZE]) - - is_response = bool((code >> 15) & 0x01) - opcode = (code >> 11) & 0x0F - flags = (code >> 4) & 0x7F - rcode = code & 0x0F - numnames = struct.unpack('B', data[self.HEADER_STRUCT_SIZE + 44])[0] - - if numnames > 0: - ret = [ ] - offset = self.HEADER_STRUCT_SIZE + 45 - - for i in range(0, numnames): - mynme = data[offset:offset + 15] - mynme = mynme.strip() - ret.append(( mynme, ord(data[offset+15]) )) - offset += 18 - - return trn_id, ret - else: - return trn_id, None - - # - # Contributed by Jason Anderson - # - def prepareNetNameQuery(self, trn_id, is_broadcast = True): - header = struct.pack(self.HEADER_STRUCT_FORMAT, - trn_id, (is_broadcast and 0x0010) or 0x0000, 1, 0, 0, 0) - payload = encode_name('*', 0) + '\x00\x21\x00\x01' - - return header + payload + +import struct, logging, random +from nmb_constants import * +from nmb_structs import * +from utils import encode_name + +class NMBSession: + + log = logging.getLogger('NMB.NMBSession') + + def __init__(self, my_name, remote_name, host_type = TYPE_SERVER, is_direct_tcp = False): + self.my_name = my_name.upper() + self.remote_name = remote_name.upper() + self.host_type = host_type + self.data_buf = '' + + if is_direct_tcp: + self.data_nmb = DirectTCPSessionMessage() + self.sendNMBPacket = self._sendNMBPacket_DirectTCP + else: + self.data_nmb = NMBSessionMessage() + self.sendNMBPacket = self._sendNMBPacket_NetBIOS + + # + # Overridden Methods + # + + def write(self, data): + raise NotImplementedError + + def onNMBSessionMessage(self, flags, data): + pass + + def onNMBSessionOK(self): + pass + + def onNMBSessionFailed(self): + pass + + # + # Public Methods + # + + def feedData(self, data): + self.data_buf = self.data_buf + data + + offset = 0 + while True: + length = self.data_nmb.decode(self.data_buf, offset) + if length == 0: + break + elif length > 0: + offset += length + self._processNMBSessionPacket(self.data_nmb) + else: + raise NMBError + + if offset > 0: + self.data_buf = self.data_buf[offset:] + + def sendNMBMessage(self, data): + self.sendNMBPacket(SESSION_MESSAGE, data) + + def requestNMBSession(self): + my_name_encoded = encode_name(self.my_name, TYPE_WORKSTATION) + remote_name_encoded = encode_name(self.remote_name, self.host_type) + self.sendNMBPacket(SESSION_REQUEST, remote_name_encoded + my_name_encoded) + + # + # Protected Methods + # + + def _processNMBSessionPacket(self, packet): + if packet.type == SESSION_MESSAGE: + self.onNMBSessionMessage(packet.flags, packet.data) + elif packet.type == POSITIVE_SESSION_RESPONSE: + self.onNMBSessionOK() + elif packet.type == NEGATIVE_SESSION_RESPONSE: + self.onNMBSessionFailed() + elif packet.type == SESSION_KEEPALIVE: + # Discard keepalive packets - [RFC1002]: 5.2.2.1 + pass + else: + self.log.warning('Unrecognized NMB session type: 0x%02x', packet.type) + + def _sendNMBPacket_NetBIOS(self, packet_type, data): + length = len(data) + assert length <= 0x01FFFF + flags = 0 + if length > 0xFFFF: + flags |= 0x01 + length &= 0xFFFF + self.write(struct.pack('>BBH', packet_type, flags, length) + data) + + def _sendNMBPacket_DirectTCP(self, packet_type, data): + length = len(data) + assert length <= 0x00FFFFFF + self.write(struct.pack('>I', length) + data) + + +class NBNS: + + log = logging.getLogger('NMB.NBNS') + + HEADER_STRUCT_FORMAT = '>HHHHHH' + HEADER_STRUCT_SIZE = struct.calcsize(HEADER_STRUCT_FORMAT) + + def write(self, data, ip, port): + raise NotImplementedError + + def decodePacket(self, data): + if len(data) < self.HEADER_STRUCT_SIZE: + raise Exception + + trn_id, code, question_count, answer_count, authority_count, additional_count = struct.unpack(self.HEADER_STRUCT_FORMAT, data[:self.HEADER_STRUCT_SIZE]) + + is_response = bool((code >> 15) & 0x01) + opcode = (code >> 11) & 0x0F + flags = (code >> 4) & 0x7F + rcode = code & 0x0F + + if opcode == 0x0000 and is_response: + name_len = ord(data[self.HEADER_STRUCT_SIZE]) + offset = self.HEADER_STRUCT_SIZE+2+name_len+8 # constant 2 for the padding bytes before/after the Name and constant 8 for the Type, Class and TTL fields in the Answer section after the Name + record_count = (struct.unpack('>H', data[offset:offset+2])[0]) / 6 + + offset += 4 # Constant 4 for the Data Length and Flags field + ret = [ ] + for i in range(0, record_count): + ret.append('%d.%d.%d.%d' % struct.unpack('4B', (data[offset:offset + 4]))) + offset += 6 + return trn_id, ret + else: + return trn_id, None + + + def prepareNameQuery(self, trn_id, name, is_broadcast = True): + header = struct.pack(self.HEADER_STRUCT_FORMAT, + trn_id, (is_broadcast and 0x0110) or 0x0100, 1, 0, 0, 0) + payload = encode_name(name, 0x20) + '\x00\x20\x00\x01' + + return header + payload + + # + # Contributed by Jason Anderson + # + def decodeIPQueryPacket(self, data): + if len(data) < self.HEADER_STRUCT_SIZE: + raise Exception + + trn_id, code, question_count, answer_count, authority_count, additional_count = struct.unpack(self.HEADER_STRUCT_FORMAT, data[:self.HEADER_STRUCT_SIZE]) + + is_response = bool((code >> 15) & 0x01) + opcode = (code >> 11) & 0x0F + flags = (code >> 4) & 0x7F + rcode = code & 0x0F + numnames = struct.unpack('B', data[self.HEADER_STRUCT_SIZE + 44])[0] + + if numnames > 0: + ret = [ ] + offset = self.HEADER_STRUCT_SIZE + 45 + + for i in range(0, numnames): + mynme = data[offset:offset + 15] + mynme = mynme.strip() + ret.append(( mynme, ord(data[offset+15]) )) + offset += 18 + + return trn_id, ret + else: + return trn_id, None + + # + # Contributed by Jason Anderson + # + def prepareNetNameQuery(self, trn_id, is_broadcast = True): + header = struct.pack(self.HEADER_STRUCT_FORMAT, + trn_id, (is_broadcast and 0x0010) or 0x0000, 1, 0, 0, 0) + payload = encode_name('*', 0) + '\x00\x21\x00\x01' + + return header + payload diff --git a/plugin.video.alfa/lib/sambatools/nmb/nmb_constants.py b/plugin.video.alfa/lib/sambatools/nmb/nmb_constants.py index 1c576543..fcf6007a 100755 --- a/plugin.video.alfa/lib/sambatools/nmb/nmb_constants.py +++ b/plugin.video.alfa/lib/sambatools/nmb/nmb_constants.py @@ -1,38 +1,38 @@ - -# Default port for NetBIOS name service -NETBIOS_NS_PORT = 137 - -# Default port for NetBIOS session service -NETBIOS_SESSION_PORT = 139 - -# Owner Node Type Constants -NODE_B = 0x00 -NODE_P = 0x01 -NODE_M = 0x10 -NODE_RESERVED = 0x11 - -# Name Type Constants -TYPE_UNKNOWN = 0x01 -TYPE_WORKSTATION = 0x00 -TYPE_CLIENT = 0x03 -TYPE_SERVER = 0x20 -TYPE_DOMAIN_MASTER = 0x1B -TYPE_MASTER_BROWSER = 0x1D -TYPE_BROWSER = 0x1E - -TYPE_NAMES = { TYPE_UNKNOWN: 'Unknown', - TYPE_WORKSTATION: 'Workstation', - TYPE_CLIENT: 'Client', - TYPE_SERVER: 'Server', - TYPE_MASTER_BROWSER: 'Master Browser', - TYPE_BROWSER: 'Browser Server', - TYPE_DOMAIN_MASTER: 'Domain Master' - } - -# Values for Session Packet Type field in Session Packets -SESSION_MESSAGE = 0x00 -SESSION_REQUEST = 0x81 -POSITIVE_SESSION_RESPONSE = 0x82 -NEGATIVE_SESSION_RESPONSE = 0x83 -REGTARGET_SESSION_RESPONSE = 0x84 -SESSION_KEEPALIVE = 0x85 + +# Default port for NetBIOS name service +NETBIOS_NS_PORT = 137 + +# Default port for NetBIOS session service +NETBIOS_SESSION_PORT = 139 + +# Owner Node Type Constants +NODE_B = 0x00 +NODE_P = 0x01 +NODE_M = 0x10 +NODE_RESERVED = 0x11 + +# Name Type Constants +TYPE_UNKNOWN = 0x01 +TYPE_WORKSTATION = 0x00 +TYPE_CLIENT = 0x03 +TYPE_SERVER = 0x20 +TYPE_DOMAIN_MASTER = 0x1B +TYPE_MASTER_BROWSER = 0x1D +TYPE_BROWSER = 0x1E + +TYPE_NAMES = { TYPE_UNKNOWN: 'Unknown', + TYPE_WORKSTATION: 'Workstation', + TYPE_CLIENT: 'Client', + TYPE_SERVER: 'Server', + TYPE_MASTER_BROWSER: 'Master Browser', + TYPE_BROWSER: 'Browser Server', + TYPE_DOMAIN_MASTER: 'Domain Master' + } + +# Values for Session Packet Type field in Session Packets +SESSION_MESSAGE = 0x00 +SESSION_REQUEST = 0x81 +POSITIVE_SESSION_RESPONSE = 0x82 +NEGATIVE_SESSION_RESPONSE = 0x83 +REGTARGET_SESSION_RESPONSE = 0x84 +SESSION_KEEPALIVE = 0x85 diff --git a/plugin.video.alfa/lib/sambatools/nmb/nmb_structs.py b/plugin.video.alfa/lib/sambatools/nmb/nmb_structs.py index e78b87f4..71e603c3 100755 --- a/plugin.video.alfa/lib/sambatools/nmb/nmb_structs.py +++ b/plugin.video.alfa/lib/sambatools/nmb/nmb_structs.py @@ -1,69 +1,69 @@ - -import struct - -class NMBError(Exception): pass - - -class NotConnectedError(NMBError): - """ - Raisd when the underlying NMB connection has been disconnected or not connected yet - """ - pass - - -class NMBSessionMessage: - - HEADER_STRUCT_FORMAT = '>BBH' - HEADER_STRUCT_SIZE = struct.calcsize(HEADER_STRUCT_FORMAT) - - def __init__(self): - self.reset() - - def reset(self): - self.type = 0 - self.flags = 0 - self.data = '' - - def decode(self, data, offset): - data_len = len(data) - - if data_len < offset + self.HEADER_STRUCT_SIZE: - # Not enough data for decoding - return 0 - - self.reset() - self.type, self.flags, length = struct.unpack(self.HEADER_STRUCT_FORMAT, data[offset:offset+self.HEADER_STRUCT_SIZE]) - - if self.flags & 0x01: - length |= 0x010000 - - if data_len < offset + self.HEADER_STRUCT_SIZE + length: - return 0 - - self.data = data[offset+self.HEADER_STRUCT_SIZE:offset+self.HEADER_STRUCT_SIZE+length] - return self.HEADER_STRUCT_SIZE + length - - -class DirectTCPSessionMessage(NMBSessionMessage): - - HEADER_STRUCT_FORMAT = '>I' - HEADER_STRUCT_SIZE = struct.calcsize(HEADER_STRUCT_FORMAT) - - def decode(self, data, offset): - data_len = len(data) - - if data_len < offset + self.HEADER_STRUCT_SIZE: - # Not enough data for decoding - return 0 - - self.reset() - length = struct.unpack(self.HEADER_STRUCT_FORMAT, data[offset:offset+self.HEADER_STRUCT_SIZE])[0] - - if length >> 24 != 0: - raise NMBError("Invalid protocol header for Direct TCP session message") - - if data_len < offset + self.HEADER_STRUCT_SIZE + length: - return 0 - - self.data = data[offset+self.HEADER_STRUCT_SIZE:offset+self.HEADER_STRUCT_SIZE+length] - return self.HEADER_STRUCT_SIZE + length + +import struct + +class NMBError(Exception): pass + + +class NotConnectedError(NMBError): + """ + Raisd when the underlying NMB connection has been disconnected or not connected yet + """ + pass + + +class NMBSessionMessage: + + HEADER_STRUCT_FORMAT = '>BBH' + HEADER_STRUCT_SIZE = struct.calcsize(HEADER_STRUCT_FORMAT) + + def __init__(self): + self.reset() + + def reset(self): + self.type = 0 + self.flags = 0 + self.data = '' + + def decode(self, data, offset): + data_len = len(data) + + if data_len < offset + self.HEADER_STRUCT_SIZE: + # Not enough data for decoding + return 0 + + self.reset() + self.type, self.flags, length = struct.unpack(self.HEADER_STRUCT_FORMAT, data[offset:offset+self.HEADER_STRUCT_SIZE]) + + if self.flags & 0x01: + length |= 0x010000 + + if data_len < offset + self.HEADER_STRUCT_SIZE + length: + return 0 + + self.data = data[offset+self.HEADER_STRUCT_SIZE:offset+self.HEADER_STRUCT_SIZE+length] + return self.HEADER_STRUCT_SIZE + length + + +class DirectTCPSessionMessage(NMBSessionMessage): + + HEADER_STRUCT_FORMAT = '>I' + HEADER_STRUCT_SIZE = struct.calcsize(HEADER_STRUCT_FORMAT) + + def decode(self, data, offset): + data_len = len(data) + + if data_len < offset + self.HEADER_STRUCT_SIZE: + # Not enough data for decoding + return 0 + + self.reset() + length = struct.unpack(self.HEADER_STRUCT_FORMAT, data[offset:offset+self.HEADER_STRUCT_SIZE])[0] + + if length >> 24 != 0: + raise NMBError("Invalid protocol header for Direct TCP session message") + + if data_len < offset + self.HEADER_STRUCT_SIZE + length: + return 0 + + self.data = data[offset+self.HEADER_STRUCT_SIZE:offset+self.HEADER_STRUCT_SIZE+length] + return self.HEADER_STRUCT_SIZE + length diff --git a/plugin.video.alfa/lib/sambatools/nmb/utils.py b/plugin.video.alfa/lib/sambatools/nmb/utils.py index 1de7b1fe..45d625c2 100755 --- a/plugin.video.alfa/lib/sambatools/nmb/utils.py +++ b/plugin.video.alfa/lib/sambatools/nmb/utils.py @@ -1,50 +1,50 @@ -import re -import string - - -def encode_name(name, type, scope = None): - """ - Perform first and second level encoding of name as specified in RFC 1001 (Section 4) - """ - if name == '*': - name = name + '\0' * 15 - elif len(name) > 15: - name = name[:15] + chr(type) - else: - name = string.ljust(name, 15) + chr(type) - - def _do_first_level_encoding(m): - s = ord(m.group(0)) - return string.uppercase[s >> 4] + string.uppercase[s & 0x0f] - - encoded_name = chr(len(name) * 2) + re.sub('.', _do_first_level_encoding, name) - if scope: - encoded_scope = '' - for s in string.split(scope, '.'): - encoded_scope = encoded_scope + chr(len(s)) + s - return encoded_name + encoded_scope + '\0' - else: - return encoded_name + '\0' - - -def decode_name(name): - name_length = ord(name[0]) - assert name_length == 32 - - def _do_first_level_decoding(m): - s = m.group(0) - return chr(((ord(s[0]) - ord('A')) << 4) | (ord(s[1]) - ord('A'))) - - decoded_name = re.sub('..', _do_first_level_decoding, name[1:33]) - if name[33] == '\0': - return 34, decoded_name, '' - else: - decoded_domain = '' - offset = 34 - while 1: - domain_length = ord(name[offset]) - if domain_length == 0: - break - decoded_domain = '.' + name[offset:offset + domain_length] - offset = offset + domain_length - return offset + 1, decoded_name, decoded_domain + +import string, re + + +def encode_name(name, type, scope = None): + """ + Perform first and second level encoding of name as specified in RFC 1001 (Section 4) + """ + if name == '*': + name = name + '\0' * 15 + elif len(name) > 15: + name = name[:15] + chr(type) + else: + name = string.ljust(name, 15) + chr(type) + + def _do_first_level_encoding(m): + s = ord(m.group(0)) + return string.uppercase[s >> 4] + string.uppercase[s & 0x0f] + + encoded_name = chr(len(name) * 2) + re.sub('.', _do_first_level_encoding, name) + if scope: + encoded_scope = '' + for s in string.split(scope, '.'): + encoded_scope = encoded_scope + chr(len(s)) + s + return encoded_name + encoded_scope + '\0' + else: + return encoded_name + '\0' + + +def decode_name(name): + name_length = ord(name[0]) + assert name_length == 32 + + def _do_first_level_decoding(m): + s = m.group(0) + return chr(((ord(s[0]) - ord('A')) << 4) | (ord(s[1]) - ord('A'))) + + decoded_name = re.sub('..', _do_first_level_decoding, name[1:33]) + if name[33] == '\0': + return 34, decoded_name, '' + else: + decoded_domain = '' + offset = 34 + while 1: + domain_length = ord(name[offset]) + if domain_length == 0: + break + decoded_domain = '.' + name[offset:offset + domain_length] + offset = offset + domain_length + return offset + 1, decoded_name, decoded_domain diff --git a/plugin.video.alfa/lib/sambatools/smb/SMBConnection.py b/plugin.video.alfa/lib/sambatools/smb/SMBConnection.py index 7940d7f2..603c48df 100755 --- a/plugin.video.alfa/lib/sambatools/smb/SMBConnection.py +++ b/plugin.video.alfa/lib/sambatools/smb/SMBConnection.py @@ -1,582 +1,630 @@ -import select -import socket -import types - -from base import SMB, NotConnectedError, SMBTimeout -from smb_structs import * - - -class SMBConnection(SMB): - - log = logging.getLogger('SMB.SMBConnection') - - #: SMB messages will never be signed regardless of remote server's configurations; access errors will occur if the remote server requires signing. - SIGN_NEVER = 0 - #: SMB messages will be signed when remote server supports signing but not requires signing. - SIGN_WHEN_SUPPORTED = 1 - #: SMB messages will only be signed when remote server requires signing. - SIGN_WHEN_REQUIRED = 2 - - def __init__(self, username, password, my_name, remote_name, domain = '', use_ntlm_v2 = True, sign_options = SIGN_WHEN_REQUIRED, is_direct_tcp = False): - """ - Create a new SMBConnection instance. - - *username* and *password* are the user credentials required to authenticate the underlying SMB connection with the remote server. - File operations can only be proceeded after the connection has been authenticated successfully. - - Note that you need to call *connect* method to actually establish the SMB connection to the remote server and perform authentication. - - The default TCP port for most SMB/CIFS servers using NetBIOS over TCP/IP is 139. - Some newer server installations might also support Direct hosting of SMB over TCP/IP; for these servers, the default TCP port is 445. - - :param string my_name: The local NetBIOS machine name that will identify where this connection is originating from. - You can freely choose a name as long as it contains a maximum of 15 alphanumeric characters and does not contain spaces and any of ``\/:*?";|+`` - :param string remote_name: The NetBIOS machine name of the remote server. - On windows, you can find out the machine name by right-clicking on the "My Computer" and selecting "Properties". - This parameter must be the same as what has been configured on the remote server, or else the connection will be rejected. - :param string domain: The network domain. On windows, it is known as the workgroup. Usually, it is safe to leave this parameter as an empty string. - :param boolean use_ntlm_v2: Indicates whether pysmb should be NTLMv1 or NTLMv2 authentication algorithm for authentication. - The choice of NTLMv1 and NTLMv2 is configured on the remote server, and there is no mechanism to auto-detect which algorithm has been configured. - Hence, we can only "guess" or try both algorithms. - On Sambda, Windows Vista and Windows 7, NTLMv2 is enabled by default. On Windows XP, we can use NTLMv1 before NTLMv2. - :param int sign_options: Determines whether SMB messages will be signed. Default is *SIGN_WHEN_REQUIRED*. - If *SIGN_WHEN_REQUIRED* (value=2), SMB messages will only be signed when remote server requires signing. - If *SIGN_WHEN_SUPPORTED* (value=1), SMB messages will be signed when remote server supports signing but not requires signing. - If *SIGN_NEVER* (value=0), SMB messages will never be signed regardless of remote server's configurations; access errors will occur if the remote server requires signing. - :param boolean is_direct_tcp: Controls whether the NetBIOS over TCP/IP (is_direct_tcp=False) or the newer Direct hosting of SMB over TCP/IP (is_direct_tcp=True) will be used for the communication. - The default parameter is False which will use NetBIOS over TCP/IP for wider compatibility (TCP port: 139). - """ - SMB.__init__(self, username, password, my_name, remote_name, domain, use_ntlm_v2, sign_options, is_direct_tcp) - self.sock = None - self.auth_result = None - self.is_busy = False - self.is_direct_tcp = is_direct_tcp - - # - # SMB (and its superclass) Methods - # - - def onAuthOK(self): - self.auth_result = True - - def onAuthFailed(self): - self.auth_result = False - - def write(self, data): - assert self.sock - data_len = len(data) - total_sent = 0 - while total_sent < data_len: - sent = self.sock.send(data[total_sent:]) - if sent == 0: - raise NotConnectedError('Server disconnected') - total_sent = total_sent + sent - - # - # Misc Properties - # - - @property - def isUsingSMB2(self): - """A convenient property to return True if the underlying SMB connection is using SMB2 protocol.""" - return self.is_using_smb2 - - - # - # Public Methods - # - - def connect(self, ip, port = 139, sock_family = socket.AF_INET, timeout = 60): - """ - Establish the SMB connection to the remote SMB/CIFS server. - - You must call this method before attempting any of the file operations with the remote server. - This method will block until the SMB connection has attempted at least one authentication. - - :return: A boolean value indicating the result of the authentication atttempt: True if authentication is successful; False, if otherwise. - """ - if self.sock: - self.sock.close() - - self.auth_result = None - self.sock = socket.socket(sock_family) - self.sock.settimeout(timeout) - self.sock.connect(( ip, port )) - - self.is_busy = True - try: - if not self.is_direct_tcp: - self.requestNMBSession() - else: - self.onNMBSessionOK() - while self.auth_result is None: - self._pollForNetBIOSPacket(timeout) - finally: - self.is_busy = False - - return self.auth_result - - def close(self): - """ - Terminate the SMB connection (if it has been started) and release any sources held by the underlying socket. - """ - if self.sock: - self.sock.close() - self.sock = None - - def listShares(self, timeout = 30): - """ - Retrieve a list of shared resources on remote server. - - :return: A list of :doc:`smb.base.SharedDevice` instances describing the shared resource - """ - if not self.sock: - raise NotConnectedError('Not connected to server') - - results = [ ] - - def cb(entries): - self.is_busy = False - results.extend(entries) - - def eb(failure): - self.is_busy = False - raise failure - - self.is_busy = True - try: - self._listShares(cb, eb, timeout) - while self.is_busy: - self._pollForNetBIOSPacket(timeout) - finally: - self.is_busy = False - - return results - - def listPath(self, service_name, path, - search = SMB_FILE_ATTRIBUTE_READONLY | SMB_FILE_ATTRIBUTE_HIDDEN | SMB_FILE_ATTRIBUTE_SYSTEM | SMB_FILE_ATTRIBUTE_DIRECTORY | SMB_FILE_ATTRIBUTE_ARCHIVE, - pattern = '*', timeout = 30): - """ - Retrieve a directory listing of files/folders at *path* - - :param string/unicode service_name: the name of the shared folder for the *path* - :param string/unicode path: path relative to the *service_name* where we are interested to learn about its files/sub-folders. - :param integer search: integer value made up from a bitwise-OR of *SMB_FILE_ATTRIBUTE_xxx* bits (see smb_constants.py). - The default *search* value will query for all read-only, hidden, system, archive files and directories. - :param string/unicode pattern: the filter to apply to the results before returning to the client. - :return: A list of :doc:`smb.base.SharedFile` instances. - """ - if not self.sock: - raise NotConnectedError('Not connected to server') - - results = [ ] - - def cb(entries): - self.is_busy = False - results.extend(entries) - - def eb(failure): - self.is_busy = False - raise failure - - self.is_busy = True - try: - self._listPath(service_name, path, cb, eb, search = search, pattern = pattern, timeout = timeout) - while self.is_busy: - self._pollForNetBIOSPacket(timeout) - finally: - self.is_busy = False - - return results - - def listSnapshots(self, service_name, path, timeout = 30): - """ - Retrieve a list of available snapshots (shadow copies) for *path*. - - Note that snapshot features are only supported on Windows Vista Business, Enterprise and Ultimate, and on all Windows 7 editions. - - :param string/unicode service_name: the name of the shared folder for the *path* - :param string/unicode path: path relative to the *service_name* where we are interested in the list of available snapshots - :return: A list of python *datetime.DateTime* instances in GMT/UTC time zone - """ - if not self.sock: - raise NotConnectedError('Not connected to server') - - results = [ ] - - def cb(entries): - self.is_busy = False - results.extend(entries) - - def eb(failure): - self.is_busy = False - raise failure - - self.is_busy = True - try: - self._listSnapshots(service_name, path, cb, eb, timeout = timeout) - while self.is_busy: - self._pollForNetBIOSPacket(timeout) - finally: - self.is_busy = False - - return results - - def getAttributes(self, service_name, path, timeout = 30): - """ - Retrieve information about the file at *path* on the *service_name*. - - :param string/unicode service_name: the name of the shared folder for the *path* - :param string/unicode path: Path of the file on the remote server. If the file cannot be opened for reading, an :doc:`OperationFailure` will be raised. - :return: A :doc:`smb.base.SharedFile` instance containing the attributes of the file. - """ - if not self.sock: - raise NotConnectedError('Not connected to server') - - results = [ ] - - def cb(info): - self.is_busy = False - results.append(info) - - def eb(failure): - self.is_busy = False - raise failure - - self.is_busy = True - try: - self._getAttributes(service_name, path, cb, eb, timeout) - while self.is_busy: - self._pollForNetBIOSPacket(timeout) - finally: - self.is_busy = False - - return results[0] - - def retrieveFile(self, service_name, path, file_obj, timeout = 30): - """ - Retrieve the contents of the file at *path* on the *service_name* and write these contents to the provided *file_obj*. - - Use *retrieveFileFromOffset()* method if you wish to specify the offset to read from the remote *path* and/or the number of bytes to write to the *file_obj*. - - :param string/unicode service_name: the name of the shared folder for the *path* - :param string/unicode path: Path of the file on the remote server. If the file cannot be opened for reading, an :doc:`OperationFailure` will be raised. - :param file_obj: A file-like object that has a *write* method. Data will be written continuously to *file_obj* until EOF is received from the remote service. - :return: A 2-element tuple of ( file attributes of the file on server, number of bytes written to *file_obj* ). - The file attributes is an integer value made up from a bitwise-OR of *SMB_FILE_ATTRIBUTE_xxx* bits (see smb_constants.py) - """ - return self.retrieveFileFromOffset(service_name, path, file_obj, 0L, -1L, timeout) - - def retrieveFileFromOffset(self, service_name, path, file_obj, offset = 0L, max_length = -1L, timeout = 30): - """ - Retrieve the contents of the file at *path* on the *service_name* and write these contents to the provided *file_obj*. - - :param string/unicode service_name: the name of the shared folder for the *path* - :param string/unicode path: Path of the file on the remote server. If the file cannot be opened for reading, an :doc:`OperationFailure` will be raised. - :param file_obj: A file-like object that has a *write* method. Data will be written continuously to *file_obj* up to *max_length* number of bytes. - :param integer/long offset: the offset in the remote *path* where the first byte will be read and written to *file_obj*. Must be either zero or a positive integer/long value. - :param integer/long max_length: maximum number of bytes to read from the remote *path* and write to the *file_obj*. Specify a negative value to read from *offset* to the EOF. - If zero, the method returns immediately after the file is opened successfully for reading. - :return: A 2-element tuple of ( file attributes of the file on server, number of bytes written to *file_obj* ). - The file attributes is an integer value made up from a bitwise-OR of *SMB_FILE_ATTRIBUTE_xxx* bits (see smb_constants.py) - """ - if not self.sock: - raise NotConnectedError('Not connected to server') - - results = [ ] - - def cb(r): - self.is_busy = False - results.append(r[1:]) - - def eb(failure): - self.is_busy = False - raise failure - - self.is_busy = True - try: - self._retrieveFileFromOffset(service_name, path, file_obj, cb, eb, offset, max_length, timeout = timeout) - while self.is_busy: - self._pollForNetBIOSPacket(timeout) - finally: - self.is_busy = False - - return results[0] - - def storeFile(self, service_name, path, file_obj, timeout = 30): - """ - Store the contents of the *file_obj* at *path* on the *service_name*. - If the file already exists on the remote server, it will be truncated and overwritten. - - :param string/unicode service_name: the name of the shared folder for the *path* - :param string/unicode path: Path of the file on the remote server. If the file at *path* does not exist, it will be created. Otherwise, it will be overwritten. - If the *path* refers to a folder or the file cannot be opened for writing, an :doc:`OperationFailure` will be raised. - :param file_obj: A file-like object that has a *read* method. Data will read continuously from *file_obj* until EOF. - :return: Number of bytes uploaded - """ - return self.storeFileFromOffset(service_name, path, file_obj, 0L, True, timeout) - - def storeFileFromOffset(self, service_name, path, file_obj, offset = 0L, truncate = False, timeout = 30): - """ - Store the contents of the *file_obj* at *path* on the *service_name*. - - :param string/unicode service_name: the name of the shared folder for the *path* - :param string/unicode path: Path of the file on the remote server. If the file at *path* does not exist, it will be created. - If the *path* refers to a folder or the file cannot be opened for writing, an :doc:`OperationFailure` will be raised. - :param file_obj: A file-like object that has a *read* method. Data will read continuously from *file_obj* until EOF. - :param offset: Long integer value which specifies the offset in the remote server to start writing. First byte of the file is 0. - :param truncate: Boolean value. If True and the file exists on the remote server, it will be truncated first before writing. Default is False. - :return: the file position where the next byte will be written. - """ - if not self.sock: - raise NotConnectedError('Not connected to server') - - results = [ ] - - def cb(r): - self.is_busy = False - results.append(r[1]) - - def eb(failure): - self.is_busy = False - raise failure - - self.is_busy = True - try: - self._storeFileFromOffset(service_name, path, file_obj, cb, eb, offset, truncate = truncate, timeout = timeout) - while self.is_busy: - self._pollForNetBIOSPacket(timeout) - finally: - self.is_busy = False - - return results[0] - - def deleteFiles(self, service_name, path_file_pattern, timeout = 30): - """ - Delete one or more regular files. It supports the use of wildcards in file names, allowing for deletion of multiple files in a single request. - - :param string/unicode service_name: Contains the name of the shared folder. - :param string/unicode path_file_pattern: The pathname of the file(s) to be deleted, relative to the service_name. - Wildcards may be used in th filename component of the path. - If your path/filename contains non-English characters, you must pass in an unicode string. - :return: None - """ - if not self.sock: - raise NotConnectedError('Not connected to server') - - def cb(r): - self.is_busy = False - - def eb(failure): - self.is_busy = False - raise failure - - self.is_busy = True - try: - self._deleteFiles(service_name, path_file_pattern, cb, eb, timeout = timeout) - while self.is_busy: - self._pollForNetBIOSPacket(timeout) - finally: - self.is_busy = False - - def resetFileAttributes(self, service_name, path_file_pattern, timeout = 30): - """ - Reset file attributes of one or more regular files or folders. - It supports the use of wildcards in file names, allowing for unlocking of multiple files/folders in a single request. - This function is very helpful when deleting files/folders that are read-only. - Note: this function is currently only implemented for SMB2! Technically, it sets the FILE_ATTRIBUTE_NORMAL flag, therefore clearing all other flags. (See https://msdn.microsoft.com/en-us/library/cc232110.aspx for further information) - :param string/unicode service_name: Contains the name of the shared folder. - :param string/unicode path_file_pattern: The pathname of the file(s) to be deleted, relative to the service_name. - Wildcards may be used in th filename component of the path. - If your path/filename contains non-English characters, you must pass in an unicode string. - :return: None - """ - if not self.sock: - raise NotConnectedError('Not connected to server') - - def cb(r): - self.is_busy = False - - def eb(failure): - self.is_busy = False - raise failure - - self.is_busy = True - try: - self._resetFileAttributes(service_name, path_file_pattern, cb, eb, timeout = timeout) - while self.is_busy: - self._pollForNetBIOSPacket(timeout) - finally: - self.is_busy = False - - def createDirectory(self, service_name, path, timeout = 30): - """ - Creates a new directory *path* on the *service_name*. - - :param string/unicode service_name: Contains the name of the shared folder. - :param string/unicode path: The path of the new folder (relative to) the shared folder. - If the path contains non-English characters, an unicode string must be used to pass in the path. - :return: None - """ - if not self.sock: - raise NotConnectedError('Not connected to server') - - def cb(r): - self.is_busy = False - - def eb(failure): - self.is_busy = False - raise failure - - self.is_busy = True - try: - self._createDirectory(service_name, path, cb, eb, timeout = timeout) - while self.is_busy: - self._pollForNetBIOSPacket(timeout) - finally: - self.is_busy = False - - def deleteDirectory(self, service_name, path, timeout = 30): - """ - Delete the empty folder at *path* on *service_name* - - :param string/unicode service_name: Contains the name of the shared folder. - :param string/unicode path: The path of the to-be-deleted folder (relative to) the shared folder. - If the path contains non-English characters, an unicode string must be used to pass in the path. - :return: None - """ - if not self.sock: - raise NotConnectedError('Not connected to server') - - def cb(r): - self.is_busy = False - - def eb(failure): - self.is_busy = False - raise failure - - self.is_busy = True - try: - self._deleteDirectory(service_name, path, cb, eb, timeout = timeout) - while self.is_busy: - self._pollForNetBIOSPacket(timeout) - finally: - self.is_busy = False - - def rename(self, service_name, old_path, new_path, timeout = 30): - """ - Rename a file or folder at *old_path* to *new_path* shared at *service_name*. Note that this method cannot be used to rename file/folder across different shared folders - - *old_path* and *new_path* are string/unicode referring to the old and new path of the renamed resources (relative to) the shared folder. - If the path contains non-English characters, an unicode string must be used to pass in the path. - - :param string/unicode service_name: Contains the name of the shared folder. - :return: None - """ - if not self.sock: - raise NotConnectedError('Not connected to server') - - def cb(r): - self.is_busy = False - - def eb(failure): - self.is_busy = False - raise failure - - self.is_busy = True - try: - self._rename(service_name, old_path, new_path, cb, eb) - while self.is_busy: - self._pollForNetBIOSPacket(timeout) - finally: - self.is_busy = False - - def echo(self, data, timeout = 10): - """ - Send an echo command containing *data* to the remote SMB/CIFS server. The remote SMB/CIFS will reply with the same *data*. - - :param string data: Data to send to the remote server. - :return: The *data* parameter - """ - if not self.sock: - raise NotConnectedError('Not connected to server') - - results = [ ] - - def cb(r): - self.is_busy = False - results.append(r) - - def eb(failure): - self.is_busy = False - raise failure - - self.is_busy = True - try: - self._echo(data, cb, eb) - while self.is_busy: - self._pollForNetBIOSPacket(timeout) - finally: - self.is_busy = False - - return results[0] - - # - # Protected Methods - # - - def _pollForNetBIOSPacket(self, timeout): - expiry_time = time.time() + timeout - read_len = 4 - data = '' - - while read_len > 0: - try: - if expiry_time < time.time(): - raise SMBTimeout - - ready, _, _ = select.select([ self.sock.fileno() ], [ ], [ ], timeout) - if not ready: - raise SMBTimeout - - d = self.sock.recv(read_len) - if len(d) == 0: - raise NotConnectedError - - data = data + d - read_len -= len(d) - except select.error, ex: - if type(ex) is types.TupleType: - if ex[0] != errno.EINTR and ex[0] != errno.EAGAIN: - raise ex - else: - raise ex - - type, flags, length = struct.unpack('>BBH', data) - if flags & 0x01: - length = length | 0x10000 - - read_len = length - while read_len > 0: - try: - if expiry_time < time.time(): - raise SMBTimeout - - ready, _, _ = select.select([ self.sock.fileno() ], [ ], [ ], timeout) - if not ready: - raise SMBTimeout - - d = self.sock.recv(read_len) - if len(d) == 0: - raise NotConnectedError - - data = data + d - read_len -= len(d) - except select.error, ex: - if type(ex) is types.TupleType: - if ex[0] != errno.EINTR and ex[0] != errno.EAGAIN: - raise ex - else: - raise ex - - self.feedData(data) + +import os, logging, select, socket, struct, errno +from smb_constants import * +from smb_structs import * +from base import SMB, NotConnectedError, NotReadyError, SMBTimeout + + +class SMBConnection(SMB): + + log = logging.getLogger('SMB.SMBConnection') + + #: SMB messages will never be signed regardless of remote server's configurations; access errors will occur if the remote server requires signing. + SIGN_NEVER = 0 + #: SMB messages will be signed when remote server supports signing but not requires signing. + SIGN_WHEN_SUPPORTED = 1 + #: SMB messages will only be signed when remote server requires signing. + SIGN_WHEN_REQUIRED = 2 + + def __init__(self, username, password, my_name, remote_name, domain = '', use_ntlm_v2 = True, sign_options = SIGN_WHEN_REQUIRED, is_direct_tcp = False): + """ + Create a new SMBConnection instance. + + *username* and *password* are the user credentials required to authenticate the underlying SMB connection with the remote server. + File operations can only be proceeded after the connection has been authenticated successfully. + + Note that you need to call *connect* method to actually establish the SMB connection to the remote server and perform authentication. + + The default TCP port for most SMB/CIFS servers using NetBIOS over TCP/IP is 139. + Some newer server installations might also support Direct hosting of SMB over TCP/IP; for these servers, the default TCP port is 445. + + :param string my_name: The local NetBIOS machine name that will identify where this connection is originating from. + You can freely choose a name as long as it contains a maximum of 15 alphanumeric characters and does not contain spaces and any of ``\/:*?";|+`` + :param string remote_name: The NetBIOS machine name of the remote server. + On windows, you can find out the machine name by right-clicking on the "My Computer" and selecting "Properties". + This parameter must be the same as what has been configured on the remote server, or else the connection will be rejected. + :param string domain: The network domain. On windows, it is known as the workgroup. Usually, it is safe to leave this parameter as an empty string. + :param boolean use_ntlm_v2: Indicates whether pysmb should be NTLMv1 or NTLMv2 authentication algorithm for authentication. + The choice of NTLMv1 and NTLMv2 is configured on the remote server, and there is no mechanism to auto-detect which algorithm has been configured. + Hence, we can only "guess" or try both algorithms. + On Sambda, Windows Vista and Windows 7, NTLMv2 is enabled by default. On Windows XP, we can use NTLMv1 before NTLMv2. + :param int sign_options: Determines whether SMB messages will be signed. Default is *SIGN_WHEN_REQUIRED*. + If *SIGN_WHEN_REQUIRED* (value=2), SMB messages will only be signed when remote server requires signing. + If *SIGN_WHEN_SUPPORTED* (value=1), SMB messages will be signed when remote server supports signing but not requires signing. + If *SIGN_NEVER* (value=0), SMB messages will never be signed regardless of remote server's configurations; access errors will occur if the remote server requires signing. + :param boolean is_direct_tcp: Controls whether the NetBIOS over TCP/IP (is_direct_tcp=False) or the newer Direct hosting of SMB over TCP/IP (is_direct_tcp=True) will be used for the communication. + The default parameter is False which will use NetBIOS over TCP/IP for wider compatibility (TCP port: 139). + """ + SMB.__init__(self, username, password, my_name, remote_name, domain, use_ntlm_v2, sign_options, is_direct_tcp) + self.sock = None + self.auth_result = None + self.is_busy = False + self.is_direct_tcp = is_direct_tcp + + # + # SMB (and its superclass) Methods + # + + def onAuthOK(self): + self.auth_result = True + + def onAuthFailed(self): + self.auth_result = False + + def write(self, data): + assert self.sock + data_len = len(data) + total_sent = 0 + while total_sent < data_len: + sent = self.sock.send(data[total_sent:]) + if sent == 0: + raise NotConnectedError('Server disconnected') + total_sent = total_sent + sent + + # + # Misc Properties + # + + @property + def isUsingSMB2(self): + """A convenient property to return True if the underlying SMB connection is using SMB2 protocol.""" + return self.is_using_smb2 + + + # + # Public Methods + # + + def connect(self, ip, port = 139, sock_family = socket.AF_INET, timeout = 60): + """ + Establish the SMB connection to the remote SMB/CIFS server. + + You must call this method before attempting any of the file operations with the remote server. + This method will block until the SMB connection has attempted at least one authentication. + + :return: A boolean value indicating the result of the authentication atttempt: True if authentication is successful; False, if otherwise. + """ + if self.sock: + self.sock.close() + + self.auth_result = None + self.sock = socket.socket(sock_family) + self.sock.settimeout(timeout) + self.sock.connect(( ip, port )) + + self.is_busy = True + try: + if not self.is_direct_tcp: + self.requestNMBSession() + else: + self.onNMBSessionOK() + while self.auth_result is None: + self._pollForNetBIOSPacket(timeout) + finally: + self.is_busy = False + + return self.auth_result + + def close(self): + """ + Terminate the SMB connection (if it has been started) and release any sources held by the underlying socket. + """ + if self.sock: + self.sock.close() + self.sock = None + + def listShares(self, timeout = 30): + """ + Retrieve a list of shared resources on remote server. + + :return: A list of :doc:`smb.base.SharedDevice` instances describing the shared resource + """ + if not self.sock: + raise NotConnectedError('Not connected to server') + + results = [ ] + + def cb(entries): + self.is_busy = False + results.extend(entries) + + def eb(failure): + self.is_busy = False + raise failure + + self.is_busy = True + try: + self._listShares(cb, eb, timeout) + while self.is_busy: + self._pollForNetBIOSPacket(timeout) + finally: + self.is_busy = False + + return results + + def listPath(self, service_name, path, + search = SMB_FILE_ATTRIBUTE_READONLY | SMB_FILE_ATTRIBUTE_HIDDEN | SMB_FILE_ATTRIBUTE_SYSTEM | SMB_FILE_ATTRIBUTE_DIRECTORY | SMB_FILE_ATTRIBUTE_ARCHIVE | SMB_FILE_ATTRIBUTE_INCL_NORMAL, + pattern = '*', timeout = 30): + """ + Retrieve a directory listing of files/folders at *path* + + For simplicity, pysmb defines a "normal" file as a file entry that is not read-only, not hidden, not system, not archive and not a directory. + It ignores other attributes like compression, indexed, sparse, temporary and encryption. + + Note that the default search parameter will query for all read-only (SMB_FILE_ATTRIBUTE_READONLY), hidden (SMB_FILE_ATTRIBUTE_HIDDEN), + system (SMB_FILE_ATTRIBUTE_SYSTEM), archive (SMB_FILE_ATTRIBUTE_ARCHIVE), normal (SMB_FILE_ATTRIBUTE_INCL_NORMAL) files + and directories (SMB_FILE_ATTRIBUTE_DIRECTORY). + If you do not need to include "normal" files in the result, define your own search parameter without the SMB_FILE_ATTRIBUTE_INCL_NORMAL constant. + SMB_FILE_ATTRIBUTE_NORMAL should be used by itself and not be used with other bit constants. + + :param string/unicode service_name: the name of the shared folder for the *path* + :param string/unicode path: path relative to the *service_name* where we are interested to learn about its files/sub-folders. + :param integer search: integer value made up from a bitwise-OR of *SMB_FILE_ATTRIBUTE_xxx* bits (see smb_constants.py). + :param string/unicode pattern: the filter to apply to the results before returning to the client. + :return: A list of :doc:`smb.base.SharedFile` instances. + """ + if not self.sock: + raise NotConnectedError('Not connected to server') + + results = [ ] + + def cb(entries): + self.is_busy = False + results.extend(entries) + + def eb(failure): + self.is_busy = False + raise failure + + self.is_busy = True + try: + self._listPath(service_name, path, cb, eb, search = search, pattern = pattern, timeout = timeout) + while self.is_busy: + self._pollForNetBIOSPacket(timeout) + finally: + self.is_busy = False + + return results + + def listSnapshots(self, service_name, path, timeout = 30): + """ + Retrieve a list of available snapshots (shadow copies) for *path*. + + Note that snapshot features are only supported on Windows Vista Business, Enterprise and Ultimate, and on all Windows 7 editions. + + :param string/unicode service_name: the name of the shared folder for the *path* + :param string/unicode path: path relative to the *service_name* where we are interested in the list of available snapshots + :return: A list of python *datetime.DateTime* instances in GMT/UTC time zone + """ + if not self.sock: + raise NotConnectedError('Not connected to server') + + results = [ ] + + def cb(entries): + self.is_busy = False + results.extend(entries) + + def eb(failure): + self.is_busy = False + raise failure + + self.is_busy = True + try: + self._listSnapshots(service_name, path, cb, eb, timeout = timeout) + while self.is_busy: + self._pollForNetBIOSPacket(timeout) + finally: + self.is_busy = False + + return results + + def getAttributes(self, service_name, path, timeout = 30): + """ + Retrieve information about the file at *path* on the *service_name*. + + :param string/unicode service_name: the name of the shared folder for the *path* + :param string/unicode path: Path of the file on the remote server. If the file cannot be opened for reading, an :doc:`OperationFailure` will be raised. + :return: A :doc:`smb.base.SharedFile` instance containing the attributes of the file. + """ + if not self.sock: + raise NotConnectedError('Not connected to server') + + results = [ ] + + def cb(info): + self.is_busy = False + results.append(info) + + def eb(failure): + self.is_busy = False + raise failure + + self.is_busy = True + try: + self._getAttributes(service_name, path, cb, eb, timeout) + while self.is_busy: + self._pollForNetBIOSPacket(timeout) + finally: + self.is_busy = False + + return results[0] + + def getSecurity(self, service_name, path, timeout = 30): + """ + Retrieve the security descriptor of the file at *path* on the *service_name*. + + :param string/unicode service_name: the name of the shared folder for the *path* + :param string/unicode path: Path of the file on the remote server. If the file cannot be opened for reading, an :doc:`OperationFailure` will be raised. + :return: A :class:`smb.security_descriptors.SecurityDescriptor` instance containing the security information of the file. + """ + if not self.sock: + raise NotConnectedError('Not connected to server') + + results = [ ] + + def cb(info): + self.is_busy = False + results.append(info) + + def eb(failure): + self.is_busy = False + raise failure + + self.is_busy = True + try: + self._getSecurity(service_name, path, cb, eb, timeout) + while self.is_busy: + self._pollForNetBIOSPacket(timeout) + finally: + self.is_busy = False + + return results[0] + + def retrieveFile(self, service_name, path, file_obj, timeout = 30): + """ + Retrieve the contents of the file at *path* on the *service_name* and write these contents to the provided *file_obj*. + + Use *retrieveFileFromOffset()* method if you wish to specify the offset to read from the remote *path* and/or the number of bytes to write to the *file_obj*. + + :param string/unicode service_name: the name of the shared folder for the *path* + :param string/unicode path: Path of the file on the remote server. If the file cannot be opened for reading, an :doc:`OperationFailure` will be raised. + :param file_obj: A file-like object that has a *write* method. Data will be written continuously to *file_obj* until EOF is received from the remote service. + :return: A 2-element tuple of ( file attributes of the file on server, number of bytes written to *file_obj* ). + The file attributes is an integer value made up from a bitwise-OR of *SMB_FILE_ATTRIBUTE_xxx* bits (see smb_constants.py) + """ + return self.retrieveFileFromOffset(service_name, path, file_obj, 0L, -1L, timeout) + + def retrieveFileFromOffset(self, service_name, path, file_obj, offset = 0L, max_length = -1L, timeout = 30): + """ + Retrieve the contents of the file at *path* on the *service_name* and write these contents to the provided *file_obj*. + + :param string/unicode service_name: the name of the shared folder for the *path* + :param string/unicode path: Path of the file on the remote server. If the file cannot be opened for reading, an :doc:`OperationFailure` will be raised. + :param file_obj: A file-like object that has a *write* method. Data will be written continuously to *file_obj* up to *max_length* number of bytes. + :param integer/long offset: the offset in the remote *path* where the first byte will be read and written to *file_obj*. Must be either zero or a positive integer/long value. + :param integer/long max_length: maximum number of bytes to read from the remote *path* and write to the *file_obj*. Specify a negative value to read from *offset* to the EOF. + If zero, the method returns immediately after the file is opened successfully for reading. + :return: A 2-element tuple of ( file attributes of the file on server, number of bytes written to *file_obj* ). + The file attributes is an integer value made up from a bitwise-OR of *SMB_FILE_ATTRIBUTE_xxx* bits (see smb_constants.py) + """ + if not self.sock: + raise NotConnectedError('Not connected to server') + + results = [ ] + + def cb(r): + self.is_busy = False + results.append(r[1:]) + + def eb(failure): + self.is_busy = False + raise failure + + self.is_busy = True + try: + self._retrieveFileFromOffset(service_name, path, file_obj, cb, eb, offset, max_length, timeout = timeout) + while self.is_busy: + self._pollForNetBIOSPacket(timeout) + finally: + self.is_busy = False + + return results[0] + + def storeFile(self, service_name, path, file_obj, timeout = 30): + """ + Store the contents of the *file_obj* at *path* on the *service_name*. + If the file already exists on the remote server, it will be truncated and overwritten. + + :param string/unicode service_name: the name of the shared folder for the *path* + :param string/unicode path: Path of the file on the remote server. If the file at *path* does not exist, it will be created. Otherwise, it will be overwritten. + If the *path* refers to a folder or the file cannot be opened for writing, an :doc:`OperationFailure` will be raised. + :param file_obj: A file-like object that has a *read* method. Data will read continuously from *file_obj* until EOF. + :return: Number of bytes uploaded + """ + return self.storeFileFromOffset(service_name, path, file_obj, 0L, True, timeout) + + def storeFileFromOffset(self, service_name, path, file_obj, offset = 0L, truncate = False, timeout = 30): + """ + Store the contents of the *file_obj* at *path* on the *service_name*. + + :param string/unicode service_name: the name of the shared folder for the *path* + :param string/unicode path: Path of the file on the remote server. If the file at *path* does not exist, it will be created. + If the *path* refers to a folder or the file cannot be opened for writing, an :doc:`OperationFailure` will be raised. + :param file_obj: A file-like object that has a *read* method. Data will read continuously from *file_obj* until EOF. + :param offset: Long integer value which specifies the offset in the remote server to start writing. First byte of the file is 0. + :param truncate: Boolean value. If True and the file exists on the remote server, it will be truncated first before writing. Default is False. + :return: the file position where the next byte will be written. + """ + if not self.sock: + raise NotConnectedError('Not connected to server') + + results = [ ] + + def cb(r): + self.is_busy = False + results.append(r[1]) + + def eb(failure): + self.is_busy = False + raise failure + + self.is_busy = True + try: + self._storeFileFromOffset(service_name, path, file_obj, cb, eb, offset, truncate = truncate, timeout = timeout) + while self.is_busy: + self._pollForNetBIOSPacket(timeout) + finally: + self.is_busy = False + + return results[0] + + def deleteFiles(self, service_name, path_file_pattern, timeout = 30): + """ + Delete one or more regular files. It supports the use of wildcards in file names, allowing for deletion of multiple files in a single request. + + :param string/unicode service_name: Contains the name of the shared folder. + :param string/unicode path_file_pattern: The pathname of the file(s) to be deleted, relative to the service_name. + Wildcards may be used in th filename component of the path. + If your path/filename contains non-English characters, you must pass in an unicode string. + :return: None + """ + if not self.sock: + raise NotConnectedError('Not connected to server') + + def cb(r): + self.is_busy = False + + def eb(failure): + self.is_busy = False + raise failure + + self.is_busy = True + try: + self._deleteFiles(service_name, path_file_pattern, cb, eb, timeout = timeout) + while self.is_busy: + self._pollForNetBIOSPacket(timeout) + finally: + self.is_busy = False + + def resetFileAttributes(self, service_name, path_file_pattern, timeout = 30): + """ + Reset file attributes of one or more regular files or folders. + It supports the use of wildcards in file names, allowing for unlocking of multiple files/folders in a single request. + This function is very helpful when deleting files/folders that are read-only. + Note: this function is currently only implemented for SMB2! Technically, it sets the FILE_ATTRIBUTE_NORMAL flag, therefore clearing all other flags. (See https://msdn.microsoft.com/en-us/library/cc232110.aspx for further information) + + :param string/unicode service_name: Contains the name of the shared folder. + :param string/unicode path_file_pattern: The pathname of the file(s) to be deleted, relative to the service_name. + Wildcards may be used in the filename component of the path. + If your path/filename contains non-English characters, you must pass in an unicode string. + :return: None + """ + if not self.sock: + raise NotConnectedError('Not connected to server') + + def cb(r): + self.is_busy = False + + def eb(failure): + self.is_busy = False + raise failure + + self.is_busy = True + try: + self._resetFileAttributes(service_name, path_file_pattern, cb, eb, timeout = timeout) + while self.is_busy: + self._pollForNetBIOSPacket(timeout) + finally: + self.is_busy = False + + def createDirectory(self, service_name, path, timeout = 30): + """ + Creates a new directory *path* on the *service_name*. + + :param string/unicode service_name: Contains the name of the shared folder. + :param string/unicode path: The path of the new folder (relative to) the shared folder. + If the path contains non-English characters, an unicode string must be used to pass in the path. + :return: None + """ + if not self.sock: + raise NotConnectedError('Not connected to server') + + def cb(r): + self.is_busy = False + + def eb(failure): + self.is_busy = False + raise failure + + self.is_busy = True + try: + self._createDirectory(service_name, path, cb, eb, timeout = timeout) + while self.is_busy: + self._pollForNetBIOSPacket(timeout) + finally: + self.is_busy = False + + def deleteDirectory(self, service_name, path, timeout = 30): + """ + Delete the empty folder at *path* on *service_name* + + :param string/unicode service_name: Contains the name of the shared folder. + :param string/unicode path: The path of the to-be-deleted folder (relative to) the shared folder. + If the path contains non-English characters, an unicode string must be used to pass in the path. + :return: None + """ + if not self.sock: + raise NotConnectedError('Not connected to server') + + def cb(r): + self.is_busy = False + + def eb(failure): + self.is_busy = False + raise failure + + self.is_busy = True + try: + self._deleteDirectory(service_name, path, cb, eb, timeout = timeout) + while self.is_busy: + self._pollForNetBIOSPacket(timeout) + finally: + self.is_busy = False + + def rename(self, service_name, old_path, new_path, timeout = 30): + """ + Rename a file or folder at *old_path* to *new_path* shared at *service_name*. Note that this method cannot be used to rename file/folder across different shared folders + + *old_path* and *new_path* are string/unicode referring to the old and new path of the renamed resources (relative to) the shared folder. + If the path contains non-English characters, an unicode string must be used to pass in the path. + + :param string/unicode service_name: Contains the name of the shared folder. + :return: None + """ + if not self.sock: + raise NotConnectedError('Not connected to server') + + def cb(r): + self.is_busy = False + + def eb(failure): + self.is_busy = False + raise failure + + self.is_busy = True + try: + self._rename(service_name, old_path, new_path, cb, eb) + while self.is_busy: + self._pollForNetBIOSPacket(timeout) + finally: + self.is_busy = False + + def echo(self, data, timeout = 10): + """ + Send an echo command containing *data* to the remote SMB/CIFS server. The remote SMB/CIFS will reply with the same *data*. + + :param bytes data: Data to send to the remote server. Must be a bytes object. + :return: The *data* parameter + """ + if not self.sock: + raise NotConnectedError('Not connected to server') + + results = [ ] + + def cb(r): + self.is_busy = False + results.append(r) + + def eb(failure): + self.is_busy = False + raise failure + + self.is_busy = True + try: + self._echo(data, cb, eb) + while self.is_busy: + self._pollForNetBIOSPacket(timeout) + finally: + self.is_busy = False + + return results[0] + + # + # Protected Methods + # + + def _pollForNetBIOSPacket(self, timeout): + expiry_time = time.time() + timeout + read_len = 4 + data = '' + + while read_len > 0: + try: + if expiry_time < time.time(): + raise SMBTimeout + + ready, _, _ = select.select([ self.sock.fileno() ], [ ], [ ], timeout) + if not ready: + raise SMBTimeout + + d = self.sock.recv(read_len) + if len(d) == 0: + raise NotConnectedError + + data = data + d + read_len -= len(d) + except select.error, ex: + if isinstance(ex, types.TupleType): + if ex[0] != errno.EINTR and ex[0] != errno.EAGAIN: + raise ex + else: + raise ex + + type_, flags, length = struct.unpack('>BBH', data) + if type_ == 0x0: + # This is a Direct TCP packet + # The length is specified in the header from byte 8. (0-indexed) + # we read a structure assuming NBT, so to get the real length + # combine the length and flag fields together + length = length + (flags << 16) + else: + # This is a NetBIOS over TCP (NBT) packet + # The length is specified in the header from byte 16. (0-indexed) + if flags & 0x01: + length = length | 0x10000 + + read_len = length + while read_len > 0: + try: + if expiry_time < time.time(): + raise SMBTimeout + + ready, _, _ = select.select([ self.sock.fileno() ], [ ], [ ], timeout) + if not ready: + raise SMBTimeout + + d = self.sock.recv(read_len) + if len(d) == 0: + raise NotConnectedError + + data = data + d + read_len -= len(d) + except select.error, ex: + if isinstance(ex, types.TupleType): + if ex[0] != errno.EINTR and ex[0] != errno.EAGAIN: + raise ex + else: + raise ex + + self.feedData(data) diff --git a/plugin.video.alfa/lib/sambatools/smb/SMBHandler.py b/plugin.video.alfa/lib/sambatools/smb/SMBHandler.py index f8685f73..137e943a 100755 --- a/plugin.video.alfa/lib/sambatools/smb/SMBHandler.py +++ b/plugin.video.alfa/lib/sambatools/smb/SMBHandler.py @@ -1,102 +1,97 @@ -import mimetools -import mimetypes -import os -import socket -import sys -import tempfile -import urllib2 -from urllib import (unquote, addinfourl, splitport, splitattr, splituser, splitpasswd) - -from nmb.NetBIOS import NetBIOS - -from smb.SMBConnection import SMBConnection - -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - -USE_NTLM = True -MACHINE_NAME = None - -class SMBHandler(urllib2.BaseHandler): - - def smb_open(self, req): - global USE_NTLM, MACHINE_NAME - - host = req.get_host() - if not host: - raise urllib2.URLError('SMB error: no host given') - host, port = splitport(host) - if port is None: - port = 139 - else: - port = int(port) - - # username/password handling - user, host = splituser(host) - if user: - user, passwd = splitpasswd(user) - else: - passwd = None - host = unquote(host) - user = user or '' - - domain = '' - if ';' in user: - domain, user = user.split(';', 1) - - passwd = passwd or '' - myname = MACHINE_NAME or self.generateClientMachineName() - - n = NetBIOS() - names = n.queryIPForName(host) - if names: - server_name = names[0] - else: - raise urllib2.URLError('SMB error: Hostname does not reply back with its machine name') - - path, attrs = splitattr(req.get_selector()) - if path.startswith('/'): - path = path[1:] - dirs = path.split('/') - dirs = map(unquote, dirs) - service, path = dirs[0], '/'.join(dirs[1:]) - - try: - conn = SMBConnection(user, passwd, myname, server_name, domain=domain, use_ntlm_v2 = USE_NTLM) - conn.connect(host, port) - - if req.has_data(): - data_fp = req.get_data() - filelen = conn.storeFile(service, path, data_fp) - - headers = "Content-length: 0\n" - fp = StringIO("") - else: - fp = self.createTempFile() - file_attrs, retrlen = conn.retrieveFile(service, path, fp) - fp.seek(0) - - headers = "" - mtype = mimetypes.guess_type(req.get_full_url())[0] - if mtype: - headers += "Content-type: %s\n" % mtype - if retrlen is not None and retrlen >= 0: - headers += "Content-length: %d\n" % retrlen - - sf = StringIO(headers) - headers = mimetools.Message(sf) - - return addinfourl(fp, headers, req.get_full_url()) - except Exception, ex: - raise urllib2.URLError, ('smb error: %s' % ex), sys.exc_info()[2] - - def createTempFile(self): - return tempfile.TemporaryFile() - - def generateClientMachineName(self): - hostname = socket.gethostname() - if hostname: - return hostname.split('.')[0] - return 'SMB%d' % os.getpid() + +import os, sys, socket, urllib2, mimetypes, mimetools, tempfile +from urllib import (unwrap, unquote, splittype, splithost, quote, + addinfourl, splitport, splittag, + splitattr, ftpwrapper, splituser, splitpasswd, splitvalue) +from nmb.NetBIOS import NetBIOS +from smb.SMBConnection import SMBConnection + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +USE_NTLM = True +MACHINE_NAME = None + +class SMBHandler(urllib2.BaseHandler): + + def smb_open(self, req): + global USE_NTLM, MACHINE_NAME + + host = req.get_host() + if not host: + raise urllib2.URLError('SMB error: no host given') + host, port = splitport(host) + if port is None: + port = 139 + else: + port = int(port) + + # username/password handling + user, host = splituser(host) + if user: + user, passwd = splitpasswd(user) + else: + passwd = None + host = unquote(host) + user = user or '' + + domain = '' + if ';' in user: + domain, user = user.split(';', 1) + + passwd = passwd or '' + myname = MACHINE_NAME or self.generateClientMachineName() + + n = NetBIOS() + names = n.queryIPForName(host) + if names: + server_name = names[0] + else: + raise urllib2.URLError('SMB error: Hostname does not reply back with its machine name') + + path, attrs = splitattr(req.get_selector()) + if path.startswith('/'): + path = path[1:] + dirs = path.split('/') + dirs = map(unquote, dirs) + service, path = dirs[0], '/'.join(dirs[1:]) + + try: + conn = SMBConnection(user, passwd, myname, server_name, domain=domain, use_ntlm_v2 = USE_NTLM) + conn.connect(host, port) + + if req.has_data(): + data_fp = req.get_data() + filelen = conn.storeFile(service, path, data_fp) + + headers = "Content-length: 0\n" + fp = StringIO("") + else: + fp = self.createTempFile() + file_attrs, retrlen = conn.retrieveFile(service, path, fp) + fp.seek(0) + + headers = "" + mtype = mimetypes.guess_type(req.get_full_url())[0] + if mtype: + headers += "Content-type: %s\n" % mtype + if retrlen is not None and retrlen >= 0: + headers += "Content-length: %d\n" % retrlen + + sf = StringIO(headers) + headers = mimetools.Message(sf) + + return addinfourl(fp, headers, req.get_full_url()) + except Exception, ex: + raise urllib2.URLError, ('smb error: %s' % ex), sys.exc_info()[2] + + def createTempFile(self): + return tempfile.TemporaryFile() + + def generateClientMachineName(self): + hostname = socket.gethostname() + if hostname: + return hostname.split('.')[0] + return 'SMB%d' % os.getpid() diff --git a/plugin.video.alfa/lib/sambatools/smb/SMBProtocol.py b/plugin.video.alfa/lib/sambatools/smb/SMBProtocol.py index 3795fae2..1238d637 100755 --- a/plugin.video.alfa/lib/sambatools/smb/SMBProtocol.py +++ b/plugin.video.alfa/lib/sambatools/smb/SMBProtocol.py @@ -1,398 +1,409 @@ -from twisted.internet import reactor, defer -from twisted.internet.protocol import ClientFactory, Protocol - -from base import SMB, NotConnectedError, NotReadyError, SMBTimeout -from smb_structs import * - -__all__ = [ 'SMBProtocolFactory', 'NotConnectedError', 'NotReadyError' ] - - -class SMBProtocol(Protocol, SMB): - - log = logging.getLogger('SMB.SMBProtocol') - - # - # Protocol Methods - # - - def connectionMade(self): - self.factory.instance = self - if not self.is_direct_tcp: - self.requestNMBSession() - else: - self.onNMBSessionOK() - - def connectionLost(self, reason): - if self.factory.instance == self: - self.instance = None - - def dataReceived(self, data): - self.feedData(data) - - # - # SMB (and its superclass) Methods - # - - def write(self, data): - self.transport.write(data) - - def onAuthOK(self): - if self.factory.instance == self: - self.factory.onAuthOK() - reactor.callLater(1, self._cleanupPendingRequests) - - def onAuthFailed(self): - if self.factory.instance == self: - self.factory.onAuthFailed() - - def onNMBSessionFailed(self): - self.log.error('Cannot establish NetBIOS session. You might have provided a wrong remote_name') - - # - # Protected Methods - # - - def _cleanupPendingRequests(self): - if self.factory.instance == self: - now = time.time() - to_remove = [] - for mid, r in self.pending_requests.iteritems(): - if r.expiry_time < now: - try: - r.errback(SMBTimeout()) - except Exception: pass - to_remove.append(mid) - - for mid in to_remove: - del self.pending_requests[mid] - - reactor.callLater(1, self._cleanupPendingRequests) - - -class SMBProtocolFactory(ClientFactory): - - protocol = SMBProtocol - log = logging.getLogger('SMB.SMBFactory') - - #: SMB messages will never be signed regardless of remote server's configurations; access errors will occur if the remote server requires signing. - SIGN_NEVER = 0 - #: SMB messages will be signed when remote server supports signing but not requires signing. - SIGN_WHEN_SUPPORTED = 1 - #: SMB messages will only be signed when remote server requires signing. - SIGN_WHEN_REQUIRED = 2 - - def __init__(self, username, password, my_name, remote_name, domain = '', use_ntlm_v2 = True, sign_options = SIGN_WHEN_REQUIRED, is_direct_tcp = False): - """ - Create a new SMBProtocolFactory instance. You will pass this instance to *reactor.connectTCP()* which will then instantiate the TCP connection to the remote SMB/CIFS server. - Note that the default TCP port for most SMB/CIFS servers using NetBIOS over TCP/IP is 139. - Some newer server installations might also support Direct hosting of SMB over TCP/IP; for these servers, the default TCP port is 445. - - *username* and *password* are the user credentials required to authenticate the underlying SMB connection with the remote server. - File operations can only be proceeded after the connection has been authenticated successfully. - - :param string my_name: The local NetBIOS machine name that will identify where this connection is originating from. - You can freely choose a name as long as it contains a maximum of 15 alphanumeric characters and does not contain spaces and any of ``\/:*?";|+``. - :param string remote_name: The NetBIOS machine name of the remote server. - On windows, you can find out the machine name by right-clicking on the "My Computer" and selecting "Properties". - This parameter must be the same as what has been configured on the remote server, or else the connection will be rejected. - :param string domain: The network domain. On windows, it is known as the workgroup. Usually, it is safe to leave this parameter as an empty string. - :param boolean use_ntlm_v2: Indicates whether pysmb should be NTLMv1 or NTLMv2 authentication algorithm for authentication. - The choice of NTLMv1 and NTLMv2 is configured on the remote server, and there is no mechanism to auto-detect which algorithm has been configured. - Hence, we can only "guess" or try both algorithms. - On Sambda, Windows Vista and Windows 7, NTLMv2 is enabled by default. On Windows XP, we can use NTLMv1 before NTLMv2. - :param int sign_options: Determines whether SMB messages will be signed. Default is *SIGN_WHEN_REQUIRED*. - If *SIGN_WHEN_REQUIRED* (value=2), SMB messages will only be signed when remote server requires signing. - If *SIGN_WHEN_SUPPORTED* (value=1), SMB messages will be signed when remote server supports signing but not requires signing. - If *SIGN_NEVER* (value=0), SMB messages will never be signed regardless of remote server's configurations; access errors will occur if the remote server requires signing. - :param boolean is_direct_tcp: Controls whether the NetBIOS over TCP/IP (is_direct_tcp=False) or the newer Direct hosting of SMB over TCP/IP (is_direct_tcp=True) will be used for the communication. - The default parameter is False which will use NetBIOS over TCP/IP for wider compatibility (TCP port: 139). - """ - self.username = username - self.password = password - self.my_name = my_name - self.remote_name = remote_name - self.domain = domain - self.use_ntlm_v2 = use_ntlm_v2 - self.sign_options = sign_options - self.is_direct_tcp = is_direct_tcp - self.instance = None #: The single SMBProtocol instance for each SMBProtocolFactory instance. Usually, you should not need to touch this attribute directly. - - # - # Public Property - # - - @property - def isReady(self): - """A convenient property to return True if the underlying SMB connection has connected to remote server, has successfully authenticated itself and is ready for file operations.""" - return bool(self.instance and self.instance.has_authenticated) - - @property - def isUsingSMB2(self): - """A convenient property to return True if the underlying SMB connection is using SMB2 protocol.""" - return self.instance and self.instance.is_using_smb2 - - # - # Public Methods for Callbacks - # - - def onAuthOK(self): - """ - Override this method in your *SMBProtocolFactory* subclass to add in post-authentication handling. - This method will be called when the server has replied that the SMB connection has been successfully authenticated. - File operations can proceed when this method has been called. - """ - pass - - def onAuthFailed(self): - """ - Override this method in your *SMBProtocolFactory* subclass to add in post-authentication handling. - This method will be called when the server has replied that the SMB connection has been successfully authenticated. - - If you want to retry authenticating from this method, - 1. Disconnect the underlying SMB connection (call ``self.instance.transport.loseConnection()``) - 2. Create a new SMBProtocolFactory subclass instance with different user credientials or different NTLM algorithm flag. - 3. Call ``reactor.connectTCP`` with the new instance to re-establish the SMB connection - """ - pass - - # - # Public Methods - # - - def listShares(self, timeout = 30): - """ - Retrieve a list of shared resources on remote server. - - :param integer/float timeout: Number of seconds that pysmb will wait before raising *SMBTimeout* via the returned *Deferred* instance's *errback* method. - :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a list of :doc:`smb.base.SharedDevice` instances. - """ - if not self.instance: - raise NotConnectedError('Not connected to server') - - d = defer.Deferred() - self.instance._listShares(d.callback, d.errback, timeout) - return d - - def listPath(self, service_name, path, - search = SMB_FILE_ATTRIBUTE_READONLY | SMB_FILE_ATTRIBUTE_HIDDEN | SMB_FILE_ATTRIBUTE_SYSTEM | SMB_FILE_ATTRIBUTE_DIRECTORY | SMB_FILE_ATTRIBUTE_ARCHIVE, - pattern = '*', timeout = 30): - """ - Retrieve a directory listing of files/folders at *path* - - :param string/unicode service_name: the name of the shared folder for the *path* - :param string/unicode path: path relative to the *service_name* where we are interested to learn about its files/sub-folders. - :param integer search: integer value made up from a bitwise-OR of *SMB_FILE_ATTRIBUTE_xxx* bits (see smb_constants.py). - The default *search* value will query for all read-only, hidden, system, archive files and directories. - :param string/unicode pattern: the filter to apply to the results before returning to the client. - :param integer/float timeout: Number of seconds that pysmb will wait before raising *SMBTimeout* via the returned *Deferred* instance's *errback* method. - :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a list of :doc:`smb.base.SharedFile` instances. - """ - if not self.instance: - raise NotConnectedError('Not connected to server') - - d = defer.Deferred() - self.instance._listPath(service_name, path, d.callback, d.errback, search = search, pattern = pattern, timeout = timeout) - return d - - def listSnapshots(self, service_name, path, timeout = 30): - """ - Retrieve a list of available snapshots (a.k.a. shadow copies) for *path*. - - Note that snapshot features are only supported on Windows Vista Business, Enterprise and Ultimate, and on all Windows 7 editions. - - :param string/unicode service_name: the name of the shared folder for the *path* - :param string/unicode path: path relative to the *service_name* where we are interested in the list of available snapshots - :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a list of python *datetime.DateTime* - instances in GMT/UTC time zone - """ - if not self.instance: - raise NotConnectedError('Not connected to server') - - d = defer.Deferred() - self.instance._listSnapshots(service_name, path, d.callback, d.errback, timeout = timeout) - return d - - def getAttributes(self, service_name, path, timeout = 30): - """ - Retrieve information about the file at *path* on the *service_name*. - - :param string/unicode service_name: the name of the shared folder for the *path* - :param string/unicode path: Path of the file on the remote server. If the file cannot be opened for reading, an :doc:`OperationFailure` will be raised. - :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a :doc:`smb.base.SharedFile` instance containing the attributes of the file. - """ - if not self.instance: - raise NotConnectedError('Not connected to server') - - d = defer.Deferred() - self.instance._getAttributes(service_name, path, d.callback, d.errback, timeout = timeout) - return d - - def retrieveFile(self, service_name, path, file_obj, timeout = 30): - """ - Retrieve the contents of the file at *path* on the *service_name* and write these contents to the provided *file_obj*. - - Use *retrieveFileFromOffset()* method if you need to specify the offset to read from the remote *path* and/or the maximum number of bytes to write to the *file_obj*. - - The meaning of the *timeout* parameter will be different from other file operation methods. As the downloaded file usually exceeeds the maximum size - of each SMB/CIFS data message, it will be packetized into a series of request messages (each message will request about about 60kBytes). - The *timeout* parameter is an integer/float value that specifies the timeout interval for these individual SMB/CIFS message to be transmitted and downloaded from the remote SMB/CIFS server. - - :param string/unicode service_name: the name of the shared folder for the *path* - :param string/unicode path: Path of the file on the remote server. If the file cannot be opened for reading, an :doc:`OperationFailure` will be called in the returned *Deferred* errback. - :param file_obj: A file-like object that has a *write* method. Data will be written continuously to *file_obj* until EOF is received from the remote service. - :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a 3-element tuple of ( *file_obj*, file attributes of the file on server, number of bytes written to *file_obj* ). - The file attributes is an integer value made up from a bitwise-OR of *SMB_FILE_ATTRIBUTE_xxx* bits (see smb_constants.py) - """ - return self.retrieveFileFromOffset(service_name, path, file_obj, 0L, -1L, timeout) - - def retrieveFileFromOffset(self, service_name, path, file_obj, offset = 0L, max_length = -1L, timeout = 30): - """ - Retrieve the contents of the file at *path* on the *service_name* and write these contents to the provided *file_obj*. - - The meaning of the *timeout* parameter will be different from other file operation methods. As the downloaded file usually exceeeds the maximum size - of each SMB/CIFS data message, it will be packetized into a series of request messages (each message will request about about 60kBytes). - The *timeout* parameter is an integer/float value that specifies the timeout interval for these individual SMB/CIFS message to be transmitted and downloaded from the remote SMB/CIFS server. - - :param string/unicode service_name: the name of the shared folder for the *path* - :param string/unicode path: Path of the file on the remote server. If the file cannot be opened for reading, an :doc:`OperationFailure` will be called in the returned *Deferred* errback. - :param file_obj: A file-like object that has a *write* method. Data will be written continuously to *file_obj* until EOF is received from the remote service. - :param integer/long offset: the offset in the remote *path* where the first byte will be read and written to *file_obj*. Must be either zero or a positive integer/long value. - :param integer/long max_length: maximum number of bytes to read from the remote *path* and write to the *file_obj*. Specify a negative value to read from *offset* to the EOF. - If zero, the *Deferred* callback is invoked immediately after the file is opened successfully for reading. - :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a 3-element tuple of ( *file_obj*, file attributes of the file on server, number of bytes written to *file_obj* ). - The file attributes is an integer value made up from a bitwise-OR of *SMB_FILE_ATTRIBUTE_xxx* bits (see smb_constants.py) - """ - if not self.instance: - raise NotConnectedError('Not connected to server') - - d = defer.Deferred() - self.instance._retrieveFileFromOffset(service_name, path, file_obj, d.callback, d.errback, offset, max_length, timeout = timeout) - return d - - def storeFile(self, service_name, path, file_obj, timeout = 30): - """ - Store the contents of the *file_obj* at *path* on the *service_name*. - - The meaning of the *timeout* parameter will be different from other file operation methods. As the uploaded file usually exceeeds the maximum size - of each SMB/CIFS data message, it will be packetized into a series of messages (usually about 60kBytes). - The *timeout* parameter is an integer/float value that specifies the timeout interval for these individual SMB/CIFS message to be transmitted and acknowledged - by the remote SMB/CIFS server. - - :param string/unicode service_name: the name of the shared folder for the *path* - :param string/unicode path: Path of the file on the remote server. If the file at *path* does not exist, it will be created. Otherwise, it will be overwritten. - If the *path* refers to a folder or the file cannot be opened for writing, an :doc:`OperationFailure` will be called in the returned *Deferred* errback. - :param file_obj: A file-like object that has a *read* method. Data will read continuously from *file_obj* until EOF. - :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a 2-element tuple of ( *file_obj*, number of bytes uploaded ). - """ - if not self.instance: - raise NotConnectedError('Not connected to server') - - d = defer.Deferred() - self.instance._storeFile(service_name, path, file_obj, d.callback, d.errback, timeout = timeout) - return d - - def deleteFiles(self, service_name, path_file_pattern, timeout = 30): - """ - Delete one or more regular files. It supports the use of wildcards in file names, allowing for deletion of multiple files in a single request. - - :param string/unicode service_name: Contains the name of the shared folder. - :param string/unicode path_file_pattern: The pathname of the file(s) to be deleted, relative to the service_name. - Wildcards may be used in th filename component of the path. - If your path/filename contains non-English characters, you must pass in an unicode string. - :param integer/float timeout: Number of seconds that pysmb will wait before raising *SMBTimeout* via the returned *Deferred* instance's *errback* method. - :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with the *path_file_pattern* parameter. - """ - if not self.instance: - raise NotConnectedError('Not connected to server') - - d = defer.Deferred() - self.instance._deleteFiles(service_name, path_file_pattern, d.callback, d.errback, timeout = timeout) - return d - - def createDirectory(self, service_name, path): - """ - Creates a new directory *path* on the *service_name*. - - :param string/unicode service_name: Contains the name of the shared folder. - :param string/unicode path: The path of the new folder (relative to) the shared folder. - If the path contains non-English characters, an unicode string must be used to pass in the path. - :param integer/float timeout: Number of seconds that pysmb will wait before raising *SMBTimeout* via the returned *Deferred* instance's *errback* method. - :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with the *path* parameter. - """ - if not self.instance: - raise NotConnectedError('Not connected to server') - - d = defer.Deferred() - self.instance._createDirectory(service_name, path, d.callback, d.errback) - return d - - def deleteDirectory(self, service_name, path): - """ - Delete the empty folder at *path* on *service_name* - - :param string/unicode service_name: Contains the name of the shared folder. - :param string/unicode path: The path of the to-be-deleted folder (relative to) the shared folder. - If the path contains non-English characters, an unicode string must be used to pass in the path. - :param integer/float timeout: Number of seconds that pysmb will wait before raising *SMBTimeout* via the returned *Deferred* instance's *errback* method. - :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with the *path* parameter. - """ - if not self.instance: - raise NotConnectedError('Not connected to server') - - d = defer.Deferred() - self.instance._deleteDirectory(service_name, path, d.callback, d.errback) - return d - - def rename(self, service_name, old_path, new_path): - """ - Rename a file or folder at *old_path* to *new_path* shared at *service_name*. Note that this method cannot be used to rename file/folder across different shared folders - - *old_path* and *new_path* are string/unicode referring to the old and new path of the renamed resources (relative to) the shared folder. - If the path contains non-English characters, an unicode string must be used to pass in the path. - - :param string/unicode service_name: Contains the name of the shared folder. - :param integer/float timeout: Number of seconds that pysmb will wait before raising *SMBTimeout* via the returned *Deferred* instance's *errback* method. - :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a 2-element tuple of ( *old_path*, *new_path* ). - """ - if not self.instance: - raise NotConnectedError('Not connected to server') - - d = defer.Deferred() - self.instance._rename(service_name, old_path, new_path, d.callback, d.errback) - return d - - def echo(self, data, timeout = 10): - """ - Send an echo command containing *data* to the remote SMB/CIFS server. The remote SMB/CIFS will reply with the same *data*. - - :param string data: Data to send to the remote server. - :param integer/float timeout: Number of seconds that pysmb will wait before raising *SMBTimeout* via the returned *Deferred* instance's *errback* method. - :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with the *data* parameter. - """ - if not self.instance: - raise NotConnectedError('Not connected to server') - - d = defer.Deferred() - self.instance._echo(data, d.callback, d.errback, timeout) - return d - - def closeConnection(self): - """ - Disconnect from the remote SMB/CIFS server. The TCP connection will be closed at the earliest opportunity after this method returns. - - :return: None - """ - if not self.instance: - raise NotConnectedError('Not connected to server') - - self.instance.transport.loseConnection() - - # - # ClientFactory methods - # (Do not touch these unless you know what you are doing) - # - - def buildProtocol(self, addr): - p = self.protocol(self.username, self.password, self.my_name, self.remote_name, self.domain, self.use_ntlm_v2, self.sign_options, self.is_direct_tcp) - p.factory = self - return p + +import os, logging, time +from twisted.internet import reactor, defer +from twisted.internet.protocol import ClientFactory, Protocol +from smb_constants import * +from smb_structs import * +from base import SMB, NotConnectedError, NotReadyError, SMBTimeout + + +__all__ = [ 'SMBProtocolFactory', 'NotConnectedError', 'NotReadyError' ] + + +class SMBProtocol(Protocol, SMB): + + log = logging.getLogger('SMB.SMBProtocol') + + # + # Protocol Methods + # + + def connectionMade(self): + self.factory.instance = self + if not self.is_direct_tcp: + self.requestNMBSession() + else: + self.onNMBSessionOK() + + def connectionLost(self, reason): + if self.factory.instance == self: + self.instance = None + + def dataReceived(self, data): + self.feedData(data) + + # + # SMB (and its superclass) Methods + # + + def write(self, data): + self.transport.write(data) + + def onAuthOK(self): + if self.factory.instance == self: + self.factory.onAuthOK() + reactor.callLater(1, self._cleanupPendingRequests) + + def onAuthFailed(self): + if self.factory.instance == self: + self.factory.onAuthFailed() + + def onNMBSessionFailed(self): + self.log.error('Cannot establish NetBIOS session. You might have provided a wrong remote_name') + + # + # Protected Methods + # + + def _cleanupPendingRequests(self): + if self.factory.instance == self: + now = time.time() + to_remove = [] + for mid, r in self.pending_requests.iteritems(): + if r.expiry_time < now: + try: + r.errback(SMBTimeout()) + except Exception: pass + to_remove.append(mid) + + for mid in to_remove: + del self.pending_requests[mid] + + reactor.callLater(1, self._cleanupPendingRequests) + + +class SMBProtocolFactory(ClientFactory): + + protocol = SMBProtocol + log = logging.getLogger('SMB.SMBFactory') + + #: SMB messages will never be signed regardless of remote server's configurations; access errors will occur if the remote server requires signing. + SIGN_NEVER = 0 + #: SMB messages will be signed when remote server supports signing but not requires signing. + SIGN_WHEN_SUPPORTED = 1 + #: SMB messages will only be signed when remote server requires signing. + SIGN_WHEN_REQUIRED = 2 + + def __init__(self, username, password, my_name, remote_name, domain = '', use_ntlm_v2 = True, sign_options = SIGN_WHEN_REQUIRED, is_direct_tcp = False): + """ + Create a new SMBProtocolFactory instance. You will pass this instance to *reactor.connectTCP()* which will then instantiate the TCP connection to the remote SMB/CIFS server. + Note that the default TCP port for most SMB/CIFS servers using NetBIOS over TCP/IP is 139. + Some newer server installations might also support Direct hosting of SMB over TCP/IP; for these servers, the default TCP port is 445. + + *username* and *password* are the user credentials required to authenticate the underlying SMB connection with the remote server. + File operations can only be proceeded after the connection has been authenticated successfully. + + :param string my_name: The local NetBIOS machine name that will identify where this connection is originating from. + You can freely choose a name as long as it contains a maximum of 15 alphanumeric characters and does not contain spaces and any of ``\/:*?";|+``. + :param string remote_name: The NetBIOS machine name of the remote server. + On windows, you can find out the machine name by right-clicking on the "My Computer" and selecting "Properties". + This parameter must be the same as what has been configured on the remote server, or else the connection will be rejected. + :param string domain: The network domain. On windows, it is known as the workgroup. Usually, it is safe to leave this parameter as an empty string. + :param boolean use_ntlm_v2: Indicates whether pysmb should be NTLMv1 or NTLMv2 authentication algorithm for authentication. + The choice of NTLMv1 and NTLMv2 is configured on the remote server, and there is no mechanism to auto-detect which algorithm has been configured. + Hence, we can only "guess" or try both algorithms. + On Sambda, Windows Vista and Windows 7, NTLMv2 is enabled by default. On Windows XP, we can use NTLMv1 before NTLMv2. + :param int sign_options: Determines whether SMB messages will be signed. Default is *SIGN_WHEN_REQUIRED*. + If *SIGN_WHEN_REQUIRED* (value=2), SMB messages will only be signed when remote server requires signing. + If *SIGN_WHEN_SUPPORTED* (value=1), SMB messages will be signed when remote server supports signing but not requires signing. + If *SIGN_NEVER* (value=0), SMB messages will never be signed regardless of remote server's configurations; access errors will occur if the remote server requires signing. + :param boolean is_direct_tcp: Controls whether the NetBIOS over TCP/IP (is_direct_tcp=False) or the newer Direct hosting of SMB over TCP/IP (is_direct_tcp=True) will be used for the communication. + The default parameter is False which will use NetBIOS over TCP/IP for wider compatibility (TCP port: 139). + """ + self.username = username + self.password = password + self.my_name = my_name + self.remote_name = remote_name + self.domain = domain + self.use_ntlm_v2 = use_ntlm_v2 + self.sign_options = sign_options + self.is_direct_tcp = is_direct_tcp + self.instance = None #: The single SMBProtocol instance for each SMBProtocolFactory instance. Usually, you should not need to touch this attribute directly. + + # + # Public Property + # + + @property + def isReady(self): + """A convenient property to return True if the underlying SMB connection has connected to remote server, has successfully authenticated itself and is ready for file operations.""" + return bool(self.instance and self.instance.has_authenticated) + + @property + def isUsingSMB2(self): + """A convenient property to return True if the underlying SMB connection is using SMB2 protocol.""" + return self.instance and self.instance.is_using_smb2 + + # + # Public Methods for Callbacks + # + + def onAuthOK(self): + """ + Override this method in your *SMBProtocolFactory* subclass to add in post-authentication handling. + This method will be called when the server has replied that the SMB connection has been successfully authenticated. + File operations can proceed when this method has been called. + """ + pass + + def onAuthFailed(self): + """ + Override this method in your *SMBProtocolFactory* subclass to add in post-authentication handling. + This method will be called when the server has replied that the SMB connection has been successfully authenticated. + + If you want to retry authenticating from this method, + 1. Disconnect the underlying SMB connection (call ``self.instance.transport.loseConnection()``) + 2. Create a new SMBProtocolFactory subclass instance with different user credientials or different NTLM algorithm flag. + 3. Call ``reactor.connectTCP`` with the new instance to re-establish the SMB connection + """ + pass + + # + # Public Methods + # + + def listShares(self, timeout = 30): + """ + Retrieve a list of shared resources on remote server. + + :param integer/float timeout: Number of seconds that pysmb will wait before raising *SMBTimeout* via the returned *Deferred* instance's *errback* method. + :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a list of :doc:`smb.base.SharedDevice` instances. + """ + if not self.instance: + raise NotConnectedError('Not connected to server') + + d = defer.Deferred() + self.instance._listShares(d.callback, d.errback, timeout) + return d + + def listPath(self, service_name, path, + search = SMB_FILE_ATTRIBUTE_READONLY | SMB_FILE_ATTRIBUTE_HIDDEN | SMB_FILE_ATTRIBUTE_SYSTEM | SMB_FILE_ATTRIBUTE_DIRECTORY | SMB_FILE_ATTRIBUTE_ARCHIVE | SMB_FILE_ATTRIBUTE_INCL_NORMAL, + pattern = '*', timeout = 30): + """ + Retrieve a directory listing of files/folders at *path* + + For simplicity, pysmb defines a "normal" file as a file entry that is not read-only, not hidden, not system, not archive and not a directory. + It ignores other attributes like compression, indexed, sparse, temporary and encryption. + + Note that the default search parameter will query for all read-only (SMB_FILE_ATTRIBUTE_READONLY), hidden (SMB_FILE_ATTRIBUTE_HIDDEN), + system (SMB_FILE_ATTRIBUTE_SYSTEM), archive (SMB_FILE_ATTRIBUTE_ARCHIVE), normal (SMB_FILE_ATTRIBUTE_INCL_NORMAL) files + and directories (SMB_FILE_ATTRIBUTE_DIRECTORY). + If you do not need to include "normal" files in the result, define your own search parameter without the SMB_FILE_ATTRIBUTE_INCL_NORMAL constant. + SMB_FILE_ATTRIBUTE_NORMAL should be used by itself and not be used with other bit constants. + + :param string/unicode service_name: the name of the shared folder for the *path* + :param string/unicode path: path relative to the *service_name* where we are interested to learn about its files/sub-folders. + :param integer search: integer value made up from a bitwise-OR of *SMB_FILE_ATTRIBUTE_xxx* bits (see smb_constants.py). + :param string/unicode pattern: the filter to apply to the results before returning to the client. + :param integer/float timeout: Number of seconds that pysmb will wait before raising *SMBTimeout* via the returned *Deferred* instance's *errback* method. + :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a list of :doc:`smb.base.SharedFile` instances. + """ + if not self.instance: + raise NotConnectedError('Not connected to server') + + d = defer.Deferred() + self.instance._listPath(service_name, path, d.callback, d.errback, search = search, pattern = pattern, timeout = timeout) + return d + + def listSnapshots(self, service_name, path, timeout = 30): + """ + Retrieve a list of available snapshots (a.k.a. shadow copies) for *path*. + + Note that snapshot features are only supported on Windows Vista Business, Enterprise and Ultimate, and on all Windows 7 editions. + + :param string/unicode service_name: the name of the shared folder for the *path* + :param string/unicode path: path relative to the *service_name* where we are interested in the list of available snapshots + :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a list of python *datetime.DateTime* + instances in GMT/UTC time zone + """ + if not self.instance: + raise NotConnectedError('Not connected to server') + + d = defer.Deferred() + self.instance._listSnapshots(service_name, path, d.callback, d.errback, timeout = timeout) + return d + + def getAttributes(self, service_name, path, timeout = 30): + """ + Retrieve information about the file at *path* on the *service_name*. + + :param string/unicode service_name: the name of the shared folder for the *path* + :param string/unicode path: Path of the file on the remote server. If the file cannot be opened for reading, an :doc:`OperationFailure` will be raised. + :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a :doc:`smb.base.SharedFile` instance containing the attributes of the file. + """ + if not self.instance: + raise NotConnectedError('Not connected to server') + + d = defer.Deferred() + self.instance._getAttributes(service_name, path, d.callback, d.errback, timeout = timeout) + return d + + def retrieveFile(self, service_name, path, file_obj, timeout = 30): + """ + Retrieve the contents of the file at *path* on the *service_name* and write these contents to the provided *file_obj*. + + Use *retrieveFileFromOffset()* method if you need to specify the offset to read from the remote *path* and/or the maximum number of bytes to write to the *file_obj*. + + The meaning of the *timeout* parameter will be different from other file operation methods. As the downloaded file usually exceeeds the maximum size + of each SMB/CIFS data message, it will be packetized into a series of request messages (each message will request about about 60kBytes). + The *timeout* parameter is an integer/float value that specifies the timeout interval for these individual SMB/CIFS message to be transmitted and downloaded from the remote SMB/CIFS server. + + :param string/unicode service_name: the name of the shared folder for the *path* + :param string/unicode path: Path of the file on the remote server. If the file cannot be opened for reading, an :doc:`OperationFailure` will be called in the returned *Deferred* errback. + :param file_obj: A file-like object that has a *write* method. Data will be written continuously to *file_obj* until EOF is received from the remote service. + :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a 3-element tuple of ( *file_obj*, file attributes of the file on server, number of bytes written to *file_obj* ). + The file attributes is an integer value made up from a bitwise-OR of *SMB_FILE_ATTRIBUTE_xxx* bits (see smb_constants.py) + """ + return self.retrieveFileFromOffset(service_name, path, file_obj, 0L, -1L, timeout) + + def retrieveFileFromOffset(self, service_name, path, file_obj, offset = 0L, max_length = -1L, timeout = 30): + """ + Retrieve the contents of the file at *path* on the *service_name* and write these contents to the provided *file_obj*. + + The meaning of the *timeout* parameter will be different from other file operation methods. As the downloaded file usually exceeeds the maximum size + of each SMB/CIFS data message, it will be packetized into a series of request messages (each message will request about about 60kBytes). + The *timeout* parameter is an integer/float value that specifies the timeout interval for these individual SMB/CIFS message to be transmitted and downloaded from the remote SMB/CIFS server. + + :param string/unicode service_name: the name of the shared folder for the *path* + :param string/unicode path: Path of the file on the remote server. If the file cannot be opened for reading, an :doc:`OperationFailure` will be called in the returned *Deferred* errback. + :param file_obj: A file-like object that has a *write* method. Data will be written continuously to *file_obj* until EOF is received from the remote service. + :param integer/long offset: the offset in the remote *path* where the first byte will be read and written to *file_obj*. Must be either zero or a positive integer/long value. + :param integer/long max_length: maximum number of bytes to read from the remote *path* and write to the *file_obj*. Specify a negative value to read from *offset* to the EOF. + If zero, the *Deferred* callback is invoked immediately after the file is opened successfully for reading. + :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a 3-element tuple of ( *file_obj*, file attributes of the file on server, number of bytes written to *file_obj* ). + The file attributes is an integer value made up from a bitwise-OR of *SMB_FILE_ATTRIBUTE_xxx* bits (see smb_constants.py) + """ + if not self.instance: + raise NotConnectedError('Not connected to server') + + d = defer.Deferred() + self.instance._retrieveFileFromOffset(service_name, path, file_obj, d.callback, d.errback, offset, max_length, timeout = timeout) + return d + + def storeFile(self, service_name, path, file_obj, timeout = 30): + """ + Store the contents of the *file_obj* at *path* on the *service_name*. + + The meaning of the *timeout* parameter will be different from other file operation methods. As the uploaded file usually exceeeds the maximum size + of each SMB/CIFS data message, it will be packetized into a series of messages (usually about 60kBytes). + The *timeout* parameter is an integer/float value that specifies the timeout interval for these individual SMB/CIFS message to be transmitted and acknowledged + by the remote SMB/CIFS server. + + :param string/unicode service_name: the name of the shared folder for the *path* + :param string/unicode path: Path of the file on the remote server. If the file at *path* does not exist, it will be created. Otherwise, it will be overwritten. + If the *path* refers to a folder or the file cannot be opened for writing, an :doc:`OperationFailure` will be called in the returned *Deferred* errback. + :param file_obj: A file-like object that has a *read* method. Data will read continuously from *file_obj* until EOF. + :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a 2-element tuple of ( *file_obj*, number of bytes uploaded ). + """ + if not self.instance: + raise NotConnectedError('Not connected to server') + + d = defer.Deferred() + self.instance._storeFile(service_name, path, file_obj, d.callback, d.errback, timeout = timeout) + return d + + def deleteFiles(self, service_name, path_file_pattern, timeout = 30): + """ + Delete one or more regular files. It supports the use of wildcards in file names, allowing for deletion of multiple files in a single request. + + :param string/unicode service_name: Contains the name of the shared folder. + :param string/unicode path_file_pattern: The pathname of the file(s) to be deleted, relative to the service_name. + Wildcards may be used in th filename component of the path. + If your path/filename contains non-English characters, you must pass in an unicode string. + :param integer/float timeout: Number of seconds that pysmb will wait before raising *SMBTimeout* via the returned *Deferred* instance's *errback* method. + :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with the *path_file_pattern* parameter. + """ + if not self.instance: + raise NotConnectedError('Not connected to server') + + d = defer.Deferred() + self.instance._deleteFiles(service_name, path_file_pattern, d.callback, d.errback, timeout = timeout) + return d + + def createDirectory(self, service_name, path): + """ + Creates a new directory *path* on the *service_name*. + + :param string/unicode service_name: Contains the name of the shared folder. + :param string/unicode path: The path of the new folder (relative to) the shared folder. + If the path contains non-English characters, an unicode string must be used to pass in the path. + :param integer/float timeout: Number of seconds that pysmb will wait before raising *SMBTimeout* via the returned *Deferred* instance's *errback* method. + :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with the *path* parameter. + """ + if not self.instance: + raise NotConnectedError('Not connected to server') + + d = defer.Deferred() + self.instance._createDirectory(service_name, path, d.callback, d.errback) + return d + + def deleteDirectory(self, service_name, path): + """ + Delete the empty folder at *path* on *service_name* + + :param string/unicode service_name: Contains the name of the shared folder. + :param string/unicode path: The path of the to-be-deleted folder (relative to) the shared folder. + If the path contains non-English characters, an unicode string must be used to pass in the path. + :param integer/float timeout: Number of seconds that pysmb will wait before raising *SMBTimeout* via the returned *Deferred* instance's *errback* method. + :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with the *path* parameter. + """ + if not self.instance: + raise NotConnectedError('Not connected to server') + + d = defer.Deferred() + self.instance._deleteDirectory(service_name, path, d.callback, d.errback) + return d + + def rename(self, service_name, old_path, new_path): + """ + Rename a file or folder at *old_path* to *new_path* shared at *service_name*. Note that this method cannot be used to rename file/folder across different shared folders + + *old_path* and *new_path* are string/unicode referring to the old and new path of the renamed resources (relative to) the shared folder. + If the path contains non-English characters, an unicode string must be used to pass in the path. + + :param string/unicode service_name: Contains the name of the shared folder. + :param integer/float timeout: Number of seconds that pysmb will wait before raising *SMBTimeout* via the returned *Deferred* instance's *errback* method. + :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with a 2-element tuple of ( *old_path*, *new_path* ). + """ + if not self.instance: + raise NotConnectedError('Not connected to server') + + d = defer.Deferred() + self.instance._rename(service_name, old_path, new_path, d.callback, d.errback) + return d + + def echo(self, data, timeout = 10): + """ + Send an echo command containing *data* to the remote SMB/CIFS server. The remote SMB/CIFS will reply with the same *data*. + + :param bytes data: Data to send to the remote server. Must be a bytes object. + :param integer/float timeout: Number of seconds that pysmb will wait before raising *SMBTimeout* via the returned *Deferred* instance's *errback* method. + :return: A *twisted.internet.defer.Deferred* instance. The callback function will be called with the *data* parameter. + """ + if not self.instance: + raise NotConnectedError('Not connected to server') + + d = defer.Deferred() + self.instance._echo(data, d.callback, d.errback, timeout) + return d + + def closeConnection(self): + """ + Disconnect from the remote SMB/CIFS server. The TCP connection will be closed at the earliest opportunity after this method returns. + + :return: None + """ + if not self.instance: + raise NotConnectedError('Not connected to server') + + self.instance.transport.loseConnection() + + # + # ClientFactory methods + # (Do not touch these unless you know what you are doing) + # + + def buildProtocol(self, addr): + p = self.protocol(self.username, self.password, self.my_name, self.remote_name, self.domain, self.use_ntlm_v2, self.sign_options, self.is_direct_tcp) + p.factory = self + return p diff --git a/plugin.video.alfa/lib/sambatools/smb/__init__.py b/plugin.video.alfa/lib/sambatools/smb/__init__.py index d3f5a12f..8b137891 100755 --- a/plugin.video.alfa/lib/sambatools/smb/__init__.py +++ b/plugin.video.alfa/lib/sambatools/smb/__init__.py @@ -1 +1 @@ - + diff --git a/plugin.video.alfa/lib/sambatools/smb/base.py b/plugin.video.alfa/lib/sambatools/smb/base.py index 7de84e12..5dd9c826 100755 --- a/plugin.video.alfa/lib/sambatools/smb/base.py +++ b/plugin.video.alfa/lib/sambatools/smb/base.py @@ -1,2660 +1,2933 @@ -import hmac -from datetime import datetime - -from nmb.base import NMBSession - -import ntlm -import securityblob -from smb2_structs import * -from smb_structs import * -from utils import convertFILETIMEtoEpoch - -try: - import hashlib - sha256 = hashlib.sha256 -except ImportError: - from utils import sha256 - - -class NotReadyError(Exception): - """Raised when SMB connection is not ready (i.e. not authenticated or authentication failed)""" - pass - -class NotConnectedError(Exception): - """Raised when underlying SMB connection has been disconnected or not connected yet""" - pass - -class SMBTimeout(Exception): - """Raised when a timeout has occurred while waiting for a response or for a SMB/CIFS operation to complete.""" - pass - - -def _convert_to_unicode(string): - if not isinstance(string, unicode): - string = unicode(string, "utf-8") - return string - - -class SMB(NMBSession): - """ - This class represents a "connection" to the remote SMB/CIFS server. - It is not meant to be used directly in an application as it does not have any network transport implementations. - - For application use, please refer to - - L{SMBProtocol.SMBProtocolFactory} if you are using Twisted framework - - In [MS-CIFS], this class will contain attributes of Client, Client.Connection and Client.Session abstract data models. - - References: - =========== - - [MS-CIFS]: 3.2.1 - """ - - log = logging.getLogger('SMB.SMB') - - SIGN_NEVER = 0 - SIGN_WHEN_SUPPORTED = 1 - SIGN_WHEN_REQUIRED = 2 - - def __init__(self, username, password, my_name, remote_name, domain = '', use_ntlm_v2 = True, sign_options = SIGN_WHEN_REQUIRED, is_direct_tcp = False): - NMBSession.__init__(self, my_name, remote_name, is_direct_tcp = is_direct_tcp) - self.username = _convert_to_unicode(username) - self.password = _convert_to_unicode(password) - self.domain = _convert_to_unicode(domain) - self.sign_options = sign_options - self.is_direct_tcp = is_direct_tcp - self.use_ntlm_v2 = use_ntlm_v2 #: Similar to LMAuthenticationPolicy and NTAuthenticationPolicy as described in [MS-CIFS] 3.2.1.1 - self.smb_message = SMBMessage() - self.is_using_smb2 = False #: Are we communicating using SMB2 protocol? self.smb_message will be a SMB2Message instance if this flag is True - self.pending_requests = { } #: MID mapped to _PendingRequest instance - self.connected_trees = { } #: Share name mapped to TID - self.next_rpc_call_id = 1 #: Next RPC callID value. Not used directly in SMB message. Usually encapsulated in sub-commands under SMB_COM_TRANSACTION or SMB_COM_TRANSACTION2 messages - - self.has_negotiated = False - self.has_authenticated = False - self.is_signing_active = False #: True if the remote server accepts message signing. All outgoing messages will be signed. Simiar to IsSigningActive as described in [MS-CIFS] 3.2.1.2 - self.signing_session_key = None #: Session key for signing packets, if signing is active. Similar to SigningSessionKey as described in [MS-CIFS] 3.2.1.2 - self.signing_challenge_response = None #: Contains the challenge response for signing, if signing is active. Similar to SigningChallengeResponse as described in [MS-CIFS] 3.2.1.2 - self.mid = 0 - self.uid = 0 - self.next_signing_id = 2 #: Similar to ClientNextSendSequenceNumber as described in [MS-CIFS] 3.2.1.2 - - # SMB1 and SMB2 attributes - # Note that the interpretations of the values may differ between SMB1 and SMB2 protocols - self.capabilities = 0 - self.security_mode = 0 #: Initialized from the SecurityMode field of the SMB_COM_NEGOTIATE message - - # SMB1 attributes - # Most of the following attributes will be initialized upon receipt of SMB_COM_NEGOTIATE message from server (via self._updateServerInfo_SMB1 method) - self.use_plaintext_authentication = False #: Similar to PlaintextAuthenticationPolicy in in [MS-CIFS] 3.2.1.1 - self.max_raw_size = 0 - self.max_buffer_size = 0 #: Similar to MaxBufferSize as described in [MS-CIFS] 3.2.1.1 - self.max_mpx_count = 0 #: Similar to MaxMpxCount as described in [MS-CIFS] 3.2.1.1 - - # SMB2 attributes - self.max_read_size = 0 #: Similar to MaxReadSize as described in [MS-SMB2] 2.2.4 - self.max_write_size = 0 #: Similar to MaxWriteSize as described in [MS-SMB2] 2.2.4 - self.max_transact_size = 0 #: Similar to MaxTransactSize as described in [MS-SMB2] 2.2.4 - self.session_id = 0 #: Similar to SessionID as described in [MS-SMB2] 2.2.4. This will be set in _updateState_SMB2 method - - self._setupSMB1Methods() - - self.log.info('Authentication with remote machine "%s" for user "%s" will be using NTLM %s authentication (%s extended security)', - self.remote_name, self.username, - (self.use_ntlm_v2 and 'v2') or 'v1', - (SUPPORT_EXTENDED_SECURITY and 'with') or 'without') - - - # - # NMBSession Methods - # - - def onNMBSessionOK(self): - self._sendSMBMessage(SMBMessage(ComNegotiateRequest())) - - def onNMBSessionFailed(self): - pass - - def onNMBSessionMessage(self, flags, data): - while True: - try: - i = self.smb_message.decode(data) - except SMB2ProtocolHeaderError: - self.log.info('Now switching over to SMB2 protocol communication') - self.is_using_smb2 = True - self.mid = 0 # Must reset messageID counter, or else remote SMB2 server will disconnect - self._setupSMB2Methods() - self.smb_message = self._klassSMBMessage() - i = self.smb_message.decode(data) - - next_message_offset = 0 - if self.is_using_smb2: - next_message_offset = self.smb_message.next_command_offset - - if i > 0: - if not self.is_using_smb2: - self.log.debug('Received SMB message "%s" (command:0x%2X flags:0x%02X flags2:0x%04X TID:%d UID:%d)', - SMB_COMMAND_NAMES.get(self.smb_message.command, ''), - self.smb_message.command, self.smb_message.flags, self.smb_message.flags2, self.smb_message.tid, self.smb_message.uid) - else: - self.log.debug('Received SMB2 message "%s" (command:0x%04X flags:0x%04x)', - SMB2_COMMAND_NAMES.get(self.smb_message.command, ''), - self.smb_message.command, self.smb_message.flags) - if self._updateState(self.smb_message): - # We need to create a new instance instead of calling reset() because the instance could be captured in the message history. - self.smb_message = self._klassSMBMessage() - - if next_message_offset > 0: - data = data[next_message_offset:] - else: - break - - # - # Public Methods for Overriding in Subclasses - # - - def onAuthOK(self): - pass - - def onAuthFailed(self): - pass - - # - # Protected Methods - # - - def _setupSMB1Methods(self): - self._klassSMBMessage = SMBMessage - self._updateState = self._updateState_SMB1 - self._updateServerInfo = self._updateServerInfo_SMB1 - self._handleNegotiateResponse = self._handleNegotiateResponse_SMB1 - self._sendSMBMessage = self._sendSMBMessage_SMB1 - self._handleSessionChallenge = self._handleSessionChallenge_SMB1 - self._listShares = self._listShares_SMB1 - self._listPath = self._listPath_SMB1 - self._listSnapshots = self._listSnapshots_SMB1 - self._getAttributes = self._getAttributes_SMB1 - self._retrieveFile = self._retrieveFile_SMB1 - self._retrieveFileFromOffset = self._retrieveFileFromOffset_SMB1 - self._storeFile = self._storeFile_SMB1 - self._storeFileFromOffset = self._storeFileFromOffset_SMB1 - self._deleteFiles = self._deleteFiles_SMB1 - self._resetFileAttributes = self._resetFileAttributes_SMB1 - self._createDirectory = self._createDirectory_SMB1 - self._deleteDirectory = self._deleteDirectory_SMB1 - self._rename = self._rename_SMB1 - self._echo = self._echo_SMB1 - - def _setupSMB2Methods(self): - self._klassSMBMessage = SMB2Message - self._updateState = self._updateState_SMB2 - self._updateServerInfo = self._updateServerInfo_SMB2 - self._handleNegotiateResponse = self._handleNegotiateResponse_SMB2 - self._sendSMBMessage = self._sendSMBMessage_SMB2 - self._handleSessionChallenge = self._handleSessionChallenge_SMB2 - self._listShares = self._listShares_SMB2 - self._listPath = self._listPath_SMB2 - self._listSnapshots = self._listSnapshots_SMB2 - self._getAttributes = self._getAttributes_SMB2 - self._retrieveFile = self._retrieveFile_SMB2 - self._retrieveFileFromOffset = self._retrieveFileFromOffset_SMB2 - self._storeFile = self._storeFile_SMB2 - self._storeFileFromOffset = self._storeFileFromOffset_SMB2 - self._deleteFiles = self._deleteFiles_SMB2 - self._resetFileAttributes = self._resetFileAttributes_SMB2 - self._createDirectory = self._createDirectory_SMB2 - self._deleteDirectory = self._deleteDirectory_SMB2 - self._rename = self._rename_SMB2 - self._echo = self._echo_SMB2 - - def _getNextRPCCallID(self): - self.next_rpc_call_id += 1 - return self.next_rpc_call_id - - # - # SMB2 Methods Family - # - - def _sendSMBMessage_SMB2(self, smb_message): - if smb_message.mid == 0: - smb_message.mid = self._getNextMID_SMB2() - - if smb_message.command != SMB2_COM_NEGOTIATE and smb_message.command != SMB2_COM_ECHO: - smb_message.session_id = self.session_id - - if self.is_signing_active: - smb_message.flags |= SMB2_FLAGS_SIGNED - raw_data = smb_message.encode() - smb_message.signature = hmac.new(self.signing_session_key, raw_data, sha256).digest()[:16] - - smb_message.raw_data = smb_message.encode() - self.log.debug('MID is %d. Signature is %s. Total raw message is %d bytes', smb_message.mid, binascii.hexlify(smb_message.signature), len(smb_message.raw_data)) - else: - smb_message.raw_data = smb_message.encode() - self.sendNMBMessage(smb_message.raw_data) - - def _getNextMID_SMB2(self): - self.mid += 1 - return self.mid - - def _updateState_SMB2(self, message): - if message.isReply: - if message.command == SMB2_COM_NEGOTIATE: - if message.status == 0: - self.has_negotiated = True - self.log.info('SMB2 dialect negotiation successful') - self._updateServerInfo(message.payload) - self._handleNegotiateResponse(message) - else: - raise ProtocolError('Unknown status value (0x%08X) in SMB2_COM_NEGOTIATE' % message.status, - message.raw_data, message) - elif message.command == SMB2_COM_SESSION_SETUP: - if message.status == 0: - self.session_id = message.session_id - try: - result = securityblob.decodeAuthResponseSecurityBlob(message.payload.security_blob) - if result == securityblob.RESULT_ACCEPT_COMPLETED: - self.has_authenticated = True - self.log.info('Authentication (on SMB2) successful!') - self.onAuthOK() - else: - raise ProtocolError('SMB2_COM_SESSION_SETUP status is 0 but security blob negResult value is %d' % result, message.raw_data, message) - except securityblob.BadSecurityBlobError, ex: - raise ProtocolError(str(ex), message.raw_data, message) - elif message.status == 0xc0000016: # STATUS_MORE_PROCESSING_REQUIRED - self.session_id = message.session_id - try: - result, ntlm_token = securityblob.decodeChallengeSecurityBlob(message.payload.security_blob) - if result == securityblob.RESULT_ACCEPT_INCOMPLETE: - self._handleSessionChallenge(message, ntlm_token) - except ( securityblob.BadSecurityBlobError, securityblob.UnsupportedSecurityProvider ), ex: - raise ProtocolError(str(ex), message.raw_data, message) - elif message.status == 0xc000006d: # STATUS_LOGON_FAILURE - self.has_authenticated = False - self.log.info('Authentication (on SMB2) failed. Please check username and password.') - self.onAuthFailed() - else: - raise ProtocolError('Unknown status value (0x%08X) in SMB_COM_SESSION_SETUP_ANDX (with extended security)' % message.status, - message.raw_data, message) - - req = self.pending_requests.pop(message.mid, None) - if req: - req.callback(message, **req.kwargs) - return True - - - def _updateServerInfo_SMB2(self, payload): - self.capabilities = payload.capabilities - self.security_mode = payload.security_mode - self.max_transact_size = payload.max_transact_size - self.max_read_size = payload.max_read_size - self.max_write_size = payload.max_write_size - self.use_plaintext_authentication = False # SMB2 never allows plaintext authentication - - - def _handleNegotiateResponse_SMB2(self, message): - ntlm_data = ntlm.generateNegotiateMessage() - blob = securityblob.generateNegotiateSecurityBlob(ntlm_data) - self._sendSMBMessage(SMB2Message(SMB2SessionSetupRequest(blob))) - - - def _handleSessionChallenge_SMB2(self, message, ntlm_token): - server_challenge, server_flags, server_info = ntlm.decodeChallengeMessage(ntlm_token) - - self.log.info('Performing NTLMv2 authentication (on SMB2) with server challenge "%s"', binascii.hexlify(server_challenge)) - - if self.use_ntlm_v2: - self.log.info('Performing NTLMv2 authentication (on SMB2) with server challenge "%s"', binascii.hexlify(server_challenge)) - nt_challenge_response, lm_challenge_response, session_key = ntlm.generateChallengeResponseV2(self.password, - self.username, - server_challenge, - server_info, - self.domain) - - else: - self.log.info('Performing NTLMv1 authentication (on SMB2) with server challenge "%s"', binascii.hexlify(server_challenge)) - nt_challenge_response, lm_challenge_response, session_key = ntlm.generateChallengeResponseV1(self.password, server_challenge, True) - - ntlm_data = ntlm.generateAuthenticateMessage(server_flags, - nt_challenge_response, - lm_challenge_response, - session_key, - self.username, - self.domain) - - if self.log.isEnabledFor(logging.DEBUG): - self.log.debug('NT challenge response is "%s" (%d bytes)', binascii.hexlify(nt_challenge_response), len(nt_challenge_response)) - self.log.debug('LM challenge response is "%s" (%d bytes)', binascii.hexlify(lm_challenge_response), len(lm_challenge_response)) - - blob = securityblob.generateAuthSecurityBlob(ntlm_data) - self._sendSMBMessage(SMB2Message(SMB2SessionSetupRequest(blob))) - - if self.security_mode & SMB2_NEGOTIATE_SIGNING_REQUIRED: - self.log.info('Server requires all SMB messages to be signed') - self.is_signing_active = (self.sign_options != SMB.SIGN_NEVER) - elif self.security_mode & SMB2_NEGOTIATE_SIGNING_ENABLED: - self.log.info('Server supports SMB signing') - self.is_signing_active = (self.sign_options == SMB.SIGN_WHEN_SUPPORTED) - else: - self.is_signing_active = False - - if self.is_signing_active: - self.log.info("SMB signing activated. All SMB messages will be signed.") - self.signing_session_key = (session_key + '\0'*16)[:16] - if self.capabilities & CAP_EXTENDED_SECURITY: - self.signing_challenge_response = None - else: - self.signing_challenge_response = blob - else: - self.log.info("SMB signing deactivated. SMB messages will NOT be signed.") - - - def _listShares_SMB2(self, callback, errback, timeout = 30): - if not self.has_authenticated: - raise NotReadyError('SMB connection not authenticated') - - expiry_time = time.time() + timeout - path = 'IPC$' - messages_history = [ ] - - def connectSrvSvc(tid): - m = SMB2Message(SMB2CreateRequest('srvsvc', - file_attributes = 0, - access_mask = FILE_READ_DATA | FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_READ_EA | FILE_WRITE_EA | READ_CONTROL | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | SYNCHRONIZE, - share_access = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - oplock = SMB2_OPLOCK_LEVEL_NONE, - impersonation = SEC_IMPERSONATE, - create_options = FILE_NON_DIRECTORY_FILE | FILE_OPEN_NO_RECALL, - create_disp = FILE_OPEN)) - - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectSrvSvcCB, errback) - messages_history.append(m) - - def connectSrvSvcCB(create_message, **kwargs): - messages_history.append(create_message) - if create_message.status == 0: - call_id = self._getNextRPCCallID() - # The data_bytes are binding call to Server Service RPC using DCE v1.1 RPC over SMB. See [MS-SRVS] and [C706] - # If you wish to understand the meanings of the byte stream, I would suggest you use a recent version of WireShark to packet capture the stream - data_bytes = \ - binascii.unhexlify("""05 00 0b 03 10 00 00 00 74 00 00 00""".replace(' ', '')) + \ - struct.pack(' data_length: - return data_bytes[offset:] - - next_offset, _, \ - create_time, last_access_time, last_write_time, last_attr_change_time, \ - file_size, alloc_size, file_attributes, filename_length, ea_size, \ - short_name_length, _, short_name = struct.unpack(info_format, data_bytes[offset:offset+info_size]) - - offset2 = offset + info_size - if offset2 + filename_length > data_length: - return data_bytes[offset:] - - filename = data_bytes[offset2:offset2+filename_length].decode('UTF-16LE') - short_name = short_name.decode('UTF-16LE') - results.append(SharedFile(convertFILETIMEtoEpoch(create_time), convertFILETIMEtoEpoch(last_access_time), - convertFILETIMEtoEpoch(last_write_time), convertFILETIMEtoEpoch(last_attr_change_time), - file_size, alloc_size, file_attributes, short_name, filename)) - - if next_offset: - offset += next_offset - else: - break - return '' - - def closeFid(tid, fid, results = None, error = None): - m = SMB2Message(SMB2CloseRequest(fid)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, closeCB, errback, results = results, error = error) - messages_history.append(m) - - def closeCB(close_message, **kwargs): - if kwargs['results'] is not None: - callback(kwargs['results']) - elif kwargs['error'] is not None: - errback(OperationFailure('Failed to list %s on %s: Query failed with errorcode 0x%08x' % ( path, service_name, kwargs['error'] ), messages_history)) - - if not self.connected_trees.has_key(service_name): - def connectCB(connect_message, **kwargs): - messages_history.append(connect_message) - if connect_message.status == 0: - self.connected_trees[service_name] = connect_message.tid - sendCreate(connect_message.tid) - else: - errback(OperationFailure('Failed to list %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) - - m = SMB2Message(SMB2TreeConnectRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ))) - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectCB, errback, path = service_name) - messages_history.append(m) - else: - sendCreate(self.connected_trees[service_name]) - - def _getAttributes_SMB2(self, service_name, path, callback, errback, timeout = 30): - if not self.has_authenticated: - raise NotReadyError('SMB connection not authenticated') - - expiry_time = time.time() + timeout - path = path.replace('/', '\\') - if path.startswith('\\'): - path = path[1:] - if path.endswith('\\'): - path = path[:-1] - messages_history = [ ] - - def sendCreate(tid): - create_context_data = binascii.unhexlify(""" -28 00 00 00 10 00 04 00 00 00 18 00 10 00 00 00 -44 48 6e 51 00 00 00 00 00 00 00 00 00 00 00 00 -00 00 00 00 00 00 00 00 18 00 00 00 10 00 04 00 -00 00 18 00 00 00 00 00 4d 78 41 63 00 00 00 00 -00 00 00 00 10 00 04 00 00 00 18 00 00 00 00 00 -51 46 69 64 00 00 00 00 -""".replace(' ', '').replace('\n', '')) - m = SMB2Message(SMB2CreateRequest(path, - file_attributes = 0, - access_mask = FILE_READ_DATA | FILE_READ_EA | FILE_READ_ATTRIBUTES | SYNCHRONIZE, - share_access = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - oplock = SMB2_OPLOCK_LEVEL_NONE, - impersonation = SEC_IMPERSONATE, - create_options = 0, - create_disp = FILE_OPEN, - create_context_data = create_context_data)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, createCB, errback) - messages_history.append(m) - - def createCB(create_message, **kwargs): - messages_history.append(create_message) - if create_message.status == 0: - p = create_message.payload - info = SharedFile(p.create_time, p.lastaccess_time, p.lastwrite_time, p.change_time, - p.file_size, p.allocation_size, p.file_attributes, - unicode(path), unicode(path)) - closeFid(create_message.tid, p.fid, info = info) - else: - errback(OperationFailure('Failed to get attributes for %s on %s: Unable to open remote file object' % ( path, service_name ), messages_history)) - - def closeFid(tid, fid, info = None, error = None): - m = SMB2Message(SMB2CloseRequest(fid)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, closeCB, errback, info = info, error = error) - messages_history.append(m) - - def closeCB(close_message, **kwargs): - if kwargs['info'] is not None: - callback(kwargs['info']) - elif kwargs['error'] is not None: - errback(OperationFailure('Failed to get attributes for %s on %s: Query failed with errorcode 0x%08x' % ( path, service_name, kwargs['error'] ), messages_history)) - - if not self.connected_trees.has_key(service_name): - def connectCB(connect_message, **kwargs): - messages_history.append(connect_message) - if connect_message.status == 0: - self.connected_trees[service_name] = connect_message.tid - sendCreate(connect_message.tid) - else: - errback(OperationFailure('Failed to get attributes for %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) - - m = SMB2Message(SMB2TreeConnectRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ))) - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectCB, errback, path = service_name) - messages_history.append(m) - else: - sendCreate(self.connected_trees[service_name]) - - def _retrieveFile_SMB2(self, service_name, path, file_obj, callback, errback, timeout = 30): - return self._retrieveFileFromOffset(service_name, path, file_obj, callback, errback, 0L, -1L, timeout) - - def _retrieveFileFromOffset_SMB2(self, service_name, path, file_obj, callback, errback, starting_offset, max_length, timeout = 30): - if not self.has_authenticated: - raise NotReadyError('SMB connection not authenticated') - - expiry_time = time.time() + timeout - path = path.replace('/', '\\') - if path.startswith('\\'): - path = path[1:] - if path.endswith('\\'): - path = path[:-1] - messages_history = [ ] - results = [ ] - - def sendCreate(tid): - create_context_data = binascii.unhexlify(""" -28 00 00 00 10 00 04 00 00 00 18 00 10 00 00 00 -44 48 6e 51 00 00 00 00 00 00 00 00 00 00 00 00 -00 00 00 00 00 00 00 00 18 00 00 00 10 00 04 00 -00 00 18 00 00 00 00 00 4d 78 41 63 00 00 00 00 -00 00 00 00 10 00 04 00 00 00 18 00 00 00 00 00 -51 46 69 64 00 00 00 00 -""".replace(' ', '').replace('\n', '')) - m = SMB2Message(SMB2CreateRequest(path, - file_attributes = 0, - access_mask = FILE_READ_DATA | FILE_READ_EA | FILE_READ_ATTRIBUTES | READ_CONTROL | SYNCHRONIZE, - share_access = FILE_SHARE_READ, - oplock = SMB2_OPLOCK_LEVEL_NONE, - impersonation = SEC_IMPERSONATE, - create_options = FILE_SEQUENTIAL_ONLY | FILE_NON_DIRECTORY_FILE, - create_disp = FILE_OPEN, - create_context_data = create_context_data)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, createCB, errback, tid = tid) - messages_history.append(m) - - def createCB(create_message, **kwargs): - messages_history.append(create_message) - if create_message.status == 0: - m = SMB2Message(SMB2QueryInfoRequest(create_message.payload.fid, - flags = 0, - additional_info = 0, - info_type = SMB2_INFO_FILE, - file_info_class = 0x16, # FileStreamInformation [MS-FSCC] 2.4 - input_buf = '', - output_buf_len = 4096)) - m.tid = create_message.tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, infoCB, errback, - fid = create_message.payload.fid, file_attributes = create_message.payload.file_attributes) - messages_history.append(m) - else: - errback(OperationFailure('Failed to list %s on %s: Unable to open file' % ( path, service_name ), messages_history)) - - def infoCB(info_message, **kwargs): - messages_history.append(info_message) - if info_message.status == 0: - file_len = struct.unpack(' file_len: - closeFid(info_message.tid, kwargs['fid']) - callback(( file_obj, kwargs['file_attributes'], 0 )) # Note that this is a tuple of 3-elements - else: - remaining_len = max_length - if remaining_len < 0: - remaining_len = file_len - if starting_offset + remaining_len > file_len: - remaining_len = file_len - starting_offset - sendRead(info_message.tid, kwargs['fid'], starting_offset, remaining_len, 0, kwargs['file_attributes']) - else: - errback(OperationFailure('Failed to list %s on %s: Unable to retrieve information on file' % ( path, service_name ), messages_history)) - - def sendRead(tid, fid, offset, remaining_len, read_len, file_attributes): - read_count = min(self.max_read_size, remaining_len) - m = SMB2Message(SMB2ReadRequest(fid, offset, read_count)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, readCB, errback, - fid = fid, offset = offset, - remaining_len = remaining_len, - read_len = read_len, - file_attributes = file_attributes) - - def readCB(read_message, **kwargs): - # To avoid crazy memory usage when retrieving large files, we do not save every read_message in messages_history. - if read_message.status == 0: - data_len = read_message.payload.data_length - file_obj.write(read_message.payload.data) - - remaining_len = kwargs['remaining_len'] - data_len - - if remaining_len > 0: - sendRead(read_message.tid, kwargs['fid'], kwargs['offset'] + data_len, remaining_len, kwargs['read_len'] + data_len, kwargs['file_attributes']) - else: - closeFid(read_message.tid, kwargs['fid'], ret = ( file_obj, kwargs['file_attributes'], kwargs['read_len'] + data_len )) - else: - messages_history.append(read_message) - closeFid(read_message.tid, kwargs['fid'], error = read_message.status) - - def closeFid(tid, fid, ret = None, error = None): - m = SMB2Message(SMB2CloseRequest(fid)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, closeCB, errback, ret = ret, error = error) - messages_history.append(m) - - def closeCB(close_message, **kwargs): - if kwargs['ret'] is not None: - callback(kwargs['ret']) - elif kwargs['error'] is not None: - errback(OperationFailure('Failed to retrieve %s on %s: Read failed' % ( path, service_name ), messages_history)) - - if not self.connected_trees.has_key(service_name): - def connectCB(connect_message, **kwargs): - messages_history.append(connect_message) - if connect_message.status == 0: - self.connected_trees[service_name] = connect_message.tid - sendCreate(connect_message.tid) - else: - errback(OperationFailure('Failed to retrieve %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) - - m = SMB2Message(SMB2TreeConnectRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ))) - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectCB, errback, path = service_name) - messages_history.append(m) - else: - sendCreate(self.connected_trees[service_name]) - - def _storeFile_SMB2(self, service_name, path, file_obj, callback, errback, timeout = 30): - self._storeFileFromOffset_SMB2(service_name, path, file_obj, callback, errback, 0L, True, timeout) - - def _storeFileFromOffset_SMB2(self, service_name, path, file_obj, callback, errback, starting_offset, truncate = False, timeout = 30): - if not self.has_authenticated: - raise NotReadyError('SMB connection not authenticated') - - path = path.replace('/', '\\') - if path.startswith('\\'): - path = path[1:] - if path.endswith('\\'): - path = path[:-1] - messages_history = [ ] - - def sendCreate(tid): - create_context_data = binascii.unhexlify(""" -28 00 00 00 10 00 04 00 00 00 18 00 10 00 00 00 -44 48 6e 51 00 00 00 00 00 00 00 00 00 00 00 00 -00 00 00 00 00 00 00 00 20 00 00 00 10 00 04 00 -00 00 18 00 08 00 00 00 41 6c 53 69 00 00 00 00 -85 62 00 00 00 00 00 00 18 00 00 00 10 00 04 00 -00 00 18 00 00 00 00 00 4d 78 41 63 00 00 00 00 -00 00 00 00 10 00 04 00 00 00 18 00 00 00 00 00 -51 46 69 64 00 00 00 00 -""".replace(' ', '').replace('\n', '')) - m = SMB2Message(SMB2CreateRequest(path, - file_attributes = ATTR_ARCHIVE, - access_mask = FILE_READ_DATA | FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | FILE_READ_EA | FILE_WRITE_EA | READ_CONTROL | SYNCHRONIZE, - share_access = 0, - oplock = SMB2_OPLOCK_LEVEL_NONE, - impersonation = SEC_IMPERSONATE, - create_options = FILE_SEQUENTIAL_ONLY | FILE_NON_DIRECTORY_FILE, - create_disp = FILE_OVERWRITE_IF if truncate else FILE_OPEN_IF, - create_context_data = create_context_data)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, createCB, errback, tid = tid) - messages_history.append(m) - - def createCB(create_message, **kwargs): - messages_history.append(create_message) - if create_message.status == 0: - sendWrite(create_message.tid, create_message.payload.fid, starting_offset) - else: - errback(OperationFailure('Failed to store %s on %s: Unable to open file' % ( path, service_name ), messages_history)) - - def sendWrite(tid, fid, offset): - write_count = self.max_write_size - data = file_obj.read(write_count) - data_len = len(data) - if data_len > 0: - m = SMB2Message(SMB2WriteRequest(fid, data, offset)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, writeCB, errback, fid = fid, offset = offset+data_len) - else: - closeFid(tid, fid, offset = offset) - - def writeCB(write_message, **kwargs): - # To avoid crazy memory usage when saving large files, we do not save every write_message in messages_history. - if write_message.status == 0: - sendWrite(write_message.tid, kwargs['fid'], kwargs['offset']) - else: - messages_history.append(write_message) - closeFid(write_message.tid, kwargs['fid']) - errback(OperationFailure('Failed to store %s on %s: Write failed' % ( path, service_name ), messages_history)) - - def closeFid(tid, fid, error = None, offset = None): - m = SMB2Message(SMB2CloseRequest(fid)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, closeCB, errback, fid = fid, offset = offset, error = error) - messages_history.append(m) - - def closeCB(close_message, **kwargs): - if kwargs['offset'] is not None: - callback(( file_obj, kwargs['offset'] )) # Note that this is a tuple of 2-elements - elif kwargs['error'] is not None: - errback(OperationFailure('Failed to store %s on %s: Write failed' % ( path, service_name ), messages_history)) - - if not self.connected_trees.has_key(service_name): - def connectCB(connect_message, **kwargs): - messages_history.append(connect_message) - if connect_message.status == 0: - self.connected_trees[service_name] = connect_message.tid - sendCreate(connect_message.tid) - else: - errback(OperationFailure('Failed to store %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) - - m = SMB2Message(SMB2TreeConnectRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ))) - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, connectCB, errback, path = service_name) - messages_history.append(m) - else: - sendCreate(self.connected_trees[service_name]) - - - def _deleteFiles_SMB2(self, service_name, path_file_pattern, callback, errback, timeout = 30): - if not self.has_authenticated: - raise NotReadyError('SMB connection not authenticated') - - expiry_time = time.time() + timeout - path = path_file_pattern.replace('/', '\\') - if path.startswith('\\'): - path = path[1:] - if path.endswith('\\'): - path = path[:-1] - messages_history = [ ] - - def sendCreate(tid): - create_context_data = binascii.unhexlify(""" -28 00 00 00 10 00 04 00 00 00 18 00 10 00 00 00 -44 48 6e 51 00 00 00 00 00 00 00 00 00 00 00 00 -00 00 00 00 00 00 00 00 18 00 00 00 10 00 04 00 -00 00 18 00 00 00 00 00 4d 78 41 63 00 00 00 00 -00 00 00 00 10 00 04 00 00 00 18 00 00 00 00 00 -51 46 69 64 00 00 00 00 -""".replace(' ', '').replace('\n', '')) - m = SMB2Message(SMB2CreateRequest(path, - file_attributes = 0, - access_mask = DELETE | FILE_READ_ATTRIBUTES, - share_access = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - oplock = SMB2_OPLOCK_LEVEL_NONE, - impersonation = SEC_IMPERSONATE, - create_options = FILE_NON_DIRECTORY_FILE, - create_disp = FILE_OPEN, - create_context_data = create_context_data)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, createCB, errback, tid = tid) - messages_history.append(m) - - def createCB(open_message, **kwargs): - messages_history.append(open_message) - if open_message.status == 0: - sendDelete(open_message.tid, open_message.payload.fid) - else: - errback(OperationFailure('Failed to delete %s on %s: Unable to open file' % ( path, service_name ), messages_history)) - - def sendDelete(tid, fid): - m = SMB2Message(SMB2SetInfoRequest(fid, - additional_info = 0, - info_type = SMB2_INFO_FILE, - file_info_class = 0x0d, # SMB2_FILE_DISPOSITION_INFO - data = '\x01')) - ''' - Resources: - https://msdn.microsoft.com/en-us/library/cc246560.aspx - https://msdn.microsoft.com/en-us/library/cc232098.aspx - ''' - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, deleteCB, errback, fid = fid) - messages_history.append(m) - - def deleteCB(delete_message, **kwargs): - messages_history.append(delete_message) - if delete_message.status == 0: - closeFid(delete_message.tid, kwargs['fid'], status = 0) - else: - closeFid(delete_message.tid, kwargs['fid'], status = delete_message.status) - - def closeFid(tid, fid, status = None): - m = SMB2Message(SMB2CloseRequest(fid)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, closeCB, errback, status = status) - messages_history.append(m) - - def closeCB(close_message, **kwargs): - if kwargs['status'] == 0: - callback(path_file_pattern) - else: - errback(OperationFailure('Failed to delete %s on %s: Delete failed' % ( path, service_name ), messages_history)) - - if not self.connected_trees.has_key(service_name): - def connectCB(connect_message, **kwargs): - messages_history.append(connect_message) - if connect_message.status == 0: - self.connected_trees[service_name] = connect_message.tid - sendCreate(connect_message.tid) - else: - errback(OperationFailure('Failed to delete %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) - - m = SMB2Message(SMB2TreeConnectRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ))) - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectCB, errback, path = service_name) - messages_history.append(m) - else: - sendCreate(self.connected_trees[service_name]) - - def _resetFileAttributes_SMB2(self, service_name, path_file_pattern, callback, errback, timeout = 30): - if not self.has_authenticated: - raise NotReadyError('SMB connection not authenticated') - - expiry_time = time.time() + timeout - path = path_file_pattern.replace('/', '\\') - if path.startswith('\\'): - path = path[1:] - if path.endswith('\\'): - path = path[:-1] - messages_history = [ ] - - def sendCreate(tid): - create_context_data = binascii.unhexlify(""" -28 00 00 00 10 00 04 00 00 00 18 00 10 00 00 00 -44 48 6e 51 00 00 00 00 00 00 00 00 00 00 00 00 -00 00 00 00 00 00 00 00 18 00 00 00 10 00 04 00 -00 00 18 00 00 00 00 00 4d 78 41 63 00 00 00 00 -00 00 00 00 10 00 04 00 00 00 18 00 00 00 00 00 -51 46 69 64 00 00 00 00 -""".replace(' ', '').replace('\n', '')) - - m = SMB2Message(SMB2CreateRequest(path, - file_attributes = 0, - access_mask = FILE_WRITE_ATTRIBUTES, - share_access = FILE_SHARE_READ | FILE_SHARE_WRITE, - oplock = SMB2_OPLOCK_LEVEL_NONE, - impersonation = SEC_IMPERSONATE, - create_options = 0, - create_disp = FILE_OPEN, - create_context_data = create_context_data)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, createCB, errback, tid = tid) - messages_history.append(m) - - def createCB(open_message, **kwargs): - messages_history.append(open_message) - if open_message.status == 0: - sendReset(open_message.tid, open_message.payload.fid) - else: - errback(OperationFailure('Failed to reset attributes of %s on %s: Unable to open file' % ( path, service_name ), messages_history)) - - def sendReset(tid, fid): - m = SMB2Message(SMB2SetInfoRequest(fid, - additional_info = 0, - info_type = SMB2_INFO_FILE, - file_info_class = 4, # FileBasicInformation - data = struct.pack('qqqqii',0,0,0,0,0x80,0))) # FILE_ATTRIBUTE_NORMAL - ''' - Resources: - https://msdn.microsoft.com/en-us/library/cc246560.aspx - https://msdn.microsoft.com/en-us/library/cc232064.aspx - https://msdn.microsoft.com/en-us/library/cc232094.aspx - https://msdn.microsoft.com/en-us/library/cc232110.aspx - ''' - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, resetCB, errback, fid = fid) - messages_history.append(m) - - def resetCB(reset_message, **kwargs): - messages_history.append(reset_message) - if reset_message.status == 0: - closeFid(reset_message.tid, kwargs['fid'], status = 0) - else: - closeFid(reset_message.tid, kwargs['fid'], status = reset_message.status) - - def closeFid(tid, fid, status = None): - m = SMB2Message(SMB2CloseRequest(fid)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, closeCB, errback, status = status) - messages_history.append(m) - - def closeCB(close_message, **kwargs): - if kwargs['status'] == 0: - callback(path_file_pattern) - else: - errback(OperationFailure('Failed to reset attributes of %s on %s: Reset failed' % ( path, service_name ), messages_history)) - - if not self.connected_trees.has_key(service_name): - def connectCB(connect_message, **kwargs): - messages_history.append(connect_message) - if connect_message.status == 0: - self.connected_trees[service_name] = connect_message.tid - sendCreate(connect_message.tid) - else: - errback(OperationFailure('Failed to reset attributes of %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) - - m = SMB2Message(SMB2TreeConnectRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ))) - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectCB, errback, path = service_name) - messages_history.append(m) - else: - sendCreate(self.connected_trees[service_name]) - - def _createDirectory_SMB2(self, service_name, path, callback, errback, timeout = 30): - if not self.has_authenticated: - raise NotReadyError('SMB connection not authenticated') - - expiry_time = time.time() + timeout - path = path.replace('/', '\\') - if path.startswith('\\'): - path = path[1:] - if path.endswith('\\'): - path = path[:-1] - messages_history = [ ] - - def sendCreate(tid): - create_context_data = binascii.unhexlify(""" -28 00 00 00 10 00 04 00 00 00 18 00 10 00 00 00 -44 48 6e 51 00 00 00 00 00 00 00 00 00 00 00 00 -00 00 00 00 00 00 00 00 18 00 00 00 10 00 04 00 -00 00 18 00 00 00 00 00 4d 78 41 63 00 00 00 00 -00 00 00 00 10 00 04 00 00 00 18 00 00 00 00 00 -51 46 69 64 00 00 00 00 -""".replace(' ', '').replace('\n', '')) - m = SMB2Message(SMB2CreateRequest(path, - file_attributes = 0, - access_mask = FILE_READ_DATA | FILE_WRITE_DATA | FILE_READ_EA | FILE_WRITE_EA | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | READ_CONTROL | DELETE | SYNCHRONIZE, - share_access = 0, - oplock = SMB2_OPLOCK_LEVEL_NONE, - impersonation = SEC_IMPERSONATE, - create_options = FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, - create_disp = FILE_CREATE, - create_context_data = create_context_data)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, createCB, errback) - messages_history.append(m) - - def createCB(create_message, **kwargs): - messages_history.append(create_message) - if create_message.status == 0: - closeFid(create_message.tid, create_message.payload.fid) - else: - errback(OperationFailure('Failed to create directory %s on %s: Create failed' % ( path, service_name ), messages_history)) - - def closeFid(tid, fid): - m = SMB2Message(SMB2CloseRequest(fid)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, closeCB, errback) - messages_history.append(m) - - def closeCB(close_message, **kwargs): - callback(path) - - if not self.connected_trees.has_key(service_name): - def connectCB(connect_message, **kwargs): - messages_history.append(connect_message) - if connect_message.status == 0: - self.connected_trees[service_name] = connect_message.tid - sendCreate(connect_message.tid) - else: - errback(OperationFailure('Failed to create directory %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) - - m = SMB2Message(SMB2TreeConnectRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ))) - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectCB, errback, path = service_name) - messages_history.append(m) - else: - sendCreate(self.connected_trees[service_name]) - - def _deleteDirectory_SMB2(self, service_name, path, callback, errback, timeout = 30): - if not self.has_authenticated: - raise NotReadyError('SMB connection not authenticated') - - expiry_time = time.time() + timeout - path = path.replace('/', '\\') - if path.startswith('\\'): - path = path[1:] - if path.endswith('\\'): - path = path[:-1] - messages_history = [ ] - - def sendCreate(tid): - create_context_data = binascii.unhexlify(""" -28 00 00 00 10 00 04 00 00 00 18 00 10 00 00 00 -44 48 6e 51 00 00 00 00 00 00 00 00 00 00 00 00 -00 00 00 00 00 00 00 00 18 00 00 00 10 00 04 00 -00 00 18 00 00 00 00 00 4d 78 41 63 00 00 00 00 -00 00 00 00 10 00 04 00 00 00 18 00 00 00 00 00 -51 46 69 64 00 00 00 00 -""".replace(' ', '').replace('\n', '')) - m = SMB2Message(SMB2CreateRequest(path, - file_attributes = 0, - access_mask = DELETE | FILE_READ_ATTRIBUTES, - share_access = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - oplock = SMB2_OPLOCK_LEVEL_NONE, - impersonation = SEC_IMPERSONATE, - create_options = FILE_DIRECTORY_FILE, - create_disp = FILE_OPEN, - create_context_data = create_context_data)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, createCB, errback, tid = tid) - messages_history.append(m) - - def createCB(open_message, **kwargs): - messages_history.append(open_message) - if open_message.status == 0: - sendDelete(open_message.tid, open_message.payload.fid) - else: - errback(OperationFailure('Failed to delete %s on %s: Unable to open directory' % ( path, service_name ), messages_history)) - - def sendDelete(tid, fid): - m = SMB2Message(SMB2SetInfoRequest(fid, - additional_info = 0, - info_type = SMB2_INFO_FILE, - file_info_class = 0x0d, # SMB2_FILE_DISPOSITION_INFO - data = '\x01')) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, deleteCB, errback, fid = fid) - messages_history.append(m) - - def deleteCB(delete_message, **kwargs): - messages_history.append(delete_message) - if delete_message.status == 0: - closeFid(delete_message.tid, kwargs['fid'], status = 0) - else: - closeFid(delete_message.tid, kwargs['fid'], status = delete_message.status) - - def closeFid(tid, fid, status = None): - m = SMB2Message(SMB2CloseRequest(fid)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, closeCB, errback, status = status) - messages_history.append(m) - - def closeCB(close_message, **kwargs): - if kwargs['status'] == 0: - callback(path) - else: - errback(OperationFailure('Failed to delete %s on %s: Delete failed' % ( path, service_name ), messages_history)) - - if not self.connected_trees.has_key(service_name): - def connectCB(connect_message, **kwargs): - messages_history.append(connect_message) - if connect_message.status == 0: - self.connected_trees[service_name] = connect_message.tid - sendCreate(connect_message.tid) - else: - errback(OperationFailure('Failed to delete %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) - - m = SMB2Message(SMB2TreeConnectRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ))) - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectCB, errback, path = service_name) - messages_history.append(m) - else: - sendCreate(self.connected_trees[service_name]) - - def _rename_SMB2(self, service_name, old_path, new_path, callback, errback, timeout = 30): - if not self.has_authenticated: - raise NotReadyError('SMB connection not authenticated') - - expiry_time = time.time() + timeout - messages_history = [ ] - - new_path = new_path.replace('/', '\\') - if new_path.startswith('\\'): - new_path = new_path[1:] - if new_path.endswith('\\'): - new_path = new_path[:-1] - - old_path = old_path.replace('/', '\\') - if old_path.startswith('\\'): - old_path = old_path[1:] - if old_path.endswith('\\'): - old_path = old_path[:-1] - - def sendCreate(tid): - create_context_data = binascii.unhexlify(""" -28 00 00 00 10 00 04 00 00 00 18 00 10 00 00 00 -44 48 6e 51 00 00 00 00 00 00 00 00 00 00 00 00 -00 00 00 00 00 00 00 00 18 00 00 00 10 00 04 00 -00 00 18 00 00 00 00 00 4d 78 41 63 00 00 00 00 -00 00 00 00 10 00 04 00 00 00 18 00 00 00 00 00 -51 46 69 64 00 00 00 00 -""".replace(' ', '').replace('\n', '')) - m = SMB2Message(SMB2CreateRequest(old_path, - file_attributes = 0, - access_mask = DELETE | FILE_READ_ATTRIBUTES | SYNCHRONIZE, - share_access = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - oplock = SMB2_OPLOCK_LEVEL_NONE, - impersonation = SEC_IMPERSONATE, - create_options = FILE_SYNCHRONOUS_IO_NONALERT, - create_disp = FILE_OPEN, - create_context_data = create_context_data)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, createCB, errback, tid = tid) - messages_history.append(m) - - def createCB(create_message, **kwargs): - messages_history.append(create_message) - if create_message.status == 0: - sendRename(create_message.tid, create_message.payload.fid) - else: - errback(OperationFailure('Failed to rename %s on %s: Unable to open file/directory' % ( old_path, service_name ), messages_history)) - - def sendRename(tid, fid): - data = '\x00'*16 + struct.pack('= 0xFFFF: # MID cannot be 0xFFFF. [MS-CIFS]: 2.2.1.6.2 - # We don't use MID of 0 as MID can be reused for SMB_COM_TRANSACTION2_SECONDARY messages - # where if mid=0, _sendSMBMessage will re-assign new MID values again - self.mid = 1 - return self.mid - - def _updateState_SMB1(self, message): - if message.isReply: - if message.command == SMB_COM_NEGOTIATE: - if not message.status.hasError: - self.has_negotiated = True - self.log.info('SMB dialect negotiation successful (ExtendedSecurity:%s)', message.hasExtendedSecurity) - self._updateServerInfo(message.payload) - self._handleNegotiateResponse(message) - else: - raise ProtocolError('Unknown status value (0x%08X) in SMB_COM_NEGOTIATE' % message.status.internal_value, - message.raw_data, message) - elif message.command == SMB_COM_SESSION_SETUP_ANDX: - if message.hasExtendedSecurity: - if not message.status.hasError: - try: - result = securityblob.decodeAuthResponseSecurityBlob(message.payload.security_blob) - if result == securityblob.RESULT_ACCEPT_COMPLETED: - self.log.debug('SMB uid is now %d', message.uid) - self.uid = message.uid - self.has_authenticated = True - self.log.info('Authentication (with extended security) successful!') - self.onAuthOK() - else: - raise ProtocolError('SMB_COM_SESSION_SETUP_ANDX status is 0 but security blob negResult value is %d' % result, message.raw_data, message) - except securityblob.BadSecurityBlobError, ex: - raise ProtocolError(str(ex), message.raw_data, message) - elif message.status.internal_value == 0xc0000016: # STATUS_MORE_PROCESSING_REQUIRED - try: - result, ntlm_token = securityblob.decodeChallengeSecurityBlob(message.payload.security_blob) - if result == securityblob.RESULT_ACCEPT_INCOMPLETE: - self._handleSessionChallenge(message, ntlm_token) - except ( securityblob.BadSecurityBlobError, securityblob.UnsupportedSecurityProvider ), ex: - raise ProtocolError(str(ex), message.raw_data, message) - elif message.status.internal_value == 0xc000006d: # STATUS_LOGON_FAILURE - self.has_authenticated = False - self.log.info('Authentication (with extended security) failed. Please check username and password. You may need to enable/disable NTLMv2 authentication.') - self.onAuthFailed() - else: - raise ProtocolError('Unknown status value (0x%08X) in SMB_COM_SESSION_SETUP_ANDX (with extended security)' % message.status.internal_value, - message.raw_data, message) - else: - if message.status.internal_value == 0: - self.log.debug('SMB uid is now %d', message.uid) - self.uid = message.uid - self.has_authenticated = True - self.log.info('Authentication (without extended security) successful!') - self.onAuthOK() - else: - self.has_authenticated = False - self.log.info('Authentication (without extended security) failed. Please check username and password') - self.onAuthFailed() - elif message.command == SMB_COM_TREE_CONNECT_ANDX: - try: - req = self.pending_requests[message.mid] - except KeyError: - pass - else: - if not message.status.hasError: - self.connected_trees[req.kwargs['path']] = message.tid - - req = self.pending_requests.pop(message.mid, None) - if req: - req.callback(message, **req.kwargs) - return True - - - def _updateServerInfo_SMB1(self, payload): - self.capabilities = payload.capabilities - self.security_mode = payload.security_mode - self.max_raw_size = payload.max_raw_size - self.max_buffer_size = payload.max_buffer_size - self.max_mpx_count = payload.max_mpx_count - self.use_plaintext_authentication = not bool(payload.security_mode & NEGOTIATE_ENCRYPT_PASSWORDS) - - if self.use_plaintext_authentication: - self.log.warning('Remote server only supports plaintext authentication. Your password can be stolen easily over the network.') - - - def _handleSessionChallenge_SMB1(self, message, ntlm_token): - assert message.hasExtendedSecurity - - if message.uid and not self.uid: - self.uid = message.uid - - server_challenge, server_flags, server_info = ntlm.decodeChallengeMessage(ntlm_token) - if self.use_ntlm_v2: - self.log.info('Performing NTLMv2 authentication (with extended security) with server challenge "%s"', binascii.hexlify(server_challenge)) - nt_challenge_response, lm_challenge_response, session_key = ntlm.generateChallengeResponseV2(self.password, - self.username, - server_challenge, - server_info, - self.domain) - - else: - self.log.info('Performing NTLMv1 authentication (with extended security) with server challenge "%s"', binascii.hexlify(server_challenge)) - nt_challenge_response, lm_challenge_response, session_key = ntlm.generateChallengeResponseV1(self.password, server_challenge, True) - - ntlm_data = ntlm.generateAuthenticateMessage(server_flags, - nt_challenge_response, - lm_challenge_response, - session_key, - self.username, - self.domain) - - if self.log.isEnabledFor(logging.DEBUG): - self.log.debug('NT challenge response is "%s" (%d bytes)', binascii.hexlify(nt_challenge_response), len(nt_challenge_response)) - self.log.debug('LM challenge response is "%s" (%d bytes)', binascii.hexlify(lm_challenge_response), len(lm_challenge_response)) - - blob = securityblob.generateAuthSecurityBlob(ntlm_data) - self._sendSMBMessage(SMBMessage(ComSessionSetupAndxRequest__WithSecurityExtension(0, blob))) - - if self.security_mode & NEGOTIATE_SECURITY_SIGNATURES_REQUIRE: - self.log.info('Server requires all SMB messages to be signed') - self.is_signing_active = (self.sign_options != SMB.SIGN_NEVER) - elif self.security_mode & NEGOTIATE_SECURITY_SIGNATURES_ENABLE: - self.log.info('Server supports SMB signing') - self.is_signing_active = (self.sign_options == SMB.SIGN_WHEN_SUPPORTED) - else: - self.is_signing_active = False - - if self.is_signing_active: - self.log.info("SMB signing activated. All SMB messages will be signed.") - self.signing_session_key = session_key - if self.capabilities & CAP_EXTENDED_SECURITY: - self.signing_challenge_response = None - else: - self.signing_challenge_response = blob - else: - self.log.info("SMB signing deactivated. SMB messages will NOT be signed.") - - - def _handleNegotiateResponse_SMB1(self, message): - if message.uid and not self.uid: - self.uid = message.uid - - if message.hasExtendedSecurity or message.payload.supportsExtendedSecurity: - ntlm_data = ntlm.generateNegotiateMessage() - blob = securityblob.generateNegotiateSecurityBlob(ntlm_data) - self._sendSMBMessage(SMBMessage(ComSessionSetupAndxRequest__WithSecurityExtension(message.payload.session_key, blob))) - else: - nt_password, _, _ = ntlm.generateChallengeResponseV1(self.password, message.payload.challenge, False) - self.log.info('Performing NTLMv1 authentication (without extended security) with challenge "%s" and hashed password of "%s"', - binascii.hexlify(message.payload.challenge), - binascii.hexlify(nt_password)) - self._sendSMBMessage(SMBMessage(ComSessionSetupAndxRequest__NoSecurityExtension(message.payload.session_key, - self.username, - nt_password, - True, - self.domain))) - - def _listShares_SMB1(self, callback, errback, timeout = 30): - if not self.has_authenticated: - raise NotReadyError('SMB connection not authenticated') - - expiry_time = time.time() + timeout - path = 'IPC$' - messages_history = [ ] - - def connectSrvSvc(tid): - m = SMBMessage(ComNTCreateAndxRequest('\\srvsvc', - flags = NT_CREATE_REQUEST_EXTENDED_RESPONSE, - access_mask = READ_CONTROL | FILE_WRITE_ATTRIBUTES | FILE_READ_ATTRIBUTES | FILE_WRITE_EA | FILE_READ_EA | FILE_APPEND_DATA | FILE_WRITE_DATA | FILE_READ_DATA, - share_access = FILE_SHARE_READ | FILE_SHARE_WRITE, - create_disp = FILE_OPEN, - create_options = FILE_OPEN_NO_RECALL | FILE_NON_DIRECTORY_FILE, - impersonation = SEC_IMPERSONATE, - security_flags = 0)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectSrvSvcCB, errback) - messages_history.append(m) - - def connectSrvSvcCB(create_message, **kwargs): - messages_history.append(create_message) - if not create_message.status.hasError: - call_id = self._getNextRPCCallID() - # See [MS-CIFS]: 2.2.5.6.1 for more information on TRANS_TRANSACT_NMPIPE (0x0026) parameters - setup_bytes = struct.pack(' data_length: - return data_bytes[offset:] - - next_offset, _, \ - create_time, last_access_time, last_write_time, last_attr_change_time, \ - file_size, alloc_size, file_attributes, filename_length, ea_size, \ - short_name_length, _, short_name = struct.unpack(info_format, data_bytes[offset:offset+info_size]) - - offset2 = offset + info_size - if offset2 + filename_length > data_length: - return data_bytes[offset:] - - filename = data_bytes[offset2:offset2+filename_length].decode('UTF-16LE') - short_name = short_name.decode('UTF-16LE') - results.append(SharedFile(convertFILETIMEtoEpoch(create_time), convertFILETIMEtoEpoch(last_access_time), - convertFILETIMEtoEpoch(last_write_time), convertFILETIMEtoEpoch(last_attr_change_time), - file_size, alloc_size, file_attributes, short_name, filename)) - - if next_offset: - offset += next_offset - else: - break - return '' - - def findFirstCB(find_message, **kwargs): - messages_history.append(find_message) - if not find_message.status.hasError: - if not kwargs.has_key('total_count'): - # TRANS2_FIND_FIRST2 response. [MS-CIFS]: 2.2.6.2.2 - sid, search_count, end_of_search, _, last_name_offset = struct.unpack(' 0: - if data_len > remaining_len: - file_obj.write(read_message.payload.data[:remaining_len]) - read_len += remaining_len - remaining_len = 0 - else: - file_obj.write(read_message.payload.data) - remaining_len -= data_len - read_len += data_len - else: - file_obj.write(read_message.payload.data) - read_len += data_len - - if (max_length > 0 and remaining_len <= 0) or data_len < (self.max_raw_size - 2): - closeFid(read_message.tid, kwargs['fid']) - callback(( file_obj, kwargs['file_attributes'], read_len )) # Note that this is a tuple of 3-elements - else: - sendRead(read_message.tid, kwargs['fid'], kwargs['offset']+data_len, kwargs['file_attributes'], read_len, remaining_len) - else: - messages_history.append(read_message) - closeFid(read_message.tid, kwargs['fid']) - errback(OperationFailure('Failed to retrieve %s on %s: Read failed' % ( path, service_name ), messages_history)) - - def closeFid(tid, fid): - m = SMBMessage(ComCloseRequest(fid)) - m.tid = tid - self._sendSMBMessage(m) - messages_history.append(m) - - if not self.connected_trees.has_key(service_name): - def connectCB(connect_message, **kwargs): - messages_history.append(connect_message) - if not connect_message.status.hasError: - self.connected_trees[service_name] = connect_message.tid - sendOpen(connect_message.tid) - else: - errback(OperationFailure('Failed to retrieve %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) - - m = SMBMessage(ComTreeConnectAndxRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ), SERVICE_ANY, '')) - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, connectCB, errback, path = service_name) - messages_history.append(m) - else: - sendOpen(self.connected_trees[service_name]) - - def _storeFile_SMB1(self, service_name, path, file_obj, callback, errback, timeout = 30): - self._storeFileFromOffset_SMB1(service_name, path, file_obj, callback, errback, 0L, True, timeout) - - def _storeFileFromOffset_SMB1(self, service_name, path, file_obj, callback, errback, starting_offset, truncate = False, timeout = 30): - if not self.has_authenticated: - raise NotReadyError('SMB connection not authenticated') - - path = path.replace('/', '\\') - messages_history = [ ] - - def sendOpen(tid): - m = SMBMessage(ComOpenAndxRequest(filename = path, - access_mode = 0x0041, # Sharing mode: Deny nothing to others + Open for writing - open_mode = 0x0012 if truncate else 0x0011, # Create file if file does not exist. Overwrite or append depending on truncate parameter. - search_attributes = SMB_FILE_ATTRIBUTE_HIDDEN | SMB_FILE_ATTRIBUTE_SYSTEM, - timeout = timeout * 1000)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, openCB, errback) - messages_history.append(m) - - def openCB(open_message, **kwargs): - messages_history.append(open_message) - if not open_message.status.hasError: - sendWrite(open_message.tid, open_message.payload.fid, starting_offset) - else: - errback(OperationFailure('Failed to store %s on %s: Unable to open file' % ( path, service_name ), messages_history)) - - def sendWrite(tid, fid, offset): - # For message signing, the total SMB message size must be not exceed the max_buffer_size. Non-message signing does not have this limitation - write_count = min((self.is_signing_active and (self.max_buffer_size-64)) or self.max_raw_size, 0xFFFF-1) # Need to minus 1 byte from 0xFFFF because of the first NULL byte in the ComWriteAndxRequest message data - data_bytes = file_obj.read(write_count) - data_len = len(data_bytes) - if data_len > 0: - m = SMBMessage(ComWriteAndxRequest(fid = fid, offset = offset, data_bytes = data_bytes)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, writeCB, errback, fid = fid, offset = offset+data_len) - else: - closeFid(tid, fid) - callback(( file_obj, offset )) # Note that this is a tuple of 2-elements - - def writeCB(write_message, **kwargs): - # To avoid crazy memory usage when saving large files, we do not save every write_message in messages_history. - if not write_message.status.hasError: - sendWrite(write_message.tid, kwargs['fid'], kwargs['offset']) - else: - messages_history.append(write_message) - closeFid(write_message.tid, kwargs['fid']) - errback(OperationFailure('Failed to store %s on %s: Write failed' % ( path, service_name ), messages_history)) - - def closeFid(tid, fid): - m = SMBMessage(ComCloseRequest(fid)) - m.tid = tid - self._sendSMBMessage(m) - messages_history.append(m) - - if not self.connected_trees.has_key(service_name): - def connectCB(connect_message, **kwargs): - messages_history.append(connect_message) - if not connect_message.status.hasError: - self.connected_trees[service_name] = connect_message.tid - sendOpen(connect_message.tid) - else: - errback(OperationFailure('Failed to store %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) - - m = SMBMessage(ComTreeConnectAndxRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ), SERVICE_ANY, '')) - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, connectCB, errback, path = service_name) - messages_history.append(m) - else: - sendOpen(self.connected_trees[service_name]) - - def _deleteFiles_SMB1(self, service_name, path_file_pattern, callback, errback, timeout = 30): - if not self.has_authenticated: - raise NotReadyError('SMB connection not authenticated') - - path = path_file_pattern.replace('/', '\\') - messages_history = [ ] - - def sendDelete(tid): - m = SMBMessage(ComDeleteRequest(filename_pattern = path, - search_attributes = SMB_FILE_ATTRIBUTE_HIDDEN | SMB_FILE_ATTRIBUTE_SYSTEM)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, deleteCB, errback) - messages_history.append(m) - - def deleteCB(delete_message, **kwargs): - messages_history.append(delete_message) - if not delete_message.status.hasError: - callback(path_file_pattern) - else: - errback(OperationFailure('Failed to store %s on %s: Delete failed' % ( path, service_name ), messages_history)) - - if not self.connected_trees.has_key(service_name): - def connectCB(connect_message, **kwargs): - messages_history.append(connect_message) - if not connect_message.status.hasError: - self.connected_trees[service_name] = connect_message.tid - sendDelete(connect_message.tid) - else: - errback(OperationFailure('Failed to delete %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) - - m = SMBMessage(ComTreeConnectAndxRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ), SERVICE_ANY, '')) - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, connectCB, errback, path = service_name) - messages_history.append(m) - else: - sendDelete(self.connected_trees[service_name]) - - def _resetFileAttributes_SMB1(self, service_name, path_file_pattern, callback, errback, timeout = 30): - raise NotReadyError('resetFileAttributes is not yet implemented for SMB1') - - def _createDirectory_SMB1(self, service_name, path, callback, errback, timeout = 30): - if not self.has_authenticated: - raise NotReadyError('SMB connection not authenticated') - - path = path.replace('/', '\\') - messages_history = [ ] - - def sendCreate(tid): - m = SMBMessage(ComCreateDirectoryRequest(path)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, createCB, errback) - messages_history.append(m) - - def createCB(create_message, **kwargs): - messages_history.append(create_message) - if not create_message.status.hasError: - callback(path) - else: - errback(OperationFailure('Failed to create directory %s on %s: Create failed' % ( path, service_name ), messages_history)) - - if not self.connected_trees.has_key(service_name): - def connectCB(connect_message, **kwargs): - messages_history.append(connect_message) - if not connect_message.status.hasError: - self.connected_trees[service_name] = connect_message.tid - sendCreate(connect_message.tid) - else: - errback(OperationFailure('Failed to create directory %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) - - m = SMBMessage(ComTreeConnectAndxRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ), SERVICE_ANY, '')) - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, connectCB, errback, path = service_name) - messages_history.append(m) - else: - sendCreate(self.connected_trees[service_name]) - - def _deleteDirectory_SMB1(self, service_name, path, callback, errback, timeout = 30): - if not self.has_authenticated: - raise NotReadyError('SMB connection not authenticated') - - path = path.replace('/', '\\') - messages_history = [ ] - - def sendDelete(tid): - m = SMBMessage(ComDeleteDirectoryRequest(path)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, deleteCB, errback) - messages_history.append(m) - - def deleteCB(delete_message, **kwargs): - messages_history.append(delete_message) - if not delete_message.status.hasError: - callback(path) - else: - errback(OperationFailure('Failed to delete directory %s on %s: Delete failed' % ( path, service_name ), messages_history)) - - if not self.connected_trees.has_key(service_name): - def connectCB(connect_message, **kwargs): - messages_history.append(connect_message) - if not connect_message.status.hasError: - self.connected_trees[service_name] = connect_message.tid - sendDelete(connect_message.tid) - else: - errback(OperationFailure('Failed to delete %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) - - m = SMBMessage(ComTreeConnectAndxRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ), SERVICE_ANY, '')) - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, connectCB, errback, path = service_name) - messages_history.append(m) - else: - sendDelete(self.connected_trees[service_name]) - - def _rename_SMB1(self, service_name, old_path, new_path, callback, errback, timeout = 30): - if not self.has_authenticated: - raise NotReadyError('SMB connection not authenticated') - - new_path = new_path.replace('/', '\\') - old_path = old_path.replace('/', '\\') - messages_history = [ ] - - def sendRename(tid): - m = SMBMessage(ComRenameRequest(old_path = old_path, - new_path = new_path, - search_attributes = SMB_FILE_ATTRIBUTE_HIDDEN | SMB_FILE_ATTRIBUTE_SYSTEM)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, renameCB, errback) - messages_history.append(m) - - def renameCB(rename_message, **kwargs): - messages_history.append(rename_message) - if not rename_message.status.hasError: - callback(( old_path, new_path )) # Note that this is a tuple of 2-elements - else: - errback(OperationFailure('Failed to rename %s on %s: Rename failed' % ( old_path, service_name ), messages_history)) - - if not self.connected_trees.has_key(service_name): - def connectCB(connect_message, **kwargs): - messages_history.append(connect_message) - if not connect_message.status.hasError: - self.connected_trees[service_name] = connect_message.tid - sendRename(connect_message.tid) - else: - errback(OperationFailure('Failed to rename %s on %s: Unable to connect to shared device' % ( old_path, service_name ), messages_history)) - - m = SMBMessage(ComTreeConnectAndxRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ), SERVICE_ANY, '')) - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, connectCB, errback, path = service_name) - messages_history.append(m) - else: - sendRename(self.connected_trees[service_name]) - - def _listSnapshots_SMB1(self, service_name, path, callback, errback, timeout = 30): - if not self.has_authenticated: - raise NotReadyError('SMB connection not authenticated') - - expiry_time = time.time() + timeout - path = path.replace('/', '\\') - if not path.endswith('\\'): - path += '\\' - messages_history = [ ] - results = [ ] - - def sendOpen(tid): - m = SMBMessage(ComOpenAndxRequest(filename = path, - access_mode = 0x0040, # Sharing mode: Deny nothing to others - open_mode = 0x0001, # Failed if file does not exist - search_attributes = 0, - timeout = timeout * 1000)) - m.tid = tid - self._sendSMBMessage(m) - self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, openCB, errback) - messages_history.append(m) - - def openCB(open_message, **kwargs): - messages_history.append(open_message) - if not open_message.status.hasError: - sendEnumSnapshots(open_message.tid, open_message.payload.fid) - else: - errback(OperationFailure('Failed to list snapshots %s on %s: Unable to open path' % ( path, service_name ), messages_history)) - - def sendEnumSnapshots(tid, fid): - # [MS-CIFS]: 2.2.7.2 - # [MS-SMB]: 2.2.7.2.1 - setup_bytes = struct.pack('`. - - If you encounter *SharedFile* instance where its short_name attribute is empty but the filename attribute contains a short name which does not correspond - to any files/folders on your remote shared device, it could be that the original filename on the file/folder entry on the shared device contains - one of these prohibited characters: "\/[]:+|<>=;?,* (see [MS-CIFS]: 2.2.1.1.1 for more details). - """ - - def __init__(self, create_time, last_access_time, last_write_time, last_attr_change_time, file_size, alloc_size, file_attributes, short_name, filename): - self.create_time = create_time #: Float value in number of seconds since 1970-01-01 00:00:00 to the time of creation of this file resource on the remote server - self.last_access_time = last_access_time #: Float value in number of seconds since 1970-01-01 00:00:00 to the time of last access of this file resource on the remote server - self.last_write_time = last_write_time #: Float value in number of seconds since 1970-01-01 00:00:00 to the time of last modification of this file resource on the remote server - self.last_attr_change_time = last_attr_change_time #: Float value in number of seconds since 1970-01-01 00:00:00 to the time of last attribute change of this file resource on the remote server - self.file_size = file_size #: File size in number of bytes - self.alloc_size = alloc_size #: Total number of bytes allocated to store this file - self.file_attributes = file_attributes #: A SMB_EXT_FILE_ATTR integer value. See [MS-CIFS]: 2.2.1.2.3 - self.short_name = short_name #: Unicode string containing the short name of this file (usually in 8.3 notation) - self.filename = filename #: Unicode string containing the long filename of this file. Each OS has a limit to the length of this file name. On Windows, it is 256 characters. - - @property - def isDirectory(self): - """A convenient property to return True if this file resource is a directory on the remote server""" - return bool(self.file_attributes & ATTR_DIRECTORY) - - @property - def isReadOnly(self): - """A convenient property to return True if this file resource is read-only on the remote server""" - return bool(self.file_attributes & ATTR_READONLY) - - def __unicode__(self): - return u'Shared file: %s (FileSize:%d bytes, isDirectory:%s)' % ( self.filename, self.file_size, self.isDirectory ) - - -class _PendingRequest: - - def __init__(self, mid, expiry_time, callback, errback, **kwargs): - self.mid = mid - self.expiry_time = expiry_time - self.callback = callback - self.errback = errback - self.kwargs = kwargs + +import logging, binascii, time, hmac +from datetime import datetime +from smb_constants import * +from smb2_constants import * +from smb_structs import * +from smb2_structs import * +from .security_descriptors import SecurityDescriptor +from nmb.base import NMBSession +from utils import convertFILETIMEtoEpoch +import ntlm, securityblob + +try: + import hashlib + sha256 = hashlib.sha256 +except ImportError: + from utils import sha256 + + +class NotReadyError(Exception): + """Raised when SMB connection is not ready (i.e. not authenticated or authentication failed)""" + pass + +class NotConnectedError(Exception): + """Raised when underlying SMB connection has been disconnected or not connected yet""" + pass + +class SMBTimeout(Exception): + """Raised when a timeout has occurred while waiting for a response or for a SMB/CIFS operation to complete.""" + pass + + +def _convert_to_unicode(string): + if not isinstance(string, unicode): + string = unicode(string, "utf-8") + return string + + +class SMB(NMBSession): + """ + This class represents a "connection" to the remote SMB/CIFS server. + It is not meant to be used directly in an application as it does not have any network transport implementations. + + For application use, please refer to + - L{SMBProtocol.SMBProtocolFactory} if you are using Twisted framework + + In [MS-CIFS], this class will contain attributes of Client, Client.Connection and Client.Session abstract data models. + + References: + =========== + - [MS-CIFS]: 3.2.1 + """ + + log = logging.getLogger('SMB.SMB') + + SIGN_NEVER = 0 + SIGN_WHEN_SUPPORTED = 1 + SIGN_WHEN_REQUIRED = 2 + + def __init__(self, username, password, my_name, remote_name, domain = '', use_ntlm_v2 = True, sign_options = SIGN_WHEN_REQUIRED, is_direct_tcp = False): + NMBSession.__init__(self, my_name, remote_name, is_direct_tcp = is_direct_tcp) + self.username = _convert_to_unicode(username) + self.password = _convert_to_unicode(password) + self.domain = _convert_to_unicode(domain) + self.sign_options = sign_options + self.is_direct_tcp = is_direct_tcp + self.use_ntlm_v2 = use_ntlm_v2 #: Similar to LMAuthenticationPolicy and NTAuthenticationPolicy as described in [MS-CIFS] 3.2.1.1 + self.smb_message = SMBMessage(self) + self.is_using_smb2 = False #: Are we communicating using SMB2 protocol? self.smb_message will be a SMB2Message instance if this flag is True + self.async_requests = { } #: AsyncID mapped to _PendingRequest instance + self.pending_requests = { } #: MID mapped to _PendingRequest instance + self.connected_trees = { } #: Share name mapped to TID + self.next_rpc_call_id = 1 #: Next RPC callID value. Not used directly in SMB message. Usually encapsulated in sub-commands under SMB_COM_TRANSACTION or SMB_COM_TRANSACTION2 messages + + self.has_negotiated = False + self.has_authenticated = False + self.is_signing_active = False #: True if the remote server accepts message signing. All outgoing messages will be signed. Simiar to IsSigningActive as described in [MS-CIFS] 3.2.1.2 + self.signing_session_key = None #: Session key for signing packets, if signing is active. Similar to SigningSessionKey as described in [MS-CIFS] 3.2.1.2 + self.signing_challenge_response = None #: Contains the challenge response for signing, if signing is active. Similar to SigningChallengeResponse as described in [MS-CIFS] 3.2.1.2 + self.mid = 0 + self.uid = 0 + self.next_signing_id = 2 #: Similar to ClientNextSendSequenceNumber as described in [MS-CIFS] 3.2.1.2 + + # SMB1 and SMB2 attributes + # Note that the interpretations of the values may differ between SMB1 and SMB2 protocols + self.capabilities = 0 + self.security_mode = 0 #: Initialized from the SecurityMode field of the SMB_COM_NEGOTIATE message + + # SMB1 attributes + # Most of the following attributes will be initialized upon receipt of SMB_COM_NEGOTIATE message from server (via self._updateServerInfo_SMB1 method) + self.use_plaintext_authentication = False #: Similar to PlaintextAuthenticationPolicy in in [MS-CIFS] 3.2.1.1 + self.max_raw_size = 0 + self.max_buffer_size = 0 #: Similar to MaxBufferSize as described in [MS-CIFS] 3.2.1.1 + self.max_mpx_count = 0 #: Similar to MaxMpxCount as described in [MS-CIFS] 3.2.1.1 + + # SMB2 attributes + self.max_read_size = 0 #: Similar to MaxReadSize as described in [MS-SMB2] 2.2.4 + self.max_write_size = 0 #: Similar to MaxWriteSize as described in [MS-SMB2] 2.2.4 + self.max_transact_size = 0 #: Similar to MaxTransactSize as described in [MS-SMB2] 2.2.4 + self.session_id = 0 #: Similar to SessionID as described in [MS-SMB2] 2.2.4. This will be set in _updateState_SMB2 method + self.smb2_dialect = 0 + + + # SMB 2.1 attributes + self.cap_leasing = False + self.cap_multi_credit = False + self.credits = 0 # how many credits we're allowed to spend per request + + self._setupSMB1Methods() + + self.log.info('Authentication with remote machine "%s" for user "%s" will be using NTLM %s authentication (%s extended security)', + self.remote_name, self.username, + (self.use_ntlm_v2 and 'v2') or 'v1', + (SUPPORT_EXTENDED_SECURITY and 'with') or 'without') + + + # + # NMBSession Methods + # + + def onNMBSessionOK(self): + self._sendSMBMessage(SMBMessage(self, ComNegotiateRequest())) + + def onNMBSessionFailed(self): + pass + + def onNMBSessionMessage(self, flags, data): + while True: + try: + i = self.smb_message.decode(data) + except SMB2ProtocolHeaderError: + self.log.info('Now switching over to SMB2 protocol communication') + self.is_using_smb2 = True + self.mid = 0 # Must reset messageID counter, or else remote SMB2 server will disconnect + self._setupSMB2Methods() + self.smb_message = self._klassSMBMessage(self) + i = self.smb_message.decode(data) + self.log.info('SMB2 dialect is 0x%04x', self.smb2_dialect) + + next_message_offset = 0 + if self.is_using_smb2: + next_message_offset = self.smb_message.next_command_offset + + # update how many credits we're allowed to spend on requests + self.credits = self.smb_message.credit_response + + # SMB2 CANCEL commands do not consume message IDs + if self.smb_message.command != SMB2_COM_CANCEL: + self.log.debug('Received SMB2 packet from server - "%s" (command:0x%02X). Credit charge recv: %s', + SMB_COMMAND_NAMES.get(self.smb_message.command, ''), self.smb_message.command, self.smb_message.credit_charge) + if self.smb_message.credit_charge > 0: + # Let's update the sequenceWindow based on the CreditsCharged + # In the SMB 2.0.2 dialect, this field MUST NOT be used and MUST be reserved. + # The sender MUST set this to 0, and the receiver MUST ignore it. + # In all other dialects, this field indicates the number of credits that this request consumes. + self.log.debug("Updating MID to add credit charge from server...") + self.log.debug("*** Before: " + str(self.mid)) + self.mid = self.mid + (self.smb_message.credit_charge - 1) + self.log.debug("*** After: " + str(self.mid)) + + if i > 0: + if not self.is_using_smb2: + self.log.debug('Received SMB message "%s" (command:0x%2X flags:0x%02X flags2:0x%04X TID:%d UID:%d)', + SMB_COMMAND_NAMES.get(self.smb_message.command, ''), + self.smb_message.command, self.smb_message.flags, self.smb_message.flags2, self.smb_message.tid, self.smb_message.uid) + else: + self.log.debug('Received SMB2 message "%s" (command:0x%04X flags:0x%04x)', + SMB2_COMMAND_NAMES.get(self.smb_message.command, ''), + self.smb_message.command, self.smb_message.flags) + if self._updateState(self.smb_message): + # We need to create a new instance instead of calling reset() because the instance could be captured in the message history. + self.smb_message = self._klassSMBMessage(self) + + if next_message_offset > 0: + data = data[next_message_offset:] + else: + break + + # + # Public Methods for Overriding in Subclasses + # + + def onAuthOK(self): + pass + + def onAuthFailed(self): + pass + + # + # Protected Methods + # + + def _setupSMB1Methods(self): + self._klassSMBMessage = SMBMessage + self._updateState = self._updateState_SMB1 + self._updateServerInfo = self._updateServerInfo_SMB1 + self._handleNegotiateResponse = self._handleNegotiateResponse_SMB1 + self._sendSMBMessage = self._sendSMBMessage_SMB1 + self._handleSessionChallenge = self._handleSessionChallenge_SMB1 + self._listShares = self._listShares_SMB1 + self._listPath = self._listPath_SMB1 + self._listSnapshots = self._listSnapshots_SMB1 + self._getSecurity = self._getSecurity_SMB1 + self._getAttributes = self._getAttributes_SMB1 + self._retrieveFile = self._retrieveFile_SMB1 + self._retrieveFileFromOffset = self._retrieveFileFromOffset_SMB1 + self._storeFile = self._storeFile_SMB1 + self._storeFileFromOffset = self._storeFileFromOffset_SMB1 + self._deleteFiles = self._deleteFiles_SMB1 + self._resetFileAttributes = self._resetFileAttributes_SMB1 + self._createDirectory = self._createDirectory_SMB1 + self._deleteDirectory = self._deleteDirectory_SMB1 + self._rename = self._rename_SMB1 + self._echo = self._echo_SMB1 + + def _setupSMB2Methods(self): + self._klassSMBMessage = SMB2Message + self._updateState = self._updateState_SMB2 + self._updateServerInfo = self._updateServerInfo_SMB2 + self._handleNegotiateResponse = self._handleNegotiateResponse_SMB2 + self._sendSMBMessage = self._sendSMBMessage_SMB2 + self._handleSessionChallenge = self._handleSessionChallenge_SMB2 + self._listShares = self._listShares_SMB2 + self._listPath = self._listPath_SMB2 + self._listSnapshots = self._listSnapshots_SMB2 + self._getAttributes = self._getAttributes_SMB2 + self._getSecurity = self._getSecurity_SMB2 + self._retrieveFile = self._retrieveFile_SMB2 + self._retrieveFileFromOffset = self._retrieveFileFromOffset_SMB2 + self._storeFile = self._storeFile_SMB2 + self._storeFileFromOffset = self._storeFileFromOffset_SMB2 + self._deleteFiles = self._deleteFiles_SMB2 + self._resetFileAttributes = self._resetFileAttributes_SMB2 + self._createDirectory = self._createDirectory_SMB2 + self._deleteDirectory = self._deleteDirectory_SMB2 + self._rename = self._rename_SMB2 + self._echo = self._echo_SMB2 + + def _getNextRPCCallID(self): + self.next_rpc_call_id += 1 + return self.next_rpc_call_id + + # + # SMB2 Methods Family + # + + def _sendSMBMessage_SMB2(self, smb_message): + if smb_message.mid == 0: + smb_message.mid = self._getNextMID_SMB2() + + if smb_message.command != SMB2_COM_NEGOTIATE: + smb_message.session_id = self.session_id + + if self.is_signing_active: + smb_message.flags |= SMB2_FLAGS_SIGNED + raw_data = smb_message.encode() + smb_message.signature = hmac.new(self.signing_session_key, raw_data, sha256).digest()[:16] + + smb_message.raw_data = smb_message.encode() + self.log.debug('MID is %d. Signature is %s. Total raw message is %d bytes', smb_message.mid, binascii.hexlify(smb_message.signature), len(smb_message.raw_data)) + else: + smb_message.raw_data = smb_message.encode() + self.sendNMBMessage(smb_message.raw_data) + + def _getNextMID_SMB2(self): + self.mid += 1 + return self.mid + + def _updateState_SMB2(self, message): + if message.isReply: + if message.command == SMB2_COM_NEGOTIATE: + if message.status == 0: + + if self.smb_message.payload.dialect_revision == SMB2_DIALECT_2ALL: + # Dialects from SMB 2.1 must be negotiated in a second negotiate phase + # We send a SMB2 Negotiate Request to accomplish this + self._sendSMBMessage(SMB2Message(self, SMB2NegotiateRequest())) + else: + if self.smb_message.payload.dialect_revision == SMB2_DIALECT_21: + # We negotiated SMB 2.1. + # we must now send credit requests (MUST!) + #self.send_credits_request = True + pass + + self.has_negotiated = True + self.log.info('SMB2 dialect negotiation successful') + self.dialect = self.smb_message.payload.dialect_revision + self._updateServerInfo(message.payload) + self._handleNegotiateResponse(message) + else: + raise ProtocolError('Unknown status value (0x%08X) in SMB2_COM_NEGOTIATE' % message.status, + message.raw_data, message) + elif message.command == SMB2_COM_SESSION_SETUP: + if message.status == 0: + self.session_id = message.session_id + try: + result = securityblob.decodeAuthResponseSecurityBlob(message.payload.security_blob) + if result == securityblob.RESULT_ACCEPT_COMPLETED: + self.has_authenticated = True + self.log.info('Authentication (on SMB2) successful!') + + # [MS-SMB2]: 3.2.5.3.1 + # If the security subsystem indicates that the session was established by an anonymous user, + # Session.SigningRequired MUST be set to FALSE. + # If the SMB2_SESSION_FLAG_IS_GUEST bit is set in the SessionFlags field of the + # SMB2 SESSION_SETUP Response and if Session.SigningRequired is TRUE, this indicates a SESSION_SETUP + # failure and the connection MUST be terminated. If the SMB2_SESSION_FLAG_IS_GUEST bit is set in the SessionFlags + # field of the SMB2 SESSION_SETUP Response and if RequireMessageSigning is FALSE, Session.SigningRequired + # MUST be set to FALSE. + if message.payload.isGuestSession or message.payload.isAnonymousSession: + self.is_signing_active = False + self.log.info('Signing disabled because session is guest/anonymous') + + self.onAuthOK() + else: + raise ProtocolError('SMB2_COM_SESSION_SETUP status is 0 but security blob negResult value is %d' % result, message.raw_data, message) + except securityblob.BadSecurityBlobError, ex: + raise ProtocolError(str(ex), message.raw_data, message) + elif message.status == 0xc0000016: # STATUS_MORE_PROCESSING_REQUIRED + self.session_id = message.session_id + try: + result, ntlm_token = securityblob.decodeChallengeSecurityBlob(message.payload.security_blob) + if result == securityblob.RESULT_ACCEPT_INCOMPLETE: + self._handleSessionChallenge(message, ntlm_token) + except ( securityblob.BadSecurityBlobError, securityblob.UnsupportedSecurityProvider ), ex: + raise ProtocolError(str(ex), message.raw_data, message) + elif (message.status == 0xc000006d # STATUS_LOGON_FAILURE + or message.status == 0xc0000064 # STATUS_NO_SUCH_USER + or message.status == 0xc000006a):# STATUS_WRONG_PASSWORD + self.has_authenticated = False + self.log.info('Authentication (on SMB2) failed. Please check username and password.') + self.onAuthFailed() + elif (message.status == 0xc0000193 # STATUS_ACCOUNT_EXPIRED + or message.status == 0xC0000071): # STATUS_PASSWORD_EXPIRED + self.has_authenticated = False + self.log.info('Authentication (on SMB2) failed. Account or password has expired.') + self.onAuthFailed() + elif message.status == 0xc0000234: # STATUS_ACCOUNT_LOCKED_OUT + self.has_authenticated = False + self.log.info('Authentication (on SMB2) failed. Account has been locked due to too many invalid logon attempts.') + self.onAuthFailed() + elif message.status == 0xc0000072: # STATUS_ACCOUNT_DISABLED + self.has_authenticated = False + self.log.info('Authentication (on SMB2) failed. Account has been disabled.') + self.onAuthFailed() + elif (message.status == 0xc000006f # STATUS_INVALID_LOGON_HOURS + or message.status == 0xc000015b # STATUS_LOGON_TYPE_NOT_GRANTED + or message.status == 0xc0000070): # STATUS_INVALID_WORKSTATION + self.has_authenticated = False + self.log.info('Authentication (on SMB2) failed. Not allowed.') + self.onAuthFailed() + elif message.status == 0xc000018c: # STATUS_TRUSTED_DOMAIN_FAILURE + self.has_authenticated = False + self.log.info('Authentication (on SMB2) failed. Domain not trusted.') + self.onAuthFailed() + elif message.status == 0xc000018d: # STATUS_TRUSTED_RELATIONSHIP_FAILURE + self.has_authenticated = False + self.log.info('Authentication (on SMB2) failed. Workstation not trusted.') + self.onAuthFailed() + else: + raise ProtocolError('Unknown status value (0x%08X) in SMB_COM_SESSION_SETUP_ANDX (with extended security)' % message.status, + message.raw_data, message) + + if message.isAsync: + if message.status == 0x00000103: # STATUS_PENDING + req = self.pending_requests.pop(message.mid, None) + if req: + self.async_requests[message.async_id] = req + else: # All other status including SUCCESS + req = self.async_requests.pop(message.async_id, None) + if req: + req.callback(message, **req.kwargs) + return True + else: + req = self.pending_requests.pop(message.mid, None) + if req: + req.callback(message, **req.kwargs) + return True + + + def _updateServerInfo_SMB2(self, payload): + self.capabilities = payload.capabilities + self.security_mode = payload.security_mode + self.max_transact_size = payload.max_transact_size + self.max_read_size = payload.max_read_size + self.max_write_size = payload.max_write_size + self.use_plaintext_authentication = False # SMB2 never allows plaintext authentication + + if (self.capabilities & SMB2_GLOBAL_CAP_LEASING) == SMB2_GLOBAL_CAP_LEASING: + self.cap_leasing = True + + if (self.capabilities & SMB2_GLOBAL_CAP_LARGE_MTU) == SMB2_GLOBAL_CAP_LARGE_MTU: + self.cap_multi_credit = True + + + def _handleNegotiateResponse_SMB2(self, message): + ntlm_data = ntlm.generateNegotiateMessage() + blob = securityblob.generateNegotiateSecurityBlob(ntlm_data) + self._sendSMBMessage(SMB2Message(self, SMB2SessionSetupRequest(blob))) + + + def _handleSessionChallenge_SMB2(self, message, ntlm_token): + server_challenge, server_flags, server_info = ntlm.decodeChallengeMessage(ntlm_token) + + self.log.info('Performing NTLMv2 authentication (on SMB2) with server challenge "%s"', binascii.hexlify(server_challenge)) + + if self.use_ntlm_v2: + self.log.info('Performing NTLMv2 authentication (on SMB2) with server challenge "%s"', binascii.hexlify(server_challenge)) + nt_challenge_response, lm_challenge_response, session_key = ntlm.generateChallengeResponseV2(self.password, + self.username, + server_challenge, + server_info, + self.domain) + + else: + self.log.info('Performing NTLMv1 authentication (on SMB2) with server challenge "%s"', binascii.hexlify(server_challenge)) + nt_challenge_response, lm_challenge_response, session_key = ntlm.generateChallengeResponseV1(self.password, server_challenge, True) + + ntlm_data = ntlm.generateAuthenticateMessage(server_flags, + nt_challenge_response, + lm_challenge_response, + session_key, + self.username, + self.domain, + self.my_name) + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug('NT challenge response is "%s" (%d bytes)', binascii.hexlify(nt_challenge_response), len(nt_challenge_response)) + self.log.debug('LM challenge response is "%s" (%d bytes)', binascii.hexlify(lm_challenge_response), len(lm_challenge_response)) + + blob = securityblob.generateAuthSecurityBlob(ntlm_data) + self._sendSMBMessage(SMB2Message(self, SMB2SessionSetupRequest(blob))) + + if self.security_mode & SMB2_NEGOTIATE_SIGNING_REQUIRED: + self.log.info('Server requires all SMB messages to be signed') + self.is_signing_active = (self.sign_options != SMB.SIGN_NEVER) + elif self.security_mode & SMB2_NEGOTIATE_SIGNING_ENABLED: + self.log.info('Server supports SMB signing') + self.is_signing_active = (self.sign_options == SMB.SIGN_WHEN_SUPPORTED) + else: + self.is_signing_active = False + + if self.is_signing_active: + self.log.info("SMB signing activated. All SMB messages will be signed.") + self.signing_session_key = (session_key + '\0'*16)[:16] + if self.capabilities & CAP_EXTENDED_SECURITY: + self.signing_challenge_response = None + else: + self.signing_challenge_response = blob + else: + self.log.info("SMB signing deactivated. SMB messages will NOT be signed.") + + + def _listShares_SMB2(self, callback, errback, timeout = 30): + if not self.has_authenticated: + raise NotReadyError('SMB connection not authenticated') + + expiry_time = time.time() + timeout + path = 'IPC$' + messages_history = [ ] + + def connectSrvSvc(tid): + m = SMB2Message(self, SMB2CreateRequest('srvsvc', + file_attributes = 0, + access_mask = FILE_READ_DATA | FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_READ_EA | FILE_WRITE_EA | READ_CONTROL | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | SYNCHRONIZE, + share_access = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + oplock = SMB2_OPLOCK_LEVEL_NONE, + impersonation = SEC_IMPERSONATE, + create_options = FILE_NON_DIRECTORY_FILE | FILE_OPEN_NO_RECALL, + create_disp = FILE_OPEN)) + + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectSrvSvcCB, errback, tid = tid) + messages_history.append(m) + + def connectSrvSvcCB(create_message, **kwargs): + messages_history.append(create_message) + if create_message.status == 0: + call_id = self._getNextRPCCallID() + # The data_bytes are binding call to Server Service RPC using DCE v1.1 RPC over SMB. See [MS-SRVS] and [C706] + # If you wish to understand the meanings of the byte stream, I would suggest you use a recent version of WireShark to packet capture the stream + data_bytes = \ + binascii.unhexlify("""05 00 0b 03 10 00 00 00 74 00 00 00""".replace(' ', '')) + \ + struct.pack(' data_length: + return data_bytes[offset:] + + next_offset, _, \ + create_time, last_access_time, last_write_time, last_attr_change_time, \ + file_size, alloc_size, file_attributes, filename_length, ea_size, \ + short_name_length, _, short_name, _, file_id = struct.unpack(info_format, data_bytes[offset:offset+info_size]) + + offset2 = offset + info_size + if offset2 + filename_length > data_length: + return data_bytes[offset:] + + filename = data_bytes[offset2:offset2+filename_length].decode('UTF-16LE') + short_name = short_name[:short_name_length].decode('UTF-16LE') + + accept_result = False + if (file_attributes & 0xff) in ( 0x00, ATTR_NORMAL ): # Only the first 8-bits are compared. We ignore other bits like temp, compressed, encryption, sparse, indexed, etc + accept_result = (search == SMB_FILE_ATTRIBUTE_NORMAL) or (search & SMB_FILE_ATTRIBUTE_INCL_NORMAL) + else: + accept_result = (file_attributes & search) > 0 + if accept_result: + results.append(SharedFile(convertFILETIMEtoEpoch(create_time), convertFILETIMEtoEpoch(last_access_time), + convertFILETIMEtoEpoch(last_write_time), convertFILETIMEtoEpoch(last_attr_change_time), + file_size, alloc_size, file_attributes, short_name, filename, file_id)) + + if next_offset: + offset += next_offset + else: + break + return '' + + def closeFid(tid, fid, results = None, error = None): + m = SMB2Message(self, SMB2CloseRequest(fid)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, closeCB, errback, results = results, error = error) + messages_history.append(m) + + def closeCB(close_message, **kwargs): + if kwargs['results'] is not None: + callback(kwargs['results']) + elif kwargs['error'] is not None: + errback(OperationFailure('Failed to list %s on %s: Query failed with errorcode 0x%08x' % ( path, service_name, kwargs['error'] ), messages_history)) + + if not self.connected_trees.has_key(service_name): + def connectCB(connect_message, **kwargs): + messages_history.append(connect_message) + if connect_message.status == 0: + self.connected_trees[service_name] = connect_message.tid + sendCreate(connect_message.tid) + else: + errback(OperationFailure('Failed to list %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) + + m = SMB2Message(self, SMB2TreeConnectRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ))) + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectCB, errback, path = service_name) + messages_history.append(m) + else: + sendCreate(self.connected_trees[service_name]) + + def _getAttributes_SMB2(self, service_name, path, callback, errback, timeout = 30): + if not self.has_authenticated: + raise NotReadyError('SMB connection not authenticated') + + expiry_time = time.time() + timeout + path = path.replace('/', '\\') + if path.startswith('\\'): + path = path[1:] + if path.endswith('\\'): + path = path[:-1] + messages_history = [ ] + + def sendCreate(tid): + create_context_data = binascii.unhexlify(""" +28 00 00 00 10 00 04 00 00 00 18 00 10 00 00 00 +44 48 6e 51 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 18 00 00 00 10 00 04 00 +00 00 18 00 00 00 00 00 4d 78 41 63 00 00 00 00 +00 00 00 00 10 00 04 00 00 00 18 00 00 00 00 00 +51 46 69 64 00 00 00 00 +""".replace(' ', '').replace('\n', '')) + m = SMB2Message(self, SMB2CreateRequest(path, + file_attributes = 0, + access_mask = FILE_READ_DATA | FILE_READ_EA | FILE_READ_ATTRIBUTES | SYNCHRONIZE, + share_access = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + oplock = SMB2_OPLOCK_LEVEL_NONE, + impersonation = SEC_IMPERSONATE, + create_options = 0, + create_disp = FILE_OPEN, + create_context_data = create_context_data)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, createCB, errback, tid = tid) + messages_history.append(m) + + def createCB(create_message, **kwargs): + messages_history.append(create_message) + if create_message.status == 0: + p = create_message.payload + filename = self._extractLastPathComponent(unicode(path)) + info = SharedFile(p.create_time, p.lastaccess_time, p.lastwrite_time, p.change_time, + p.file_size, p.allocation_size, p.file_attributes, + filename, filename) + closeFid(kwargs['tid'], p.fid, info = info) + else: + errback(OperationFailure('Failed to get attributes for %s on %s: Unable to open remote file object' % ( path, service_name ), messages_history)) + + def closeFid(tid, fid, info = None, error = None): + m = SMB2Message(self, SMB2CloseRequest(fid)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, closeCB, errback, info = info, error = error) + messages_history.append(m) + + def closeCB(close_message, **kwargs): + if kwargs['info'] is not None: + callback(kwargs['info']) + elif kwargs['error'] is not None: + errback(OperationFailure('Failed to get attributes for %s on %s: Query failed with errorcode 0x%08x' % ( path, service_name, kwargs['error'] ), messages_history)) + + if not self.connected_trees.has_key(service_name): + def connectCB(connect_message, **kwargs): + messages_history.append(connect_message) + if connect_message.status == 0: + self.connected_trees[service_name] = connect_message.tid + sendCreate(connect_message.tid) + else: + errback(OperationFailure('Failed to get attributes for %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) + + m = SMB2Message(self, SMB2TreeConnectRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ))) + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectCB, errback, path = service_name) + messages_history.append(m) + else: + sendCreate(self.connected_trees[service_name]) + + def _getSecurity_SMB2(self, service_name, path, callback, errback, timeout = 30): + if not self.has_authenticated: + raise NotReadyError('SMB connection not authenticated') + + expiry_time = time.time() + timeout + path = path.replace('/', '\\') + if path.startswith('\\'): + path = path[1:] + if path.endswith('\\'): + path = path[:-1] + messages_history = [ ] + results = [ ] + + def sendCreate(tid): + m = SMB2Message(self, SMB2CreateRequest(path, + file_attributes = 0, + access_mask = FILE_READ_DATA | FILE_READ_EA | FILE_READ_ATTRIBUTES | READ_CONTROL | SYNCHRONIZE, + share_access = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + oplock = SMB2_OPLOCK_LEVEL_NONE, + impersonation = SEC_IMPERSONATE, + create_options = 0, + create_disp = FILE_OPEN)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, createCB, errback, tid = tid) + messages_history.append(m) + + def createCB(create_message, **kwargs): + messages_history.append(create_message) + if create_message.status == 0: + if self.smb2_dialect != SMB2_DIALECT_2 and self.cap_multi_credit: + output_buf_len = 64 * 1024 * (self.credits - 1) + else: + output_buf_len = self.max_transact_size + + m = SMB2Message(self, SMB2QueryInfoRequest(create_message.payload.fid, + flags = 0, + additional_info = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, + info_type = SMB2_INFO_SECURITY, + file_info_class = 0, # [MS-SMB2] 2.2.37, 3.2.4.12 + input_buf = '', + output_buf_len = output_buf_len)) + m.tid = kwargs['tid'] + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, queryCB, errback, tid = kwargs['tid'], fid = create_message.payload.fid) + messages_history.append(m) + else: + errback(OperationFailure('Failed to get the security descriptor of %s on %s: Unable to open file or directory' % ( path, service_name ), messages_history)) + + def queryCB(query_message, **kwargs): + messages_history.append(query_message) + if query_message.status == 0: + security = SecurityDescriptor.from_bytes(query_message.payload.data) + closeFid(kwargs['tid'], kwargs['fid'], result = security) + else: + closeFid(kwargs['tid'], kwargs['fid'], error = query_message.status) + + def closeFid(tid, fid, result = None, error = None): + m = SMB2Message(self, SMB2CloseRequest(fid)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, closeCB, errback, result = result, error = error) + messages_history.append(m) + + def closeCB(close_message, **kwargs): + if kwargs['result'] is not None: + callback(kwargs['result']) + elif kwargs['error'] is not None: + errback(OperationFailure('Failed to get the security descriptor of %s on %s: Query failed with errorcode 0x%08x' % ( path, service_name, kwargs['error'] ), messages_history)) + + if not self.connected_trees.has_key(service_name): + def connectCB(connect_message, **kwargs): + messages_history.append(connect_message) + if connect_message.status == 0: + self.connected_trees[service_name] = connect_message.tid + sendCreate(connect_message.tid) + else: + errback(OperationFailure('Failed to get the security descriptor of %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) + + m = SMB2Message(self, SMB2TreeConnectRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ))) + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectCB, errback, path = service_name) + messages_history.append(m) + else: + sendCreate(self.connected_trees[service_name]) + + def _retrieveFile_SMB2(self, service_name, path, file_obj, callback, errback, timeout = 30): + return self._retrieveFileFromOffset(service_name, path, file_obj, callback, errback, 0L, -1L, timeout) + + def _retrieveFileFromOffset_SMB2(self, service_name, path, file_obj, callback, errback, starting_offset, max_length, timeout = 30): + if not self.has_authenticated: + raise NotReadyError('SMB connection not authenticated') + + expiry_time = time.time() + timeout + path = path.replace('/', '\\') + if path.startswith('\\'): + path = path[1:] + if path.endswith('\\'): + path = path[:-1] + messages_history = [ ] + results = [ ] + + def sendCreate(tid): + create_context_data = binascii.unhexlify(""" +28 00 00 00 10 00 04 00 00 00 18 00 10 00 00 00 +44 48 6e 51 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 18 00 00 00 10 00 04 00 +00 00 18 00 00 00 00 00 4d 78 41 63 00 00 00 00 +00 00 00 00 10 00 04 00 00 00 18 00 00 00 00 00 +51 46 69 64 00 00 00 00 +""".replace(' ', '').replace('\n', '')) + m = SMB2Message(self, SMB2CreateRequest(path, + file_attributes = 0, + access_mask = FILE_READ_DATA | FILE_READ_EA | FILE_READ_ATTRIBUTES | READ_CONTROL | SYNCHRONIZE, + share_access = FILE_SHARE_READ | FILE_SHARE_WRITE, + oplock = SMB2_OPLOCK_LEVEL_NONE, + impersonation = SEC_IMPERSONATE, + create_options = FILE_SEQUENTIAL_ONLY | FILE_NON_DIRECTORY_FILE, + create_disp = FILE_OPEN, + create_context_data = create_context_data)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, createCB, errback, tid = tid) + messages_history.append(m) + + def createCB(create_message, **kwargs): + messages_history.append(create_message) + if create_message.status == 0: + m = SMB2Message(self, SMB2QueryInfoRequest(create_message.payload.fid, + flags = 0, + additional_info = 0, + info_type = SMB2_INFO_FILE, + file_info_class = 0x16, # FileStreamInformation [MS-FSCC] 2.4 + input_buf = '', + output_buf_len = 4096)) + m.tid = kwargs['tid'] + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, infoCB, errback, + tid = kwargs['tid'], + fid = create_message.payload.fid, + file_attributes = create_message.payload.file_attributes) + messages_history.append(m) + else: + errback(OperationFailure('Failed to retrieve %s on %s: Unable to open file' % ( path, service_name ), messages_history)) + + def infoCB(info_message, **kwargs): + messages_history.append(info_message) + if info_message.status == 0: + file_len = struct.unpack(' file_len: + closeFid(info_message.tid, kwargs['fid']) + callback(( file_obj, kwargs['file_attributes'], 0 )) # Note that this is a tuple of 3-elements + else: + remaining_len = max_length + if remaining_len < 0: + remaining_len = file_len + if starting_offset + remaining_len > file_len: + remaining_len = file_len - starting_offset + sendRead(kwargs['tid'], kwargs['fid'], starting_offset, remaining_len, 0, kwargs['file_attributes']) + else: + errback(OperationFailure('Failed to retrieve %s on %s: Unable to retrieve information on file' % ( path, service_name ), messages_history)) + + def sendRead(tid, fid, offset, remaining_len, read_len, file_attributes): + read_count = min(self.max_read_size, remaining_len) + + if self.smb2_dialect != SMB2_DIALECT_2 and self.cap_multi_credit: + max_read_count = 64 * 1024 * (self.credits -1) + read_count = min(read_count, max_read_count) + + m = SMB2Message(self, SMB2ReadRequest(fid, offset, read_count)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, readCB, errback, + tid = tid, fid = fid, offset = offset, + remaining_len = remaining_len, + read_len = read_len, + file_attributes = file_attributes) + + def readCB(read_message, **kwargs): + # To avoid crazy memory usage when retrieving large files, we do not save every read_message in messages_history. + if read_message.status == 0: + data_len = read_message.payload.data_length + file_obj.write(read_message.payload.data) + + remaining_len = kwargs['remaining_len'] - data_len + + if remaining_len > 0: + sendRead(kwargs['tid'], kwargs['fid'], kwargs['offset'] + data_len, remaining_len, kwargs['read_len'] + data_len, kwargs['file_attributes']) + else: + closeFid(kwargs['tid'], kwargs['fid'], ret = ( file_obj, kwargs['file_attributes'], kwargs['read_len'] + data_len )) + else: + messages_history.append(read_message) + closeFid(kwargs['tid'], kwargs['fid'], error = read_message.status) + + def closeFid(tid, fid, ret = None, error = None): + m = SMB2Message(self, SMB2CloseRequest(fid)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, closeCB, errback, ret = ret, error = error) + messages_history.append(m) + + def closeCB(close_message, **kwargs): + if kwargs['ret'] is not None: + callback(kwargs['ret']) + elif kwargs['error'] is not None: + errback(OperationFailure('Failed to retrieve %s on %s: Read failed' % ( path, service_name ), messages_history)) + + if not self.connected_trees.has_key(service_name): + def connectCB(connect_message, **kwargs): + messages_history.append(connect_message) + if connect_message.status == 0: + self.connected_trees[service_name] = connect_message.tid + sendCreate(connect_message.tid) + else: + errback(OperationFailure('Failed to retrieve %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) + + m = SMB2Message(self, SMB2TreeConnectRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ))) + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectCB, errback, path = service_name) + messages_history.append(m) + else: + sendCreate(self.connected_trees[service_name]) + + def _storeFile_SMB2(self, service_name, path, file_obj, callback, errback, timeout = 30): + self._storeFileFromOffset_SMB2(service_name, path, file_obj, callback, errback, 0L, True, timeout) + + def _storeFileFromOffset_SMB2(self, service_name, path, file_obj, callback, errback, starting_offset, truncate = False, timeout = 30): + if not self.has_authenticated: + raise NotReadyError('SMB connection not authenticated') + + expiry_time = time.time() + timeout + path = path.replace('/', '\\') + if path.startswith('\\'): + path = path[1:] + if path.endswith('\\'): + path = path[:-1] + messages_history = [ ] + + def sendCreate(tid): + create_context_data = binascii.unhexlify(""" +28 00 00 00 10 00 04 00 00 00 18 00 10 00 00 00 +44 48 6e 51 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 20 00 00 00 10 00 04 00 +00 00 18 00 08 00 00 00 41 6c 53 69 00 00 00 00 +85 62 00 00 00 00 00 00 18 00 00 00 10 00 04 00 +00 00 18 00 00 00 00 00 4d 78 41 63 00 00 00 00 +00 00 00 00 10 00 04 00 00 00 18 00 00 00 00 00 +51 46 69 64 00 00 00 00 +""".replace(' ', '').replace('\n', '')) + m = SMB2Message(self, SMB2CreateRequest(path, + file_attributes = ATTR_ARCHIVE, + access_mask = FILE_READ_DATA | FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | FILE_READ_EA | FILE_WRITE_EA | READ_CONTROL | SYNCHRONIZE, + share_access = 0, + oplock = SMB2_OPLOCK_LEVEL_NONE, + impersonation = SEC_IMPERSONATE, + create_options = FILE_SEQUENTIAL_ONLY | FILE_NON_DIRECTORY_FILE, + create_disp = FILE_OVERWRITE_IF if truncate else FILE_OPEN_IF, + create_context_data = create_context_data)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, createCB, errback, tid = tid) + messages_history.append(m) + + def createCB(create_message, **kwargs): + create_message.tid = kwargs['tid'] + messages_history.append(create_message) + if create_message.status == 0: + sendWrite(create_message.tid, create_message.payload.fid, starting_offset) + else: + errback(OperationFailure('Failed to store %s on %s: Unable to open file' % ( path, service_name ), messages_history)) + + def sendWrite(tid, fid, offset): + if self.smb2_dialect != SMB2_DIALECT_2 and self.cap_multi_credit: + write_count = 64 * 1024 * (self.credits -1) + else: + write_count = self.max_write_size + data = file_obj.read(write_count) + data_len = len(data) + if data_len > 0: + m = SMB2Message(self, SMB2WriteRequest(fid, data, offset)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, writeCB, errback, tid = tid, fid = fid, offset = offset+data_len) + else: + closeFid(tid, fid, offset = offset) + + def writeCB(write_message, **kwargs): + # To avoid crazy memory usage when saving large files, we do not save every write_message in messages_history. + if write_message.status == 0: + sendWrite(kwargs['tid'], kwargs['fid'], kwargs['offset']) + else: + messages_history.append(write_message) + closeFid(kwargs['tid'], kwargs['fid']) + errback(OperationFailure('Failed to store %s on %s: Write failed' % ( path, service_name ), messages_history)) + + def closeFid(tid, fid, error = None, offset = None): + m = SMB2Message(self, SMB2CloseRequest(fid)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, closeCB, errback, fid = fid, offset = offset, error = error) + messages_history.append(m) + + def closeCB(close_message, **kwargs): + if kwargs['offset'] is not None: + callback(( file_obj, kwargs['offset'] )) # Note that this is a tuple of 2-elements + elif kwargs['error'] is not None: + errback(OperationFailure('Failed to store %s on %s: Write failed' % ( path, service_name ), messages_history)) + + if not self.connected_trees.has_key(service_name): + def connectCB(connect_message, **kwargs): + messages_history.append(connect_message) + if connect_message.status == 0: + self.connected_trees[service_name] = connect_message.tid + sendCreate(connect_message.tid) + else: + errback(OperationFailure('Failed to store %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) + + m = SMB2Message(self, SMB2TreeConnectRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ))) + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, connectCB, errback, path = service_name) + messages_history.append(m) + else: + sendCreate(self.connected_trees[service_name]) + + + def _deleteFiles_SMB2(self, service_name, path_file_pattern, callback, errback, timeout = 30): + if not self.has_authenticated: + raise NotReadyError('SMB connection not authenticated') + + expiry_time = time.time() + timeout + path = path_file_pattern.replace('/', '\\') + if path.startswith('\\'): + path = path[1:] + if path.endswith('\\'): + path = path[:-1] + messages_history = [ ] + + def sendCreate(tid): + create_context_data = binascii.unhexlify(""" +28 00 00 00 10 00 04 00 00 00 18 00 10 00 00 00 +44 48 6e 51 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 18 00 00 00 10 00 04 00 +00 00 18 00 00 00 00 00 4d 78 41 63 00 00 00 00 +00 00 00 00 10 00 04 00 00 00 18 00 00 00 00 00 +51 46 69 64 00 00 00 00 +""".replace(' ', '').replace('\n', '')) + m = SMB2Message(self, SMB2CreateRequest(path, + file_attributes = 0, + access_mask = DELETE | FILE_READ_ATTRIBUTES, + share_access = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + oplock = SMB2_OPLOCK_LEVEL_NONE, + impersonation = SEC_IMPERSONATE, + create_options = FILE_NON_DIRECTORY_FILE, + create_disp = FILE_OPEN, + create_context_data = create_context_data)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, createCB, errback, tid = tid) + messages_history.append(m) + + def createCB(open_message, **kwargs): + open_message.tid = kwargs['tid'] + messages_history.append(open_message) + if open_message.status == 0: + sendDelete(open_message.tid, open_message.payload.fid) + else: + errback(OperationFailure('Failed to delete %s on %s: Unable to open file' % ( path, service_name ), messages_history)) + + def sendDelete(tid, fid): + m = SMB2Message(self, SMB2SetInfoRequest(fid, + additional_info = 0, + info_type = SMB2_INFO_FILE, + file_info_class = 0x0d, # SMB2_FILE_DISPOSITION_INFO + data = '\x01')) + # [MS-SMB2]: 2.2.39, [MS-FSCC]: 2.4.11 + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, deleteCB, errback, tid = tid, fid = fid) + messages_history.append(m) + + def deleteCB(delete_message, **kwargs): + messages_history.append(delete_message) + if delete_message.status == 0: + closeFid(kwargs['tid'], kwargs['fid'], status = 0) + else: + closeFid(kwargs['tid'], kwargs['fid'], status = delete_message.status) + + def closeFid(tid, fid, status = None): + m = SMB2Message(self, SMB2CloseRequest(fid)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, closeCB, errback, status = status) + messages_history.append(m) + + def closeCB(close_message, **kwargs): + if kwargs['status'] == 0: + callback(path_file_pattern) + else: + errback(OperationFailure('Failed to delete %s on %s: Delete failed' % ( path, service_name ), messages_history)) + + if not self.connected_trees.has_key(service_name): + def connectCB(connect_message, **kwargs): + messages_history.append(connect_message) + if connect_message.status == 0: + self.connected_trees[service_name] = connect_message.tid + sendCreate(connect_message.tid) + else: + errback(OperationFailure('Failed to delete %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) + + m = SMB2Message(self, SMB2TreeConnectRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ))) + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectCB, errback, path = service_name) + messages_history.append(m) + else: + sendCreate(self.connected_trees[service_name]) + + def _resetFileAttributes_SMB2(self, service_name, path_file_pattern, callback, errback, timeout = 30): + if not self.has_authenticated: + raise NotReadyError('SMB connection not authenticated') + + expiry_time = time.time() + timeout + path = path_file_pattern.replace('/', '\\') + if path.startswith('\\'): + path = path[1:] + if path.endswith('\\'): + path = path[:-1] + messages_history = [ ] + + def sendCreate(tid): + create_context_data = binascii.unhexlify(""" +28 00 00 00 10 00 04 00 00 00 18 00 10 00 00 00 +44 48 6e 51 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 18 00 00 00 10 00 04 00 +00 00 18 00 00 00 00 00 4d 78 41 63 00 00 00 00 +00 00 00 00 10 00 04 00 00 00 18 00 00 00 00 00 +51 46 69 64 00 00 00 00 +""".replace(' ', '').replace('\n', '')) + + m = SMB2Message(self, SMB2CreateRequest(path, + file_attributes = 0, + access_mask = FILE_WRITE_ATTRIBUTES, + share_access = FILE_SHARE_READ | FILE_SHARE_WRITE, + oplock = SMB2_OPLOCK_LEVEL_NONE, + impersonation = SEC_IMPERSONATE, + create_options = 0, + create_disp = FILE_OPEN, + create_context_data = create_context_data)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, createCB, errback, tid = tid) + messages_history.append(m) + + def createCB(open_message, **kwargs): + messages_history.append(open_message) + if open_message.status == 0: + sendReset(kwargs['tid'], open_message.payload.fid) + else: + errback(OperationFailure('Failed to reset attributes of %s on %s: Unable to open file' % ( path, service_name ), messages_history)) + + def sendReset(tid, fid): + m = SMB2Message(self, SMB2SetInfoRequest(fid, + additional_info = 0, + info_type = SMB2_INFO_FILE, + file_info_class = 4, # FileBasicInformation + data = struct.pack('qqqqii',0,0,0,0,0x80,0))) # FILE_ATTRIBUTE_NORMAL + # [MS-SMB2]: 2.2.39, [MS-FSCC]: 2.4, [MS-FSCC]: 2.4.7, [MS-FSCC]: 2.6 + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, resetCB, errback, tid = tid, fid = fid) + messages_history.append(m) + + def resetCB(reset_message, **kwargs): + messages_history.append(reset_message) + if reset_message.status == 0: + closeFid(kwargs['tid'], kwargs['fid'], status = 0) + else: + closeFid(kwargs['tid'], kwargs['fid'], status = reset_message.status) + + def closeFid(tid, fid, status = None): + m = SMB2Message(self, SMB2CloseRequest(fid)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, closeCB, errback, status = status) + messages_history.append(m) + + def closeCB(close_message, **kwargs): + if kwargs['status'] == 0: + callback(path_file_pattern) + else: + errback(OperationFailure('Failed to reset attributes of %s on %s: Reset failed' % ( path, service_name ), messages_history)) + + if not self.connected_trees.has_key(service_name): + def connectCB(connect_message, **kwargs): + messages_history.append(connect_message) + if connect_message.status == 0: + self.connected_trees[service_name] = connect_message.tid + sendCreate(connect_message.tid) + else: + errback(OperationFailure('Failed to reset attributes of %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) + + m = SMB2Message(self, SMB2TreeConnectRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ))) + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectCB, errback, path = service_name) + messages_history.append(m) + else: + sendCreate(self.connected_trees[service_name]) + + def _createDirectory_SMB2(self, service_name, path, callback, errback, timeout = 30): + if not self.has_authenticated: + raise NotReadyError('SMB connection not authenticated') + + expiry_time = time.time() + timeout + path = path.replace('/', '\\') + if path.startswith('\\'): + path = path[1:] + if path.endswith('\\'): + path = path[:-1] + messages_history = [ ] + + def sendCreate(tid): + create_context_data = binascii.unhexlify(""" +28 00 00 00 10 00 04 00 00 00 18 00 10 00 00 00 +44 48 6e 51 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 18 00 00 00 10 00 04 00 +00 00 18 00 00 00 00 00 4d 78 41 63 00 00 00 00 +00 00 00 00 10 00 04 00 00 00 18 00 00 00 00 00 +51 46 69 64 00 00 00 00 +""".replace(' ', '').replace('\n', '')) + m = SMB2Message(self, SMB2CreateRequest(path, + file_attributes = 0, + access_mask = FILE_READ_DATA | FILE_WRITE_DATA | FILE_READ_EA | FILE_WRITE_EA | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | READ_CONTROL | DELETE | SYNCHRONIZE, + share_access = 0, + oplock = SMB2_OPLOCK_LEVEL_NONE, + impersonation = SEC_IMPERSONATE, + create_options = FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, + create_disp = FILE_CREATE, + create_context_data = create_context_data)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, createCB, errback, tid = tid) + messages_history.append(m) + + def createCB(create_message, **kwargs): + messages_history.append(create_message) + if create_message.status == 0: + closeFid(kwargs['tid'], create_message.payload.fid) + else: + errback(OperationFailure('Failed to create directory %s on %s: Create failed' % ( path, service_name ), messages_history)) + + def closeFid(tid, fid): + m = SMB2Message(self, SMB2CloseRequest(fid)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, closeCB, errback) + messages_history.append(m) + + def closeCB(close_message, **kwargs): + callback(path) + + if not self.connected_trees.has_key(service_name): + def connectCB(connect_message, **kwargs): + messages_history.append(connect_message) + if connect_message.status == 0: + self.connected_trees[service_name] = connect_message.tid + sendCreate(connect_message.tid) + else: + errback(OperationFailure('Failed to create directory %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) + + m = SMB2Message(self, SMB2TreeConnectRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ))) + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectCB, errback, path = service_name) + messages_history.append(m) + else: + sendCreate(self.connected_trees[service_name]) + + def _deleteDirectory_SMB2(self, service_name, path, callback, errback, timeout = 30): + if not self.has_authenticated: + raise NotReadyError('SMB connection not authenticated') + + expiry_time = time.time() + timeout + path = path.replace('/', '\\') + if path.startswith('\\'): + path = path[1:] + if path.endswith('\\'): + path = path[:-1] + messages_history = [ ] + + def sendCreate(tid): + create_context_data = binascii.unhexlify(""" +28 00 00 00 10 00 04 00 00 00 18 00 10 00 00 00 +44 48 6e 51 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 18 00 00 00 10 00 04 00 +00 00 18 00 00 00 00 00 4d 78 41 63 00 00 00 00 +00 00 00 00 10 00 04 00 00 00 18 00 00 00 00 00 +51 46 69 64 00 00 00 00 +""".replace(' ', '').replace('\n', '')) + m = SMB2Message(self, SMB2CreateRequest(path, + file_attributes = 0, + access_mask = DELETE | FILE_READ_ATTRIBUTES, + share_access = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + oplock = SMB2_OPLOCK_LEVEL_NONE, + impersonation = SEC_IMPERSONATE, + create_options = FILE_DIRECTORY_FILE, + create_disp = FILE_OPEN, + create_context_data = create_context_data)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, createCB, errback, tid = tid) + messages_history.append(m) + + def createCB(open_message, **kwargs): + messages_history.append(open_message) + if open_message.status == 0: + sendDelete(kwargs['tid'], open_message.payload.fid) + else: + errback(OperationFailure('Failed to delete %s on %s: Unable to open directory' % ( path, service_name ), messages_history)) + + def sendDelete(tid, fid): + m = SMB2Message(self, SMB2SetInfoRequest(fid, + additional_info = 0, + info_type = SMB2_INFO_FILE, + file_info_class = 0x0d, # SMB2_FILE_DISPOSITION_INFO + data = '\x01')) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, deleteCB, errback, tid = tid, fid = fid) + messages_history.append(m) + + def deleteCB(delete_message, **kwargs): + messages_history.append(delete_message) + if delete_message.status == 0: + closeFid(kwargs['tid'], kwargs['fid'], status = 0) + else: + closeFid(kwargs['tid'], kwargs['fid'], status = delete_message.status) + + def closeFid(tid, fid, status = None): + m = SMB2Message(self, SMB2CloseRequest(fid)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, closeCB, errback, status = status) + messages_history.append(m) + + def closeCB(close_message, **kwargs): + if kwargs['status'] == 0: + callback(path) + else: + errback(OperationFailure('Failed to delete %s on %s: Delete failed' % ( path, service_name ), messages_history)) + + if not self.connected_trees.has_key(service_name): + def connectCB(connect_message, **kwargs): + messages_history.append(connect_message) + if connect_message.status == 0: + self.connected_trees[service_name] = connect_message.tid + sendCreate(connect_message.tid) + else: + errback(OperationFailure('Failed to delete %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) + + m = SMB2Message(self, SMB2TreeConnectRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ))) + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectCB, errback, path = service_name) + messages_history.append(m) + else: + sendCreate(self.connected_trees[service_name]) + + def _rename_SMB2(self, service_name, old_path, new_path, callback, errback, timeout = 30): + if not self.has_authenticated: + raise NotReadyError('SMB connection not authenticated') + + expiry_time = time.time() + timeout + messages_history = [ ] + + new_path = new_path.replace('/', '\\') + if new_path.startswith('\\'): + new_path = new_path[1:] + if new_path.endswith('\\'): + new_path = new_path[:-1] + + old_path = old_path.replace('/', '\\') + if old_path.startswith('\\'): + old_path = old_path[1:] + if old_path.endswith('\\'): + old_path = old_path[:-1] + + def sendCreate(tid): + create_context_data = binascii.unhexlify(""" +28 00 00 00 10 00 04 00 00 00 18 00 10 00 00 00 +44 48 6e 51 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 18 00 00 00 10 00 04 00 +00 00 18 00 00 00 00 00 4d 78 41 63 00 00 00 00 +00 00 00 00 10 00 04 00 00 00 18 00 00 00 00 00 +51 46 69 64 00 00 00 00 +""".replace(' ', '').replace('\n', '')) + m = SMB2Message(self, SMB2CreateRequest(old_path, + file_attributes = 0, + access_mask = DELETE | FILE_READ_ATTRIBUTES | SYNCHRONIZE, + share_access = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + oplock = SMB2_OPLOCK_LEVEL_NONE, + impersonation = SEC_IMPERSONATE, + create_options = FILE_SYNCHRONOUS_IO_NONALERT, + create_disp = FILE_OPEN, + create_context_data = create_context_data)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, createCB, errback, tid = tid) + messages_history.append(m) + + def createCB(create_message, **kwargs): + messages_history.append(create_message) + if create_message.status == 0: + sendRename(kwargs['tid'], create_message.payload.fid) + else: + errback(OperationFailure('Failed to rename %s on %s: Unable to open file/directory' % ( old_path, service_name ), messages_history)) + + def sendRename(tid, fid): + data = '\x00'*16 + struct.pack('= 0xFFFF: # MID cannot be 0xFFFF. [MS-CIFS]: 2.2.1.6.2 + # We don't use MID of 0 as MID can be reused for SMB_COM_TRANSACTION2_SECONDARY messages + # where if mid=0, _sendSMBMessage will re-assign new MID values again + self.mid = 1 + return self.mid + + def _updateState_SMB1(self, message): + if message.isReply: + if message.command == SMB_COM_NEGOTIATE: + if not message.status.hasError: + self.has_negotiated = True + self.log.info('SMB dialect negotiation successful (ExtendedSecurity:%s)', message.hasExtendedSecurity) + self._updateServerInfo(message.payload) + self._handleNegotiateResponse(message) + else: + raise ProtocolError('Unknown status value (0x%08X) in SMB_COM_NEGOTIATE' % message.status.internal_value, + message.raw_data, message) + elif message.command == SMB_COM_SESSION_SETUP_ANDX: + if message.hasExtendedSecurity: + if not message.status.hasError: + try: + result = securityblob.decodeAuthResponseSecurityBlob(message.payload.security_blob) + if result == securityblob.RESULT_ACCEPT_COMPLETED: + self.log.debug('SMB uid is now %d', message.uid) + self.uid = message.uid + self.has_authenticated = True + self.log.info('Authentication (with extended security) successful!') + self.onAuthOK() + else: + raise ProtocolError('SMB_COM_SESSION_SETUP_ANDX status is 0 but security blob negResult value is %d' % result, message.raw_data, message) + except securityblob.BadSecurityBlobError, ex: + raise ProtocolError(str(ex), message.raw_data, message) + elif message.status.internal_value == 0xc0000016: # STATUS_MORE_PROCESSING_REQUIRED + try: + result, ntlm_token = securityblob.decodeChallengeSecurityBlob(message.payload.security_blob) + if result == securityblob.RESULT_ACCEPT_INCOMPLETE: + self._handleSessionChallenge(message, ntlm_token) + except ( securityblob.BadSecurityBlobError, securityblob.UnsupportedSecurityProvider ), ex: + raise ProtocolError(str(ex), message.raw_data, message) + elif (message.status.internal_value == 0xc000006d # STATUS_LOGON_FAILURE + or message.status.internal_value == 0xc0000064 # STATUS_NO_SUCH_USER + or message.status.internal_value == 0xc000006a): # STATUS_WRONG_PASSWORD + self.has_authenticated = False + self.log.info('Authentication (with extended security) failed. Please check username and password.') + self.onAuthFailed() + elif (message.status.internal_value == 0xc0000193 # STATUS_ACCOUNT_EXPIRED + or message.status.internal_value == 0xC0000071): # STATUS_PASSWORD_EXPIRED + self.has_authenticated = False + self.log.info('Authentication (with extended security) failed. Account or password has expired.') + self.onAuthFailed() + elif message.status.internal_value == 0xc0000234: # STATUS_ACCOUNT_LOCKED_OUT + self.has_authenticated = False + self.log.info('Authentication (with extended security) failed. Account has been locked due to too many invalid logon attempts.') + self.onAuthFailed() + elif message.status.internal_value == 0xc0000072: # STATUS_ACCOUNT_DISABLED + self.has_authenticated = False + self.log.info('Authentication (with extended security) failed. Account has been disabled.') + self.onAuthFailed() + elif (message.status.internal_value == 0xc000006f # STATUS_INVALID_LOGON_HOURS + or message.status.internal_value == 0xc000015b # STATUS_LOGON_TYPE_NOT_GRANTED + or message.status.internal_value == 0xc0000070): # STATUS_INVALID_WORKSTATION + self.has_authenticated = False + self.log.info('Authentication (with extended security) failed. Not allowed.') + self.onAuthFailed() + elif message.status.internal_value == 0xc000018c: # STATUS_TRUSTED_DOMAIN_FAILURE + self.has_authenticated = False + self.log.info('Authentication (with extended security) failed. Domain not trusted.') + self.onAuthFailed() + elif message.status.internal_value == 0xc000018d: # STATUS_TRUSTED_RELATIONSHIP_FAILURE + self.has_authenticated = False + self.log.info('Authentication (with extended security) failed. Workstation not trusted.') + self.onAuthFailed() + else: + raise ProtocolError('Unknown status value (0x%08X) in SMB_COM_SESSION_SETUP_ANDX (with extended security)' % message.status.internal_value, + message.raw_data, message) + else: + if message.status.internal_value == 0: + self.log.debug('SMB uid is now %d', message.uid) + self.uid = message.uid + self.has_authenticated = True + self.log.info('Authentication (without extended security) successful!') + self.onAuthOK() + else: + self.has_authenticated = False + self.log.info('Authentication (without extended security) failed. Please check username and password') + self.onAuthFailed() + elif message.command == SMB_COM_TREE_CONNECT_ANDX: + try: + req = self.pending_requests[message.mid] + except KeyError: + pass + else: + if not message.status.hasError: + self.connected_trees[req.kwargs['path']] = message.tid + + req = self.pending_requests.pop(message.mid, None) + if req: + req.callback(message, **req.kwargs) + return True + + + def _updateServerInfo_SMB1(self, payload): + self.capabilities = payload.capabilities + self.security_mode = payload.security_mode + self.max_raw_size = payload.max_raw_size + self.max_buffer_size = payload.max_buffer_size + self.max_mpx_count = payload.max_mpx_count + self.use_plaintext_authentication = not bool(payload.security_mode & NEGOTIATE_ENCRYPT_PASSWORDS) + + if self.use_plaintext_authentication: + self.log.warning('Remote server only supports plaintext authentication. Your password can be stolen easily over the network.') + + + def _handleSessionChallenge_SMB1(self, message, ntlm_token): + assert message.hasExtendedSecurity + + if message.uid and not self.uid: + self.uid = message.uid + + server_challenge, server_flags, server_info = ntlm.decodeChallengeMessage(ntlm_token) + if self.use_ntlm_v2: + self.log.info('Performing NTLMv2 authentication (with extended security) with server challenge "%s"', binascii.hexlify(server_challenge)) + nt_challenge_response, lm_challenge_response, session_key = ntlm.generateChallengeResponseV2(self.password, + self.username, + server_challenge, + server_info, + self.domain) + + else: + self.log.info('Performing NTLMv1 authentication (with extended security) with server challenge "%s"', binascii.hexlify(server_challenge)) + nt_challenge_response, lm_challenge_response, session_key = ntlm.generateChallengeResponseV1(self.password, server_challenge, True) + + ntlm_data = ntlm.generateAuthenticateMessage(server_flags, + nt_challenge_response, + lm_challenge_response, + session_key, + self.username, + self.domain, + self.my_name) + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug('NT challenge response is "%s" (%d bytes)', binascii.hexlify(nt_challenge_response), len(nt_challenge_response)) + self.log.debug('LM challenge response is "%s" (%d bytes)', binascii.hexlify(lm_challenge_response), len(lm_challenge_response)) + + blob = securityblob.generateAuthSecurityBlob(ntlm_data) + self._sendSMBMessage(SMBMessage(self, ComSessionSetupAndxRequest__WithSecurityExtension(0, blob))) + + if self.security_mode & NEGOTIATE_SECURITY_SIGNATURES_REQUIRE: + self.log.info('Server requires all SMB messages to be signed') + self.is_signing_active = (self.sign_options != SMB.SIGN_NEVER) + elif self.security_mode & NEGOTIATE_SECURITY_SIGNATURES_ENABLE: + self.log.info('Server supports SMB signing') + self.is_signing_active = (self.sign_options == SMB.SIGN_WHEN_SUPPORTED) + else: + self.is_signing_active = False + + if self.is_signing_active: + self.log.info("SMB signing activated. All SMB messages will be signed.") + self.signing_session_key = session_key + if self.capabilities & CAP_EXTENDED_SECURITY: + self.signing_challenge_response = None + else: + self.signing_challenge_response = blob + else: + self.log.info("SMB signing deactivated. SMB messages will NOT be signed.") + + + def _handleNegotiateResponse_SMB1(self, message): + if message.uid and not self.uid: + self.uid = message.uid + + if message.hasExtendedSecurity or message.payload.supportsExtendedSecurity: + ntlm_data = ntlm.generateNegotiateMessage() + blob = securityblob.generateNegotiateSecurityBlob(ntlm_data) + self._sendSMBMessage(SMBMessage(self, ComSessionSetupAndxRequest__WithSecurityExtension(message.payload.session_key, blob))) + else: + nt_password, _, _ = ntlm.generateChallengeResponseV1(self.password, message.payload.challenge, False) + self.log.info('Performing NTLMv1 authentication (without extended security) with challenge "%s" and hashed password of "%s"', + binascii.hexlify(message.payload.challenge), + binascii.hexlify(nt_password)) + self._sendSMBMessage(SMBMessage(self, ComSessionSetupAndxRequest__NoSecurityExtension(message.payload.session_key, + self.username, + nt_password, + True, + self.domain))) + + def _listShares_SMB1(self, callback, errback, timeout = 30): + if not self.has_authenticated: + raise NotReadyError('SMB connection not authenticated') + + expiry_time = time.time() + timeout + path = 'IPC$' + messages_history = [ ] + + def connectSrvSvc(tid): + m = SMBMessage(self, ComNTCreateAndxRequest('\\srvsvc', + flags = NT_CREATE_REQUEST_EXTENDED_RESPONSE, + access_mask = READ_CONTROL | FILE_WRITE_ATTRIBUTES | FILE_READ_ATTRIBUTES | FILE_WRITE_EA | FILE_READ_EA | FILE_APPEND_DATA | FILE_WRITE_DATA | FILE_READ_DATA, + share_access = FILE_SHARE_READ | FILE_SHARE_WRITE, + create_disp = FILE_OPEN, + create_options = FILE_OPEN_NO_RECALL | FILE_NON_DIRECTORY_FILE, + impersonation = SEC_IMPERSONATE, + security_flags = 0)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectSrvSvcCB, errback) + messages_history.append(m) + + def connectSrvSvcCB(create_message, **kwargs): + messages_history.append(create_message) + if not create_message.status.hasError: + call_id = self._getNextRPCCallID() + # See [MS-CIFS]: 2.2.5.6.1 for more information on TRANS_TRANSACT_NMPIPE (0x0026) parameters + setup_bytes = struct.pack(' data_length: + return data_bytes[offset:] + + next_offset, _, \ + create_time, last_access_time, last_write_time, last_attr_change_time, \ + file_size, alloc_size, file_attributes, filename_length, ea_size, \ + short_name_length, _, short_name = struct.unpack(info_format, data_bytes[offset:offset+info_size]) + + offset2 = offset + info_size + if offset2 + filename_length > data_length: + return data_bytes[offset:] + + filename = data_bytes[offset2:offset2+filename_length].decode('UTF-16LE') + short_name = short_name.decode('UTF-16LE') + + accept_result = False + if (file_attributes & 0xff) in ( 0x00, ATTR_NORMAL ): # Only the first 8-bits are compared. We ignore other bits like temp, compressed, encryption, sparse, indexed, etc + accept_result = (search == SMB_FILE_ATTRIBUTE_NORMAL) or (search & SMB_FILE_ATTRIBUTE_INCL_NORMAL) + else: + accept_result = (file_attributes & search) > 0 + if accept_result: + results.append(SharedFile(convertFILETIMEtoEpoch(create_time), convertFILETIMEtoEpoch(last_access_time), + convertFILETIMEtoEpoch(last_write_time), convertFILETIMEtoEpoch(last_attr_change_time), + file_size, alloc_size, file_attributes, short_name, filename)) + + if next_offset: + offset += next_offset + else: + break + return '' + + def findFirstCB(find_message, **kwargs): + messages_history.append(find_message) + if not find_message.status.hasError: + if not kwargs.has_key('total_count'): + # TRANS2_FIND_FIRST2 response. [MS-CIFS]: 2.2.6.2.2 + sid, search_count, end_of_search, _, last_name_offset = struct.unpack(' 0: + if data_len > remaining_len: + file_obj.write(read_message.payload.data[:remaining_len]) + read_len += remaining_len + remaining_len = 0 + else: + file_obj.write(read_message.payload.data) + remaining_len -= data_len + read_len += data_len + else: + file_obj.write(read_message.payload.data) + read_len += data_len + + if (max_length > 0 and remaining_len <= 0) or data_len < (self.max_raw_size - 2): + closeFid(read_message.tid, kwargs['fid']) + callback(( file_obj, kwargs['file_attributes'], read_len )) # Note that this is a tuple of 3-elements + else: + sendRead(read_message.tid, kwargs['fid'], kwargs['offset']+data_len, kwargs['file_attributes'], read_len, remaining_len) + else: + messages_history.append(read_message) + closeFid(read_message.tid, kwargs['fid']) + errback(OperationFailure('Failed to retrieve %s on %s: Read failed' % ( path, service_name ), messages_history)) + + def closeFid(tid, fid): + m = SMBMessage(self, ComCloseRequest(fid)) + m.tid = tid + self._sendSMBMessage(m) + messages_history.append(m) + + if not self.connected_trees.has_key(service_name): + def connectCB(connect_message, **kwargs): + messages_history.append(connect_message) + if not connect_message.status.hasError: + self.connected_trees[service_name] = connect_message.tid + sendOpen(connect_message.tid) + else: + errback(OperationFailure('Failed to retrieve %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) + + m = SMBMessage(self, ComTreeConnectAndxRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ), SERVICE_ANY, '')) + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, connectCB, errback, path = service_name) + messages_history.append(m) + else: + sendOpen(self.connected_trees[service_name]) + + def _storeFile_SMB1(self, service_name, path, file_obj, callback, errback, timeout = 30): + self._storeFileFromOffset_SMB1(service_name, path, file_obj, callback, errback, 0L, True, timeout) + + def _storeFileFromOffset_SMB1(self, service_name, path, file_obj, callback, errback, starting_offset, truncate = False, timeout = 30): + if not self.has_authenticated: + raise NotReadyError('SMB connection not authenticated') + + path = path.replace('/', '\\') + messages_history = [ ] + + def sendOpen(tid): + m = SMBMessage(self, ComOpenAndxRequest(filename = path, + access_mode = 0x0041, # Sharing mode: Deny nothing to others + Open for writing + open_mode = 0x0012 if truncate else 0x0011, # Create file if file does not exist. Overwrite or append depending on truncate parameter. + search_attributes = SMB_FILE_ATTRIBUTE_HIDDEN | SMB_FILE_ATTRIBUTE_SYSTEM, + timeout = timeout * 1000)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, openCB, errback) + messages_history.append(m) + + def openCB(open_message, **kwargs): + messages_history.append(open_message) + if not open_message.status.hasError: + sendWrite(open_message.tid, open_message.payload.fid, starting_offset) + else: + errback(OperationFailure('Failed to store %s on %s: Unable to open file' % ( path, service_name ), messages_history)) + + def sendWrite(tid, fid, offset): + # For message signing, the total SMB message size must be not exceed the max_buffer_size. Non-message signing does not have this limitation + write_count = min((self.is_signing_active and (self.max_buffer_size-64)) or self.max_raw_size, 0xFFFF-1) # Need to minus 1 byte from 0xFFFF because of the first NULL byte in the ComWriteAndxRequest message data + data_bytes = file_obj.read(write_count) + data_len = len(data_bytes) + if data_len > 0: + m = SMBMessage(self, ComWriteAndxRequest(fid = fid, offset = offset, data_bytes = data_bytes)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, writeCB, errback, fid = fid, offset = offset+data_len) + else: + closeFid(tid, fid) + callback(( file_obj, offset )) # Note that this is a tuple of 2-elements + + def writeCB(write_message, **kwargs): + # To avoid crazy memory usage when saving large files, we do not save every write_message in messages_history. + if not write_message.status.hasError: + sendWrite(write_message.tid, kwargs['fid'], kwargs['offset']) + else: + messages_history.append(write_message) + closeFid(write_message.tid, kwargs['fid']) + errback(OperationFailure('Failed to store %s on %s: Write failed' % ( path, service_name ), messages_history)) + + def closeFid(tid, fid): + m = SMBMessage(self, ComCloseRequest(fid)) + m.tid = tid + self._sendSMBMessage(m) + messages_history.append(m) + + if not self.connected_trees.has_key(service_name): + def connectCB(connect_message, **kwargs): + messages_history.append(connect_message) + if not connect_message.status.hasError: + self.connected_trees[service_name] = connect_message.tid + sendOpen(connect_message.tid) + else: + errback(OperationFailure('Failed to store %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) + + m = SMBMessage(self, ComTreeConnectAndxRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ), SERVICE_ANY, '')) + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, connectCB, errback, path = service_name) + messages_history.append(m) + else: + sendOpen(self.connected_trees[service_name]) + + def _deleteFiles_SMB1(self, service_name, path_file_pattern, callback, errback, timeout = 30): + if not self.has_authenticated: + raise NotReadyError('SMB connection not authenticated') + + path = path_file_pattern.replace('/', '\\') + messages_history = [ ] + + def sendDelete(tid): + m = SMBMessage(self, ComDeleteRequest(filename_pattern = path, + search_attributes = SMB_FILE_ATTRIBUTE_HIDDEN | SMB_FILE_ATTRIBUTE_SYSTEM)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, deleteCB, errback) + messages_history.append(m) + + def deleteCB(delete_message, **kwargs): + messages_history.append(delete_message) + if not delete_message.status.hasError: + callback(path_file_pattern) + else: + errback(OperationFailure('Failed to store %s on %s: Delete failed' % ( path, service_name ), messages_history)) + + if not self.connected_trees.has_key(service_name): + def connectCB(connect_message, **kwargs): + messages_history.append(connect_message) + if not connect_message.status.hasError: + self.connected_trees[service_name] = connect_message.tid + sendDelete(connect_message.tid) + else: + errback(OperationFailure('Failed to delete %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) + + m = SMBMessage(self, ComTreeConnectAndxRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ), SERVICE_ANY, '')) + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, connectCB, errback, path = service_name) + messages_history.append(m) + else: + sendDelete(self.connected_trees[service_name]) + + def _resetFileAttributes_SMB1(self, service_name, path_file_pattern, callback, errback, timeout = 30): + raise NotReadyError('resetFileAttributes is not yet implemented for SMB1') + + def _createDirectory_SMB1(self, service_name, path, callback, errback, timeout = 30): + if not self.has_authenticated: + raise NotReadyError('SMB connection not authenticated') + + path = path.replace('/', '\\') + messages_history = [ ] + + def sendCreate(tid): + m = SMBMessage(self, ComCreateDirectoryRequest(path)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, createCB, errback) + messages_history.append(m) + + def createCB(create_message, **kwargs): + messages_history.append(create_message) + if not create_message.status.hasError: + callback(path) + else: + errback(OperationFailure('Failed to create directory %s on %s: Create failed' % ( path, service_name ), messages_history)) + + if not self.connected_trees.has_key(service_name): + def connectCB(connect_message, **kwargs): + messages_history.append(connect_message) + if not connect_message.status.hasError: + self.connected_trees[service_name] = connect_message.tid + sendCreate(connect_message.tid) + else: + errback(OperationFailure('Failed to create directory %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) + + m = SMBMessage(self, ComTreeConnectAndxRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ), SERVICE_ANY, '')) + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, connectCB, errback, path = service_name) + messages_history.append(m) + else: + sendCreate(self.connected_trees[service_name]) + + def _deleteDirectory_SMB1(self, service_name, path, callback, errback, timeout = 30): + if not self.has_authenticated: + raise NotReadyError('SMB connection not authenticated') + + path = path.replace('/', '\\') + messages_history = [ ] + + def sendDelete(tid): + m = SMBMessage(self, ComDeleteDirectoryRequest(path)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, deleteCB, errback) + messages_history.append(m) + + def deleteCB(delete_message, **kwargs): + messages_history.append(delete_message) + if not delete_message.status.hasError: + callback(path) + else: + errback(OperationFailure('Failed to delete directory %s on %s: Delete failed' % ( path, service_name ), messages_history)) + + if not self.connected_trees.has_key(service_name): + def connectCB(connect_message, **kwargs): + messages_history.append(connect_message) + if not connect_message.status.hasError: + self.connected_trees[service_name] = connect_message.tid + sendDelete(connect_message.tid) + else: + errback(OperationFailure('Failed to delete %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history)) + + m = SMBMessage(self, ComTreeConnectAndxRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ), SERVICE_ANY, '')) + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, connectCB, errback, path = service_name) + messages_history.append(m) + else: + sendDelete(self.connected_trees[service_name]) + + def _rename_SMB1(self, service_name, old_path, new_path, callback, errback, timeout = 30): + if not self.has_authenticated: + raise NotReadyError('SMB connection not authenticated') + + new_path = new_path.replace('/', '\\') + old_path = old_path.replace('/', '\\') + messages_history = [ ] + + def sendRename(tid): + m = SMBMessage(self, ComRenameRequest(old_path = old_path, + new_path = new_path, + search_attributes = SMB_FILE_ATTRIBUTE_HIDDEN | SMB_FILE_ATTRIBUTE_SYSTEM)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, renameCB, errback) + messages_history.append(m) + + def renameCB(rename_message, **kwargs): + messages_history.append(rename_message) + if not rename_message.status.hasError: + callback(( old_path, new_path )) # Note that this is a tuple of 2-elements + else: + errback(OperationFailure('Failed to rename %s on %s: Rename failed' % ( old_path, service_name ), messages_history)) + + if not self.connected_trees.has_key(service_name): + def connectCB(connect_message, **kwargs): + messages_history.append(connect_message) + if not connect_message.status.hasError: + self.connected_trees[service_name] = connect_message.tid + sendRename(connect_message.tid) + else: + errback(OperationFailure('Failed to rename %s on %s: Unable to connect to shared device' % ( old_path, service_name ), messages_history)) + + m = SMBMessage(self, ComTreeConnectAndxRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ), SERVICE_ANY, '')) + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, connectCB, errback, path = service_name) + messages_history.append(m) + else: + sendRename(self.connected_trees[service_name]) + + def _listSnapshots_SMB1(self, service_name, path, callback, errback, timeout = 30): + if not self.has_authenticated: + raise NotReadyError('SMB connection not authenticated') + + expiry_time = time.time() + timeout + path = path.replace('/', '\\') + if not path.endswith('\\'): + path += '\\' + messages_history = [ ] + results = [ ] + + def sendOpen(tid): + m = SMBMessage(self, ComOpenAndxRequest(filename = path, + access_mode = 0x0040, # Sharing mode: Deny nothing to others + open_mode = 0x0001, # Failed if file does not exist + search_attributes = 0, + timeout = timeout * 1000)) + m.tid = tid + self._sendSMBMessage(m) + self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, openCB, errback) + messages_history.append(m) + + def openCB(open_message, **kwargs): + messages_history.append(open_message) + if not open_message.status.hasError: + sendEnumSnapshots(open_message.tid, open_message.payload.fid) + else: + errback(OperationFailure('Failed to list snapshots %s on %s: Unable to open path' % ( path, service_name ), messages_history)) + + def sendEnumSnapshots(tid, fid): + # [MS-CIFS]: 2.2.7.2 + # [MS-SMB]: 2.2.7.2.1 + setup_bytes = struct.pack('`. + + If you encounter *SharedFile* instance where its short_name attribute is empty but the filename attribute contains a short name which does not correspond + to any files/folders on your remote shared device, it could be that the original filename on the file/folder entry on the shared device contains + one of these prohibited characters: "\/[]:+|<>=;?,* (see [MS-CIFS]: 2.2.1.1.1 for more details). + + The following attributes are available: + + * create_time : Float value in number of seconds since 1970-01-01 00:00:00 to the time of creation of this file resource on the remote server + * last_access_time : Float value in number of seconds since 1970-01-01 00:00:00 to the time of last access of this file resource on the remote server + * last_write_time : Float value in number of seconds since 1970-01-01 00:00:00 to the time of last modification of this file resource on the remote server + * last_attr_change_time : Float value in number of seconds since 1970-01-01 00:00:00 to the time of last attribute change of this file resource on the remote server + * file_size : File size in number of bytes + * alloc_size : Total number of bytes allocated to store this file + * file_attributes : A SMB_EXT_FILE_ATTR integer value. See [MS-CIFS]: 2.2.1.2.3. You can perform bit-wise tests to determine the status of the file using the ATTR_xxx constants in smb_constants.py. + * short_name : Unicode string containing the short name of this file (usually in 8.3 notation) + * filename : Unicode string containing the long filename of this file. Each OS has a limit to the length of this file name. On Windows, it is 256 characters. + * file_id : Long value representing the file reference number for the file. If the remote system does not support this field, this field will be None or 0. See [MS-FSCC]: 2.4.17 + """ + + def __init__(self, create_time, last_access_time, last_write_time, last_attr_change_time, file_size, alloc_size, file_attributes, short_name, filename, file_id=None): + self.create_time = create_time #: Float value in number of seconds since 1970-01-01 00:00:00 to the time of creation of this file resource on the remote server + self.last_access_time = last_access_time #: Float value in number of seconds since 1970-01-01 00:00:00 to the time of last access of this file resource on the remote server + self.last_write_time = last_write_time #: Float value in number of seconds since 1970-01-01 00:00:00 to the time of last modification of this file resource on the remote server + self.last_attr_change_time = last_attr_change_time #: Float value in number of seconds since 1970-01-01 00:00:00 to the time of last attribute change of this file resource on the remote server + self.file_size = file_size #: File size in number of bytes + self.alloc_size = alloc_size #: Total number of bytes allocated to store this file + self.file_attributes = file_attributes #: A SMB_EXT_FILE_ATTR integer value. See [MS-CIFS]: 2.2.1.2.3. You can perform bit-wise tests to determine the status of the file using the ATTR_xxx constants in smb_constants.py. + self.short_name = short_name #: Unicode string containing the short name of this file (usually in 8.3 notation) + self.filename = filename #: Unicode string containing the long filename of this file. Each OS has a limit to the length of this file name. On Windows, it is 256 characters. + self.file_id = file_id #: Long value representing the file reference number for the file. If the remote system does not support this field, this field will be None or 0. See [MS-FSCC]: 2.4.17 + + @property + def isDirectory(self): + """A convenient property to return True if this file resource is a directory on the remote server""" + return bool(self.file_attributes & ATTR_DIRECTORY) + + @property + def isReadOnly(self): + """A convenient property to return True if this file resource is read-only on the remote server""" + return bool(self.file_attributes & ATTR_READONLY) + + @property + def isNormal(self): + """ + A convenient property to return True if this is a normal file. + + Note that pysmb defines a normal file as a file entry that is not read-only, not hidden, not system, not archive and not a directory. + It ignores other attributes like compression, indexed, sparse, temporary and encryption. + """ + return (self.file_attributes==ATTR_NORMAL) or ((self.file_attributes & 0xff)==0) + + def __unicode__(self): + return u'Shared file: %s (FileSize:%d bytes, isDirectory:%s)' % ( self.filename, self.file_size, self.isDirectory ) + + +class _PendingRequest: + + def __init__(self, mid, expiry_time, callback, errback, **kwargs): + self.mid = mid + self.expiry_time = expiry_time + self.callback = callback + self.errback = errback + self.kwargs = kwargs diff --git a/plugin.video.alfa/lib/sambatools/smb/ntlm.py b/plugin.video.alfa/lib/sambatools/smb/ntlm.py index f8bff724..ae6fc9e7 100755 --- a/plugin.video.alfa/lib/sambatools/smb/ntlm.py +++ b/plugin.video.alfa/lib/sambatools/smb/ntlm.py @@ -1,249 +1,248 @@ -import hmac -import random -import struct - -from utils.pyDes import des - -try: - import hashlib - hashlib.new('md4') - - def MD4(): return hashlib.new('md4') -except ( ImportError, ValueError ): - from utils.md4 import MD4 - -try: - import hashlib - def MD5(s): return hashlib.md5(s) -except ImportError: - import md5 - def MD5(s): return md5.new(s) - -################ -# NTLMv2 Methods -################ - -# The following constants are defined in accordance to [MS-NLMP]: 2.2.2.5 - -NTLM_NegotiateUnicode = 0x00000001 -NTLM_NegotiateOEM = 0x00000002 -NTLM_RequestTarget = 0x00000004 -NTLM_Unknown9 = 0x00000008 -NTLM_NegotiateSign = 0x00000010 -NTLM_NegotiateSeal = 0x00000020 -NTLM_NegotiateDatagram = 0x00000040 -NTLM_NegotiateLanManagerKey = 0x00000080 -NTLM_Unknown8 = 0x00000100 -NTLM_NegotiateNTLM = 0x00000200 -NTLM_NegotiateNTOnly = 0x00000400 -NTLM_Anonymous = 0x00000800 -NTLM_NegotiateOemDomainSupplied = 0x00001000 -NTLM_NegotiateOemWorkstationSupplied = 0x00002000 -NTLM_Unknown6 = 0x00004000 -NTLM_NegotiateAlwaysSign = 0x00008000 -NTLM_TargetTypeDomain = 0x00010000 -NTLM_TargetTypeServer = 0x00020000 -NTLM_TargetTypeShare = 0x00040000 -NTLM_NegotiateExtendedSecurity = 0x00080000 -NTLM_NegotiateIdentify = 0x00100000 -NTLM_Unknown5 = 0x00200000 -NTLM_RequestNonNTSessionKey = 0x00400000 -NTLM_NegotiateTargetInfo = 0x00800000 -NTLM_Unknown4 = 0x01000000 -NTLM_NegotiateVersion = 0x02000000 -NTLM_Unknown3 = 0x04000000 -NTLM_Unknown2 = 0x08000000 -NTLM_Unknown1 = 0x10000000 -NTLM_Negotiate128 = 0x20000000 -NTLM_NegotiateKeyExchange = 0x40000000 -NTLM_Negotiate56 = 0x80000000 - -NTLM_FLAGS = NTLM_NegotiateUnicode | \ - NTLM_RequestTarget | \ - NTLM_NegotiateNTLM | \ - NTLM_NegotiateAlwaysSign | \ - NTLM_NegotiateExtendedSecurity | \ - NTLM_NegotiateTargetInfo | \ - NTLM_NegotiateVersion | \ - NTLM_Negotiate128 | \ - NTLM_NegotiateKeyExchange | \ - NTLM_Negotiate56 - -def generateNegotiateMessage(): - """ - References: - =========== - - [MS-NLMP]: 2.2.1.1 - """ - s = struct.pack('<8sII8s8s8s', - 'NTLMSSP\0', 0x01, NTLM_FLAGS, - '\0' * 8, # Domain - '\0' * 8, # Workstation - '\x06\x00\x72\x17\x00\x00\x00\x0F') # Version [MS-NLMP]: 2.2.2.10 - return s - - -def generateAuthenticateMessage(challenge_flags, nt_response, lm_response, session_key, user, domain = 'WORKGROUP', workstation = 'LOCALHOST'): - """ - References: - =========== - - [MS-NLMP]: 2.2.1.3 - """ - FORMAT = '<8sIHHIHHIHHIHHIHHIHHII' - FORMAT_SIZE = struct.calcsize(FORMAT) - - lm_response_length = len(lm_response) - lm_response_offset = FORMAT_SIZE - nt_response_length = len(nt_response) - nt_response_offset = lm_response_offset + lm_response_length - domain_unicode = domain.encode('UTF-16LE') - domain_length = len(domain_unicode) - domain_offset = nt_response_offset + nt_response_length - - padding = '' - if domain_offset % 2 != 0: - padding = '\0' - domain_offset += 1 - - user_unicode = user.encode('UTF-16LE') - user_length = len(user_unicode) - user_offset = domain_offset + domain_length - workstation_unicode = workstation.encode('UTF-16LE') - workstation_length = len(workstation_unicode) - workstation_offset = user_offset + user_length - session_key_length = len(session_key) - session_key_offset = workstation_offset + workstation_length - - auth_flags = challenge_flags - auth_flags &= ~NTLM_NegotiateVersion - - s = struct.pack(FORMAT, - 'NTLMSSP\0', 0x03, - lm_response_length, lm_response_length, lm_response_offset, - nt_response_length, nt_response_length, nt_response_offset, - domain_length, domain_length, domain_offset, - user_length, user_length, user_offset, - workstation_length, workstation_length, workstation_offset, - session_key_length, session_key_length, session_key_offset, - auth_flags) - - return s + lm_response + nt_response + padding + domain_unicode + user_unicode + workstation_unicode + session_key - - -def decodeChallengeMessage(ntlm_data): - """ - References: - =========== - - [MS-NLMP]: 2.2.1.2 - - [MS-NLMP]: 2.2.2.1 (AV_PAIR) - """ - FORMAT = '<8sIHHII8s8sHHI' - FORMAT_SIZE = struct.calcsize(FORMAT) - - signature, message_type, \ - targetname_len, targetname_maxlen, targetname_offset, \ - flags, challenge, _, \ - targetinfo_len, targetinfo_maxlen, targetinfo_offset, \ - = struct.unpack(FORMAT, ntlm_data[:FORMAT_SIZE]) - - assert signature == 'NTLMSSP\0' - assert message_type == 0x02 - - return challenge, flags, ntlm_data[targetinfo_offset:targetinfo_offset+targetinfo_len] - - -def generateChallengeResponseV2(password, user, server_challenge, server_info, domain = '', client_challenge = None): - client_timestamp = '\0' * 8 - - if not client_challenge: - client_challenge = '' - for i in range(0, 8): - client_challenge += chr(random.getrandbits(8)) - assert len(client_challenge) == 8 - - d = MD4() - d.update(password.encode('UTF-16LE')) - ntlm_hash = d.digest() # The NT password hash - response_key = hmac.new(ntlm_hash, (user.upper() + domain).encode('UTF-16LE')).digest() # The NTLMv2 password hash. In [MS-NLMP], this is the result of NTOWFv2 and LMOWFv2 functions - temp = client_timestamp + client_challenge + domain.encode('UTF-16LE') + server_info - - nt_challenge_response = hmac.new(response_key, server_challenge + temp).digest() - lm_challenge_response = hmac.new(response_key, server_challenge + client_challenge).digest() + client_challenge - session_key = hmac.new(response_key, nt_challenge_response).digest() - - return nt_challenge_response, lm_challenge_response, session_key - - -################ -# NTLMv1 Methods -################ - -def expandDesKey(key): - """Expand the key from a 7-byte password key into a 8-byte DES key""" - s = chr(((ord(key[0]) >> 1) & 0x7f) << 1) - s = s + chr(((ord(key[0]) & 0x01) << 6 | ((ord(key[1]) >> 2) & 0x3f)) << 1) - s = s + chr(((ord(key[1]) & 0x03) << 5 | ((ord(key[2]) >> 3) & 0x1f)) << 1) - s = s + chr(((ord(key[2]) & 0x07) << 4 | ((ord(key[3]) >> 4) & 0x0f)) << 1) - s = s + chr(((ord(key[3]) & 0x0f) << 3 | ((ord(key[4]) >> 5) & 0x07)) << 1) - s = s + chr(((ord(key[4]) & 0x1f) << 2 | ((ord(key[5]) >> 6) & 0x03)) << 1) - s = s + chr(((ord(key[5]) & 0x3f) << 1 | ((ord(key[6]) >> 7) & 0x01)) << 1) - s = s + chr((ord(key[6]) & 0x7f) << 1) - return s - - -def DESL(K, D): - """ - References: - =========== - - http://ubiqx.org/cifs/SMB.html (2.8.3.4) - - [MS-NLMP]: Section 6 - """ - d1 = des(expandDesKey(K[0:7])) - d2 = des(expandDesKey(K[7:14])) - d3 = des(expandDesKey(K[14:16] + '\0' * 5)) - return d1.encrypt(D) + d2.encrypt(D) + d3.encrypt(D) - - -def generateChallengeResponseV1(password, server_challenge, has_extended_security = False, client_challenge = None): - """ - Generate a NTLMv1 response - - @param password: User password string - @param server_challange: A 8-byte challenge string sent from the server - @param has_extended_security: A boolean value indicating whether NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY flag is enabled in the NTLM negFlag - @param client_challenge: A 8-byte string representing client challenge. If None, it will be generated randomly if needed by the response generation - @return: a tuple of ( NT challenge response string, LM challenge response string ) - - References: - =========== - - http://ubiqx.org/cifs/SMB.html (2.8.3.3 and 2.8.3.4) - - [MS-NLMP]: 3.3.1 - """ - _password = (password.upper() + '\0' * 14)[:14] - d1 = des(expandDesKey(_password[:7])) - d2 = des(expandDesKey(_password[7:])) - lm_response_key = d1.encrypt("KGS!@#$%") + d2.encrypt("KGS!@#$%") # LM password hash. In [MS-NLMP], this is the result of LMOWFv1 function - - d = MD4() - d.update(password.encode('UTF-16LE')) - nt_response_key = d.digest() # In [MS-NLMP], this is the result of NTOWFv1 function - - if has_extended_security: - if not client_challenge: - client_challenge = '' - for i in range(0, 8): - client_challenge += chr(random.getrandbits(8)) - - assert len(client_challenge) == 8 - - lm_challenge_response = client_challenge + '\0'*16 - nt_challenge_response = DESL(nt_response_key, MD5(server_challenge + client_challenge).digest()[0:8]) - else: - nt_challenge_response = DESL(nt_response_key, server_challenge) # The result after DESL is the NT response - lm_challenge_response = DESL(lm_response_key, server_challenge) # The result after DESL is the LM response - - d = MD4() - d.update(nt_response_key) - session_key = d.digest() - - return nt_challenge_response, lm_challenge_response, session_key + +import types, hmac, binascii, struct, random +from utils.pyDes import des + +try: + import hashlib + hashlib.new('md4') + + def MD4(): return hashlib.new('md4') +except ( ImportError, ValueError ): + from utils.md4 import MD4 + +try: + import hashlib + def MD5(s): return hashlib.md5(s) +except ImportError: + import md5 + def MD5(s): return md5.new(s) + +################ +# NTLMv2 Methods +################ + +# The following constants are defined in accordance to [MS-NLMP]: 2.2.2.5 + +NTLM_NegotiateUnicode = 0x00000001 +NTLM_NegotiateOEM = 0x00000002 +NTLM_RequestTarget = 0x00000004 +NTLM_Unknown9 = 0x00000008 +NTLM_NegotiateSign = 0x00000010 +NTLM_NegotiateSeal = 0x00000020 +NTLM_NegotiateDatagram = 0x00000040 +NTLM_NegotiateLanManagerKey = 0x00000080 +NTLM_Unknown8 = 0x00000100 +NTLM_NegotiateNTLM = 0x00000200 +NTLM_NegotiateNTOnly = 0x00000400 +NTLM_Anonymous = 0x00000800 +NTLM_NegotiateOemDomainSupplied = 0x00001000 +NTLM_NegotiateOemWorkstationSupplied = 0x00002000 +NTLM_Unknown6 = 0x00004000 +NTLM_NegotiateAlwaysSign = 0x00008000 +NTLM_TargetTypeDomain = 0x00010000 +NTLM_TargetTypeServer = 0x00020000 +NTLM_TargetTypeShare = 0x00040000 +NTLM_NegotiateExtendedSecurity = 0x00080000 +NTLM_NegotiateIdentify = 0x00100000 +NTLM_Unknown5 = 0x00200000 +NTLM_RequestNonNTSessionKey = 0x00400000 +NTLM_NegotiateTargetInfo = 0x00800000 +NTLM_Unknown4 = 0x01000000 +NTLM_NegotiateVersion = 0x02000000 +NTLM_Unknown3 = 0x04000000 +NTLM_Unknown2 = 0x08000000 +NTLM_Unknown1 = 0x10000000 +NTLM_Negotiate128 = 0x20000000 +NTLM_NegotiateKeyExchange = 0x40000000 +NTLM_Negotiate56 = 0x80000000 + +NTLM_FLAGS = NTLM_NegotiateUnicode | \ + NTLM_RequestTarget | \ + NTLM_NegotiateNTLM | \ + NTLM_NegotiateAlwaysSign | \ + NTLM_NegotiateExtendedSecurity | \ + NTLM_NegotiateTargetInfo | \ + NTLM_NegotiateVersion | \ + NTLM_Negotiate128 | \ + NTLM_NegotiateKeyExchange | \ + NTLM_Negotiate56 + +def generateNegotiateMessage(): + """ + References: + =========== + - [MS-NLMP]: 2.2.1.1 + """ + s = struct.pack('<8sII8s8s8s', + 'NTLMSSP\0', 0x01, NTLM_FLAGS, + '\0' * 8, # Domain + '\0' * 8, # Workstation + '\x06\x00\x72\x17\x00\x00\x00\x0F') # Version [MS-NLMP]: 2.2.2.10 + return s + + +def generateAuthenticateMessage(challenge_flags, nt_response, lm_response, session_key, user, domain = 'WORKGROUP', workstation = 'LOCALHOST'): + """ + References: + =========== + - [MS-NLMP]: 2.2.1.3 + """ + FORMAT = '<8sIHHIHHIHHIHHIHHIHHII' + FORMAT_SIZE = struct.calcsize(FORMAT) + + lm_response_length = len(lm_response) + lm_response_offset = FORMAT_SIZE + nt_response_length = len(nt_response) + nt_response_offset = lm_response_offset + lm_response_length + domain_unicode = domain.encode('UTF-16LE') + domain_length = len(domain_unicode) + domain_offset = nt_response_offset + nt_response_length + + padding = '' + if domain_offset % 2 != 0: + padding = '\0' + domain_offset += 1 + + user_unicode = user.encode('UTF-16LE') + user_length = len(user_unicode) + user_offset = domain_offset + domain_length + workstation_unicode = workstation.encode('UTF-16LE') + workstation_length = len(workstation_unicode) + workstation_offset = user_offset + user_length + session_key_length = len(session_key) + session_key_offset = workstation_offset + workstation_length + + auth_flags = challenge_flags + auth_flags &= ~NTLM_NegotiateVersion + + s = struct.pack(FORMAT, + 'NTLMSSP\0', 0x03, + lm_response_length, lm_response_length, lm_response_offset, + nt_response_length, nt_response_length, nt_response_offset, + domain_length, domain_length, domain_offset, + user_length, user_length, user_offset, + workstation_length, workstation_length, workstation_offset, + session_key_length, session_key_length, session_key_offset, + auth_flags) + + return s + lm_response + nt_response + padding + domain_unicode + user_unicode + workstation_unicode + session_key + + +def decodeChallengeMessage(ntlm_data): + """ + References: + =========== + - [MS-NLMP]: 2.2.1.2 + - [MS-NLMP]: 2.2.2.1 (AV_PAIR) + """ + FORMAT = '<8sIHHII8s8sHHI' + FORMAT_SIZE = struct.calcsize(FORMAT) + + signature, message_type, \ + targetname_len, targetname_maxlen, targetname_offset, \ + flags, challenge, _, \ + targetinfo_len, targetinfo_maxlen, targetinfo_offset, \ + = struct.unpack(FORMAT, ntlm_data[:FORMAT_SIZE]) + + assert signature == 'NTLMSSP\0' + assert message_type == 0x02 + + return challenge, flags, ntlm_data[targetinfo_offset:targetinfo_offset+targetinfo_len] + + +def generateChallengeResponseV2(password, user, server_challenge, server_info, domain = '', client_challenge = None): + client_timestamp = '\0' * 8 + + if not client_challenge: + client_challenge = '' + for i in range(0, 8): + client_challenge += chr(random.getrandbits(8)) + assert len(client_challenge) == 8 + + d = MD4() + d.update(password.encode('UTF-16LE')) + ntlm_hash = d.digest() # The NT password hash + response_key = hmac.new(ntlm_hash, (user.upper() + domain).encode('UTF-16LE')).digest() # The NTLMv2 password hash. In [MS-NLMP], this is the result of NTOWFv2 and LMOWFv2 functions + temp = '\x01\x01' + '\0'*6 + client_timestamp + client_challenge + '\0'*4 + server_info + ntproofstr = hmac.new(response_key, server_challenge + temp).digest() + + nt_challenge_response = ntproofstr + temp + lm_challenge_response = hmac.new(response_key, server_challenge + client_challenge).digest() + client_challenge + session_key = hmac.new(response_key, ntproofstr).digest() + + return nt_challenge_response, lm_challenge_response, session_key + + +################ +# NTLMv1 Methods +################ + +def expandDesKey(key): + """Expand the key from a 7-byte password key into a 8-byte DES key""" + s = chr(((ord(key[0]) >> 1) & 0x7f) << 1) + s = s + chr(((ord(key[0]) & 0x01) << 6 | ((ord(key[1]) >> 2) & 0x3f)) << 1) + s = s + chr(((ord(key[1]) & 0x03) << 5 | ((ord(key[2]) >> 3) & 0x1f)) << 1) + s = s + chr(((ord(key[2]) & 0x07) << 4 | ((ord(key[3]) >> 4) & 0x0f)) << 1) + s = s + chr(((ord(key[3]) & 0x0f) << 3 | ((ord(key[4]) >> 5) & 0x07)) << 1) + s = s + chr(((ord(key[4]) & 0x1f) << 2 | ((ord(key[5]) >> 6) & 0x03)) << 1) + s = s + chr(((ord(key[5]) & 0x3f) << 1 | ((ord(key[6]) >> 7) & 0x01)) << 1) + s = s + chr((ord(key[6]) & 0x7f) << 1) + return s + + +def DESL(K, D): + """ + References: + =========== + - http://ubiqx.org/cifs/SMB.html (2.8.3.4) + - [MS-NLMP]: Section 6 + """ + d1 = des(expandDesKey(K[0:7])) + d2 = des(expandDesKey(K[7:14])) + d3 = des(expandDesKey(K[14:16] + '\0' * 5)) + return d1.encrypt(D) + d2.encrypt(D) + d3.encrypt(D) + + +def generateChallengeResponseV1(password, server_challenge, has_extended_security = False, client_challenge = None): + """ + Generate a NTLMv1 response + + @param password: User password string + @param server_challange: A 8-byte challenge string sent from the server + @param has_extended_security: A boolean value indicating whether NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY flag is enabled in the NTLM negFlag + @param client_challenge: A 8-byte string representing client challenge. If None, it will be generated randomly if needed by the response generation + @return: a tuple of ( NT challenge response string, LM challenge response string ) + + References: + =========== + - http://ubiqx.org/cifs/SMB.html (2.8.3.3 and 2.8.3.4) + - [MS-NLMP]: 3.3.1 + """ + _password = (password.upper() + '\0' * 14)[:14] + d1 = des(expandDesKey(_password[:7])) + d2 = des(expandDesKey(_password[7:])) + lm_response_key = d1.encrypt("KGS!@#$%") + d2.encrypt("KGS!@#$%") # LM password hash. In [MS-NLMP], this is the result of LMOWFv1 function + + d = MD4() + d.update(password.encode('UTF-16LE')) + nt_response_key = d.digest() # In [MS-NLMP], this is the result of NTOWFv1 function + + if has_extended_security: + if not client_challenge: + client_challenge = '' + for i in range(0, 8): + client_challenge += chr(random.getrandbits(8)) + + assert len(client_challenge) == 8 + + lm_challenge_response = client_challenge + '\0'*16 + nt_challenge_response = DESL(nt_response_key, MD5(server_challenge + client_challenge).digest()[0:8]) + else: + nt_challenge_response = DESL(nt_response_key, server_challenge) # The result after DESL is the NT response + lm_challenge_response = DESL(lm_response_key, server_challenge) # The result after DESL is the LM response + + d = MD4() + d.update(nt_response_key) + session_key = d.digest() + + return nt_challenge_response, lm_challenge_response, session_key diff --git a/plugin.video.alfa/lib/sambatools/smb/security_descriptors.py b/plugin.video.alfa/lib/sambatools/smb/security_descriptors.py new file mode 100644 index 00000000..9e6ebe14 --- /dev/null +++ b/plugin.video.alfa/lib/sambatools/smb/security_descriptors.py @@ -0,0 +1,367 @@ +""" +This module implements security descriptors, and the partial structures +used in them, as specified in [MS-DTYP]. +""" + +import struct + + +# Security descriptor control flags +# [MS-DTYP]: 2.4.6 +SECURITY_DESCRIPTOR_OWNER_DEFAULTED = 0x0001 +SECURITY_DESCRIPTOR_GROUP_DEFAULTED = 0x0002 +SECURITY_DESCRIPTOR_DACL_PRESENT = 0x0004 +SECURITY_DESCRIPTOR_DACL_DEFAULTED = 0x0008 +SECURITY_DESCRIPTOR_SACL_PRESENT = 0x0010 +SECURITY_DESCRIPTOR_SACL_DEFAULTED = 0x0020 +SECURITY_DESCRIPTOR_SERVER_SECURITY = 0x0040 +SECURITY_DESCRIPTOR_DACL_TRUSTED = 0x0080 +SECURITY_DESCRIPTOR_DACL_COMPUTED_INHERITANCE_REQUIRED = 0x0100 +SECURITY_DESCRIPTOR_SACL_COMPUTED_INHERITANCE_REQUIRED = 0x0200 +SECURITY_DESCRIPTOR_DACL_AUTO_INHERITED = 0x0400 +SECURITY_DESCRIPTOR_SACL_AUTO_INHERITED = 0x0800 +SECURITY_DESCRIPTOR_DACL_PROTECTED = 0x1000 +SECURITY_DESCRIPTOR_SACL_PROTECTED = 0x2000 +SECURITY_DESCRIPTOR_RM_CONTROL_VALID = 0x4000 +SECURITY_DESCRIPTOR_SELF_RELATIVE = 0x8000 + +# ACE types +# [MS-DTYP]: 2.4.4.1 +ACE_TYPE_ACCESS_ALLOWED = 0x00 +ACE_TYPE_ACCESS_DENIED = 0x01 +ACE_TYPE_SYSTEM_AUDIT = 0x02 +ACE_TYPE_SYSTEM_ALARM = 0x03 +ACE_TYPE_ACCESS_ALLOWED_COMPOUND = 0x04 +ACE_TYPE_ACCESS_ALLOWED_OBJECT = 0x05 +ACE_TYPE_ACCESS_DENIED_OBJECT = 0x06 +ACE_TYPE_SYSTEM_AUDIT_OBJECT = 0x07 +ACE_TYPE_SYSTEM_ALARM_OBJECT = 0x08 +ACE_TYPE_ACCESS_ALLOWED_CALLBACK = 0x09 +ACE_TYPE_ACCESS_DENIED_CALLBACK = 0x0A +ACE_TYPE_ACCESS_ALLOWED_CALLBACK_OBJECT = 0x0B +ACE_TYPE_ACCESS_DENIED_CALLBACK_OBJECT = 0x0C +ACE_TYPE_SYSTEM_AUDIT_CALLBACK = 0x0D +ACE_TYPE_SYSTEM_ALARM_CALLBACK = 0x0E +ACE_TYPE_SYSTEM_AUDIT_CALLBACK_OBJECT = 0x0F +ACE_TYPE_SYSTEM_ALARM_CALLBACK_OBJECT = 0x10 +ACE_TYPE_SYSTEM_MANDATORY_LABEL = 0x11 +ACE_TYPE_SYSTEM_RESOURCE_ATTRIBUTE = 0x12 +ACE_TYPE_SYSTEM_SCOPED_POLICY_ID = 0x13 + +# ACE flags +# [MS-DTYP]: 2.4.4.1 +ACE_FLAG_OBJECT_INHERIT = 0x01 +ACE_FLAG_CONTAINER_INHERIT = 0x02 +ACE_FLAG_NO_PROPAGATE_INHERIT = 0x04 +ACE_FLAG_INHERIT_ONLY = 0x08 +ACE_FLAG_INHERITED = 0x10 +ACE_FLAG_SUCCESSFUL_ACCESS = 0x40 +ACE_FLAG_FAILED_ACCESS = 0x80 + +# Pre-defined well-known SIDs +# [MS-DTYP]: 2.4.2.4 +SID_NULL = "S-1-0-0" +SID_EVERYONE = "S-1-1-0" +SID_LOCAL = "S-1-2-0" +SID_CONSOLE_LOGON = "S-1-2-1" +SID_CREATOR_OWNER = "S-1-3-0" +SID_CREATOR_GROUP = "S-1-3-1" +SID_OWNER_SERVER = "S-1-3-2" +SID_GROUP_SERVER = "S-1-3-3" +SID_OWNER_RIGHTS = "S-1-3-4" +SID_NT_AUTHORITY = "S-1-5" +SID_DIALUP = "S-1-5-1" +SID_NETWORK = "S-1-5-2" +SID_BATCH = "S-1-5-3" +SID_INTERACTIVE = "S-1-5-4" +SID_SERVICE = "S-1-5-6" +SID_ANONYMOUS = "S-1-5-7" +SID_PROXY = "S-1-5-8" +SID_ENTERPRISE_DOMAIN_CONTROLLERS = "S-1-5-9" +SID_PRINCIPAL_SELF = "S-1-5-10" +SID_AUTHENTICATED_USERS = "S-1-5-11" +SID_RESTRICTED_CODE = "S-1-5-12" +SID_TERMINAL_SERVER_USER = "S-1-5-13" +SID_REMOTE_INTERACTIVE_LOGON = "S-1-5-14" +SID_THIS_ORGANIZATION = "S-1-5-15" +SID_IUSR = "S-1-5-17" +SID_LOCAL_SYSTEM = "S-1-5-18" +SID_LOCAL_SERVICE = "S-1-5-19" +SID_NETWORK_SERVICE = "S-1-5-20" +SID_COMPOUNDED_AUTHENTICATION = "S-1-5-21-0-0-0-496" +SID_CLAIMS_VALID = "S-1-5-21-0-0-0-497" +SID_BUILTIN_ADMINISTRATORS = "S-1-5-32-544" +SID_BUILTIN_USERS = "S-1-5-32-545" +SID_BUILTIN_GUESTS = "S-1-5-32-546" +SID_POWER_USERS = "S-1-5-32-547" +SID_ACCOUNT_OPERATORS = "S-1-5-32-548" +SID_SERVER_OPERATORS = "S-1-5-32-549" +SID_PRINTER_OPERATORS = "S-1-5-32-550" +SID_BACKUP_OPERATORS = "S-1-5-32-551" +SID_REPLICATOR = "S-1-5-32-552" +SID_ALIAS_PREW2KCOMPACC = "S-1-5-32-554" +SID_REMOTE_DESKTOP = "S-1-5-32-555" +SID_NETWORK_CONFIGURATION_OPS = "S-1-5-32-556" +SID_INCOMING_FOREST_TRUST_BUILDERS = "S-1-5-32-557" +SID_PERFMON_USERS = "S-1-5-32-558" +SID_PERFLOG_USERS = "S-1-5-32-559" +SID_WINDOWS_AUTHORIZATION_ACCESS_GROUP = "S-1-5-32-560" +SID_TERMINAL_SERVER_LICENSE_SERVERS = "S-1-5-32-561" +SID_DISTRIBUTED_COM_USERS = "S-1-5-32-562" +SID_IIS_IUSRS = "S-1-5-32-568" +SID_CRYPTOGRAPHIC_OPERATORS = "S-1-5-32-569" +SID_EVENT_LOG_READERS = "S-1-5-32-573" +SID_CERTIFICATE_SERVICE_DCOM_ACCESS = "S-1-5-32-574" +SID_RDS_REMOTE_ACCESS_SERVERS = "S-1-5-32-575" +SID_RDS_ENDPOINT_SERVERS = "S-1-5-32-576" +SID_RDS_MANAGEMENT_SERVERS = "S-1-5-32-577" +SID_HYPER_V_ADMINS = "S-1-5-32-578" +SID_ACCESS_CONTROL_ASSISTANCE_OPS = "S-1-5-32-579" +SID_REMOTE_MANAGEMENT_USERS = "S-1-5-32-580" +SID_WRITE_RESTRICTED_CODE = "S-1-5-33" +SID_NTLM_AUTHENTICATION = "S-1-5-64-10" +SID_SCHANNEL_AUTHENTICATION = "S-1-5-64-14" +SID_DIGEST_AUTHENTICATION = "S-1-5-64-21" +SID_THIS_ORGANIZATION_CERTIFICATE = "S-1-5-65-1" +SID_NT_SERVICE = "S-1-5-80" +SID_USER_MODE_DRIVERS = "S-1-5-84-0-0-0-0-0" +SID_LOCAL_ACCOUNT = "S-1-5-113" +SID_LOCAL_ACCOUNT_AND_MEMBER_OF_ADMINISTRATORS_GROUP = "S-1-5-114" +SID_OTHER_ORGANIZATION = "S-1-5-1000" +SID_ALL_APP_PACKAGES = "S-1-15-2-1" +SID_ML_UNTRUSTED = "S-1-16-0" +SID_ML_LOW = "S-1-16-4096" +SID_ML_MEDIUM = "S-1-16-8192" +SID_ML_MEDIUM_PLUS = "S-1-16-8448" +SID_ML_HIGH = "S-1-16-12288" +SID_ML_SYSTEM = "S-1-16-16384" +SID_ML_PROTECTED_PROCESS = "S-1-16-20480" +SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY = "S-1-18-1" +SID_SERVICE_ASSERTED_IDENTITY = "S-1-18-2" +SID_FRESH_PUBLIC_KEY_IDENTITY = "S-1-18-3" +SID_KEY_TRUST_IDENTITY = "S-1-18-4" +SID_KEY_PROPERTY_MFA = "S-1-18-5" +SID_KEY_PROPERTY_ATTESTATION = "S-1-18-6" + + +class SID(object): + """ + A Windows security identifier. Represents a single principal, such a + user or a group, as a sequence of numbers consisting of the revision, + identifier authority, and a variable-length list of subauthorities. + + See [MS-DTYP]: 2.4.2 + """ + def __init__(self, revision, identifier_authority, subauthorities): + #: Revision, should always be 1. + self.revision = revision + #: An integer representing the identifier authority. + self.identifier_authority = identifier_authority + #: A list of integers representing all subauthorities. + self.subauthorities = subauthorities + + def __str__(self): + """ + String representation, as specified in [MS-DTYP]: 2.4.2.1 + """ + if self.identifier_authority >= 2**32: + id_auth = '%#x' % (self.identifier_authority,) + else: + id_auth = self.identifier_authority + auths = [self.revision, id_auth] + self.subauthorities + return 'S-' + '-'.join(str(subauth) for subauth in auths) + + def __repr__(self): + return 'SID(%r)' % (str(self),) + + @classmethod + def from_bytes(cls, data, return_tail=False): + revision, subauth_count = struct.unpack('Q', '\x00\x00' + data[2:8])[0] + subauth_data = data[8:] + subauthorities = [struct.unpack('= size + + body = data[header_size:size] + additional_data = {} + + # In all ACE types, the mask immediately follows the header. + mask = struct.unpack('= size + + for i in range(count): + ace_size = struct.unpack(''), os.linesep )) - b.write('Status: 0x%08X %s' % ( self.status, os.linesep )) - b.write('Flags: 0x%02X %s' % ( self.flags, os.linesep )) - b.write('PID: %d %s' % ( self.pid, os.linesep )) - b.write('MID: %d %s' % ( self.mid, os.linesep )) - b.write('TID: %d %s' % ( self.tid, os.linesep )) - b.write('Data: %d bytes %s%s %s' % ( len(self.data), os.linesep, binascii.hexlify(self.data), os.linesep )) - return b.getvalue() - - def reset(self): - self.raw_data = '' - self.command = 0 - self.status = 0 - self.flags = 0 - - self.next_command_offset = 0 - self.mid = 0 - self.session_id = 0 - self.signature = '\0'*16 - self.payload = None - self.data = '' - - # For async SMB2 message - self.async_id = 0 - - # For sync SMB2 message - self.pid = 0 - self.tid = 0 - - # Not used in this class. Maintained for compatibility with SMBMessage class - self.flags2 = 0 - self.uid = 0 - self.security = 0L - self.parameters_data = '' - - def encode(self): - """ - Encode this SMB2 message into a series of bytes suitable to be embedded with a NetBIOS session message. - AssertionError will be raised if this SMB message has not been initialized with a Payload instance - - @return: a string containing the encoded SMB2 message - """ - assert self.payload - - self.pid = os.getpid() - self.payload.prepare(self) - - headers_data = struct.pack(self.HEADER_STRUCT_FORMAT, - '\xFESMB', self.HEADER_SIZE, 0, self.status, self.command, 0, self.flags) + \ - struct.pack(self.SYNC_HEADER_STRUCT_FORMAT, self.next_command_offset, self.mid, self.pid, self.tid, self.session_id, self.signature) - return headers_data + self.data - - def decode(self, buf): - """ - Decodes the SMB message in buf. - All fields of the SMB2Message object will be reset to default values before decoding. - On errors, do not assume that the fields will be reinstated back to what they are before - this method is invoked. - - References - ========== - - [MS-SMB2]: 2.2.1 - - @param buf: data containing one complete SMB2 message - @type buf: string - @return: a positive integer indicating the number of bytes used in buf to decode this SMB message - @raise ProtocolError: raised when decoding fails - """ - buf_len = len(buf) - if buf_len < 64: # All SMB2 headers must be at least 64 bytes. [MS-SMB2]: 2.2.1.1, 2.2.1.2 - raise ProtocolError('Not enough data to decode SMB2 header', buf) - - self.reset() - - protocol, struct_size, self.credit_charge, self.status, \ - self.command, self.credit_re, self.flags = struct.unpack(self.HEADER_STRUCT_FORMAT, buf[:self.HEADER_STRUCT_SIZE]) - - if protocol != '\xFESMB': - raise ProtocolError('Invalid 4-byte SMB2 protocol field', buf) - - if struct_size != self.HEADER_SIZE: - raise ProtocolError('Invalid SMB2 header structure size') - - if self.isAsync: - if buf_len < self.HEADER_STRUCT_SIZE+self.ASYNC_HEADER_STRUCT_SIZE: - raise ProtocolError('Not enough data to decode SMB2 header', buf) - - self.next_command_offset, self.mid, self.async_id, self.session_id, \ - self.signature = struct.unpack(self.ASYNC_HEADER_STRUCT_FORMAT, - buf[self.HEADER_STRUCT_SIZE:self.HEADER_STRUCT_SIZE+self.ASYNC_HEADER_STRUCT_SIZE]) - else: - if buf_len < self.HEADER_STRUCT_SIZE+self.SYNC_HEADER_STRUCT_SIZE: - raise ProtocolError('Not enough data to decode SMB2 header', buf) - - self.next_command_offset, self.mid, self.pid, self.tid, self.session_id, \ - self.signature = struct.unpack(self.SYNC_HEADER_STRUCT_FORMAT, - buf[self.HEADER_STRUCT_SIZE:self.HEADER_STRUCT_SIZE+self.SYNC_HEADER_STRUCT_SIZE]) - - if self.next_command_offset > 0: - self.raw_data = buf[:self.next_command_offset] - self.data = buf[self.HEADER_SIZE:self.next_command_offset] - else: - self.raw_data = buf - self.data = buf[self.HEADER_SIZE:] - - self._decodeCommand() - if self.payload: - self.payload.decode(self) - - return len(self.raw_data) - - def _decodeCommand(self): - if self.command == SMB2_COM_READ: - self.payload = SMB2ReadResponse() - elif self.command == SMB2_COM_WRITE: - self.payload = SMB2WriteResponse() - elif self.command == SMB2_COM_QUERY_DIRECTORY: - self.payload = SMB2QueryDirectoryResponse() - elif self.command == SMB2_COM_CREATE: - self.payload = SMB2CreateResponse() - elif self.command == SMB2_COM_CLOSE: - self.payload = SMB2CloseResponse() - elif self.command == SMB2_COM_QUERY_INFO: - self.payload = SMB2QueryInfoResponse() - elif self.command == SMB2_COM_SET_INFO: - self.payload = SMB2SetInfoResponse() - elif self.command == SMB2_COM_IOCTL: - self.payload = SMB2IoctlResponse() - elif self.command == SMB2_COM_TREE_CONNECT: - self.payload = SMB2TreeConnectResponse() - elif self.command == SMB2_COM_SESSION_SETUP: - self.payload = SMB2SessionSetupResponse() - elif self.command == SMB2_COM_NEGOTIATE: - self.payload = SMB2NegotiateResponse() - elif self.command == SMB2_COM_ECHO: - self.payload = SMB2EchoResponse() - - @property - def isAsync(self): - return bool(self.flags & SMB2_FLAGS_ASYNC_COMMAND) - - @property - def isReply(self): - return bool(self.flags & SMB2_FLAGS_SERVER_TO_REDIR) - - -class Structure: - - def initMessage(self, message): - pass - - def prepare(self, message): - raise NotImplementedError - - def decode(self, message): - raise NotImplementedError - - -class SMB2NegotiateResponse(Structure): - """ - Contains information on the SMB2_NEGOTIATE response from server - - After calling the decode method, each instance will contain the following attributes, - - security_mode (integer) - - dialect_revision (integer) - - server_guid (string) - - max_transact_size (integer) - - max_read_size (integer) - - max_write_size (integer) - - system_time (long) - - server_start_time (long) - - security_blob (string) - - References: - =========== - - [MS-SMB2]: 2.2.4 - """ - - STRUCTURE_FORMAT = " 0: - self.in_data = message.raw_data[input_offset:input_offset+input_len] - else: - self.in_data = '' - - if output_len > 0: - self.out_data = message.raw_data[output_offset:output_offset+output_len] - else: - self.out_data = '' - - -class SMB2CloseRequest(Structure): - """ - References: - =========== - - [MS-SMB2]: 2.2.15 - """ - - STRUCTURE_FORMAT = "'), os.linesep )) + b.write('Status: 0x%08X %s' % ( self.status, os.linesep )) + b.write('Flags: 0x%02X %s' % ( self.flags, os.linesep )) + b.write('PID: %d %s' % ( self.pid, os.linesep )) + b.write('MID: %d %s' % ( self.mid, os.linesep )) + b.write('TID: %d %s' % ( self.tid, os.linesep )) + b.write('Data: %d bytes %s%s %s' % ( len(self.data), os.linesep, binascii.hexlify(self.data), os.linesep )) + return b.getvalue() + + def reset(self): + self.raw_data = '' + self.command = 0 + self.status = 0 + self.flags = 0 + + self.next_command_offset = 0 + self.mid = 0 + self.session_id = 0 + self.signature = '\0'*16 + self.payload = None + self.data = '' + + # For async SMB2 message + self.async_id = 0 + + # For sync SMB2 message + self.pid = 0 + self.tid = 0 + + # credit related + self.credit_charge = 0 + self.credit_request = 1 + + # Not used in this class. Maintained for compatibility with SMBMessage class + self.flags2 = 0 + self.uid = 0 + self.security = 0L + self.parameters_data = '' + + def encode(self): + """ + Encode this SMB2 message into a series of bytes suitable to be embedded with a NetBIOS session message. + AssertionError will be raised if this SMB message has not been initialized with an SMB instance + AssertionError will be raised if this SMB message has not been initialized with a Payload instance + + The header format is: + - Protocol ID + - Structure Size + - Credit Charge + - Status / Channel Sequence + - Command + - Credit Request / Credit Response + - Flags + - Next Compound + - MessageId + - Reserved + - TreeId + - Session ID + - Signature + + @return: a string containing the encoded SMB2 message + """ + assert self.payload + assert self.conn + + self.pid = os.getpid() + self.payload.prepare(self) + + # If Connection.Dialect is not "2.0.2" and if Connection.SupportsMultiCredit is TRUE, the + # CreditCharge field in the SMB2 header MUST be set to ( 1 + (OutputBufferLength - 1) / 65536 ) + # This only applies to SMB2ReadRequest, SMB2WriteRequest, SMB2IoctlRequest and SMB2QueryDirectory + # See: MS-SMB2 3.2.4.1.5: For all other requests, the client MUST set CreditCharge to 1, even if the + # payload size of a request or the anticipated response is greater than 65536. + if self.conn.smb2_dialect != SMB2_DIALECT_2: + if self.conn.cap_multi_credit: + # self.credit_charge will be set by some commands if necessary (Read/Write/Ioctl/QueryDirectory) + # If not set, but dialect is SMB 2.1 or above, we must set it to 1 + if self.credit_charge is 0: + self.credit_charge = 1 + else: + # If >= SMB 2.1, but server does not support multi credit operations we must set to 1 + self.credit_charge = 1 + + if self.mid > 3: + self.credit_request = 127 + + headers_data = struct.pack(self.HEADER_STRUCT_FORMAT, + '\xFESMB', # Protocol ID + self.HEADER_SIZE, # Structure Size + self.credit_charge, # Credit Charge + self.status, # Status / Channel Sequence + self.command, # Command + self.credit_request, # Credit Request / Credit Response + self.flags, # Flags + ) + \ + struct.pack(self.SYNC_HEADER_STRUCT_FORMAT, + self.next_command_offset, # Next Compound + self.mid, # Message ID + self.pid, # Process ID + self.tid, # Tree ID + self.session_id, # Session ID + self.signature) # Signature + return headers_data + self.data + + def decode(self, buf): + """ + Decodes the SMB message in buf. + All fields of the SMB2Message object will be reset to default values before decoding. + On errors, do not assume that the fields will be reinstated back to what they are before + this method is invoked. + + References + ========== + - [MS-SMB2]: 2.2.1 + + @param buf: data containing one complete SMB2 message + @type buf: string + @return: a positive integer indicating the number of bytes used in buf to decode this SMB message + @raise ProtocolError: raised when decoding fails + """ + buf_len = len(buf) + if buf_len < 64: # All SMB2 headers must be at least 64 bytes. [MS-SMB2]: 2.2.1.1, 2.2.1.2 + raise ProtocolError('Not enough data to decode SMB2 header', buf) + + self.reset() + + protocol, struct_size, self.credit_charge, self.status, \ + self.command, self.credit_response, \ + self.flags = struct.unpack(self.HEADER_STRUCT_FORMAT, buf[:self.HEADER_STRUCT_SIZE]) + + if protocol != '\xFESMB': + raise ProtocolError('Invalid 4-byte SMB2 protocol field', buf) + + if struct_size != self.HEADER_SIZE: + raise ProtocolError('Invalid SMB2 header structure size') + + if self.isAsync: + if buf_len < self.HEADER_STRUCT_SIZE+self.ASYNC_HEADER_STRUCT_SIZE: + raise ProtocolError('Not enough data to decode SMB2 header', buf) + + self.next_command_offset, self.mid, self.async_id, self.session_id, \ + self.signature = struct.unpack(self.ASYNC_HEADER_STRUCT_FORMAT, + buf[self.HEADER_STRUCT_SIZE:self.HEADER_STRUCT_SIZE+self.ASYNC_HEADER_STRUCT_SIZE]) + else: + if buf_len < self.HEADER_STRUCT_SIZE+self.SYNC_HEADER_STRUCT_SIZE: + raise ProtocolError('Not enough data to decode SMB2 header', buf) + + self.next_command_offset, self.mid, self.pid, self.tid, self.session_id, \ + self.signature = struct.unpack(self.SYNC_HEADER_STRUCT_FORMAT, + buf[self.HEADER_STRUCT_SIZE:self.HEADER_STRUCT_SIZE+self.SYNC_HEADER_STRUCT_SIZE]) + + if self.next_command_offset > 0: + self.raw_data = buf[:self.next_command_offset] + self.data = buf[self.HEADER_SIZE:self.next_command_offset] + else: + self.raw_data = buf + self.data = buf[self.HEADER_SIZE:] + + self._decodeCommand() + if self.payload: + self.payload.decode(self) + + return len(self.raw_data) + + def _decodeCommand(self): + if self.command == SMB2_COM_READ: + self.payload = SMB2ReadResponse() + elif self.command == SMB2_COM_WRITE: + self.payload = SMB2WriteResponse() + elif self.command == SMB2_COM_QUERY_DIRECTORY: + self.payload = SMB2QueryDirectoryResponse() + elif self.command == SMB2_COM_CREATE: + self.payload = SMB2CreateResponse() + elif self.command == SMB2_COM_CLOSE: + self.payload = SMB2CloseResponse() + elif self.command == SMB2_COM_QUERY_INFO: + self.payload = SMB2QueryInfoResponse() + elif self.command == SMB2_COM_SET_INFO: + self.payload = SMB2SetInfoResponse() + elif self.command == SMB2_COM_IOCTL: + self.payload = SMB2IoctlResponse() + elif self.command == SMB2_COM_TREE_CONNECT: + self.payload = SMB2TreeConnectResponse() + elif self.command == SMB2_COM_SESSION_SETUP: + self.payload = SMB2SessionSetupResponse() + elif self.command == SMB2_COM_NEGOTIATE: + self.payload = SMB2NegotiateResponse() + elif self.command == SMB2_COM_ECHO: + self.payload = SMB2EchoResponse() + + @property + def isAsync(self): + return bool(self.flags & SMB2_FLAGS_ASYNC_COMMAND) + + @property + def isReply(self): + return bool(self.flags & SMB2_FLAGS_SERVER_TO_REDIR) + + +class Structure: + + def initMessage(self, message): + pass + + def prepare(self, message): + raise NotImplementedError + + def decode(self, message): + raise NotImplementedError + + +class SMB2NegotiateRequest(Structure): + """ + 2.2.3 SMB2 NEGOTIATE Request + The SMB2 NEGOTIATE Request packet is used by the client to notify the server what dialects of the SMB 2 Protocol + the client understands. This request is composed of an SMB2 header, as specified in section 2.2.1, + followed by this request structure: + + SMB2 Negotiate Request Packet structure: + StructureSize (2 bytes) + DialectCount (2 bytes) + SecurityMode (2 bytes) + Reserved (2 bytes) + Capabilities (4 bytes) + ClientGuid (16 bytes) + ClientStartTime (8 bytes): + ClientStartTime (8 bytes): + Dialects (variable): An array of one or more 16-bit integers + + References: + =========== + - [MS-SMB2]: 2.2.3 + + """ + + + STRUCTURE_FORMAT = " 0 # SMB2_SESSION_FLAG_IS_GUEST + + @property + def isAnonymousSession(self): + return (self.session_flags & 0x0002) > 0 # SMB2_SESSION_FLAG_IS_NULL + + def decode(self, message): + assert message.command == SMB2_COM_SESSION_SETUP + + struct_size, self.session_flags, security_blob_offset, security_blob_len \ + = struct.unpack(self.STRUCTURE_FORMAT, message.raw_data[SMB2Message.HEADER_SIZE:SMB2Message.HEADER_SIZE+self.STRUCTURE_SIZE]) + + self.security_blob = message.raw_data[security_blob_offset:security_blob_offset+security_blob_len] + + +class SMB2TreeConnectRequest(Structure): + """ + References: + =========== + - [MS-SMB2]: 2.2.9 + """ + + STRUCTURE_FORMAT = " 0: + self.in_data = message.raw_data[input_offset:input_offset+input_len] + else: + self.in_data = '' + + if output_len > 0: + self.out_data = message.raw_data[output_offset:output_offset+output_len] + else: + self.out_data = '' + + +class SMB2CloseRequest(Structure): + """ + References: + =========== + - [MS-SMB2]: 2.2.15 + """ + + STRUCTURE_FORMAT = "> 24, self.internal_value & 0xFFFF ) - - @property - def hasError(self): - return self.internal_value != 0 - - -class SMBMessage: - - HEADER_STRUCT_FORMAT = "<4sBIBHHQxxHHHHB" - HEADER_STRUCT_SIZE = struct.calcsize(HEADER_STRUCT_FORMAT) - - log = logging.getLogger('SMB.SMBMessage') - protocol = 1 - - def __init__(self, payload = None): - self.reset() - if payload: - self.payload = payload - self.payload.initMessage(self) - - def __str__(self): - b = StringIO() - b.write('Command: 0x%02X (%s) %s' % ( self.command, SMB_COMMAND_NAMES.get(self.command, ''), os.linesep )) - b.write('Status: %s %s' % ( str(self.status), os.linesep )) - b.write('Flags: 0x%02X %s' % ( self.flags, os.linesep )) - b.write('Flags2: 0x%04X %s' % ( self.flags2, os.linesep )) - b.write('PID: %d %s' % ( self.pid, os.linesep )) - b.write('UID: %d %s' % ( self.uid, os.linesep )) - b.write('MID: %d %s' % ( self.mid, os.linesep )) - b.write('TID: %d %s' % ( self.tid, os.linesep )) - b.write('Security: 0x%016X %s' % ( self.security, os.linesep )) - b.write('Parameters: %d bytes %s%s %s' % ( len(self.parameters_data), os.linesep, binascii.hexlify(self.parameters_data), os.linesep )) - b.write('Data: %d bytes %s%s %s' % ( len(self.data), os.linesep, binascii.hexlify(self.data), os.linesep )) - return b.getvalue() - - def reset(self): - self.raw_data = '' - self.command = 0 - self.status = SMBError() - self.flags = 0 - self.flags2 = 0 - self.pid = 0 - self.tid = 0 - self.uid = 0 - self.mid = 0 - self.security = 0L - self.parameters_data = '' - self.data = '' - self.payload = None - - @property - def isReply(self): - return bool(self.flags & SMB_FLAGS_REPLY) - - @property - def hasExtendedSecurity(self): - return bool(self.flags2 & SMB_FLAGS2_EXTENDED_SECURITY) - - def encode(self): - """ - Encode this SMB message into a series of bytes suitable to be embedded with a NetBIOS session message. - AssertionError will be raised if this SMB message has not been initialized with a Payload instance - - @return: a string containing the encoded SMB message - """ - assert self.payload - - self.pid = os.getpid() - self.payload.prepare(self) - - parameters_len = len(self.parameters_data) - assert parameters_len % 2 == 0 - - headers_data = struct.pack(self.HEADER_STRUCT_FORMAT, - '\xFFSMB', self.command, self.status.internal_value, self.flags, - self.flags2, (self.pid >> 16) & 0xFFFF, self.security, self.tid, - self.pid & 0xFFFF, self.uid, self.mid, int(parameters_len / 2)) - return headers_data + self.parameters_data + struct.pack(' 0 and buf_len < (datalen_offset + 2 + body_len): - # Not enough data in buf to decode body - raise ProtocolError('Not enough data. Body decoding failed', buf) - - self.parameters_data = buf[offset:datalen_offset] - - if body_len > 0: - self.data = buf[datalen_offset+2:datalen_offset+2+body_len] - - self.raw_data = buf - self._decodePayload() - - return self.HEADER_STRUCT_SIZE + params_count * 2 + 2 + body_len - - def _decodePayload(self): - if self.command == SMB_COM_READ_ANDX: - self.payload = ComReadAndxResponse() - elif self.command == SMB_COM_WRITE_ANDX: - self.payload = ComWriteAndxResponse() - elif self.command == SMB_COM_TRANSACTION: - self.payload = ComTransactionResponse() - elif self.command == SMB_COM_TRANSACTION2: - self.payload = ComTransaction2Response() - elif self.command == SMB_COM_OPEN_ANDX: - self.payload = ComOpenAndxResponse() - elif self.command == SMB_COM_NT_CREATE_ANDX: - self.payload = ComNTCreateAndxResponse() - elif self.command == SMB_COM_TREE_CONNECT_ANDX: - self.payload = ComTreeConnectAndxResponse() - elif self.command == SMB_COM_ECHO: - self.payload = ComEchoResponse() - elif self.command == SMB_COM_SESSION_SETUP_ANDX: - self.payload = ComSessionSetupAndxResponse() - elif self.command == SMB_COM_NEGOTIATE: - self.payload = ComNegotiateResponse() - - if self.payload: - self.payload.decode(self) - - -class Payload: - - DEFAULT_ANDX_PARAM_HEADER = '\xFF\x00\x00\x00' - DEFAULT_ANDX_PARAM_SIZE = 4 - - def initMessage(self, message): - # SMB_FLAGS2_UNICODE must always be enabled. Without this, almost all the Payload subclasses will need to be - # rewritten to check for OEM/Unicode strings which will be tedious. Fortunately, almost all tested CIFS services - # support SMB_FLAGS2_UNICODE by default. - assert message.payload == self - message.flags = SMB_FLAGS_CASE_INSENSITIVE | SMB_FLAGS_CANONICALIZED_PATHS - message.flags2 = SMB_FLAGS2_UNICODE | SMB_FLAGS2_NT_STATUS | SMB_FLAGS2_LONG_NAMES | SMB_FLAGS2_EAS - - if SUPPORT_EXTENDED_SECURITY: - message.flags2 |= SMB_FLAGS2_EXTENDED_SECURITY | SMB_FLAGS2_SMB_SECURITY_SIGNATURE - - def prepare(self, message): - raise NotImplementedError - - def decode(self, message): - raise NotImplementedError - - -class ComNegotiateRequest(Payload): - """ - References: - =========== - - [MS-CIFS]: 2.2.4.52.1 - - [MS-SMB]: 2.2.4.5.1 - """ - - def initMessage(self, message): - Payload.initMessage(self, message) - message.command = SMB_COM_NEGOTIATE - - def prepare(self, message): - assert message.payload == self - message.parameters_data = '' - if SUPPORT_SMB2: - message.data = ''.join(map(lambda s: '\x02'+s+'\x00', DIALECTS + DIALECTS2)) - else: - message.data = ''.join(map(lambda s: '\x02'+s+'\x00', DIALECTS)) - - -class ComNegotiateResponse(Payload): - """ - Contains information on the SMB_COM_NEGOTIATE response from server - - After calling the decode method, each instance will contain the following attributes, - - security_mode (integer) - - max_mpx_count (integer) - - max_number_vcs (integer) - - max_buffer_size (long) - - max_raw_size (long) - - session_key (long) - - capabilities (long) - - system_time (long) - - server_time_zone (integer) - - challenge_length (integer) - - If the underlying SMB message's flag2 does not have SMB_FLAGS2_EXTENDED_SECURITY bit enabled, - then the instance will have the following additional attributes, - - challenge (string) - - domain (unicode) - - If the underlying SMB message's flags2 has SMB_FLAGS2_EXTENDED_SECURITY bit enabled, - then the instance will have the following additional attributes, - - server_guid (string) - - security_blob (string) - - References: - =========== - - [MS-SMB]: 2.2.4.5.2.1 - - [MS-CIFS]: 2.2.4.52.2 - """ - - PAYLOAD_STRUCT_FORMAT = ' 0: - if data_len >= self.challenge_length: - self.challenge = message.data[:self.challenge_length] - - s = '' - offset = self.challenge_length - while offset < data_len: - _s = message.data[offset:offset+2] - if _s == '\0\0': - self.domain = s.decode('UTF-16LE') - break - else: - s += _s - offset += 2 - else: - raise ProtocolError('Not enough data to decode SMB_COM_NEGOTIATE (without security extensions) Challenge field', message.raw_data, message) - else: - if data_len < 16: - raise ProtocolError('Not enough data to decode SMB_COM_NEGOTIATE (with security extensions) ServerGUID field', message.raw_data, message) - - self.server_guid = message.data[:16] - self.security_blob = message.data[16:] - - @property - def supportsExtendedSecurity(self): - return bool(self.capabilities & CAP_EXTENDED_SECURITY) - - -class ComSessionSetupAndxRequest__WithSecurityExtension(Payload): - """ - References: - =========== - - [MS-SMB]: 2.2.4.6.1 - """ - - PAYLOAD_STRUCT_FORMAT = ' 0: - params_bytes_offset = offset - offset += params_bytes_len - else: - params_bytes_offset = 0 - - padding2 = '' - if offset % 4 != 0: - padding2 = '\0'*(4-offset%4) - offset += (4-offset%4) - - if data_bytes_len > 0: - data_bytes_offset = offset - else: - data_bytes_offset = 0 - - message.parameters_data = \ - struct.pack(self.PAYLOAD_STRUCT_FORMAT, - self.total_params_count, - self.total_data_count, - self.max_params_count, - self.max_data_count, - self.max_setup_count, - 0x00, # Reserved1. Must be 0x00 - self.flags, - self.timeout, - 0x0000, # Reserved2. Must be 0x0000 - params_bytes_len, - params_bytes_offset, - data_bytes_len, - data_bytes_offset, - int(setup_bytes_len / 2)) + \ - self.setup_bytes - - message.data = padding0 + name + padding1 + self.params_bytes + padding2 + self.data_bytes - - -class ComTransactionResponse(Payload): - """ - Contains information about a SMB_COM_TRANSACTION response from the server - - After decoding, each instance contains the following attributes: - - total_params_count (integer) - - total_data_count (integer) - - setup_bytes (string) - - data_bytes (string) - - params_bytes (string) - - References: - =========== - - [MS-CIFS]: 2.2.4.33.2 - """ - - PAYLOAD_STRUCT_FORMAT = ' 0: - setup_bytes_len = setup_count * 2 - - if len(message.parameters_data) < self.PAYLOAD_STRUCT_SIZE + setup_bytes_len: - raise ProtocolError('Not enough data to decode SMB_COM_TRANSACTION parameters', message.raw_data, message) - - self.setup_bytes = message.parameters_data[self.PAYLOAD_STRUCT_SIZE:self.PAYLOAD_STRUCT_SIZE+setup_bytes_len] - else: - self.setup_bytes = '' - - offset = message.HEADER_STRUCT_SIZE + self.PAYLOAD_STRUCT_SIZE + setup_count * 2 + 2 # constant 2 is for the ByteCount field in the SMB header (i.e. field which indicates number of data bytes after the SMB parameters) - - if params_bytes_len > 0: - self.params_bytes = message.data[params_bytes_offset-offset:params_bytes_offset-offset+params_bytes_len] - else: - self.params_bytes = '' - - if data_bytes_len > 0: - self.data_bytes = message.data[data_bytes_offset-offset:data_bytes_offset-offset+data_bytes_len] - else: - self.data_bytes = '' - - -class ComTransaction2Request(Payload): - """ - References: - =========== - - [MS-CIFS]: 2.2.4.46.1 - """ - - PAYLOAD_STRUCT_FORMAT = 'HHHHBBHIHHHHHH' - PAYLOAD_STRUCT_SIZE = struct.calcsize(PAYLOAD_STRUCT_FORMAT) - - def __init__(self, max_params_count, max_data_count, max_setup_count, - total_params_count = 0, total_data_count = 0, - params_bytes = '', data_bytes = '', setup_bytes = '', - flags = 0, timeout = 0): - self.total_params_count = total_params_count or len(params_bytes) - self.total_data_count = total_data_count or len(data_bytes) - self.max_params_count = max_params_count - self.max_data_count = max_data_count - self.max_setup_count = max_setup_count - self.flags = flags - self.timeout = timeout - self.params_bytes = params_bytes - self.data_bytes = data_bytes - self.setup_bytes = setup_bytes - - def initMessage(self, message): - Payload.initMessage(self, message) - message.command = SMB_COM_TRANSACTION2 - - def prepare(self, message): - setup_bytes_len = len(self.setup_bytes) - params_bytes_len = len(self.params_bytes) - data_bytes_len = len(self.data_bytes) - name = '\0\0' - - padding0 = '' - offset = message.HEADER_STRUCT_SIZE + self.PAYLOAD_STRUCT_SIZE + setup_bytes_len + 2 # constant 2 is for the ByteCount field in the SMB header (i.e. field which indicates number of data bytes after the SMB parameters) - if offset % 2 != 0: - padding0 = '\0' - offset += 1 - - offset += 2 # For the name field - padding1 = '' - if offset % 4 != 0: - padding1 = '\0'*(4-offset%4) - - if params_bytes_len > 0: - params_bytes_offset = offset - offset += params_bytes_len - else: - params_bytes_offset = 0 - - padding2 = '' - if offset % 4 != 0: - padding2 = '\0'*(4-offset%4) - - if data_bytes_len > 0: - data_bytes_offset = offset - else: - data_bytes_offset = 0 - - message.parameters_data = \ - struct.pack(self.PAYLOAD_STRUCT_FORMAT, - self.total_params_count, - self.total_data_count, - self.max_params_count, - self.max_data_count, - self.max_setup_count, - 0x00, # Reserved1. Must be 0x00 - self.flags, - self.timeout, - 0x0000, # Reserved2. Must be 0x0000 - params_bytes_len, - params_bytes_offset, - data_bytes_len, - data_bytes_offset, - int(setup_bytes_len / 2)) + \ - self.setup_bytes - - message.data = padding0 + name + padding1 + self.params_bytes + padding2 + self.data_bytes - - -class ComTransaction2Response(Payload): - """ - Contains information about a SMB_COM_TRANSACTION2 response from the server - - After decoding, each instance contains the following attributes: - - total_params_count (integer) - - total_data_count (integer) - - setup_bytes (string) - - data_bytes (string) - - params_bytes (string) - - References: - =========== - - [MS-CIFS]: 2.2.4.46.2 - """ - - PAYLOAD_STRUCT_FORMAT = ' 0: - setup_bytes_len = setup_count * 2 - - if len(message.parameters_data) < self.PAYLOAD_STRUCT_SIZE + setup_bytes_len: - raise ProtocolError('Not enough data to decode SMB_COM_TRANSACTION parameters', message.raw_data, message) - - self.setup_bytes = message.parameters_data[self.PAYLOAD_STRUCT_SIZE:self.PAYLOAD_STRUCT_SIZE+setup_bytes_len] - else: - self.setup_bytes = '' - - offset = message.HEADER_STRUCT_SIZE + self.PAYLOAD_STRUCT_SIZE + setup_count * 2 + 2 # constant 2 is for the ByteCount field in the SMB header (i.e. field which indicates number of data bytes after the SMB parameters) - - if params_bytes_len > 0: - self.params_bytes = message.data[params_bytes_offset-offset:params_bytes_offset-offset+params_bytes_len] - else: - self.params_bytes = '' - - if data_bytes_len > 0: - self.data_bytes = message.data[data_bytes_offset-offset:data_bytes_offset-offset+data_bytes_len] - else: - self.data_bytes = '' - - -class ComCloseRequest(Payload): - """ - References: - =========== - - [MS-CIFS]: 2.2.4.5.1 - """ - - PAYLOAD_STRUCT_FORMAT = '> 32) # OffsetHigh field defined in [MS-SMB]: 2.2.4.3.1 - - message.data = '\0' + self.data_bytes - - -class ComWriteAndxResponse(Payload): - """ - References: - =========== - - [MS-CIFS]: 2.2.4.43.2 - - [MS-SMB]: 2.2.4.3.2 - """ - - PAYLOAD_STRUCT_FORMAT = '> 32), # Note that in [MS-SMB]: 2.2.4.2.1, this field can also act as MaxCountHigh field - self.remaining, # In [MS-CIFS]: 2.2.4.42.1, this field must be set to 0x0000 - self.offset >> 32) - - message.data = '' - - -class ComReadAndxResponse(Payload): - """ - References: - =========== - - [MS-CIFS]: 2.2.4.42.2 - - [MS-SMB]: 2.2.4.2.2 - """ - - PAYLOAD_STRUCT_FORMAT = ' 0: - params_bytes_offset = offset - else: - params_bytes_offset = 0 - - offset += params_bytes_len - padding1 = '' - if offset % 4 != 0: - padding1 = '\0'*(4-offset%4) - offset += (4-offset%4) - - if data_bytes_len > 0: - data_bytes_offset = offset - else: - data_bytes_offset = 0 - - message.parameters_data = \ - struct.pack(self.PAYLOAD_STRUCT_FORMAT, - self.max_setup_count, - 0x00, # Reserved1. Must be 0x00 - self.total_params_count, - self.total_data_count, - self.max_params_count, - self.max_data_count, - params_bytes_len, - params_bytes_offset, - data_bytes_len, - data_bytes_offset, - int(setup_bytes_len / 2), - self.function) + \ - self.setup_bytes - - message.data = padding0 + self.params_bytes + padding1 + self.data_bytes - - -class ComNTTransactResponse(Payload): - """ - Contains information about a SMB_COM_NT_TRANSACT response from the server - - After decoding, each instance contains the following attributes: - - total_params_count (integer) - - total_data_count (integer) - - setup_bytes (string) - - data_bytes (string) - - params_bytes (string) - - References: - =========== - - [MS-CIFS]: 2.2.4.62.2 - """ - PAYLOAD_STRUCT_FORMAT = '<3sIIIIIIIIBH' - PAYLOAD_STRUCT_SIZE = struct.calcsize(PAYLOAD_STRUCT_FORMAT) - - def decode(self, message): - assert message.command == SMB_COM_NT_TRANSACT - - if not message.status.hasError: - _, self.total_params_count, self.total_data_count, \ - params_count, params_offset, params_displ, \ - data_count, data_offset, data_displ, setup_count = struct.unpack(self.PAYLOAD_STRUCT_FORMAT, - message.parameters_data[:self.PAYLOAD_STRUCT_SIZE]) - - self.setup_bytes = message.parameters_data[self.PAYLOAD_STRUCT_SIZE:self.PAYLOAD_STRUCT_SIZE+setup_count*2] - - if params_count > 0: - params_offset -= message.HEADER_STRUCT_SIZE + self.PAYLOAD_STRUCT_SIZE + setup_count*2 + 2 - self.params_bytes = message.data[params_offset:params_offset+params_count] - else: - self.params_bytes = '' - - if data_count > 0: - data_offset -= message.HEADER_STRUCT_SIZE + self.PAYLOAD_STRUCT_SIZE + setup_count*2 + 2 - self.data_bytes = message.data[data_offset:data_offset+data_count] - else: - self.data_bytes = '' + +import os, sys, struct, types, logging, binascii, time +from StringIO import StringIO +from smb_constants import * + + +# Set to True if you want to enable support for extended security. Required for Windows Vista and later +SUPPORT_EXTENDED_SECURITY = True + +# Set to True if you want to enable SMB2 protocol. +SUPPORT_SMB2 = True + +# Set to True if you want to enable SMB2.1 and above protocol. +SUPPORT_SMB2x = True + +# Supported dialects +NT_LAN_MANAGER_DIALECT = 0 # 'NT LM 0.12' is always the first element in the dialect list and must always be included (MS-SMB 2.2.4.5.1) + +# Return the list of support SMB dialects based on the SUPPORT_x constants +def init_dialects_list(): + dialects = [ 'NT LM 0.12' ] + if SUPPORT_SMB2: + dialects.append('SMB 2.002') + if SUPPORT_SMB2x: + dialects.append('SMB 2.???') + return dialects + +class UnsupportedFeature(Exception): + """ + Raised when an supported feature is present/required in the protocol but is not + currently supported by pysmb + """ + pass + + +class ProtocolError(Exception): + + def __init__(self, message, data_buf = None, smb_message = None): + self.message = message + self.data_buf = data_buf + self.smb_message = smb_message + + def __str__(self): + b = StringIO() + b.write(self.message + os.linesep) + if self.smb_message: + b.write('=' * 20 + ' SMB Message ' + '=' * 20 + os.linesep) + b.write(str(self.smb_message)) + + if self.data_buf: + b.write('=' * 20 + ' SMB Data Packet (hex) ' + '=' * 20 + os.linesep) + b.write(binascii.hexlify(self.data_buf)) + b.write(os.linesep) + + return b.getvalue() + +class SMB2ProtocolHeaderError(ProtocolError): + + def __init__(self): + ProtocolError.__init__(self, "Packet header belongs to SMB2") + +class OperationFailure(Exception): + + def __init__(self, message, smb_messages): + self.args = [ message ] + self.message = message + self.smb_messages = smb_messages + + def __str__(self): + b = StringIO() + b.write(self.message + os.linesep) + + for idx, m in enumerate(self.smb_messages): + b.write('=' * 20 + ' SMB Message %d ' % idx + '=' * 20 + os.linesep) + b.write('SMB Header:' + os.linesep) + b.write('-----------' + os.linesep) + b.write(str(m)) + b.write('SMB Data Packet (hex):' + os.linesep) + b.write('----------------------' + os.linesep) + b.write(binascii.hexlify(m.raw_data)) + b.write(os.linesep) + + return b.getvalue() + + +class SMBError: + + def __init__(self): + self.reset() + + def reset(self): + self.internal_value = 0L + self.is_ntstatus = True + + def __str__(self): + if self.is_ntstatus: + return 'NTSTATUS=0x%08X' % self.internal_value + else: + return 'ErrorClass=0x%02X ErrorCode=0x%04X' % ( self.internal_value >> 24, self.internal_value & 0xFFFF ) + + @property + def hasError(self): + return self.internal_value != 0 + + +class SMBMessage: + + HEADER_STRUCT_FORMAT = "<4sBIBHHQxxHHHHB" + HEADER_STRUCT_SIZE = struct.calcsize(HEADER_STRUCT_FORMAT) + + log = logging.getLogger('SMB.SMBMessage') + protocol = 1 + + def __init__(self, conn, payload = None): + self.reset() + self.conn = conn + if payload: + self.payload = payload + self.payload.initMessage(self) + + def __str__(self): + b = StringIO() + b.write('Command: 0x%02X (%s) %s' % ( self.command, SMB_COMMAND_NAMES.get(self.command, ''), os.linesep )) + b.write('Status: %s %s' % ( str(self.status), os.linesep )) + b.write('Flags: 0x%02X %s' % ( self.flags, os.linesep )) + b.write('Flags2: 0x%04X %s' % ( self.flags2, os.linesep )) + b.write('PID: %d %s' % ( self.pid, os.linesep )) + b.write('UID: %d %s' % ( self.uid, os.linesep )) + b.write('MID: %d %s' % ( self.mid, os.linesep )) + b.write('TID: %d %s' % ( self.tid, os.linesep )) + b.write('Security: 0x%016X %s' % ( self.security, os.linesep )) + b.write('Parameters: %d bytes %s%s %s' % ( len(self.parameters_data), os.linesep, binascii.hexlify(self.parameters_data), os.linesep )) + b.write('Data: %d bytes %s%s %s' % ( len(self.data), os.linesep, binascii.hexlify(self.data), os.linesep )) + return b.getvalue() + + def reset(self): + self.raw_data = '' + self.command = 0 + self.status = SMBError() + self.flags = 0 + self.flags2 = 0 + self.pid = 0 + self.tid = 0 + self.uid = 0 + self.mid = 0 + self.security = 0L + self.parameters_data = '' + self.data = '' + self.payload = None + + @property + def isReply(self): + return bool(self.flags & SMB_FLAGS_REPLY) + + @property + def hasExtendedSecurity(self): + return bool(self.flags2 & SMB_FLAGS2_EXTENDED_SECURITY) + + def encode(self): + """ + Encode this SMB message into a series of bytes suitable to be embedded with a NetBIOS session message. + AssertionError will be raised if this SMB message has not been initialized with a Payload instance + + @return: a string containing the encoded SMB message + """ + assert self.payload + + self.pid = os.getpid() + self.payload.prepare(self) + + parameters_len = len(self.parameters_data) + assert parameters_len % 2 == 0 + + headers_data = struct.pack(self.HEADER_STRUCT_FORMAT, + '\xFFSMB', self.command, self.status.internal_value, self.flags, + self.flags2, (self.pid >> 16) & 0xFFFF, self.security, self.tid, + self.pid & 0xFFFF, self.uid, self.mid, int(parameters_len / 2)) + return headers_data + self.parameters_data + struct.pack(' 0 and buf_len < (datalen_offset + 2 + body_len): + # Not enough data in buf to decode body + raise ProtocolError('Not enough data. Body decoding failed', buf) + + self.parameters_data = buf[offset:datalen_offset] + + if body_len > 0: + self.data = buf[datalen_offset+2:datalen_offset+2+body_len] + + self.raw_data = buf + self._decodePayload() + + return self.HEADER_STRUCT_SIZE + params_count * 2 + 2 + body_len + + def _decodePayload(self): + if self.command == SMB_COM_READ_ANDX: + self.payload = ComReadAndxResponse() + elif self.command == SMB_COM_WRITE_ANDX: + self.payload = ComWriteAndxResponse() + elif self.command == SMB_COM_TRANSACTION: + self.payload = ComTransactionResponse() + elif self.command == SMB_COM_TRANSACTION2: + self.payload = ComTransaction2Response() + elif self.command == SMB_COM_OPEN_ANDX: + self.payload = ComOpenAndxResponse() + elif self.command == SMB_COM_NT_CREATE_ANDX: + self.payload = ComNTCreateAndxResponse() + elif self.command == SMB_COM_TREE_CONNECT_ANDX: + self.payload = ComTreeConnectAndxResponse() + elif self.command == SMB_COM_ECHO: + self.payload = ComEchoResponse() + elif self.command == SMB_COM_SESSION_SETUP_ANDX: + self.payload = ComSessionSetupAndxResponse() + elif self.command == SMB_COM_NEGOTIATE: + self.payload = ComNegotiateResponse() + + if self.payload: + self.payload.decode(self) + + +class Payload: + + DEFAULT_ANDX_PARAM_HEADER = '\xFF\x00\x00\x00' + DEFAULT_ANDX_PARAM_SIZE = 4 + + def initMessage(self, message): + # SMB_FLAGS2_UNICODE must always be enabled. Without this, almost all the Payload subclasses will need to be + # rewritten to check for OEM/Unicode strings which will be tedious. Fortunately, almost all tested CIFS services + # support SMB_FLAGS2_UNICODE by default. + assert message.payload == self + message.flags = SMB_FLAGS_CASE_INSENSITIVE | SMB_FLAGS_CANONICALIZED_PATHS + message.flags2 = SMB_FLAGS2_UNICODE | SMB_FLAGS2_NT_STATUS | SMB_FLAGS2_LONG_NAMES | SMB_FLAGS2_EAS + + if SUPPORT_EXTENDED_SECURITY: + message.flags2 |= SMB_FLAGS2_EXTENDED_SECURITY | SMB_FLAGS2_SMB_SECURITY_SIGNATURE + + def prepare(self, message): + raise NotImplementedError + + def decode(self, message): + raise NotImplementedError + + +class ComNegotiateRequest(Payload): + """ + References: + =========== + - [MS-CIFS]: 2.2.4.52.1 + - [MS-SMB]: 2.2.4.5.1 + """ + + def initMessage(self, message): + Payload.initMessage(self, message) + message.command = SMB_COM_NEGOTIATE + + def prepare(self, message): + assert message.payload == self + message.parameters_data = '' + message.data = ''.join(map(lambda s: '\x02'+s+'\x00', init_dialects_list())) + + +class ComNegotiateResponse(Payload): + """ + Contains information on the SMB_COM_NEGOTIATE response from server + + After calling the decode method, each instance will contain the following attributes, + - security_mode (integer) + - max_mpx_count (integer) + - max_number_vcs (integer) + - max_buffer_size (long) + - max_raw_size (long) + - session_key (long) + - capabilities (long) + - system_time (long) + - server_time_zone (integer) + - challenge_length (integer) + + If the underlying SMB message's flag2 does not have SMB_FLAGS2_EXTENDED_SECURITY bit enabled, + then the instance will have the following additional attributes, + - challenge (string) + - domain (unicode) + + If the underlying SMB message's flags2 has SMB_FLAGS2_EXTENDED_SECURITY bit enabled, + then the instance will have the following additional attributes, + - server_guid (string) + - security_blob (string) + + References: + =========== + - [MS-SMB]: 2.2.4.5.2.1 + - [MS-CIFS]: 2.2.4.52.2 + """ + + PAYLOAD_STRUCT_FORMAT = ' 0: + if data_len >= self.challenge_length: + self.challenge = message.data[:self.challenge_length] + + s = '' + offset = self.challenge_length + while offset < data_len: + _s = message.data[offset:offset+2] + if _s == '\0\0': + self.domain = s.decode('UTF-16LE') + break + else: + s += _s + offset += 2 + else: + raise ProtocolError('Not enough data to decode SMB_COM_NEGOTIATE (without security extensions) Challenge field', message.raw_data, message) + else: + if data_len < 16: + raise ProtocolError('Not enough data to decode SMB_COM_NEGOTIATE (with security extensions) ServerGUID field', message.raw_data, message) + + self.server_guid = message.data[:16] + self.security_blob = message.data[16:] + + @property + def supportsExtendedSecurity(self): + return bool(self.capabilities & CAP_EXTENDED_SECURITY) + + +class ComSessionSetupAndxRequest__WithSecurityExtension(Payload): + """ + References: + =========== + - [MS-SMB]: 2.2.4.6.1 + """ + + PAYLOAD_STRUCT_FORMAT = ' 0: + params_bytes_offset = offset + offset += params_bytes_len + else: + params_bytes_offset = 0 + + padding2 = '' + if offset % 4 != 0: + padding2 = '\0'*(4-offset%4) + offset += (4-offset%4) + + if data_bytes_len > 0: + data_bytes_offset = offset + else: + data_bytes_offset = 0 + + message.parameters_data = \ + struct.pack(self.PAYLOAD_STRUCT_FORMAT, + self.total_params_count, + self.total_data_count, + self.max_params_count, + self.max_data_count, + self.max_setup_count, + 0x00, # Reserved1. Must be 0x00 + self.flags, + self.timeout, + 0x0000, # Reserved2. Must be 0x0000 + params_bytes_len, + params_bytes_offset, + data_bytes_len, + data_bytes_offset, + int(setup_bytes_len / 2)) + \ + self.setup_bytes + + message.data = padding0 + name + padding1 + self.params_bytes + padding2 + self.data_bytes + + +class ComTransactionResponse(Payload): + """ + Contains information about a SMB_COM_TRANSACTION response from the server + + After decoding, each instance contains the following attributes: + - total_params_count (integer) + - total_data_count (integer) + - setup_bytes (string) + - data_bytes (string) + - params_bytes (string) + + References: + =========== + - [MS-CIFS]: 2.2.4.33.2 + """ + + PAYLOAD_STRUCT_FORMAT = ' 0: + setup_bytes_len = setup_count * 2 + + if len(message.parameters_data) < self.PAYLOAD_STRUCT_SIZE + setup_bytes_len: + raise ProtocolError('Not enough data to decode SMB_COM_TRANSACTION parameters', message.raw_data, message) + + self.setup_bytes = message.parameters_data[self.PAYLOAD_STRUCT_SIZE:self.PAYLOAD_STRUCT_SIZE+setup_bytes_len] + else: + self.setup_bytes = '' + + offset = message.HEADER_STRUCT_SIZE + self.PAYLOAD_STRUCT_SIZE + setup_count * 2 + 2 # constant 2 is for the ByteCount field in the SMB header (i.e. field which indicates number of data bytes after the SMB parameters) + + if params_bytes_len > 0: + self.params_bytes = message.data[params_bytes_offset-offset:params_bytes_offset-offset+params_bytes_len] + else: + self.params_bytes = '' + + if data_bytes_len > 0: + self.data_bytes = message.data[data_bytes_offset-offset:data_bytes_offset-offset+data_bytes_len] + else: + self.data_bytes = '' + + +class ComTransaction2Request(Payload): + """ + References: + =========== + - [MS-CIFS]: 2.2.4.46.1 + """ + + PAYLOAD_STRUCT_FORMAT = 'HHHHBBHIHHHHHH' + PAYLOAD_STRUCT_SIZE = struct.calcsize(PAYLOAD_STRUCT_FORMAT) + + def __init__(self, max_params_count, max_data_count, max_setup_count, + total_params_count = 0, total_data_count = 0, + params_bytes = '', data_bytes = '', setup_bytes = '', + flags = 0, timeout = 0): + self.total_params_count = total_params_count or len(params_bytes) + self.total_data_count = total_data_count or len(data_bytes) + self.max_params_count = max_params_count + self.max_data_count = max_data_count + self.max_setup_count = max_setup_count + self.flags = flags + self.timeout = timeout + self.params_bytes = params_bytes + self.data_bytes = data_bytes + self.setup_bytes = setup_bytes + + def initMessage(self, message): + Payload.initMessage(self, message) + message.command = SMB_COM_TRANSACTION2 + + def prepare(self, message): + setup_bytes_len = len(self.setup_bytes) + params_bytes_len = len(self.params_bytes) + data_bytes_len = len(self.data_bytes) + name = '\0\0' + + padding0 = '' + offset = message.HEADER_STRUCT_SIZE + self.PAYLOAD_STRUCT_SIZE + setup_bytes_len + 2 # constant 2 is for the ByteCount field in the SMB header (i.e. field which indicates number of data bytes after the SMB parameters) + if offset % 2 != 0: + padding0 = '\0' + offset += 1 + + offset += 2 # For the name field + padding1 = '' + if offset % 4 != 0: + padding1 = '\0'*(4-offset%4) + + if params_bytes_len > 0: + params_bytes_offset = offset + offset += params_bytes_len + else: + params_bytes_offset = 0 + + padding2 = '' + if offset % 4 != 0: + padding2 = '\0'*(4-offset%4) + + if data_bytes_len > 0: + data_bytes_offset = offset + else: + data_bytes_offset = 0 + + message.parameters_data = \ + struct.pack(self.PAYLOAD_STRUCT_FORMAT, + self.total_params_count, + self.total_data_count, + self.max_params_count, + self.max_data_count, + self.max_setup_count, + 0x00, # Reserved1. Must be 0x00 + self.flags, + self.timeout, + 0x0000, # Reserved2. Must be 0x0000 + params_bytes_len, + params_bytes_offset, + data_bytes_len, + data_bytes_offset, + int(setup_bytes_len / 2)) + \ + self.setup_bytes + + message.data = padding0 + name + padding1 + self.params_bytes + padding2 + self.data_bytes + + +class ComTransaction2Response(Payload): + """ + Contains information about a SMB_COM_TRANSACTION2 response from the server + + After decoding, each instance contains the following attributes: + - total_params_count (integer) + - total_data_count (integer) + - setup_bytes (string) + - data_bytes (string) + - params_bytes (string) + + References: + =========== + - [MS-CIFS]: 2.2.4.46.2 + """ + + PAYLOAD_STRUCT_FORMAT = ' 0: + setup_bytes_len = setup_count * 2 + + if len(message.parameters_data) < self.PAYLOAD_STRUCT_SIZE + setup_bytes_len: + raise ProtocolError('Not enough data to decode SMB_COM_TRANSACTION parameters', message.raw_data, message) + + self.setup_bytes = message.parameters_data[self.PAYLOAD_STRUCT_SIZE:self.PAYLOAD_STRUCT_SIZE+setup_bytes_len] + else: + self.setup_bytes = '' + + offset = message.HEADER_STRUCT_SIZE + self.PAYLOAD_STRUCT_SIZE + setup_count * 2 + 2 # constant 2 is for the ByteCount field in the SMB header (i.e. field which indicates number of data bytes after the SMB parameters) + + if params_bytes_len > 0: + self.params_bytes = message.data[params_bytes_offset-offset:params_bytes_offset-offset+params_bytes_len] + else: + self.params_bytes = '' + + if data_bytes_len > 0: + self.data_bytes = message.data[data_bytes_offset-offset:data_bytes_offset-offset+data_bytes_len] + else: + self.data_bytes = '' + + +class ComCloseRequest(Payload): + """ + References: + =========== + - [MS-CIFS]: 2.2.4.5.1 + """ + + PAYLOAD_STRUCT_FORMAT = '> 32) # OffsetHigh field defined in [MS-SMB]: 2.2.4.3.1 + + message.data = '\0' + self.data_bytes + + +class ComWriteAndxResponse(Payload): + """ + References: + =========== + - [MS-CIFS]: 2.2.4.43.2 + - [MS-SMB]: 2.2.4.3.2 + """ + + PAYLOAD_STRUCT_FORMAT = '> 32), # Note that in [MS-SMB]: 2.2.4.2.1, this field can also act as MaxCountHigh field + self.remaining, # In [MS-CIFS]: 2.2.4.42.1, this field must be set to 0x0000 + self.offset >> 32) + + message.data = '' + + +class ComReadAndxResponse(Payload): + """ + References: + =========== + - [MS-CIFS]: 2.2.4.42.2 + - [MS-SMB]: 2.2.4.2.2 + """ + + PAYLOAD_STRUCT_FORMAT = ' 0: + params_bytes_offset = offset + else: + params_bytes_offset = 0 + + offset += params_bytes_len + padding1 = '' + if offset % 4 != 0: + padding1 = '\0'*(4-offset%4) + offset += (4-offset%4) + + if data_bytes_len > 0: + data_bytes_offset = offset + else: + data_bytes_offset = 0 + + message.parameters_data = \ + struct.pack(self.PAYLOAD_STRUCT_FORMAT, + self.max_setup_count, + 0x00, # Reserved1. Must be 0x00 + self.total_params_count, + self.total_data_count, + self.max_params_count, + self.max_data_count, + params_bytes_len, + params_bytes_offset, + data_bytes_len, + data_bytes_offset, + int(setup_bytes_len / 2), + self.function) + \ + self.setup_bytes + + message.data = padding0 + self.params_bytes + padding1 + self.data_bytes + + +class ComNTTransactResponse(Payload): + """ + Contains information about a SMB_COM_NT_TRANSACT response from the server + + After decoding, each instance contains the following attributes: + - total_params_count (integer) + - total_data_count (integer) + - setup_bytes (string) + - data_bytes (string) + - params_bytes (string) + + References: + =========== + - [MS-CIFS]: 2.2.4.62.2 + """ + PAYLOAD_STRUCT_FORMAT = '<3sIIIIIIIIBH' + PAYLOAD_STRUCT_SIZE = struct.calcsize(PAYLOAD_STRUCT_FORMAT) + + def decode(self, message): + assert message.command == SMB_COM_NT_TRANSACT + + if not message.status.hasError: + _, self.total_params_count, self.total_data_count, \ + params_count, params_offset, params_displ, \ + data_count, data_offset, data_displ, setup_count = struct.unpack(self.PAYLOAD_STRUCT_FORMAT, + message.parameters_data[:self.PAYLOAD_STRUCT_SIZE]) + + self.setup_bytes = message.parameters_data[self.PAYLOAD_STRUCT_SIZE:self.PAYLOAD_STRUCT_SIZE+setup_count*2] + + if params_count > 0: + params_offset -= message.HEADER_STRUCT_SIZE + self.PAYLOAD_STRUCT_SIZE + setup_count*2 + 2 + self.params_bytes = message.data[params_offset:params_offset+params_count] + else: + self.params_bytes = '' + + if data_count > 0: + data_offset -= message.HEADER_STRUCT_SIZE + self.PAYLOAD_STRUCT_SIZE + setup_count*2 + 2 + self.data_bytes = message.data[data_offset:data_offset+data_count] + else: + self.data_bytes = '' diff --git a/plugin.video.alfa/lib/sambatools/smb/utils/README.txt b/plugin.video.alfa/lib/sambatools/smb/utils/README.txt index 7dbaa8cf..c17e6721 100755 --- a/plugin.video.alfa/lib/sambatools/smb/utils/README.txt +++ b/plugin.video.alfa/lib/sambatools/smb/utils/README.txt @@ -1,12 +1,12 @@ - -md4.py and U32.py -Both modules downloaded from http://www.oocities.org/rozmanov/python/md4.html. -Licensed under LGPL - -pyDes.py 2.0.0 -Downloaded from http://twhiteman.netfirms.com/des.html -Licensed under public domain - -sha256.py -Downloaded from http://xbmc-addons.googlecode.com/svn-history/r1686/trunk/scripts/OpenSubtitles_OSD/resources/lib/sha256.py -Licensed under MIT + +md4.py and U32.py +Both modules downloaded from http://www.oocities.org/rozmanov/python/md4.html. +Licensed under LGPL + +pyDes.py 2.0.0 +Downloaded from http://twhiteman.netfirms.com/des.html +Licensed under public domain + +sha256.py +Downloaded from http://xbmc-addons.googlecode.com/svn-history/r1686/trunk/scripts/OpenSubtitles_OSD/resources/lib/sha256.py +Licensed under MIT diff --git a/plugin.video.alfa/lib/sambatools/smb/utils/__init__.py b/plugin.video.alfa/lib/sambatools/smb/utils/__init__.py index 68a05714..bce9d86c 100755 --- a/plugin.video.alfa/lib/sambatools/smb/utils/__init__.py +++ b/plugin.video.alfa/lib/sambatools/smb/utils/__init__.py @@ -1,3 +1,3 @@ - -def convertFILETIMEtoEpoch(t): - return (t - 116444736000000000L) / 10000000.0; + +def convertFILETIMEtoEpoch(t): + return (t - 116444736000000000L) / 10000000.0; diff --git a/plugin.video.alfa/lib/sambatools/smb/utils/md4.py b/plugin.video.alfa/lib/sambatools/smb/utils/md4.py index 8c2f2ee0..2f107556 100755 --- a/plugin.video.alfa/lib/sambatools/smb/utils/md4.py +++ b/plugin.video.alfa/lib/sambatools/smb/utils/md4.py @@ -1,254 +1,254 @@ -# md4.py implements md4 hash class for Python -# Version 1.0 -# Copyright (C) 2001-2002 Dmitry Rozmanov -# -# based on md4.c from "the Python Cryptography Toolkit, version 1.0.0 -# Copyright (C) 1995, A.M. Kuchling" -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# e-mail: dima@xenon.spb.ru -# -#==================================================================== - -# MD4 validation data - -md4_test= [ - ('', 0x31d6cfe0d16ae931b73c59d7e0c089c0L), - ("a", 0xbde52cb31de33e46245e05fbdbd6fb24L), - ("abc", 0xa448017aaf21d8525fc10ae87aa6729dL), - ("message digest", 0xd9130a8164549fe818874806e1c7014bL), - ("abcdefghijklmnopqrstuvwxyz", 0xd79e1c308aa5bbcdeea8ed63df412da9L), - ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", - 0x043f8582f241db351ce627e153e7f0e4L), - ("12345678901234567890123456789012345678901234567890123456789012345678901234567890", - 0xe33b4ddc9c38f2199c3e7b164fcc0536L), - ] - -#==================================================================== -from U32 import U32 - -#-------------------------------------------------------------------- -class MD4: - A = None - B = None - C = None - D = None - count, len1, len2 = None, None, None - buf = [] - - #----------------------------------------------------- - def __init__(self): - - - self.A = U32(0x67452301L) - self.B = U32(0xefcdab89L) - self.C = U32(0x98badcfeL) - self.D = U32(0x10325476L) - self.count, self.len1, self.len2 = U32(0L), U32(0L), U32(0L) - self.buf = [0x00] * 64 - - #----------------------------------------------------- - def __repr__(self): - r = 'A = %s, \nB = %s, \nC = %s, \nD = %s.\n' % (self.A.__repr__(), self.B.__repr__(), self.C.__repr__(), self.D.__repr__()) - r = r + 'count = %s, \nlen1 = %s, \nlen2 = %s.\n' % (self.count.__repr__(), self.len1.__repr__(), self.len2.__repr__()) - for i in range(4): - for j in range(16): - r = r + '%4s ' % hex(self.buf[i+j]) - r = r + '\n' - - return r - #----------------------------------------------------- - def make_copy(self): - - dest = new() - - dest.len1 = self.len1 - dest.len2 = self.len2 - dest.A = self.A - dest.B = self.B - dest.C = self.C - dest.D = self.D - dest.count = self.count - for i in range(self.count): - dest.buf[i] = self.buf[i] - - return dest - - #----------------------------------------------------- - def update(self, str): - - buf = [] - for i in str: buf.append(ord(i)) - ilen = U32(len(buf)) - - # check if the first length is out of range - # as the length is measured in bits then multiplay it by 8 - if (long(self.len1 + (ilen << 3)) < long(self.len1)): - self.len2 = self.len2 + U32(1) - - self.len1 = self.len1 + (ilen << 3) - self.len2 = self.len2 + (ilen >> 29) - - L = U32(0) - bufpos = 0 - while (long(ilen) > 0): - if (64 - long(self.count)) < long(ilen): L = U32(64 - long(self.count)) - else: L = ilen - for i in range(int(L)): self.buf[i + int(self.count)] = buf[i + bufpos] - self.count = self.count + L - ilen = ilen - L - bufpos = bufpos + int(L) - - if (long(self.count) == 64L): - self.count = U32(0L) - X = [] - i = 0 - for j in range(16): - X.append(U32(self.buf[i]) + (U32(self.buf[i+1]) << 8) + \ - (U32(self.buf[i+2]) << 16) + (U32(self.buf[i+3]) << 24)) - i = i + 4 - - A = self.A - B = self.B - C = self.C - D = self.D - - A = f1(A,B,C,D, 0, 3, X) - D = f1(D,A,B,C, 1, 7, X) - C = f1(C,D,A,B, 2,11, X) - B = f1(B,C,D,A, 3,19, X) - A = f1(A,B,C,D, 4, 3, X) - D = f1(D,A,B,C, 5, 7, X) - C = f1(C,D,A,B, 6,11, X) - B = f1(B,C,D,A, 7,19, X) - A = f1(A,B,C,D, 8, 3, X) - D = f1(D,A,B,C, 9, 7, X) - C = f1(C,D,A,B,10,11, X) - B = f1(B,C,D,A,11,19, X) - A = f1(A,B,C,D,12, 3, X) - D = f1(D,A,B,C,13, 7, X) - C = f1(C,D,A,B,14,11, X) - B = f1(B,C,D,A,15,19, X) - - A = f2(A,B,C,D, 0, 3, X) - D = f2(D,A,B,C, 4, 5, X) - C = f2(C,D,A,B, 8, 9, X) - B = f2(B,C,D,A,12,13, X) - A = f2(A,B,C,D, 1, 3, X) - D = f2(D,A,B,C, 5, 5, X) - C = f2(C,D,A,B, 9, 9, X) - B = f2(B,C,D,A,13,13, X) - A = f2(A,B,C,D, 2, 3, X) - D = f2(D,A,B,C, 6, 5, X) - C = f2(C,D,A,B,10, 9, X) - B = f2(B,C,D,A,14,13, X) - A = f2(A,B,C,D, 3, 3, X) - D = f2(D,A,B,C, 7, 5, X) - C = f2(C,D,A,B,11, 9, X) - B = f2(B,C,D,A,15,13, X) - - A = f3(A,B,C,D, 0, 3, X) - D = f3(D,A,B,C, 8, 9, X) - C = f3(C,D,A,B, 4,11, X) - B = f3(B,C,D,A,12,15, X) - A = f3(A,B,C,D, 2, 3, X) - D = f3(D,A,B,C,10, 9, X) - C = f3(C,D,A,B, 6,11, X) - B = f3(B,C,D,A,14,15, X) - A = f3(A,B,C,D, 1, 3, X) - D = f3(D,A,B,C, 9, 9, X) - C = f3(C,D,A,B, 5,11, X) - B = f3(B,C,D,A,13,15, X) - A = f3(A,B,C,D, 3, 3, X) - D = f3(D,A,B,C,11, 9, X) - C = f3(C,D,A,B, 7,11, X) - B = f3(B,C,D,A,15,15, X) - - self.A = self.A + A - self.B = self.B + B - self.C = self.C + C - self.D = self.D + D - - #----------------------------------------------------- - def digest(self): - - res = [0x00] * 16 - s = [0x00] * 8 - padding = [0x00] * 64 - padding[0] = 0x80 - padlen, oldlen1, oldlen2 = U32(0), U32(0), U32(0) - - temp = self.make_copy() - - oldlen1 = temp.len1 - oldlen2 = temp.len2 - if (56 <= long(self.count)): padlen = U32(56 - long(self.count) + 64) - else: padlen = U32(56 - long(self.count)) - - temp.update(int_array2str(padding[:int(padlen)])) - - s[0]= (oldlen1) & U32(0xFF) - s[1]=((oldlen1) >> 8) & U32(0xFF) - s[2]=((oldlen1) >> 16) & U32(0xFF) - s[3]=((oldlen1) >> 24) & U32(0xFF) - s[4]= (oldlen2) & U32(0xFF) - s[5]=((oldlen2) >> 8) & U32(0xFF) - s[6]=((oldlen2) >> 16) & U32(0xFF) - s[7]=((oldlen2) >> 24) & U32(0xFF) - temp.update(int_array2str(s)) - - res[ 0]= temp.A & U32(0xFF) - res[ 1]=(temp.A >> 8) & U32(0xFF) - res[ 2]=(temp.A >> 16) & U32(0xFF) - res[ 3]=(temp.A >> 24) & U32(0xFF) - res[ 4]= temp.B & U32(0xFF) - res[ 5]=(temp.B >> 8) & U32(0xFF) - res[ 6]=(temp.B >> 16) & U32(0xFF) - res[ 7]=(temp.B >> 24) & U32(0xFF) - res[ 8]= temp.C & U32(0xFF) - res[ 9]=(temp.C >> 8) & U32(0xFF) - res[10]=(temp.C >> 16) & U32(0xFF) - res[11]=(temp.C >> 24) & U32(0xFF) - res[12]= temp.D & U32(0xFF) - res[13]=(temp.D >> 8) & U32(0xFF) - res[14]=(temp.D >> 16) & U32(0xFF) - res[15]=(temp.D >> 24) & U32(0xFF) - - return int_array2str(res) - -#==================================================================== -# helpers -def F(x, y, z): return (((x) & (y)) | ((~x) & (z))) -def G(x, y, z): return (((x) & (y)) | ((x) & (z)) | ((y) & (z))) -def H(x, y, z): return ((x) ^ (y) ^ (z)) - -def ROL(x, n): return (((x) << n) | ((x) >> (32-n))) - -def f1(a, b, c, d, k, s, X): return ROL(a + F(b, c, d) + X[k], s) -def f2(a, b, c, d, k, s, X): return ROL(a + G(b, c, d) + X[k] + U32(0x5a827999L), s) -def f3(a, b, c, d, k, s, X): return ROL(a + H(b, c, d) + X[k] + U32(0x6ed9eba1L), s) - -#-------------------------------------------------------------------- -# helper function -def int_array2str(array): - str = '' - for i in array: - str = str + chr(i) - return str - -#-------------------------------------------------------------------- -# To be able to use md4.new() instead of md4.MD4() -new = MD4 +# md4.py implements md4 hash class for Python +# Version 1.0 +# Copyright (C) 2001-2002 Dmitry Rozmanov +# +# based on md4.c from "the Python Cryptography Toolkit, version 1.0.0 +# Copyright (C) 1995, A.M. Kuchling" +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# e-mail: dima@xenon.spb.ru +# +#==================================================================== + +# MD4 validation data + +md4_test= [ + ('', 0x31d6cfe0d16ae931b73c59d7e0c089c0L), + ("a", 0xbde52cb31de33e46245e05fbdbd6fb24L), + ("abc", 0xa448017aaf21d8525fc10ae87aa6729dL), + ("message digest", 0xd9130a8164549fe818874806e1c7014bL), + ("abcdefghijklmnopqrstuvwxyz", 0xd79e1c308aa5bbcdeea8ed63df412da9L), + ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + 0x043f8582f241db351ce627e153e7f0e4L), + ("12345678901234567890123456789012345678901234567890123456789012345678901234567890", + 0xe33b4ddc9c38f2199c3e7b164fcc0536L), + ] + +#==================================================================== +from U32 import U32 + +#-------------------------------------------------------------------- +class MD4: + A = None + B = None + C = None + D = None + count, len1, len2 = None, None, None + buf = [] + + #----------------------------------------------------- + def __init__(self): + + + self.A = U32(0x67452301L) + self.B = U32(0xefcdab89L) + self.C = U32(0x98badcfeL) + self.D = U32(0x10325476L) + self.count, self.len1, self.len2 = U32(0L), U32(0L), U32(0L) + self.buf = [0x00] * 64 + + #----------------------------------------------------- + def __repr__(self): + r = 'A = %s, \nB = %s, \nC = %s, \nD = %s.\n' % (self.A.__repr__(), self.B.__repr__(), self.C.__repr__(), self.D.__repr__()) + r = r + 'count = %s, \nlen1 = %s, \nlen2 = %s.\n' % (self.count.__repr__(), self.len1.__repr__(), self.len2.__repr__()) + for i in range(4): + for j in range(16): + r = r + '%4s ' % hex(self.buf[i+j]) + r = r + '\n' + + return r + #----------------------------------------------------- + def make_copy(self): + + dest = new() + + dest.len1 = self.len1 + dest.len2 = self.len2 + dest.A = self.A + dest.B = self.B + dest.C = self.C + dest.D = self.D + dest.count = self.count + for i in range(self.count): + dest.buf[i] = self.buf[i] + + return dest + + #----------------------------------------------------- + def update(self, str): + + buf = [] + for i in str: buf.append(ord(i)) + ilen = U32(len(buf)) + + # check if the first length is out of range + # as the length is measured in bits then multiplay it by 8 + if (long(self.len1 + (ilen << 3)) < long(self.len1)): + self.len2 = self.len2 + U32(1) + + self.len1 = self.len1 + (ilen << 3) + self.len2 = self.len2 + (ilen >> 29) + + L = U32(0) + bufpos = 0 + while (long(ilen) > 0): + if (64 - long(self.count)) < long(ilen): L = U32(64 - long(self.count)) + else: L = ilen + for i in range(int(L)): self.buf[i + int(self.count)] = buf[i + bufpos] + self.count = self.count + L + ilen = ilen - L + bufpos = bufpos + int(L) + + if (long(self.count) == 64L): + self.count = U32(0L) + X = [] + i = 0 + for j in range(16): + X.append(U32(self.buf[i]) + (U32(self.buf[i+1]) << 8) + \ + (U32(self.buf[i+2]) << 16) + (U32(self.buf[i+3]) << 24)) + i = i + 4 + + A = self.A + B = self.B + C = self.C + D = self.D + + A = f1(A,B,C,D, 0, 3, X) + D = f1(D,A,B,C, 1, 7, X) + C = f1(C,D,A,B, 2,11, X) + B = f1(B,C,D,A, 3,19, X) + A = f1(A,B,C,D, 4, 3, X) + D = f1(D,A,B,C, 5, 7, X) + C = f1(C,D,A,B, 6,11, X) + B = f1(B,C,D,A, 7,19, X) + A = f1(A,B,C,D, 8, 3, X) + D = f1(D,A,B,C, 9, 7, X) + C = f1(C,D,A,B,10,11, X) + B = f1(B,C,D,A,11,19, X) + A = f1(A,B,C,D,12, 3, X) + D = f1(D,A,B,C,13, 7, X) + C = f1(C,D,A,B,14,11, X) + B = f1(B,C,D,A,15,19, X) + + A = f2(A,B,C,D, 0, 3, X) + D = f2(D,A,B,C, 4, 5, X) + C = f2(C,D,A,B, 8, 9, X) + B = f2(B,C,D,A,12,13, X) + A = f2(A,B,C,D, 1, 3, X) + D = f2(D,A,B,C, 5, 5, X) + C = f2(C,D,A,B, 9, 9, X) + B = f2(B,C,D,A,13,13, X) + A = f2(A,B,C,D, 2, 3, X) + D = f2(D,A,B,C, 6, 5, X) + C = f2(C,D,A,B,10, 9, X) + B = f2(B,C,D,A,14,13, X) + A = f2(A,B,C,D, 3, 3, X) + D = f2(D,A,B,C, 7, 5, X) + C = f2(C,D,A,B,11, 9, X) + B = f2(B,C,D,A,15,13, X) + + A = f3(A,B,C,D, 0, 3, X) + D = f3(D,A,B,C, 8, 9, X) + C = f3(C,D,A,B, 4,11, X) + B = f3(B,C,D,A,12,15, X) + A = f3(A,B,C,D, 2, 3, X) + D = f3(D,A,B,C,10, 9, X) + C = f3(C,D,A,B, 6,11, X) + B = f3(B,C,D,A,14,15, X) + A = f3(A,B,C,D, 1, 3, X) + D = f3(D,A,B,C, 9, 9, X) + C = f3(C,D,A,B, 5,11, X) + B = f3(B,C,D,A,13,15, X) + A = f3(A,B,C,D, 3, 3, X) + D = f3(D,A,B,C,11, 9, X) + C = f3(C,D,A,B, 7,11, X) + B = f3(B,C,D,A,15,15, X) + + self.A = self.A + A + self.B = self.B + B + self.C = self.C + C + self.D = self.D + D + + #----------------------------------------------------- + def digest(self): + + res = [0x00] * 16 + s = [0x00] * 8 + padding = [0x00] * 64 + padding[0] = 0x80 + padlen, oldlen1, oldlen2 = U32(0), U32(0), U32(0) + + temp = self.make_copy() + + oldlen1 = temp.len1 + oldlen2 = temp.len2 + if (56 <= long(self.count)): padlen = U32(56 - long(self.count) + 64) + else: padlen = U32(56 - long(self.count)) + + temp.update(int_array2str(padding[:int(padlen)])) + + s[0]= (oldlen1) & U32(0xFF) + s[1]=((oldlen1) >> 8) & U32(0xFF) + s[2]=((oldlen1) >> 16) & U32(0xFF) + s[3]=((oldlen1) >> 24) & U32(0xFF) + s[4]= (oldlen2) & U32(0xFF) + s[5]=((oldlen2) >> 8) & U32(0xFF) + s[6]=((oldlen2) >> 16) & U32(0xFF) + s[7]=((oldlen2) >> 24) & U32(0xFF) + temp.update(int_array2str(s)) + + res[ 0]= temp.A & U32(0xFF) + res[ 1]=(temp.A >> 8) & U32(0xFF) + res[ 2]=(temp.A >> 16) & U32(0xFF) + res[ 3]=(temp.A >> 24) & U32(0xFF) + res[ 4]= temp.B & U32(0xFF) + res[ 5]=(temp.B >> 8) & U32(0xFF) + res[ 6]=(temp.B >> 16) & U32(0xFF) + res[ 7]=(temp.B >> 24) & U32(0xFF) + res[ 8]= temp.C & U32(0xFF) + res[ 9]=(temp.C >> 8) & U32(0xFF) + res[10]=(temp.C >> 16) & U32(0xFF) + res[11]=(temp.C >> 24) & U32(0xFF) + res[12]= temp.D & U32(0xFF) + res[13]=(temp.D >> 8) & U32(0xFF) + res[14]=(temp.D >> 16) & U32(0xFF) + res[15]=(temp.D >> 24) & U32(0xFF) + + return int_array2str(res) + +#==================================================================== +# helpers +def F(x, y, z): return (((x) & (y)) | ((~x) & (z))) +def G(x, y, z): return (((x) & (y)) | ((x) & (z)) | ((y) & (z))) +def H(x, y, z): return ((x) ^ (y) ^ (z)) + +def ROL(x, n): return (((x) << n) | ((x) >> (32-n))) + +def f1(a, b, c, d, k, s, X): return ROL(a + F(b, c, d) + X[k], s) +def f2(a, b, c, d, k, s, X): return ROL(a + G(b, c, d) + X[k] + U32(0x5a827999L), s) +def f3(a, b, c, d, k, s, X): return ROL(a + H(b, c, d) + X[k] + U32(0x6ed9eba1L), s) + +#-------------------------------------------------------------------- +# helper function +def int_array2str(array): + str = '' + for i in array: + str = str + chr(i) + return str + +#-------------------------------------------------------------------- +# To be able to use md4.new() instead of md4.MD4() +new = MD4 diff --git a/plugin.video.alfa/lib/sambatools/smb/utils/pyDes.py b/plugin.video.alfa/lib/sambatools/smb/utils/pyDes.py index b04143e8..6160e2a4 100755 --- a/plugin.video.alfa/lib/sambatools/smb/utils/pyDes.py +++ b/plugin.video.alfa/lib/sambatools/smb/utils/pyDes.py @@ -1,852 +1,852 @@ -############################################################################# -# Documentation # -############################################################################# - -# Author: Todd Whiteman -# Date: 16th March, 2009 -# Verion: 2.0.0 -# License: Public Domain - free to do as you wish -# Homepage: http://twhiteman.netfirms.com/des.html -# -# This is a pure python implementation of the DES encryption algorithm. -# It's pure python to avoid portability issues, since most DES -# implementations are programmed in C (for performance reasons). -# -# Triple DES class is also implemented, utilising the DES base. Triple DES -# is either DES-EDE3 with a 24 byte key, or DES-EDE2 with a 16 byte key. -# -# See the README.txt that should come with this python module for the -# implementation methods used. -# -# Thanks to: -# * David Broadwell for ideas, comments and suggestions. -# * Mario Wolff for pointing out and debugging some triple des CBC errors. -# * Santiago Palladino for providing the PKCS5 padding technique. -# * Shaya for correcting the PAD_PKCS5 triple des CBC errors. -# -"""A pure python implementation of the DES and TRIPLE DES encryption algorithms. - -Class initialization --------------------- -pyDes.des(key, [mode], [IV], [pad], [padmode]) -pyDes.triple_des(key, [mode], [IV], [pad], [padmode]) - -key -> Bytes containing the encryption key. 8 bytes for DES, 16 or 24 bytes - for Triple DES -mode -> Optional argument for encryption type, can be either - pyDes.ECB (Electronic Code Book) or pyDes.CBC (Cypher Block Chaining) -IV -> Optional Initial Value bytes, must be supplied if using CBC mode. - Length must be 8 bytes. -pad -> Optional argument, set the pad character (PAD_NORMAL) to use during - all encrypt/decrpt operations done with this instance. -padmode -> Optional argument, set the padding mode (PAD_NORMAL or PAD_PKCS5) - to use during all encrypt/decrpt operations done with this instance. - -I recommend to use PAD_PKCS5 padding, as then you never need to worry about any -padding issues, as the padding can be removed unambiguously upon decrypting -data that was encrypted using PAD_PKCS5 padmode. - -Common methods --------------- -encrypt(data, [pad], [padmode]) -decrypt(data, [pad], [padmode]) - -data -> Bytes to be encrypted/decrypted -pad -> Optional argument. Only when using padmode of PAD_NORMAL. For - encryption, adds this characters to the end of the data block when - data is not a multiple of 8 bytes. For decryption, will remove the - trailing characters that match this pad character from the last 8 - bytes of the unencrypted data block. -padmode -> Optional argument, set the padding mode, must be one of PAD_NORMAL - or PAD_PKCS5). Defaults to PAD_NORMAL. - - -Example -------- -from pyDes import * - -data = "Please encrypt my data" -k = des("DESCRYPT", CBC, "\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5) -# For Python3, you'll need to use bytes, i.e.: -# data = b"Please encrypt my data" -# k = des(b"DESCRYPT", CBC, b"\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5) -d = k.encrypt(data) -print "Encrypted: %r" % d -print "Decrypted: %r" % k.decrypt(d) -assert k.decrypt(d, padmode=PAD_PKCS5) == data - - -See the module source (pyDes.py) for more examples of use. -You can also run the pyDes.py file without and arguments to see a simple test. - -Note: This code was not written for high-end systems needing a fast - implementation, but rather a handy portable solution with small usage. - -""" - -import sys - -# _pythonMajorVersion is used to handle Python2 and Python3 differences. -_pythonMajorVersion = sys.version_info[0] - -# Modes of crypting / cyphering -ECB = 0 -CBC = 1 - -# Modes of padding -PAD_NORMAL = 1 -PAD_PKCS5 = 2 - -# PAD_PKCS5: is a method that will unambiguously remove all padding -# characters after decryption, when originally encrypted with -# this padding mode. -# For a good description of the PKCS5 padding technique, see: -# http://www.faqs.org/rfcs/rfc1423.html - -# The base class shared by des and triple des. -class _baseDes(object): - def __init__(self, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): - if IV: - IV = self._guardAgainstUnicode(IV) - if pad: - pad = self._guardAgainstUnicode(pad) - self.block_size = 8 - # Sanity checking of arguments. - if pad and padmode == PAD_PKCS5: - raise ValueError("Cannot use a pad character with PAD_PKCS5") - if IV and len(IV) != self.block_size: - raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") - - # Set the passed in variables - self._mode = mode - self._iv = IV - self._padding = pad - self._padmode = padmode - - def getKey(self): - """getKey() -> bytes""" - return self.__key - - def setKey(self, key): - """Will set the crypting key for this object.""" - key = self._guardAgainstUnicode(key) - self.__key = key - - def getMode(self): - """getMode() -> pyDes.ECB or pyDes.CBC""" - return self._mode - - def setMode(self, mode): - """Sets the type of crypting mode, pyDes.ECB or pyDes.CBC""" - self._mode = mode - - def getPadding(self): - """getPadding() -> bytes of length 1. Padding character.""" - return self._padding - - def setPadding(self, pad): - """setPadding() -> bytes of length 1. Padding character.""" - if pad is not None: - pad = self._guardAgainstUnicode(pad) - self._padding = pad - - def getPadMode(self): - """getPadMode() -> pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" - return self._padmode - - def setPadMode(self, mode): - """Sets the type of padding mode, pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" - self._padmode = mode - - def getIV(self): - """getIV() -> bytes""" - return self._iv - - def setIV(self, IV): - """Will set the Initial Value, used in conjunction with CBC mode""" - if not IV or len(IV) != self.block_size: - raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") - IV = self._guardAgainstUnicode(IV) - self._iv = IV - - def _padData(self, data, pad, padmode): - # Pad data depending on the mode - if padmode is None: - # Get the default padding mode. - padmode = self.getPadMode() - if pad and padmode == PAD_PKCS5: - raise ValueError("Cannot use a pad character with PAD_PKCS5") - - if padmode == PAD_NORMAL: - if len(data) % self.block_size == 0: - # No padding required. - return data - - if not pad: - # Get the default padding. - pad = self.getPadding() - if not pad: - raise ValueError("Data must be a multiple of " + str(self.block_size) + " bytes in length. Use padmode=PAD_PKCS5 or set the pad character.") - data += (self.block_size - (len(data) % self.block_size)) * pad - - elif padmode == PAD_PKCS5: - pad_len = 8 - (len(data) % self.block_size) - if _pythonMajorVersion < 3: - data += pad_len * chr(pad_len) - else: - data += bytes([pad_len] * pad_len) - - return data - - def _unpadData(self, data, pad, padmode): - # Unpad data depending on the mode. - if not data: - return data - if pad and padmode == PAD_PKCS5: - raise ValueError("Cannot use a pad character with PAD_PKCS5") - if padmode is None: - # Get the default padding mode. - padmode = self.getPadMode() - - if padmode == PAD_NORMAL: - if not pad: - # Get the default padding. - pad = self.getPadding() - if pad: - data = data[:-self.block_size] + \ - data[-self.block_size:].rstrip(pad) - - elif padmode == PAD_PKCS5: - if _pythonMajorVersion < 3: - pad_len = ord(data[-1]) - else: - pad_len = data[-1] - data = data[:-pad_len] - - return data - - def _guardAgainstUnicode(self, data): - # Only accept byte strings or ascii unicode values, otherwise - # there is no way to correctly decode the data into bytes. - if _pythonMajorVersion < 3: - if isinstance(data, unicode): - raise ValueError("pyDes can only work with bytes, not Unicode strings.") - else: - if isinstance(data, str): - # Only accept ascii unicode values. - try: - return data.encode('ascii') - except UnicodeEncodeError: - pass - raise ValueError("pyDes can only work with encoded strings, not Unicode.") - return data - -############################################################################# -# DES # -############################################################################# -class des(_baseDes): - """DES encryption/decrytpion class - - Supports ECB (Electronic Code Book) and CBC (Cypher Block Chaining) modes. - - pyDes.des(key,[mode], [IV]) - - key -> Bytes containing the encryption key, must be exactly 8 bytes - mode -> Optional argument for encryption type, can be either pyDes.ECB - (Electronic Code Book), pyDes.CBC (Cypher Block Chaining) - IV -> Optional Initial Value bytes, must be supplied if using CBC mode. - Must be 8 bytes in length. - pad -> Optional argument, set the pad character (PAD_NORMAL) to use - during all encrypt/decrpt operations done with this instance. - padmode -> Optional argument, set the padding mode (PAD_NORMAL or - PAD_PKCS5) to use during all encrypt/decrpt operations done - with this instance. - """ - - - # Permutation and translation tables for DES - __pc1 = [56, 48, 40, 32, 24, 16, 8, - 0, 57, 49, 41, 33, 25, 17, - 9, 1, 58, 50, 42, 34, 26, - 18, 10, 2, 59, 51, 43, 35, - 62, 54, 46, 38, 30, 22, 14, - 6, 61, 53, 45, 37, 29, 21, - 13, 5, 60, 52, 44, 36, 28, - 20, 12, 4, 27, 19, 11, 3 - ] - - # number left rotations of pc1 - __left_rotations = [ - 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1 - ] - - # permuted choice key (table 2) - __pc2 = [ - 13, 16, 10, 23, 0, 4, - 2, 27, 14, 5, 20, 9, - 22, 18, 11, 3, 25, 7, - 15, 6, 26, 19, 12, 1, - 40, 51, 30, 36, 46, 54, - 29, 39, 50, 44, 32, 47, - 43, 48, 38, 55, 33, 52, - 45, 41, 49, 35, 28, 31 - ] - - # initial permutation IP - __ip = [57, 49, 41, 33, 25, 17, 9, 1, - 59, 51, 43, 35, 27, 19, 11, 3, - 61, 53, 45, 37, 29, 21, 13, 5, - 63, 55, 47, 39, 31, 23, 15, 7, - 56, 48, 40, 32, 24, 16, 8, 0, - 58, 50, 42, 34, 26, 18, 10, 2, - 60, 52, 44, 36, 28, 20, 12, 4, - 62, 54, 46, 38, 30, 22, 14, 6 - ] - - # Expansion table for turning 32 bit blocks into 48 bits - __expansion_table = [ - 31, 0, 1, 2, 3, 4, - 3, 4, 5, 6, 7, 8, - 7, 8, 9, 10, 11, 12, - 11, 12, 13, 14, 15, 16, - 15, 16, 17, 18, 19, 20, - 19, 20, 21, 22, 23, 24, - 23, 24, 25, 26, 27, 28, - 27, 28, 29, 30, 31, 0 - ] - - # The (in)famous S-boxes - __sbox = [ - # S1 - [14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, - 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, - 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, - 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13], - - # S2 - [15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, - 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, - 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, - 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9], - - # S3 - [10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, - 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, - 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, - 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12], - - # S4 - [7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, - 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, - 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, - 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14], - - # S5 - [2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, - 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, - 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, - 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3], - - # S6 - [12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, - 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, - 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, - 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13], - - # S7 - [4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, - 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, - 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, - 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12], - - # S8 - [13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, - 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, - 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, - 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11], - ] - - - # 32-bit permutation function P used on the output of the S-boxes - __p = [ - 15, 6, 19, 20, 28, 11, - 27, 16, 0, 14, 22, 25, - 4, 17, 30, 9, 1, 7, - 23,13, 31, 26, 2, 8, - 18, 12, 29, 5, 21, 10, - 3, 24 - ] - - # final permutation IP^-1 - __fp = [ - 39, 7, 47, 15, 55, 23, 63, 31, - 38, 6, 46, 14, 54, 22, 62, 30, - 37, 5, 45, 13, 53, 21, 61, 29, - 36, 4, 44, 12, 52, 20, 60, 28, - 35, 3, 43, 11, 51, 19, 59, 27, - 34, 2, 42, 10, 50, 18, 58, 26, - 33, 1, 41, 9, 49, 17, 57, 25, - 32, 0, 40, 8, 48, 16, 56, 24 - ] - - # Type of crypting being done - ENCRYPT = 0x00 - DECRYPT = 0x01 - - # Initialisation - def __init__(self, key, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): - # Sanity checking of arguments. - if len(key) != 8: - raise ValueError("Invalid DES key size. Key must be exactly 8 bytes long.") - _baseDes.__init__(self, mode, IV, pad, padmode) - self.key_size = 8 - - self.L = [] - self.R = [] - self.Kn = [ [0] * 48 ] * 16 # 16 48-bit keys (K1 - K16) - self.final = [] - - self.setKey(key) - - def setKey(self, key): - """Will set the crypting key for this object. Must be 8 bytes.""" - _baseDes.setKey(self, key) - self.__create_sub_keys() - - def __String_to_BitList(self, data): - """Turn the string data, into a list of bits (1, 0)'s""" - if _pythonMajorVersion < 3: - # Turn the strings into integers. Python 3 uses a bytes - # class, which already has this behaviour. - data = [ord(c) for c in data] - l = len(data) * 8 - result = [0] * l - pos = 0 - for ch in data: - i = 7 - while i >= 0: - if ch & (1 << i) != 0: - result[pos] = 1 - else: - result[pos] = 0 - pos += 1 - i -= 1 - - return result - - def __BitList_to_String(self, data): - """Turn the list of bits -> data, into a string""" - result = [] - pos = 0 - c = 0 - while pos < len(data): - c += data[pos] << (7 - (pos % 8)) - if (pos % 8) == 7: - result.append(c) - c = 0 - pos += 1 - - if _pythonMajorVersion < 3: - return ''.join([ chr(c) for c in result ]) - else: - return bytes(result) - - def __permutate(self, table, block): - """Permutate this block with the specified table""" - return list(map(lambda x: block[x], table)) - - # Transform the secret key, so that it is ready for data processing - # Create the 16 subkeys, K[1] - K[16] - def __create_sub_keys(self): - """Create the 16 subkeys K[1] to K[16] from the given key""" - key = self.__permutate(des.__pc1, self.__String_to_BitList(self.getKey())) - i = 0 - # Split into Left and Right sections - self.L = key[:28] - self.R = key[28:] - while i < 16: - j = 0 - # Perform circular left shifts - while j < des.__left_rotations[i]: - self.L.append(self.L[0]) - del self.L[0] - - self.R.append(self.R[0]) - del self.R[0] - - j += 1 - - # Create one of the 16 subkeys through pc2 permutation - self.Kn[i] = self.__permutate(des.__pc2, self.L + self.R) - - i += 1 - - # Main part of the encryption algorithm, the number cruncher :) - def __des_crypt(self, block, crypt_type): - """Crypt the block of data through DES bit-manipulation""" - block = self.__permutate(des.__ip, block) - self.L = block[:32] - self.R = block[32:] - - # Encryption starts from Kn[1] through to Kn[16] - if crypt_type == des.ENCRYPT: - iteration = 0 - iteration_adjustment = 1 - # Decryption starts from Kn[16] down to Kn[1] - else: - iteration = 15 - iteration_adjustment = -1 - - i = 0 - while i < 16: - # Make a copy of R[i-1], this will later become L[i] - tempR = self.R[:] - - # Permutate R[i - 1] to start creating R[i] - self.R = self.__permutate(des.__expansion_table, self.R) - - # Exclusive or R[i - 1] with K[i], create B[1] to B[8] whilst here - self.R = list(map(lambda x, y: x ^ y, self.R, self.Kn[iteration])) - B = [self.R[:6], self.R[6:12], self.R[12:18], self.R[18:24], self.R[24:30], self.R[30:36], self.R[36:42], self.R[42:]] - # Optimization: Replaced below commented code with above - #j = 0 - #B = [] - #while j < len(self.R): - # self.R[j] = self.R[j] ^ self.Kn[iteration][j] - # j += 1 - # if j % 6 == 0: - # B.append(self.R[j-6:j]) - - # Permutate B[1] to B[8] using the S-Boxes - j = 0 - Bn = [0] * 32 - pos = 0 - while j < 8: - # Work out the offsets - m = (B[j][0] << 1) + B[j][5] - n = (B[j][1] << 3) + (B[j][2] << 2) + (B[j][3] << 1) + B[j][4] - - # Find the permutation value - v = des.__sbox[j][(m << 4) + n] - - # Turn value into bits, add it to result: Bn - Bn[pos] = (v & 8) >> 3 - Bn[pos + 1] = (v & 4) >> 2 - Bn[pos + 2] = (v & 2) >> 1 - Bn[pos + 3] = v & 1 - - pos += 4 - j += 1 - - # Permutate the concatination of B[1] to B[8] (Bn) - self.R = self.__permutate(des.__p, Bn) - - # Xor with L[i - 1] - self.R = list(map(lambda x, y: x ^ y, self.R, self.L)) - # Optimization: This now replaces the below commented code - #j = 0 - #while j < len(self.R): - # self.R[j] = self.R[j] ^ self.L[j] - # j += 1 - - # L[i] becomes R[i - 1] - self.L = tempR - - i += 1 - iteration += iteration_adjustment - - # Final permutation of R[16]L[16] - self.final = self.__permutate(des.__fp, self.R + self.L) - return self.final - - - # Data to be encrypted/decrypted - def crypt(self, data, crypt_type): - """Crypt the data in blocks, running it through des_crypt()""" - - # Error check the data - if not data: - return '' - if len(data) % self.block_size != 0: - if crypt_type == des.DECRYPT: # Decryption must work on 8 byte blocks - raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n.") - if not self.getPadding(): - raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n. Try setting the optional padding character") - else: - data += (self.block_size - (len(data) % self.block_size)) * self.getPadding() - # print "Len of data: %f" % (len(data) / self.block_size) - - if self.getMode() == CBC: - if self.getIV(): - iv = self.__String_to_BitList(self.getIV()) - else: - raise ValueError("For CBC mode, you must supply the Initial Value (IV) for ciphering") - - # Split the data into blocks, crypting each one seperately - i = 0 - dict = {} - result = [] - #cached = 0 - #lines = 0 - while i < len(data): - # Test code for caching encryption results - #lines += 1 - #if dict.has_key(data[i:i+8]): - #print "Cached result for: %s" % data[i:i+8] - # cached += 1 - # result.append(dict[data[i:i+8]]) - # i += 8 - # continue - - block = self.__String_to_BitList(data[i:i+8]) - - # Xor with IV if using CBC mode - if self.getMode() == CBC: - if crypt_type == des.ENCRYPT: - block = list(map(lambda x, y: x ^ y, block, iv)) - #j = 0 - #while j < len(block): - # block[j] = block[j] ^ iv[j] - # j += 1 - - processed_block = self.__des_crypt(block, crypt_type) - - if crypt_type == des.DECRYPT: - processed_block = list(map(lambda x, y: x ^ y, processed_block, iv)) - #j = 0 - #while j < len(processed_block): - # processed_block[j] = processed_block[j] ^ iv[j] - # j += 1 - iv = block - else: - iv = processed_block - else: - processed_block = self.__des_crypt(block, crypt_type) - - - # Add the resulting crypted block to our list - #d = self.__BitList_to_String(processed_block) - #result.append(d) - result.append(self.__BitList_to_String(processed_block)) - #dict[data[i:i+8]] = d - i += 8 - - # print "Lines: %d, cached: %d" % (lines, cached) - - # Return the full crypted string - if _pythonMajorVersion < 3: - return ''.join(result) - else: - return bytes.fromhex('').join(result) - - def encrypt(self, data, pad=None, padmode=None): - """encrypt(data, [pad], [padmode]) -> bytes - - data : Bytes to be encrypted - pad : Optional argument for encryption padding. Must only be one byte - padmode : Optional argument for overriding the padding mode. - - The data must be a multiple of 8 bytes and will be encrypted - with the already specified key. Data does not have to be a - multiple of 8 bytes if the padding character is supplied, or - the padmode is set to PAD_PKCS5, as bytes will then added to - ensure the be padded data is a multiple of 8 bytes. - """ - data = self._guardAgainstUnicode(data) - if pad is not None: - pad = self._guardAgainstUnicode(pad) - data = self._padData(data, pad, padmode) - return self.crypt(data, des.ENCRYPT) - - def decrypt(self, data, pad=None, padmode=None): - """decrypt(data, [pad], [padmode]) -> bytes - - data : Bytes to be encrypted - pad : Optional argument for decryption padding. Must only be one byte - padmode : Optional argument for overriding the padding mode. - - The data must be a multiple of 8 bytes and will be decrypted - with the already specified key. In PAD_NORMAL mode, if the - optional padding character is supplied, then the un-encrypted - data will have the padding characters removed from the end of - the bytes. This pad removal only occurs on the last 8 bytes of - the data (last data block). In PAD_PKCS5 mode, the special - padding end markers will be removed from the data after decrypting. - """ - data = self._guardAgainstUnicode(data) - if pad is not None: - pad = self._guardAgainstUnicode(pad) - data = self.crypt(data, des.DECRYPT) - return self._unpadData(data, pad, padmode) - - - -############################################################################# -# Triple DES # -############################################################################# -class triple_des(_baseDes): - """Triple DES encryption/decrytpion class - - This algorithm uses the DES-EDE3 (when a 24 byte key is supplied) or - the DES-EDE2 (when a 16 byte key is supplied) encryption methods. - Supports ECB (Electronic Code Book) and CBC (Cypher Block Chaining) modes. - - pyDes.des(key, [mode], [IV]) - - key -> Bytes containing the encryption key, must be either 16 or - 24 bytes long - mode -> Optional argument for encryption type, can be either pyDes.ECB - (Electronic Code Book), pyDes.CBC (Cypher Block Chaining) - IV -> Optional Initial Value bytes, must be supplied if using CBC mode. - Must be 8 bytes in length. - pad -> Optional argument, set the pad character (PAD_NORMAL) to use - during all encrypt/decrpt operations done with this instance. - padmode -> Optional argument, set the padding mode (PAD_NORMAL or - PAD_PKCS5) to use during all encrypt/decrpt operations done - with this instance. - """ - def __init__(self, key, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): - _baseDes.__init__(self, mode, IV, pad, padmode) - self.setKey(key) - - def setKey(self, key): - """Will set the crypting key for this object. Either 16 or 24 bytes long.""" - self.key_size = 24 # Use DES-EDE3 mode - if len(key) != self.key_size: - if len(key) == 16: # Use DES-EDE2 mode - self.key_size = 16 - else: - raise ValueError("Invalid triple DES key size. Key must be either 16 or 24 bytes long") - if self.getMode() == CBC: - if not self.getIV(): - # Use the first 8 bytes of the key - self._iv = key[:self.block_size] - if len(self.getIV()) != self.block_size: - raise ValueError("Invalid IV, must be 8 bytes in length") - self.__key1 = des(key[:8], self._mode, self._iv, - self._padding, self._padmode) - self.__key2 = des(key[8:16], self._mode, self._iv, - self._padding, self._padmode) - if self.key_size == 16: - self.__key3 = self.__key1 - else: - self.__key3 = des(key[16:], self._mode, self._iv, - self._padding, self._padmode) - _baseDes.setKey(self, key) - - # Override setter methods to work on all 3 keys. - - def setMode(self, mode): - """Sets the type of crypting mode, pyDes.ECB or pyDes.CBC""" - _baseDes.setMode(self, mode) - for key in (self.__key1, self.__key2, self.__key3): - key.setMode(mode) - - def setPadding(self, pad): - """setPadding() -> bytes of length 1. Padding character.""" - _baseDes.setPadding(self, pad) - for key in (self.__key1, self.__key2, self.__key3): - key.setPadding(pad) - - def setPadMode(self, mode): - """Sets the type of padding mode, pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" - _baseDes.setPadMode(self, mode) - for key in (self.__key1, self.__key2, self.__key3): - key.setPadMode(mode) - - def setIV(self, IV): - """Will set the Initial Value, used in conjunction with CBC mode""" - _baseDes.setIV(self, IV) - for key in (self.__key1, self.__key2, self.__key3): - key.setIV(IV) - - def encrypt(self, data, pad=None, padmode=None): - """encrypt(data, [pad], [padmode]) -> bytes - - data : bytes to be encrypted - pad : Optional argument for encryption padding. Must only be one byte - padmode : Optional argument for overriding the padding mode. - - The data must be a multiple of 8 bytes and will be encrypted - with the already specified key. Data does not have to be a - multiple of 8 bytes if the padding character is supplied, or - the padmode is set to PAD_PKCS5, as bytes will then added to - ensure the be padded data is a multiple of 8 bytes. - """ - ENCRYPT = des.ENCRYPT - DECRYPT = des.DECRYPT - data = self._guardAgainstUnicode(data) - if pad is not None: - pad = self._guardAgainstUnicode(pad) - # Pad the data accordingly. - data = self._padData(data, pad, padmode) - if self.getMode() == CBC: - self.__key1.setIV(self.getIV()) - self.__key2.setIV(self.getIV()) - self.__key3.setIV(self.getIV()) - i = 0 - result = [] - while i < len(data): - block = self.__key1.crypt(data[i:i+8], ENCRYPT) - block = self.__key2.crypt(block, DECRYPT) - block = self.__key3.crypt(block, ENCRYPT) - self.__key1.setIV(block) - self.__key2.setIV(block) - self.__key3.setIV(block) - result.append(block) - i += 8 - if _pythonMajorVersion < 3: - return ''.join(result) - else: - return bytes.fromhex('').join(result) - else: - data = self.__key1.crypt(data, ENCRYPT) - data = self.__key2.crypt(data, DECRYPT) - return self.__key3.crypt(data, ENCRYPT) - - def decrypt(self, data, pad=None, padmode=None): - """decrypt(data, [pad], [padmode]) -> bytes - - data : bytes to be encrypted - pad : Optional argument for decryption padding. Must only be one byte - padmode : Optional argument for overriding the padding mode. - - The data must be a multiple of 8 bytes and will be decrypted - with the already specified key. In PAD_NORMAL mode, if the - optional padding character is supplied, then the un-encrypted - data will have the padding characters removed from the end of - the bytes. This pad removal only occurs on the last 8 bytes of - the data (last data block). In PAD_PKCS5 mode, the special - padding end markers will be removed from the data after - decrypting, no pad character is required for PAD_PKCS5. - """ - ENCRYPT = des.ENCRYPT - DECRYPT = des.DECRYPT - data = self._guardAgainstUnicode(data) - if pad is not None: - pad = self._guardAgainstUnicode(pad) - if self.getMode() == CBC: - self.__key1.setIV(self.getIV()) - self.__key2.setIV(self.getIV()) - self.__key3.setIV(self.getIV()) - i = 0 - result = [] - while i < len(data): - iv = data[i:i+8] - block = self.__key3.crypt(iv, DECRYPT) - block = self.__key2.crypt(block, ENCRYPT) - block = self.__key1.crypt(block, DECRYPT) - self.__key1.setIV(iv) - self.__key2.setIV(iv) - self.__key3.setIV(iv) - result.append(block) - i += 8 - if _pythonMajorVersion < 3: - data = ''.join(result) - else: - data = bytes.fromhex('').join(result) - else: - data = self.__key3.crypt(data, DECRYPT) - data = self.__key2.crypt(data, ENCRYPT) - data = self.__key1.crypt(data, DECRYPT) - return self._unpadData(data, pad, padmode) +############################################################################# +# Documentation # +############################################################################# + +# Author: Todd Whiteman +# Date: 16th March, 2009 +# Verion: 2.0.0 +# License: Public Domain - free to do as you wish +# Homepage: http://twhiteman.netfirms.com/des.html +# +# This is a pure python implementation of the DES encryption algorithm. +# It's pure python to avoid portability issues, since most DES +# implementations are programmed in C (for performance reasons). +# +# Triple DES class is also implemented, utilising the DES base. Triple DES +# is either DES-EDE3 with a 24 byte key, or DES-EDE2 with a 16 byte key. +# +# See the README.txt that should come with this python module for the +# implementation methods used. +# +# Thanks to: +# * David Broadwell for ideas, comments and suggestions. +# * Mario Wolff for pointing out and debugging some triple des CBC errors. +# * Santiago Palladino for providing the PKCS5 padding technique. +# * Shaya for correcting the PAD_PKCS5 triple des CBC errors. +# +"""A pure python implementation of the DES and TRIPLE DES encryption algorithms. + +Class initialization +-------------------- +pyDes.des(key, [mode], [IV], [pad], [padmode]) +pyDes.triple_des(key, [mode], [IV], [pad], [padmode]) + +key -> Bytes containing the encryption key. 8 bytes for DES, 16 or 24 bytes + for Triple DES +mode -> Optional argument for encryption type, can be either + pyDes.ECB (Electronic Code Book) or pyDes.CBC (Cypher Block Chaining) +IV -> Optional Initial Value bytes, must be supplied if using CBC mode. + Length must be 8 bytes. +pad -> Optional argument, set the pad character (PAD_NORMAL) to use during + all encrypt/decrpt operations done with this instance. +padmode -> Optional argument, set the padding mode (PAD_NORMAL or PAD_PKCS5) + to use during all encrypt/decrpt operations done with this instance. + +I recommend to use PAD_PKCS5 padding, as then you never need to worry about any +padding issues, as the padding can be removed unambiguously upon decrypting +data that was encrypted using PAD_PKCS5 padmode. + +Common methods +-------------- +encrypt(data, [pad], [padmode]) +decrypt(data, [pad], [padmode]) + +data -> Bytes to be encrypted/decrypted +pad -> Optional argument. Only when using padmode of PAD_NORMAL. For + encryption, adds this characters to the end of the data block when + data is not a multiple of 8 bytes. For decryption, will remove the + trailing characters that match this pad character from the last 8 + bytes of the unencrypted data block. +padmode -> Optional argument, set the padding mode, must be one of PAD_NORMAL + or PAD_PKCS5). Defaults to PAD_NORMAL. + + +Example +------- +from pyDes import * + +data = "Please encrypt my data" +k = des("DESCRYPT", CBC, "\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5) +# For Python3, you'll need to use bytes, i.e.: +# data = b"Please encrypt my data" +# k = des(b"DESCRYPT", CBC, b"\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5) +d = k.encrypt(data) +print "Encrypted: %r" % d +print "Decrypted: %r" % k.decrypt(d) +assert k.decrypt(d, padmode=PAD_PKCS5) == data + + +See the module source (pyDes.py) for more examples of use. +You can also run the pyDes.py file without and arguments to see a simple test. + +Note: This code was not written for high-end systems needing a fast + implementation, but rather a handy portable solution with small usage. + +""" + +import sys + +# _pythonMajorVersion is used to handle Python2 and Python3 differences. +_pythonMajorVersion = sys.version_info[0] + +# Modes of crypting / cyphering +ECB = 0 +CBC = 1 + +# Modes of padding +PAD_NORMAL = 1 +PAD_PKCS5 = 2 + +# PAD_PKCS5: is a method that will unambiguously remove all padding +# characters after decryption, when originally encrypted with +# this padding mode. +# For a good description of the PKCS5 padding technique, see: +# http://www.faqs.org/rfcs/rfc1423.html + +# The base class shared by des and triple des. +class _baseDes(object): + def __init__(self, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): + if IV: + IV = self._guardAgainstUnicode(IV) + if pad: + pad = self._guardAgainstUnicode(pad) + self.block_size = 8 + # Sanity checking of arguments. + if pad and padmode == PAD_PKCS5: + raise ValueError("Cannot use a pad character with PAD_PKCS5") + if IV and len(IV) != self.block_size: + raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") + + # Set the passed in variables + self._mode = mode + self._iv = IV + self._padding = pad + self._padmode = padmode + + def getKey(self): + """getKey() -> bytes""" + return self.__key + + def setKey(self, key): + """Will set the crypting key for this object.""" + key = self._guardAgainstUnicode(key) + self.__key = key + + def getMode(self): + """getMode() -> pyDes.ECB or pyDes.CBC""" + return self._mode + + def setMode(self, mode): + """Sets the type of crypting mode, pyDes.ECB or pyDes.CBC""" + self._mode = mode + + def getPadding(self): + """getPadding() -> bytes of length 1. Padding character.""" + return self._padding + + def setPadding(self, pad): + """setPadding() -> bytes of length 1. Padding character.""" + if pad is not None: + pad = self._guardAgainstUnicode(pad) + self._padding = pad + + def getPadMode(self): + """getPadMode() -> pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" + return self._padmode + + def setPadMode(self, mode): + """Sets the type of padding mode, pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" + self._padmode = mode + + def getIV(self): + """getIV() -> bytes""" + return self._iv + + def setIV(self, IV): + """Will set the Initial Value, used in conjunction with CBC mode""" + if not IV or len(IV) != self.block_size: + raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") + IV = self._guardAgainstUnicode(IV) + self._iv = IV + + def _padData(self, data, pad, padmode): + # Pad data depending on the mode + if padmode is None: + # Get the default padding mode. + padmode = self.getPadMode() + if pad and padmode == PAD_PKCS5: + raise ValueError("Cannot use a pad character with PAD_PKCS5") + + if padmode == PAD_NORMAL: + if len(data) % self.block_size == 0: + # No padding required. + return data + + if not pad: + # Get the default padding. + pad = self.getPadding() + if not pad: + raise ValueError("Data must be a multiple of " + str(self.block_size) + " bytes in length. Use padmode=PAD_PKCS5 or set the pad character.") + data += (self.block_size - (len(data) % self.block_size)) * pad + + elif padmode == PAD_PKCS5: + pad_len = 8 - (len(data) % self.block_size) + if _pythonMajorVersion < 3: + data += pad_len * chr(pad_len) + else: + data += bytes([pad_len] * pad_len) + + return data + + def _unpadData(self, data, pad, padmode): + # Unpad data depending on the mode. + if not data: + return data + if pad and padmode == PAD_PKCS5: + raise ValueError("Cannot use a pad character with PAD_PKCS5") + if padmode is None: + # Get the default padding mode. + padmode = self.getPadMode() + + if padmode == PAD_NORMAL: + if not pad: + # Get the default padding. + pad = self.getPadding() + if pad: + data = data[:-self.block_size] + \ + data[-self.block_size:].rstrip(pad) + + elif padmode == PAD_PKCS5: + if _pythonMajorVersion < 3: + pad_len = ord(data[-1]) + else: + pad_len = data[-1] + data = data[:-pad_len] + + return data + + def _guardAgainstUnicode(self, data): + # Only accept byte strings or ascii unicode values, otherwise + # there is no way to correctly decode the data into bytes. + if _pythonMajorVersion < 3: + if isinstance(data, unicode): + raise ValueError("pyDes can only work with bytes, not Unicode strings.") + else: + if isinstance(data, str): + # Only accept ascii unicode values. + try: + return data.encode('ascii') + except UnicodeEncodeError: + pass + raise ValueError("pyDes can only work with encoded strings, not Unicode.") + return data + +############################################################################# +# DES # +############################################################################# +class des(_baseDes): + """DES encryption/decrytpion class + + Supports ECB (Electronic Code Book) and CBC (Cypher Block Chaining) modes. + + pyDes.des(key,[mode], [IV]) + + key -> Bytes containing the encryption key, must be exactly 8 bytes + mode -> Optional argument for encryption type, can be either pyDes.ECB + (Electronic Code Book), pyDes.CBC (Cypher Block Chaining) + IV -> Optional Initial Value bytes, must be supplied if using CBC mode. + Must be 8 bytes in length. + pad -> Optional argument, set the pad character (PAD_NORMAL) to use + during all encrypt/decrpt operations done with this instance. + padmode -> Optional argument, set the padding mode (PAD_NORMAL or + PAD_PKCS5) to use during all encrypt/decrpt operations done + with this instance. + """ + + + # Permutation and translation tables for DES + __pc1 = [56, 48, 40, 32, 24, 16, 8, + 0, 57, 49, 41, 33, 25, 17, + 9, 1, 58, 50, 42, 34, 26, + 18, 10, 2, 59, 51, 43, 35, + 62, 54, 46, 38, 30, 22, 14, + 6, 61, 53, 45, 37, 29, 21, + 13, 5, 60, 52, 44, 36, 28, + 20, 12, 4, 27, 19, 11, 3 + ] + + # number left rotations of pc1 + __left_rotations = [ + 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1 + ] + + # permuted choice key (table 2) + __pc2 = [ + 13, 16, 10, 23, 0, 4, + 2, 27, 14, 5, 20, 9, + 22, 18, 11, 3, 25, 7, + 15, 6, 26, 19, 12, 1, + 40, 51, 30, 36, 46, 54, + 29, 39, 50, 44, 32, 47, + 43, 48, 38, 55, 33, 52, + 45, 41, 49, 35, 28, 31 + ] + + # initial permutation IP + __ip = [57, 49, 41, 33, 25, 17, 9, 1, + 59, 51, 43, 35, 27, 19, 11, 3, + 61, 53, 45, 37, 29, 21, 13, 5, + 63, 55, 47, 39, 31, 23, 15, 7, + 56, 48, 40, 32, 24, 16, 8, 0, + 58, 50, 42, 34, 26, 18, 10, 2, + 60, 52, 44, 36, 28, 20, 12, 4, + 62, 54, 46, 38, 30, 22, 14, 6 + ] + + # Expansion table for turning 32 bit blocks into 48 bits + __expansion_table = [ + 31, 0, 1, 2, 3, 4, + 3, 4, 5, 6, 7, 8, + 7, 8, 9, 10, 11, 12, + 11, 12, 13, 14, 15, 16, + 15, 16, 17, 18, 19, 20, + 19, 20, 21, 22, 23, 24, + 23, 24, 25, 26, 27, 28, + 27, 28, 29, 30, 31, 0 + ] + + # The (in)famous S-boxes + __sbox = [ + # S1 + [14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, + 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, + 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, + 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13], + + # S2 + [15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, + 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, + 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, + 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9], + + # S3 + [10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, + 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, + 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, + 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12], + + # S4 + [7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, + 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, + 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, + 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14], + + # S5 + [2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, + 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, + 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, + 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3], + + # S6 + [12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, + 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, + 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, + 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13], + + # S7 + [4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, + 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, + 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, + 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12], + + # S8 + [13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, + 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, + 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, + 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11], + ] + + + # 32-bit permutation function P used on the output of the S-boxes + __p = [ + 15, 6, 19, 20, 28, 11, + 27, 16, 0, 14, 22, 25, + 4, 17, 30, 9, 1, 7, + 23,13, 31, 26, 2, 8, + 18, 12, 29, 5, 21, 10, + 3, 24 + ] + + # final permutation IP^-1 + __fp = [ + 39, 7, 47, 15, 55, 23, 63, 31, + 38, 6, 46, 14, 54, 22, 62, 30, + 37, 5, 45, 13, 53, 21, 61, 29, + 36, 4, 44, 12, 52, 20, 60, 28, + 35, 3, 43, 11, 51, 19, 59, 27, + 34, 2, 42, 10, 50, 18, 58, 26, + 33, 1, 41, 9, 49, 17, 57, 25, + 32, 0, 40, 8, 48, 16, 56, 24 + ] + + # Type of crypting being done + ENCRYPT = 0x00 + DECRYPT = 0x01 + + # Initialisation + def __init__(self, key, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): + # Sanity checking of arguments. + if len(key) != 8: + raise ValueError("Invalid DES key size. Key must be exactly 8 bytes long.") + _baseDes.__init__(self, mode, IV, pad, padmode) + self.key_size = 8 + + self.L = [] + self.R = [] + self.Kn = [ [0] * 48 ] * 16 # 16 48-bit keys (K1 - K16) + self.final = [] + + self.setKey(key) + + def setKey(self, key): + """Will set the crypting key for this object. Must be 8 bytes.""" + _baseDes.setKey(self, key) + self.__create_sub_keys() + + def __String_to_BitList(self, data): + """Turn the string data, into a list of bits (1, 0)'s""" + if _pythonMajorVersion < 3: + # Turn the strings into integers. Python 3 uses a bytes + # class, which already has this behaviour. + data = [ord(c) for c in data] + l = len(data) * 8 + result = [0] * l + pos = 0 + for ch in data: + i = 7 + while i >= 0: + if ch & (1 << i) != 0: + result[pos] = 1 + else: + result[pos] = 0 + pos += 1 + i -= 1 + + return result + + def __BitList_to_String(self, data): + """Turn the list of bits -> data, into a string""" + result = [] + pos = 0 + c = 0 + while pos < len(data): + c += data[pos] << (7 - (pos % 8)) + if (pos % 8) == 7: + result.append(c) + c = 0 + pos += 1 + + if _pythonMajorVersion < 3: + return ''.join([ chr(c) for c in result ]) + else: + return bytes(result) + + def __permutate(self, table, block): + """Permutate this block with the specified table""" + return list(map(lambda x: block[x], table)) + + # Transform the secret key, so that it is ready for data processing + # Create the 16 subkeys, K[1] - K[16] + def __create_sub_keys(self): + """Create the 16 subkeys K[1] to K[16] from the given key""" + key = self.__permutate(des.__pc1, self.__String_to_BitList(self.getKey())) + i = 0 + # Split into Left and Right sections + self.L = key[:28] + self.R = key[28:] + while i < 16: + j = 0 + # Perform circular left shifts + while j < des.__left_rotations[i]: + self.L.append(self.L[0]) + del self.L[0] + + self.R.append(self.R[0]) + del self.R[0] + + j += 1 + + # Create one of the 16 subkeys through pc2 permutation + self.Kn[i] = self.__permutate(des.__pc2, self.L + self.R) + + i += 1 + + # Main part of the encryption algorithm, the number cruncher :) + def __des_crypt(self, block, crypt_type): + """Crypt the block of data through DES bit-manipulation""" + block = self.__permutate(des.__ip, block) + self.L = block[:32] + self.R = block[32:] + + # Encryption starts from Kn[1] through to Kn[16] + if crypt_type == des.ENCRYPT: + iteration = 0 + iteration_adjustment = 1 + # Decryption starts from Kn[16] down to Kn[1] + else: + iteration = 15 + iteration_adjustment = -1 + + i = 0 + while i < 16: + # Make a copy of R[i-1], this will later become L[i] + tempR = self.R[:] + + # Permutate R[i - 1] to start creating R[i] + self.R = self.__permutate(des.__expansion_table, self.R) + + # Exclusive or R[i - 1] with K[i], create B[1] to B[8] whilst here + self.R = list(map(lambda x, y: x ^ y, self.R, self.Kn[iteration])) + B = [self.R[:6], self.R[6:12], self.R[12:18], self.R[18:24], self.R[24:30], self.R[30:36], self.R[36:42], self.R[42:]] + # Optimization: Replaced below commented code with above + #j = 0 + #B = [] + #while j < len(self.R): + # self.R[j] = self.R[j] ^ self.Kn[iteration][j] + # j += 1 + # if j % 6 == 0: + # B.append(self.R[j-6:j]) + + # Permutate B[1] to B[8] using the S-Boxes + j = 0 + Bn = [0] * 32 + pos = 0 + while j < 8: + # Work out the offsets + m = (B[j][0] << 1) + B[j][5] + n = (B[j][1] << 3) + (B[j][2] << 2) + (B[j][3] << 1) + B[j][4] + + # Find the permutation value + v = des.__sbox[j][(m << 4) + n] + + # Turn value into bits, add it to result: Bn + Bn[pos] = (v & 8) >> 3 + Bn[pos + 1] = (v & 4) >> 2 + Bn[pos + 2] = (v & 2) >> 1 + Bn[pos + 3] = v & 1 + + pos += 4 + j += 1 + + # Permutate the concatination of B[1] to B[8] (Bn) + self.R = self.__permutate(des.__p, Bn) + + # Xor with L[i - 1] + self.R = list(map(lambda x, y: x ^ y, self.R, self.L)) + # Optimization: This now replaces the below commented code + #j = 0 + #while j < len(self.R): + # self.R[j] = self.R[j] ^ self.L[j] + # j += 1 + + # L[i] becomes R[i - 1] + self.L = tempR + + i += 1 + iteration += iteration_adjustment + + # Final permutation of R[16]L[16] + self.final = self.__permutate(des.__fp, self.R + self.L) + return self.final + + + # Data to be encrypted/decrypted + def crypt(self, data, crypt_type): + """Crypt the data in blocks, running it through des_crypt()""" + + # Error check the data + if not data: + return '' + if len(data) % self.block_size != 0: + if crypt_type == des.DECRYPT: # Decryption must work on 8 byte blocks + raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n.") + if not self.getPadding(): + raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n. Try setting the optional padding character") + else: + data += (self.block_size - (len(data) % self.block_size)) * self.getPadding() + # print "Len of data: %f" % (len(data) / self.block_size) + + if self.getMode() == CBC: + if self.getIV(): + iv = self.__String_to_BitList(self.getIV()) + else: + raise ValueError("For CBC mode, you must supply the Initial Value (IV) for ciphering") + + # Split the data into blocks, crypting each one seperately + i = 0 + dict = {} + result = [] + #cached = 0 + #lines = 0 + while i < len(data): + # Test code for caching encryption results + #lines += 1 + #if dict.has_key(data[i:i+8]): + #print "Cached result for: %s" % data[i:i+8] + # cached += 1 + # result.append(dict[data[i:i+8]]) + # i += 8 + # continue + + block = self.__String_to_BitList(data[i:i+8]) + + # Xor with IV if using CBC mode + if self.getMode() == CBC: + if crypt_type == des.ENCRYPT: + block = list(map(lambda x, y: x ^ y, block, iv)) + #j = 0 + #while j < len(block): + # block[j] = block[j] ^ iv[j] + # j += 1 + + processed_block = self.__des_crypt(block, crypt_type) + + if crypt_type == des.DECRYPT: + processed_block = list(map(lambda x, y: x ^ y, processed_block, iv)) + #j = 0 + #while j < len(processed_block): + # processed_block[j] = processed_block[j] ^ iv[j] + # j += 1 + iv = block + else: + iv = processed_block + else: + processed_block = self.__des_crypt(block, crypt_type) + + + # Add the resulting crypted block to our list + #d = self.__BitList_to_String(processed_block) + #result.append(d) + result.append(self.__BitList_to_String(processed_block)) + #dict[data[i:i+8]] = d + i += 8 + + # print "Lines: %d, cached: %d" % (lines, cached) + + # Return the full crypted string + if _pythonMajorVersion < 3: + return ''.join(result) + else: + return bytes.fromhex('').join(result) + + def encrypt(self, data, pad=None, padmode=None): + """encrypt(data, [pad], [padmode]) -> bytes + + data : Bytes to be encrypted + pad : Optional argument for encryption padding. Must only be one byte + padmode : Optional argument for overriding the padding mode. + + The data must be a multiple of 8 bytes and will be encrypted + with the already specified key. Data does not have to be a + multiple of 8 bytes if the padding character is supplied, or + the padmode is set to PAD_PKCS5, as bytes will then added to + ensure the be padded data is a multiple of 8 bytes. + """ + data = self._guardAgainstUnicode(data) + if pad is not None: + pad = self._guardAgainstUnicode(pad) + data = self._padData(data, pad, padmode) + return self.crypt(data, des.ENCRYPT) + + def decrypt(self, data, pad=None, padmode=None): + """decrypt(data, [pad], [padmode]) -> bytes + + data : Bytes to be encrypted + pad : Optional argument for decryption padding. Must only be one byte + padmode : Optional argument for overriding the padding mode. + + The data must be a multiple of 8 bytes and will be decrypted + with the already specified key. In PAD_NORMAL mode, if the + optional padding character is supplied, then the un-encrypted + data will have the padding characters removed from the end of + the bytes. This pad removal only occurs on the last 8 bytes of + the data (last data block). In PAD_PKCS5 mode, the special + padding end markers will be removed from the data after decrypting. + """ + data = self._guardAgainstUnicode(data) + if pad is not None: + pad = self._guardAgainstUnicode(pad) + data = self.crypt(data, des.DECRYPT) + return self._unpadData(data, pad, padmode) + + + +############################################################################# +# Triple DES # +############################################################################# +class triple_des(_baseDes): + """Triple DES encryption/decrytpion class + + This algorithm uses the DES-EDE3 (when a 24 byte key is supplied) or + the DES-EDE2 (when a 16 byte key is supplied) encryption methods. + Supports ECB (Electronic Code Book) and CBC (Cypher Block Chaining) modes. + + pyDes.des(key, [mode], [IV]) + + key -> Bytes containing the encryption key, must be either 16 or + 24 bytes long + mode -> Optional argument for encryption type, can be either pyDes.ECB + (Electronic Code Book), pyDes.CBC (Cypher Block Chaining) + IV -> Optional Initial Value bytes, must be supplied if using CBC mode. + Must be 8 bytes in length. + pad -> Optional argument, set the pad character (PAD_NORMAL) to use + during all encrypt/decrpt operations done with this instance. + padmode -> Optional argument, set the padding mode (PAD_NORMAL or + PAD_PKCS5) to use during all encrypt/decrpt operations done + with this instance. + """ + def __init__(self, key, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): + _baseDes.__init__(self, mode, IV, pad, padmode) + self.setKey(key) + + def setKey(self, key): + """Will set the crypting key for this object. Either 16 or 24 bytes long.""" + self.key_size = 24 # Use DES-EDE3 mode + if len(key) != self.key_size: + if len(key) == 16: # Use DES-EDE2 mode + self.key_size = 16 + else: + raise ValueError("Invalid triple DES key size. Key must be either 16 or 24 bytes long") + if self.getMode() == CBC: + if not self.getIV(): + # Use the first 8 bytes of the key + self._iv = key[:self.block_size] + if len(self.getIV()) != self.block_size: + raise ValueError("Invalid IV, must be 8 bytes in length") + self.__key1 = des(key[:8], self._mode, self._iv, + self._padding, self._padmode) + self.__key2 = des(key[8:16], self._mode, self._iv, + self._padding, self._padmode) + if self.key_size == 16: + self.__key3 = self.__key1 + else: + self.__key3 = des(key[16:], self._mode, self._iv, + self._padding, self._padmode) + _baseDes.setKey(self, key) + + # Override setter methods to work on all 3 keys. + + def setMode(self, mode): + """Sets the type of crypting mode, pyDes.ECB or pyDes.CBC""" + _baseDes.setMode(self, mode) + for key in (self.__key1, self.__key2, self.__key3): + key.setMode(mode) + + def setPadding(self, pad): + """setPadding() -> bytes of length 1. Padding character.""" + _baseDes.setPadding(self, pad) + for key in (self.__key1, self.__key2, self.__key3): + key.setPadding(pad) + + def setPadMode(self, mode): + """Sets the type of padding mode, pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" + _baseDes.setPadMode(self, mode) + for key in (self.__key1, self.__key2, self.__key3): + key.setPadMode(mode) + + def setIV(self, IV): + """Will set the Initial Value, used in conjunction with CBC mode""" + _baseDes.setIV(self, IV) + for key in (self.__key1, self.__key2, self.__key3): + key.setIV(IV) + + def encrypt(self, data, pad=None, padmode=None): + """encrypt(data, [pad], [padmode]) -> bytes + + data : bytes to be encrypted + pad : Optional argument for encryption padding. Must only be one byte + padmode : Optional argument for overriding the padding mode. + + The data must be a multiple of 8 bytes and will be encrypted + with the already specified key. Data does not have to be a + multiple of 8 bytes if the padding character is supplied, or + the padmode is set to PAD_PKCS5, as bytes will then added to + ensure the be padded data is a multiple of 8 bytes. + """ + ENCRYPT = des.ENCRYPT + DECRYPT = des.DECRYPT + data = self._guardAgainstUnicode(data) + if pad is not None: + pad = self._guardAgainstUnicode(pad) + # Pad the data accordingly. + data = self._padData(data, pad, padmode) + if self.getMode() == CBC: + self.__key1.setIV(self.getIV()) + self.__key2.setIV(self.getIV()) + self.__key3.setIV(self.getIV()) + i = 0 + result = [] + while i < len(data): + block = self.__key1.crypt(data[i:i+8], ENCRYPT) + block = self.__key2.crypt(block, DECRYPT) + block = self.__key3.crypt(block, ENCRYPT) + self.__key1.setIV(block) + self.__key2.setIV(block) + self.__key3.setIV(block) + result.append(block) + i += 8 + if _pythonMajorVersion < 3: + return ''.join(result) + else: + return bytes.fromhex('').join(result) + else: + data = self.__key1.crypt(data, ENCRYPT) + data = self.__key2.crypt(data, DECRYPT) + return self.__key3.crypt(data, ENCRYPT) + + def decrypt(self, data, pad=None, padmode=None): + """decrypt(data, [pad], [padmode]) -> bytes + + data : bytes to be encrypted + pad : Optional argument for decryption padding. Must only be one byte + padmode : Optional argument for overriding the padding mode. + + The data must be a multiple of 8 bytes and will be decrypted + with the already specified key. In PAD_NORMAL mode, if the + optional padding character is supplied, then the un-encrypted + data will have the padding characters removed from the end of + the bytes. This pad removal only occurs on the last 8 bytes of + the data (last data block). In PAD_PKCS5 mode, the special + padding end markers will be removed from the data after + decrypting, no pad character is required for PAD_PKCS5. + """ + ENCRYPT = des.ENCRYPT + DECRYPT = des.DECRYPT + data = self._guardAgainstUnicode(data) + if pad is not None: + pad = self._guardAgainstUnicode(pad) + if self.getMode() == CBC: + self.__key1.setIV(self.getIV()) + self.__key2.setIV(self.getIV()) + self.__key3.setIV(self.getIV()) + i = 0 + result = [] + while i < len(data): + iv = data[i:i+8] + block = self.__key3.crypt(iv, DECRYPT) + block = self.__key2.crypt(block, ENCRYPT) + block = self.__key1.crypt(block, DECRYPT) + self.__key1.setIV(iv) + self.__key2.setIV(iv) + self.__key3.setIV(iv) + result.append(block) + i += 8 + if _pythonMajorVersion < 3: + data = ''.join(result) + else: + data = bytes.fromhex('').join(result) + else: + data = self.__key3.crypt(data, DECRYPT) + data = self.__key2.crypt(data, ENCRYPT) + data = self.__key1.crypt(data, DECRYPT) + return self._unpadData(data, pad, padmode) diff --git a/plugin.video.alfa/lib/sambatools/smb/utils/sha256.py b/plugin.video.alfa/lib/sambatools/smb/utils/sha256.py index df28791c..a13d6bf3 100755 --- a/plugin.video.alfa/lib/sambatools/smb/utils/sha256.py +++ b/plugin.video.alfa/lib/sambatools/smb/utils/sha256.py @@ -1,112 +1,110 @@ -#!/usr/bin/python -__author__ = 'Thomas Dixon' -__license__ = 'MIT' - -import copy -import struct -import sys - -digest_size = 32 -blocksize = 1 - -def new(m=None): - return sha256(m) - -class sha256(object): - _k = (0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, - 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, - 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, - 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, - 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, - 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, - 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, - 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, - 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, - 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, - 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, - 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, - 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, - 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, - 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, - 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2) - _h = (0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, - 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19) - _output_size = 8 - - blocksize = 1 - block_size = 64 - digest_size = 32 - - def __init__(self, m=None): - self._buffer = '' - self._counter = 0 - - if m is not None: - if type(m) is not str: - raise TypeError, '%s() argument 1 must be string, not %s' % (self.__class__.__name__, type(m).__name__) - self.update(m) - - def _rotr(self, x, y): - return ((x >> y) | (x << (32-y))) & 0xFFFFFFFF - - def _sha256_process(self, c): - w = [0]*64 - w[0:15] = struct.unpack('!16L', c) - - for i in range(16, 64): - s0 = self._rotr(w[i-15], 7) ^ self._rotr(w[i-15], 18) ^ (w[i-15] >> 3) - s1 = self._rotr(w[i-2], 17) ^ self._rotr(w[i-2], 19) ^ (w[i-2] >> 10) - w[i] = (w[i-16] + s0 + w[i-7] + s1) & 0xFFFFFFFF - - a,b,c,d,e,f,g,h = self._h - - for i in range(64): - s0 = self._rotr(a, 2) ^ self._rotr(a, 13) ^ self._rotr(a, 22) - maj = (a & b) ^ (a & c) ^ (b & c) - t2 = s0 + maj - s1 = self._rotr(e, 6) ^ self._rotr(e, 11) ^ self._rotr(e, 25) - ch = (e & f) ^ ((~e) & g) - t1 = h + s1 + ch + self._k[i] + w[i] - - h = g - g = f - f = e - e = (d + t1) & 0xFFFFFFFF - d = c - c = b - b = a - a = (t1 + t2) & 0xFFFFFFFF - - self._h = [(x+y) & 0xFFFFFFFF for x,y in zip(self._h, [a,b,c,d,e,f,g,h])] - - def update(self, m): - if not m: - return - if type(m) is not str: - raise TypeError, '%s() argument 1 must be string, not %s' % (sys._getframe().f_code.co_name, type(m).__name__) - - self._buffer += m - self._counter += len(m) - - while len(self._buffer) >= 64: - self._sha256_process(self._buffer[:64]) - self._buffer = self._buffer[64:] - - def digest(self): - mdi = self._counter & 0x3F - length = struct.pack('!Q', self._counter<<3) - - if mdi < 56: - padlen = 55-mdi - else: - padlen = 119-mdi - - r = self.copy() - r.update('\x80'+('\x00'*padlen)+length) - return ''.join([struct.pack('!L', i) for i in r._h[:self._output_size]]) - - def hexdigest(self): - return self.digest().encode('hex') - - def copy(self): - return copy.deepcopy(self) +#!/usr/bin/python +__author__ = 'Thomas Dixon' +__license__ = 'MIT' + +import copy, struct, sys + +digest_size = 32 +blocksize = 1 + +def new(m=None): + return sha256(m) + +class sha256(object): + _k = (0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2) + _h = (0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19) + _output_size = 8 + + blocksize = 1 + block_size = 64 + digest_size = 32 + + def __init__(self, m=None): + self._buffer = '' + self._counter = 0 + + if m is not None: + if type(m) is not str: + raise TypeError, '%s() argument 1 must be string, not %s' % (self.__class__.__name__, type(m).__name__) + self.update(m) + + def _rotr(self, x, y): + return ((x >> y) | (x << (32-y))) & 0xFFFFFFFF + + def _sha256_process(self, c): + w = [0]*64 + w[0:15] = struct.unpack('!16L', c) + + for i in range(16, 64): + s0 = self._rotr(w[i-15], 7) ^ self._rotr(w[i-15], 18) ^ (w[i-15] >> 3) + s1 = self._rotr(w[i-2], 17) ^ self._rotr(w[i-2], 19) ^ (w[i-2] >> 10) + w[i] = (w[i-16] + s0 + w[i-7] + s1) & 0xFFFFFFFF + + a,b,c,d,e,f,g,h = self._h + + for i in range(64): + s0 = self._rotr(a, 2) ^ self._rotr(a, 13) ^ self._rotr(a, 22) + maj = (a & b) ^ (a & c) ^ (b & c) + t2 = s0 + maj + s1 = self._rotr(e, 6) ^ self._rotr(e, 11) ^ self._rotr(e, 25) + ch = (e & f) ^ ((~e) & g) + t1 = h + s1 + ch + self._k[i] + w[i] + + h = g + g = f + f = e + e = (d + t1) & 0xFFFFFFFF + d = c + c = b + b = a + a = (t1 + t2) & 0xFFFFFFFF + + self._h = [(x+y) & 0xFFFFFFFF for x,y in zip(self._h, [a,b,c,d,e,f,g,h])] + + def update(self, m): + if not m: + return + if type(m) is not str: + raise TypeError, '%s() argument 1 must be string, not %s' % (sys._getframe().f_code.co_name, type(m).__name__) + + self._buffer += m + self._counter += len(m) + + while len(self._buffer) >= 64: + self._sha256_process(self._buffer[:64]) + self._buffer = self._buffer[64:] + + def digest(self): + mdi = self._counter & 0x3F + length = struct.pack('!Q', self._counter<<3) + + if mdi < 56: + padlen = 55-mdi + else: + padlen = 119-mdi + + r = self.copy() + r.update('\x80'+('\x00'*padlen)+length) + return ''.join([struct.pack('!L', i) for i in r._h[:self._output_size]]) + + def hexdigest(self): + return self.digest().encode('hex') + + def copy(self): + return copy.deepcopy(self) diff --git a/plugin.video.alfa/platformcode/platformtools.py b/plugin.video.alfa/platformcode/platformtools.py index f046983b..aa322a5e 100644 --- a/plugin.video.alfa/platformcode/platformtools.py +++ b/plugin.video.alfa/platformcode/platformtools.py @@ -1106,6 +1106,8 @@ def play_torrent(item, xlistitem, mediaurl): url_stat = False torrents_path = '' videolibrary_path = config.get_videolibrary_path() #Calculamos el path absoluto a partir de la Videoteca + if videolibrary_path.lower().startswith("smb://"): #Si es una conexión SMB, usamos userdata local + videolibrary_path = config.get_data_path() #Calculamos el path absoluto a partir de Userdata if not filetools.exists(videolibrary_path): #Si no existe el path, pasamos al modo clásico videolibrary_path = False else: @@ -1139,7 +1141,7 @@ def play_torrent(item, xlistitem, mediaurl): folder = movies #películas else: folder = series #o series - item.url = filetools.join(videolibrary_path, folder, item.url) #dirección del .torrent local en la Videoteca + item.url = filetools.join(config.get_videolibrary_path(), folder, item.url) #dirección del .torrent local en la Videoteca if filetools.copy(item.url, torrents_path, silent=True): #se copia a la carpeta generíca para evitar problemas de encode item.url = torrents_path if "torrentin" in torrent_options[seleccion][1]: #Si es Torrentin, hay que añadir un prefijo diff --git a/plugin.video.alfa/platformcode/xbmc_videolibrary.py b/plugin.video.alfa/platformcode/xbmc_videolibrary.py index a5948d5b..73b6de2e 100755 --- a/plugin.video.alfa/platformcode/xbmc_videolibrary.py +++ b/plugin.video.alfa/platformcode/xbmc_videolibrary.py @@ -329,6 +329,7 @@ def mark_content_as_watched_on_alfa(path): from channels import videolibrary from core import videolibrarytools from core import scrapertools + from core import filetools import re """ marca toda la serie o película como vista o no vista en la Videoteca de Alfa basado en su estado en la Videoteca de Kodi @@ -375,7 +376,8 @@ def mark_content_as_watched_on_alfa(path): nfo_name = scrapertools.find_single_match(path2, '\]\/(.*?)$') #Construyo el nombre del .nfo path1 = path1.replace(nfo_name, '') #para la SQL solo necesito la carpeta path2 = path2.replace(nfo_name, '') #para la SQL solo necesito la carpeta - + path2 = filetools.remove_smb_credential(path2) #Si el archivo está en un servidor SMB, quiamos las credenciales + #Ejecutmos la sentencia SQL sql = 'select strFileName, playCount from %s where (strPath like "%s" or strPath like "%s")' % (contentType, path1, path2) nun_records = 0 From 2f623770353c3c75e28397344dc3868aa78b459a Mon Sep 17 00:00:00 2001 From: Kingbox <37674310+lopezvg@users.noreply.github.com> Date: Wed, 28 Nov 2018 17:07:21 +0100 Subject: [PATCH 2/3] SMB: mejoras y correcciones --- plugin.video.alfa/channels/videolibrary.py | 3 ++ plugin.video.alfa/core/videolibrarytools.py | 2 +- plugin.video.alfa/lib/generictools.py | 40 +++++++++++++------ .../platformcode/xbmc_videolibrary.py | 3 +- 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/plugin.video.alfa/channels/videolibrary.py b/plugin.video.alfa/channels/videolibrary.py index e2edd5c1..c455db19 100644 --- a/plugin.video.alfa/channels/videolibrary.py +++ b/plugin.video.alfa/channels/videolibrary.py @@ -52,6 +52,9 @@ def list_movies(item, silent=False): head_nfo, new_item = videolibrarytools.read_nfo(nfo_path) + if not new_item: #Si no ha leído bien el .nfo, pasamos a la siguiente + continue + if len(new_item.library_urls) > 1: multicanal = True else: diff --git a/plugin.video.alfa/core/videolibrarytools.py b/plugin.video.alfa/core/videolibrarytools.py index b4964de2..db85bc71 100644 --- a/plugin.video.alfa/core/videolibrarytools.py +++ b/plugin.video.alfa/core/videolibrarytools.py @@ -828,7 +828,7 @@ def caching_torrents(url, torrents_path=None, timeout=10, lookup=False, data_tor return torrents_path #Si hay un error, devolvemos el "path" vacío torrent_file = response.data - if not scrapertools.find_single_match(torrent_file, '^d\d+:\w+\d+:'): #No es un archivo .torrent (RAR, ZIP, HTML,..., vacío) + if not scrapertools.find_single_match(torrent_file, '^d\d+:.*?\d+:'): #No es un archivo .torrent (RAR, ZIP, HTML,..., vacío) logger.error('No es un archivo Torrent: ' + url) torrents_path = '' if data_torrent: diff --git a/plugin.video.alfa/lib/generictools.py b/plugin.video.alfa/lib/generictools.py index eb549794..6a1dcfde 100644 --- a/plugin.video.alfa/lib/generictools.py +++ b/plugin.video.alfa/lib/generictools.py @@ -1164,7 +1164,7 @@ def post_tmdb_findvideos(item, itemlist): title_gen = '[COLOR yellow]%s [/COLOR][ALT]: %s' % (item.category.capitalize(), title_gen) #elif (config.get_setting("quit_channel_name", "videolibrary") == 1 or item.channel == channel_py) and item.contentChannel == "videolibrary": else: - title_gen = '%s: %s' % (item.category.capitalize(), title_gen) + title_gen = '[COLOR white]%s: %s' % (item.category.capitalize(), title_gen) #Si intervención judicial, alerto!!! if item.intervencion: @@ -1768,7 +1768,6 @@ def redirect_clone_newpct1(item, head_nfo=None, it=None, path=False, overwrite=F item_back = item.clone() it_back = item.clone() ow_force_param = True - channel_enabled = False update_stat = 0 delete_stat = 0 canal_org_des_list = [] @@ -1883,13 +1882,15 @@ def redirect_clone_newpct1(item, head_nfo=None, it=None, path=False, overwrite=F """ try: - if item.url: #Viene de actualización de videoteca de series + if item.url and not channel_py in item.url and it.emergency_urls: #Viene de actualización de videoteca de series #Analizamos si el canal ya tiene las urls de emergencia: guardar o borrar if (config.get_setting("emergency_urls", item.channel) == 1 and (not item.emergency_urls or (item.emergency_urls and not item.emergency_urls.get(channel_alt, False)))) or (config.get_setting("emergency_urls", item.channel) == 2 and item.emergency_urls.get(channel_alt, False)) or config.get_setting("emergency_urls", item.channel) == 3 or emergency_urls_force: intervencion += ", ('1', '%s', '%s', '', '', '', '', '', '', '', '*', '%s', 'emerg')" % (channel_alt, channel_alt, config.get_setting("emergency_urls", item.channel)) elif it.library_urls: #Viene de "listar peliculas´" for canal_vid, url_vid in it.library_urls.items(): #Se recorre "item.library_urls" para buscar canales candidatos + if canal_vid == channel_py: #Si tiene Newcpt1 en canal, es un error + continue canal_vid_alt = "'%s'" % canal_vid if canal_vid_alt in fail_over_list: #Se busca si es un clone de newpct1 channel_bis = channel_py @@ -1903,7 +1904,7 @@ def redirect_clone_newpct1(item, head_nfo=None, it=None, path=False, overwrite=F logger.error(traceback.format_exc()) #Ahora tratamos las webs intervenidas, tranformamos la url, el nfo y borramos los archivos obsoletos de la serie - if channel not in intervencion and channel_py_alt not in intervencion and category not in intervencion and channel_alt != 'videolibrary': #lookup + if (channel not in intervencion and channel_py_alt not in intervencion and category not in intervencion and channel_alt != 'videolibrary') or not item.infoLabels: #lookup return (item, it, overwrite) #... el canal/clone está listado import ast @@ -1915,7 +1916,7 @@ def redirect_clone_newpct1(item, head_nfo=None, it=None, path=False, overwrite=F for activo, canal_org, canal_des, url_org, url_des, patron1, patron2, patron3, patron4, patron5, content_inc, content_exc, ow_force in intervencion_list: opt = '' #Es esta nuestra entrada? - if activo == '1' and (canal_org == channel_alt or canal_org == item.channel or channel_alt == 'videolibrary' or ow_force == 'del' or ow_force == 'emerg'): + if activo == '1' and (canal_org == channel_alt or canal_org == item.category.lower() or channel_alt == 'videolibrary' or ow_force == 'del' or ow_force == 'emerg'): if ow_force == 'del' or ow_force == 'emerg': #Si es un borrado de estructuras erroneas, hacemos un proceso aparte canal_des_def = canal_des #Si hay canal de sustitución para item.library_urls, lo usamos @@ -1981,10 +1982,15 @@ def redirect_clone_newpct1(item, head_nfo=None, it=None, path=False, overwrite=F continue if item.contentType in content_exc: #Está el contenido excluido? continue + channel_enabled = 0 + channel_enabled_alt = 1 if item.channel != channel_py: - channel_enabled = channeltools.is_enabled(channel_alt) #Verificamos que el canal esté inactivo - channel_enabled_alt = config.get_setting('enabled', channel_alt) - channel_enabled = channel_enabled * channel_enabled_alt #Si está inactivo en algún sitio, tomamos eso + try: + if channeltools.is_enabled(channel_alt): channel_enabled = 1 #Verificamos que el canal esté inactivo + if config.get_setting('enabled', channel_alt) == False: channel_enabled_alt = 0 + channel_enabled = channel_enabled * channel_enabled_alt #Si está inactivo en algún sitio, tomamos eso + except: + pass if channel_enabled == 1 and canal_org != canal_des: #Si el canal está activo, puede ser solo... continue #... una intervención que afecte solo a una región if ow_force == 'no' and it.library_urls: #Esta regla solo vale para findvideos... @@ -2012,6 +2018,9 @@ def redirect_clone_newpct1(item, head_nfo=None, it=None, path=False, overwrite=F url += scrapertools.find_single_match(url_total, patron5) #La aplicamos a url if url: url_total = url #Guardamos la suma de los resultados intermedios + if item.channel == channel_py or channel in fail_over_list: #Si es Newpct1... + if item.contentType == "tvshow": + url_total = re.sub(r'\/\d+\/?$', '', url_total) #parece que con el título encuentra la serie, normalmente... update_stat += 1 #Ya hemos actualizado algo canal_org_des_list += [(canal_org, canal_des, url_total, opt, ow_force)] #salvamos el resultado para su proceso @@ -2019,13 +2028,10 @@ def redirect_clone_newpct1(item, head_nfo=None, it=None, path=False, overwrite=F if (update_stat > 0 and path != False) or item.ow_force == '1': logger.error('** Lista de Actualizaciones a realizar: ' + str(canal_org_des_list)) for canal_org_def, canal_des_def, url_total, opt_def, ow_force_def in canal_org_des_list: #pasamos por todas las "parejas" cambiadas + url_total_def = url_total if ow_force_def != 'del' and ow_force_def != 'emerg': - url_total_def = url_total - if (item.channel == channel_py or channel in fail_over_list): #Si es Newpct1... - if item.contentType == "tvshow": - url_total_def = re.sub(r'\/\d+\/?$', '', url_total) #parece que con el título encuentra la serie, normalmente... if item.url: - item.url = url_total_def #Salvamos la url convertida + item.url = url_total #Salvamos la url convertida if item.library_urls: item.library_urls.pop(canal_org_def, None) item.library_urls.update({canal_des_def: url_total}) @@ -2048,10 +2054,18 @@ def redirect_clone_newpct1(item, head_nfo=None, it=None, path=False, overwrite=F #Verificamos que las webs de los canales estén activas antes de borrar los .json, para asegurar que se pueden regenerar i = 0 for canal_org_def, canal_des_def, url_total, opt_def, ow_force_def in canal_org_des_list: #pasamos por las "parejas" a borrar + if "magnet:" in url_total or type(url_total) != str: #Si la url es un Magnet, o es una lista, pasamos + i += 1 + continue try: response = httptools.downloadpage(url_total, only_headers=True) except: logger.error(traceback.format_exc()) + logger.error('Web ' + canal_des_def.upper() + ' ERROR. Regla no procesada: ' + str(canal_org_des_list[i])) + item = item_back.clone() #Restauro las imágenes inciales + it = it_back.clone() + item.torrent_caching_fail = True #Marcamos el proceso como fallido + return (item, it, False) if not response.sucess: logger.error('Web ' + canal_des_def.upper() + ' INACTIVA. Regla no procesada: ' + str(canal_org_des_list[i])) item = item_back.clone() #Restauro las imágenes inciales diff --git a/plugin.video.alfa/platformcode/xbmc_videolibrary.py b/plugin.video.alfa/platformcode/xbmc_videolibrary.py index 73b6de2e..7023fcd4 100755 --- a/plugin.video.alfa/platformcode/xbmc_videolibrary.py +++ b/plugin.video.alfa/platformcode/xbmc_videolibrary.py @@ -488,7 +488,8 @@ def update(folder_content=config.get_setting("folder_tvshows"), folder=""): else: update_path = filetools.join(videolibrarypath, folder_content, folder) + "/" - payload["params"] = {"directory": update_path} + if not update_path.startswith("smb://"): + payload["params"] = {"directory": update_path} while xbmc.getCondVisibility('Library.IsScanningVideo()'): xbmc.sleep(500) From 545c509742ce92af6b5eb31e9f9ff9425eb5bb22 Mon Sep 17 00:00:00 2001 From: Kingbox <37674310+lopezvg@users.noreply.github.com> Date: Wed, 28 Nov 2018 17:09:17 +0100 Subject: [PATCH 3/3] Cacheo de enlaces de emergencias: Adaptados: - DivxTotal - Pelismagnet - Subtorrents - Todopleiculas - Zonatorrent --- plugin.video.alfa/channels/divxtotal.py | 3 +- plugin.video.alfa/channels/newpct1.json | 2 +- plugin.video.alfa/channels/pelismagnet.json | 22 +++ plugin.video.alfa/channels/pelismagnet.py | 62 +++++-- plugin.video.alfa/channels/subtorrents.json | 22 +++ plugin.video.alfa/channels/subtorrents.py | 172 ++++++++++++------ plugin.video.alfa/channels/todopeliculas.json | 22 +++ plugin.video.alfa/channels/todopeliculas.py | 105 ++++++++--- plugin.video.alfa/channels/zonatorrent.json | 22 +++ plugin.video.alfa/channels/zonatorrent.py | 96 ++++++++-- 10 files changed, 407 insertions(+), 121 deletions(-) diff --git a/plugin.video.alfa/channels/divxtotal.py b/plugin.video.alfa/channels/divxtotal.py index e03a3c1b..2e80f2f3 100644 --- a/plugin.video.alfa/channels/divxtotal.py +++ b/plugin.video.alfa/channels/divxtotal.py @@ -577,7 +577,8 @@ def findvideos(item): return item #... y nos vamos #Llamamos al método para crear el título general del vídeo, con toda la información obtenida de TMDB - item, itemlist = generictools.post_tmdb_findvideos(item, itemlist) + if not item.videolibray_emergency_urls: + item, itemlist = generictools.post_tmdb_findvideos(item, itemlist) #Ahora tratamos los enlaces .torrent for scrapedurl in matches: #leemos los torrents con la diferentes calidades diff --git a/plugin.video.alfa/channels/newpct1.json b/plugin.video.alfa/channels/newpct1.json index 24a47145..ec77f3fa 100644 --- a/plugin.video.alfa/channels/newpct1.json +++ b/plugin.video.alfa/channels/newpct1.json @@ -100,7 +100,7 @@ "id": "intervenidos_channels_list", "type": "text", "label": "Lista de canales y clones de NewPct1 intervenidos y orden de sustitución de URLs", - "default": "('0', 'canal_org', 'canal_des', 'url_org', 'url_des', 'patron1', 'patron2', 'patron3', 'patron4', 'patron5', 'content_inc', 'content_exc', 'ow_force'), ('0', 'mejortorrent', 'mejortorrent1', 'http://www.mejortorrent.com/', 'https://mejortorrent1.com/', '(http.?:\/\/.*?\/)', 'http.?:\/\/.*?\/.*?-torrent.?-[^-]+-(?:[^-]+-)([^0-9]+-)', 'http.?:\/\/.*?\/.*?-torrent.?-[^-]+-(?:[^-]+-)[^0-9]+-\\d+-(Temporada-).html', 'http.?:\/\/.*?\/.*?-torrent.?-[^-]+-(?:[^-]+-)[^0-9]+-(\\d+)-', '', 'tvshow, season', '', 'force'), ('0', 'mejortorrent', 'mejortorrent1', 'http://www.mejortorrent.com/', 'https://mejortorrent1.com/', '(http.?:\/\/.*?\/)', 'http.?:\/\/.*?\/.*?-torrent.?-[^-]+-([^.]+).html', '', '', '', 'movie', '', 'force'), ('0', 'mejortorrent', 'mejortorrent', 'http://www.mejortorrent.com/', 'http://www.mejortorrent.org/', '', '', '', '', '', '*', '', 'force'), ('0', 'plusdede', 'megadede', 'https://www.plusdede.com', 'https://www.megadede.com', '', '', '', '', '', '*', '', 'auto')", + "default": "('0', 'canal_org', 'canal_des', 'url_org', 'url_des', 'patron1', 'patron2', 'patron3', 'patron4', 'patron5', 'content_inc', 'content_exc', 'ow_force'), ('0', 'mejortorrent', 'mejortorrent1', 'http://www.mejortorrent.com/', 'https://mejortorrent1.com/', '(http.?:\/\/.*?\/)', 'http.?:\/\/.*?\/.*?-torrent.?-[^-]+-(?:[^-]+-)([^0-9]+-)', 'http.?:\/\/.*?\/.*?-torrent.?-[^-]+-(?:[^-]+-)[^0-9]+-\\d+-(Temporada-).html', 'http.?:\/\/.*?\/.*?-torrent.?-[^-]+-(?:[^-]+-)[^0-9]+-(\\d+)-', '', 'tvshow, season', '', 'force'), ('0', 'mejortorrent', 'mejortorrent1', 'http://www.mejortorrent.com/', 'https://mejortorrent1.com/', '(http.?:\/\/.*?\/)', 'http.?:\/\/.*?\/.*?-torrent.?-[^-]+-([^.]+).html', '', '', '', 'movie', '', 'force'), ('0', 'mejortorrent', 'mejortorrent', 'http://www.mejortorrent.com/', 'http://www.mejortorrent.org/', '', '', '', '', '', '*', '', 'force'), ('1', 'plusdede', 'megadede', 'https://www.plusdede.com', 'https://www.megadede.com', '', '', '', '', '', '*', '', 'auto'), ('1', 'newpct1', 'descargas2020', 'http://www.newpct1.com', 'http://descargas2020.com', '', '', '', '', '', '*', '', 'force')", "enabled": true, "visible": false }, diff --git a/plugin.video.alfa/channels/pelismagnet.json b/plugin.video.alfa/channels/pelismagnet.json index b8c65189..43e361ea 100755 --- a/plugin.video.alfa/channels/pelismagnet.json +++ b/plugin.video.alfa/channels/pelismagnet.json @@ -45,6 +45,28 @@ "VOSE" ] }, + { + "id": "emergency_urls", + "type": "list", + "label": "Se quieren guardar Enlaces de Emergencia por si se cae la Web?", + "default": 1, + "enabled": true, + "visible": true, + "lvalues": [ + "No", + "Guardar", + "Borrar", + "Actualizar" + ] + }, + { + "id": "emergency_urls_torrents", + "type": "bool", + "label": "Se quieren guardar Torrents de Emergencia por si se cae la Web?", + "default": true, + "enabled": true, + "visible": "!eq(-1,'No')" + }, { "id": "include_in_newest_torrent", "type": "bool", diff --git a/plugin.video.alfa/channels/pelismagnet.py b/plugin.video.alfa/channels/pelismagnet.py index 1a658863..414e772a 100644 --- a/plugin.video.alfa/channels/pelismagnet.py +++ b/plugin.video.alfa/channels/pelismagnet.py @@ -355,7 +355,7 @@ def listado(item): title = re.sub(r'[\(|\[]\s+[\)|\]]', '', title) title = title.replace('()', '').replace('[]', '').strip().lower().title() - item_local.from_title = title.strip().lower().title() #Guardamos esta etiqueta para posible desambiguación de título + item_local.from_title = title.strip().lower().title() #Guardamos esta etiqueta para posible desambiguación de título #Salvamos el título según el tipo de contenido if item_local.contentType == "movie": @@ -387,8 +387,8 @@ def listado(item): title = '%s' % curr_page - if cnt_matches + 1 >= last_title: #Si hemos pintado ya todo lo de esta página... - cnt_matches = 0 #... la próxima pasada leeremos otra página + if cnt_matches + 1 >= last_title: #Si hemos pintado ya todo lo de esta página... + cnt_matches = 0 #... la próxima pasada leeremos otra página next_page_url = re.sub(r'page=(\d+)', r'page=' + str(int(re.search('\d+', next_page_url).group()) + 1), next_page_url) itemlist.append(Item(channel=item.channel, action="listado", title=">> Página siguiente " + title, url=next_page_url, extra=item.extra, extra2=item.extra2, last_page=str(last_page), curr_page=str(curr_page + 1), cnt_matches=str(cnt_matches))) @@ -399,10 +399,10 @@ def listado(item): def findvideos(item): logger.info() itemlist = [] - itemlist_t = [] #Itemlist total de enlaces - itemlist_f = [] #Itemlist de enlaces filtrados + itemlist_t = [] #Itemlist total de enlaces + itemlist_f = [] #Itemlist de enlaces filtrados if not item.language: - item.language = ['CAST'] #Castellano por defecto + item.language = ['CAST'] #Castellano por defecto matches = [] item.category = categoria @@ -412,22 +412,53 @@ def findvideos(item): #logger.debug(item) matches = item.url - if not matches: #error - logger.error("ERROR 02: FINDVIDEOS: No hay enlaces o ha cambiado la estructura de la Web: " + item) + if not matches: #error + logger.error("ERROR 02: FINDVIDEOS: No hay enlaces o ha cambiado la estructura de la Web: " + str(item)) itemlist.append(item.clone(action='', title=item.channel.capitalize() + ': ERROR 02: FINDVIDEOS: No hay enlaces o ha cambiado la estructura de la Web. Verificar en la Web esto último y reportar el error con el log')) - return itemlist #si no hay más datos, algo no funciona, pintamos lo que tenemos + + if item.emergency_urls and not item.videolibray_emergency_urls: #Hay urls de emergencia? + matches = item.emergency_urls[1] #Restauramos matches + item.armagedon = True #Marcamos la situación como catastrófica + else: + if item.videolibray_emergency_urls: #Si es llamado desde creación de Videoteca... + return item #Devolvemos el Item de la llamada + else: + return itemlist #si no hay más datos, algo no funciona, pintamos lo que tenemos #logger.debug(matches) + #Si es un lookup para cargar las urls de emergencia en la Videoteca... + if item.videolibray_emergency_urls: + item.emergency_urls = [] #Iniciamos emergency_urls + item.emergency_urls.append([]) #Reservamos el espacio para los .torrents locales + item.emergency_urls.append(matches) #Salvamnos matches... + #Llamamos al método para crear el título general del vídeo, con toda la información obtenida de TMDB - item, itemlist = generictools.post_tmdb_findvideos(item, itemlist) + if not item.videolibray_emergency_urls: + item, itemlist = generictools.post_tmdb_findvideos(item, itemlist) #Ahora tratamos los enlaces .torrent - for scrapedurl, quality in matches: #leemos los magnets con la diferentes calidades + for scrapedurl, quality in matches: #leemos los magnets con la diferentes calidades #Generamos una copia de Item para trabajar sobre ella item_local = item.clone() item_local.url = scrapedurl + if item.videolibray_emergency_urls: + item.emergency_urls[0].append(scrapedurl) #guardamos la url y pasamos a la siguiente + continue + if item.emergency_urls and not item.videolibray_emergency_urls: + item_local.torrent_alt = item.emergency_urls[0][0] #Guardamos la url del .Torrent ALTERNATIVA + if item.armagedon: + item_local.url = item.emergency_urls[0][0] #... ponemos la emergencia como primaria + del item.emergency_urls[0][0] #Una vez tratado lo limpiamos + + size = '' + if not item.armagedon: + size = generictools.get_torrent_size(item_local.url) #Buscamos el tamaño en el .torrent + if size: + quality += ' [%s]' % size + if item.armagedon: #Si es catastrófico, lo marcamos + quality = '[/COLOR][COLOR hotpink][E] [COLOR limegreen]%s' % quality #Añadimos la calidad y copiamos la duración item_local.quality = quality @@ -445,9 +476,9 @@ def findvideos(item): item_local.quality = re.sub(r'\s?\[COLOR \w+\]\s?\[\/COLOR\]', '', item_local.quality).strip() item_local.quality = item_local.quality.replace("--", "").replace("[]", "").replace("()", "").replace("(/)", "").replace("[/]", "").strip() - item_local.alive = "??" #Calidad del link sin verificar - item_local.action = "play" #Visualizar vídeo - item_local.server = "torrent" #Servidor Torrent + item_local.alive = "??" #Calidad del link sin verificar + item_local.action = "play" #Visualizar vídeo + item_local.server = "torrent" #Servidor Torrent itemlist_t.append(item_local.clone()) #Pintar pantalla, si no se filtran idiomas @@ -459,6 +490,9 @@ def findvideos(item): #logger.debug(item_local) + if item.videolibray_emergency_urls: #Si ya hemos guardado todas las urls... + return item #... nos vamos + if len(itemlist_f) > 0: #Si hay entradas filtradas... itemlist.extend(itemlist_f) #Pintamos pantalla filtrada else: diff --git a/plugin.video.alfa/channels/subtorrents.json b/plugin.video.alfa/channels/subtorrents.json index 4d5db68a..f360f732 100644 --- a/plugin.video.alfa/channels/subtorrents.json +++ b/plugin.video.alfa/channels/subtorrents.json @@ -44,6 +44,28 @@ "VOSE" ] }, + { + "id": "emergency_urls", + "type": "list", + "label": "Se quieren guardar Enlaces de Emergencia por si se cae la Web?", + "default": 1, + "enabled": true, + "visible": true, + "lvalues": [ + "No", + "Guardar", + "Borrar", + "Actualizar" + ] + }, + { + "id": "emergency_urls_torrents", + "type": "bool", + "label": "Se quieren guardar Torrents de Emergencia por si se cae la Web?", + "default": true, + "enabled": true, + "visible": "!eq(-1,'No')" + }, { "id": "timeout_downloadpage", "type": "list", diff --git a/plugin.video.alfa/channels/subtorrents.py b/plugin.video.alfa/channels/subtorrents.py index 07f3c693..60b691d9 100644 --- a/plugin.video.alfa/channels/subtorrents.py +++ b/plugin.video.alfa/channels/subtorrents.py @@ -372,6 +372,7 @@ def findvideos(item): if not item.language: item.language = ['CAST'] #Castellano por defecto matches = [] + subtitles = [] item.category = categoria #logger.debug(item) @@ -389,51 +390,74 @@ def findvideos(item): if not data: logger.error("ERROR 01: FINDVIDEOS: La Web no responde o la URL es erronea: " + item.url) itemlist.append(item.clone(action='', title=item.channel.capitalize() + ': ERROR 01: FINDVIDEOS:. La Web no responde o la URL es erronea. Si la Web está activa, reportar el error con el log')) - return itemlist #si no hay más datos, algo no funciona, pintamos lo que tenemos - - #Extraemos el thumb - if not item.thumbnail: - item.thumbnail = scrapertools.find_single_match(data, patron) #guardamos thumb si no existe - #Extraemos quality, audio, year, country, size, scrapedlanguage - patron = '<\/script><\/div>