diff --git a/src/protocol.py b/src/protocol.py index ec8fc9dd..cdd50dce 100644 --- a/src/protocol.py +++ b/src/protocol.py @@ -1,7 +1,8 @@ -# pylint: disable=too-many-boolean-expressions,too-many-return-statements,too-many-locals,too-many-statements """ Low-level protocol-related functions. """ +# pylint: disable=too-many-boolean-expressions,too-many-return-statements +# pylint: disable=too-many-locals,too-many-statements import base64 import hashlib @@ -9,7 +10,6 @@ import random import socket import sys import time -import traceback from binascii import hexlify from struct import pack, unpack, Struct @@ -24,10 +24,18 @@ from fallback import RIPEMD160Hash from helper_sql import sqlExecute from version import softwareVersion - # Service flags +#: This is a normal network node NODE_NETWORK = 1 +#: This node supports SSL/TLS in the current connect (python < 2.7.9 +#: only supports an SSL client, so in that case it would only have this +#: on when the connection is a client). NODE_SSL = 2 +# (Proposal) This node may do PoW on behalf of some its peers +# (PoW offloading/delegating), but it doesn't have to. Clients may have +# to meet additional requirements (e.g. TLS authentication) +# NODE_POW = 4 +#: Node supports dandelion NODE_DANDELION = 8 # Bitfield flags @@ -89,7 +97,8 @@ def isBitSetWithinBitfield(fourByteString, n): def encodeHost(host): """Encode a given host to be used in low-level socket operations""" if host.find('.onion') > -1: - return '\xfd\x87\xd8\x7e\xeb\x43' + base64.b32decode(host.split(".")[0], True) + return '\xfd\x87\xd8\x7e\xeb\x43' + base64.b32decode( + host.split(".")[0], True) elif host.find(':') == -1: return '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF' + \ socket.inet_aton(host) @@ -134,7 +143,10 @@ def network_group(host): def checkIPAddress(host, private=False): - """Returns hostStandardFormat if it is a valid IP address, otherwise returns False""" + """ + Returns hostStandardFormat if it is a valid IP address, + otherwise returns False + """ if host[0:12] == '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF': hostStandardFormat = socket.inet_ntop(socket.AF_INET, host[12:]) return checkIPv4Address(host[12:], hostStandardFormat, private) @@ -150,35 +162,46 @@ def checkIPAddress(host, private=False): except ValueError: return False if hostStandardFormat == "": - # This can happen on Windows systems which are not 64-bit compatible - # so let us drop the IPv6 address. + # This can happen on Windows systems which are + # not 64-bit compatible so let us drop the IPv6 address. return False return checkIPv6Address(host, hostStandardFormat, private) def checkIPv4Address(host, hostStandardFormat, private=False): - """Returns hostStandardFormat if it is an IPv4 address, otherwise returns False""" + """ + Returns hostStandardFormat if it is an IPv4 address, + otherwise returns False + """ if host[0] == '\x7F': # 127/8 if not private: - logger.debug('Ignoring IP address in loopback range: %s', hostStandardFormat) + logger.debug( + 'Ignoring IP address in loopback range: %s', + hostStandardFormat) return hostStandardFormat if private else False if host[0] == '\x0A': # 10/8 if not private: - logger.debug('Ignoring IP address in private range: %s', hostStandardFormat) + logger.debug( + 'Ignoring IP address in private range: %s', hostStandardFormat) return hostStandardFormat if private else False if host[0:2] == '\xC0\xA8': # 192.168/16 if not private: - logger.debug('Ignoring IP address in private range: %s', hostStandardFormat) + logger.debug( + 'Ignoring IP address in private range: %s', hostStandardFormat) return hostStandardFormat if private else False if host[0:2] >= '\xAC\x10' and host[0:2] < '\xAC\x20': # 172.16/12 if not private: - logger.debug('Ignoring IP address in private range: %s', hostStandardFormat) + logger.debug( + 'Ignoring IP address in private range: %s', hostStandardFormat) return hostStandardFormat if private else False return False if private else hostStandardFormat def checkIPv6Address(host, hostStandardFormat, private=False): - """Returns hostStandardFormat if it is an IPv6 address, otherwise returns False""" + """ + Returns hostStandardFormat if it is an IPv6 address, + otherwise returns False + """ if host == ('\x00' * 15) + '\x01': if not private: logger.debug('Ignoring loopback address: %s', hostStandardFormat) @@ -189,7 +212,8 @@ def checkIPv6Address(host, hostStandardFormat, private=False): return hostStandardFormat if private else False if (ord(host[0]) & 0xfe) == 0xfc: if not private: - logger.debug('Ignoring unique local address: %s', hostStandardFormat) + logger.debug( + 'Ignoring unique local address: %s', hostStandardFormat) return hostStandardFormat if private else False return False if private else hostStandardFormat @@ -210,31 +234,29 @@ def haveSSL(server=False): def checkSocksIP(host): """Predicate to check if we're using a SOCKS proxy""" + sockshostname = BMConfigParser().safeGet( + 'bitmessagesettings', 'sockshostname') try: - if state.socksIP is None or not state.socksIP: - state.socksIP = socket.gethostbyname(BMConfigParser().get("bitmessagesettings", "sockshostname")) - # uninitialised - except NameError: - state.socksIP = socket.gethostbyname(BMConfigParser().get("bitmessagesettings", "sockshostname")) - # resolving failure - except socket.gaierror: - state.socksIP = BMConfigParser().get("bitmessagesettings", "sockshostname") + if not state.socksIP: + state.socksIP = socket.gethostbyname(sockshostname) + except NameError: # uninitialised + state.socksIP = socket.gethostbyname(sockshostname) + except (TypeError, socket.gaierror): # None, resolving failure + state.socksIP = sockshostname return state.socksIP == host -def isProofOfWorkSufficient(data, - nonceTrialsPerByte=0, - payloadLengthExtraBytes=0, - recvTime=0): +def isProofOfWorkSufficient( + data, nonceTrialsPerByte=0, payloadLengthExtraBytes=0, recvTime=0): """ - Validate an object's Proof of Work using method described in: - https://bitmessage.org/wiki/Proof_of_work + Validate an object's Proof of Work using method described + `here `_ Arguments: - int nonceTrialsPerByte (default: from default.py) - int payloadLengthExtraBytes (default: from default.py) + int nonceTrialsPerByte (default: from `.defaults`) + int payloadLengthExtraBytes (default: from `.defaults`) float recvTime (optional) UNIX epoch time when object was - received from the network (default: current system time) + received from the network (default: current system time) Returns: True if PoW valid and sufficient, False in all other cases """ @@ -246,18 +268,20 @@ def isProofOfWorkSufficient(data, TTL = endOfLifeTime - (int(recvTime) if recvTime else int(time.time())) if TTL < 300: TTL = 300 - POW, = unpack('>Q', hashlib.sha512(hashlib.sha512(data[ - :8] + hashlib.sha512(data[8:]).digest()).digest()).digest()[0:8]) - return POW <= 2 ** 64 / (nonceTrialsPerByte * - (len(data) + payloadLengthExtraBytes + - ((TTL * (len(data) + payloadLengthExtraBytes)) / (2 ** 16)))) + POW, = unpack('>Q', hashlib.sha512(hashlib.sha512( + data[:8] + hashlib.sha512(data[8:]).digest() + ).digest()).digest()[0:8]) + return POW <= 2 ** 64 / ( + nonceTrialsPerByte * ( + len(data) + payloadLengthExtraBytes + + ((TTL * (len(data) + payloadLengthExtraBytes)) / (2 ** 16)))) # Packet creation def CreatePacket(command, payload=''): - """Construct and return a number of bytes from a payload""" + """Construct and return a packet""" payload_length = len(payload) checksum = hashlib.sha512(payload).digest()[0:4] @@ -267,8 +291,13 @@ def CreatePacket(command, payload=''): return bytes(b) -def assembleVersionMessage(remoteHost, remotePort, participatingStreams, server=False, nodeid=None): - """Construct the payload of a version message, return the resultng bytes of running CreatePacket() on it""" +def assembleVersionMessage( + remoteHost, remotePort, participatingStreams, server=False, nodeid=None +): + """ + Construct the payload of a version message, + return the resulting bytes of running `CreatePacket` on it + """ payload = '' payload += pack('>L', 3) # protocol version. # bitflags of the services I offer. @@ -280,9 +309,10 @@ def assembleVersionMessage(remoteHost, remotePort, participatingStreams, server= ) payload += pack('>q', int(time.time())) - payload += pack( - '>q', 1) # boolservices of remote connection; ignored by the remote host. - if checkSocksIP(remoteHost) and server: # prevent leaking of tor outbound IP + # boolservices of remote connection; ignored by the remote host. + payload += pack('>q', 1) + if checkSocksIP(remoteHost) and server: + # prevent leaking of tor outbound IP payload += encodeHost('127.0.0.1') payload += pack('>H', 8444) else: @@ -301,21 +331,25 @@ def assembleVersionMessage(remoteHost, remotePort, participatingStreams, server= (NODE_SSL if haveSSL(server) else 0) | (NODE_DANDELION if state.dandelion else 0) ) - # = 127.0.0.1. This will be ignored by the remote host. The actual remote connected IP will be used. - payload += '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF' + pack('>L', 2130706433) + # = 127.0.0.1. This will be ignored by the remote host. + # The actual remote connected IP will be used. + payload += '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF' + pack( + '>L', 2130706433) # we have a separate extPort and incoming over clearnet # or outgoing through clearnet extport = BMConfigParser().safeGetInt('bitmessagesettings', 'extport') if ( extport and ((server and not checkSocksIP(remoteHost)) or ( - BMConfigParser().get('bitmessagesettings', 'socksproxytype') == - 'none' and not server)) + BMConfigParser().get('bitmessagesettings', 'socksproxytype') + == 'none' and not server)) ): payload += pack('>H', extport) elif checkSocksIP(remoteHost) and server: # incoming connection over Tor - payload += pack('>H', BMConfigParser().getint('bitmessagesettings', 'onionport')) + payload += pack( + '>H', BMConfigParser().getint('bitmessagesettings', 'onionport')) else: # no extport and not incoming over Tor - payload += pack('>H', BMConfigParser().getint('bitmessagesettings', 'port')) + payload += pack( + '>H', BMConfigParser().getint('bitmessagesettings', 'port')) if nodeid is not None: payload += nodeid[0:8] @@ -339,7 +373,10 @@ def assembleVersionMessage(remoteHost, remotePort, participatingStreams, server= def assembleErrorMessage(fatal=0, banTime=0, inventoryVector='', errorText=''): - """Construct the payload of an error message, return the resultng bytes of running CreatePacket() on it""" + """ + Construct the payload of an error message, + return the resulting bytes of running `CreatePacket` on it + """ payload = encodeVarint(fatal) payload += encodeVarint(banTime) payload += encodeVarint(len(inventoryVector)) @@ -476,7 +513,7 @@ def decryptAndCheckPubkeyPayload(data, address): except Exception: logger.critical( 'Pubkey decryption was UNsuccessful because of' - ' an unhandled exception! This is definitely a bug! \n%s', - traceback.format_exc() + ' an unhandled exception! This is definitely a bug!', + exc_info=True ) return 'failed'