electrum-personal-server

Maximally lightweight electrum server for a single user
git clone https://git.parazyd.org/electrum-personal-server
Log | Files | Refs | README

commit 2b955afb0b9b3051cc4ccb6643774e26252ccaf6
parent f9decf97364060c06fb12f0ddad6d6f42f1271e7
Author: chris-belcher <belcher@riseup.net>
Date:   Fri, 21 Jun 2019 22:37:59 +0100

Merge pull request #124 from andrewtoth/tor-broadcast

Broadcast transactions through tor
Diffstat:
Mconfig.ini_sample | 5+++++
Melectrumpersonalserver/server/__init__.py | 8++++++++
Melectrumpersonalserver/server/common.py | 23+++++++++++++++++++++--
Aelectrumpersonalserver/server/peertopeer.py | 356+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrumpersonalserver/server/socks.py | 410+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 800 insertions(+), 2 deletions(-)

diff --git a/config.ini_sample b/config.ini_sample @@ -73,10 +73,15 @@ disable_mempool_fee_histogram = false # Parameter for broadcasting unconfirmed transactions # Options are: # * own-node (broadcast using the connected full node) +# * tor (broadcast to random nodes over tor) # * system <cmd> %s (save transaction to file, and invoke system command # with file path as parameter %s) broadcast_method = own-node +# For tor broadcasting (broadcast_method = tor) configure the tor proxy host and port below +tor_host = localhost +tor_port = 9050 + [watch-only-addresses] #Add individual addresses to this section, for example paper wallets diff --git a/electrumpersonalserver/server/__init__.py b/electrumpersonalserver/server/__init__.py @@ -28,3 +28,11 @@ from electrumpersonalserver.server.deterministicwallet import ( parse_electrum_master_public_key, DeterministicWallet, ) +from electrumpersonalserver.server.socks import ( + socksocket, + setdefaultproxy, + PROXY_TYPE_SOCKS5, +) +from electrumpersonalserver.server.peertopeer import ( + tor_broadcast_tx, +) diff --git a/electrumpersonalserver/server/common.py b/electrumpersonalserver/server/common.py @@ -5,12 +5,14 @@ import traceback, sys, platform from ipaddress import ip_network, ip_address import logging import tempfile +import threading from electrumpersonalserver.server.jsonrpc import JsonRpc, JsonRpcError import electrumpersonalserver.server.hashes as hashes import electrumpersonalserver.server.merkleproof as merkleproof import electrumpersonalserver.server.deterministicwallet as deterministicwallet import electrumpersonalserver.server.transactionmonitor as transactionmonitor +import electrumpersonalserver.server.peertopeer as p2p SERVER_VERSION_NUMBER = "0.1.7" @@ -93,7 +95,7 @@ def on_disconnect(txmonitor): txmonitor.unsubscribe_all_addresses() def handle_query(sock, line, rpc, txmonitor, disable_mempool_fee_histogram, - broadcast_method): + broadcast_method, tor_hostport=None): logger = logging.getLogger('ELECTRUMPERSONALSERVER') logger.debug("=> " + line) try: @@ -244,6 +246,20 @@ def handle_query(sock, line, rpc, txmonitor, disable_mempool_fee_histogram, rpc.call("sendrawtransaction", [txhex]) except JsonRpcError as e: pass + elif broadcast_method == "tor": + # send through tor + TOR_CONNECTIONS = 8 + network = "mainnet" + chaininfo = rpc.call("getblockchaininfo", []) + if chaininfo["chain"] == "test": + network = "testnet" + elif chaininfo["chain"] == "regtest": + network = "regtest" + for i in range(TOR_CONNECTIONS): + t = threading.Thread(target=p2p.tor_broadcast_tx, + args=(txhex, tor_hostport, network, rpc,)) + t.start() + time.sleep(0.1) elif broadcast_method.startswith("system "): with tempfile.NamedTemporaryFile() as fd: system_line = broadcast_method[7:].replace("%s", fd.name) @@ -451,6 +467,9 @@ def run_electrum_server(rpc, txmonitor, config): "disable_mempool_fee_histogram", fallback=False) broadcast_method = config.get("electrum-server", "broadcast_method", fallback="own-node") + tor_host = config.get("electrum-server", "tor_host", fallback="localhost") + tor_port = int(config.get("electrum-server", "tor_port", fallback="9050")) + tor_hostport = (tor_host, tor_port) server_sock = create_server_socket(hostport) server_sock.settimeout(poll_interval_listening) @@ -491,7 +510,7 @@ def run_electrum_server(rpc, txmonitor, config): lb = recv_buffer.find(b'\n') handle_query(sock, line.decode("utf-8"), rpc, txmonitor, disable_mempool_fee_histogram, - broadcast_method) + broadcast_method, tor_hostport) except socket.timeout: on_heartbeat_connected(sock, rpc, txmonitor) except (IOError, EOFError) as e: diff --git a/electrumpersonalserver/server/peertopeer.py b/electrumpersonalserver/server/peertopeer.py @@ -0,0 +1,356 @@ +#! /usr/bin/env python + +import socket, time +import base64 +from struct import pack, unpack +from datetime import datetime + +import electrumpersonalserver.bitcoin as btc +from electrumpersonalserver.server.socks import socksocket, setdefaultproxy, PROXY_TYPE_SOCKS5 +from electrumpersonalserver.server.jsonrpc import JsonRpcError + +import logging as log + +PROTOCOL_VERSION = 70012 +DEFAULT_USER_AGENT = '/Satoshi:0.18.0/' +NODE_WITNESS = (1 << 3) + +# protocol versions above this also send a relay boolean +RELAY_TX_VERSION = 70001 + +# length of bitcoin p2p packets +HEADER_LENGTH = 24 + +# if no message has been seen for this many seconds, send a ping +KEEPALIVE_INTERVAL = 2 * 60 + +# close connection if keep alive ping isnt responded to in this many seconds +KEEPALIVE_TIMEOUT = 20 * 60 + +def ip_to_hex(ip_str): + # ipv4 only for now + return socket.inet_pton(socket.AF_INET, ip_str) + + +def create_net_addr(hexip, port): # doesnt contain time as in bitcoin wiki + services = 0 + hex = bytes(10) + b'\xFF\xFF' + hexip + return pack('<Q16s', services, hex) + pack('>H', port) + + +def create_var_str(s): + return btc.num_to_var_int(len(s)) + s.encode() + + +def read_int(ptr, payload, n, littleendian=True): + data = payload[ptr[0] : ptr[0]+n] + if littleendian: + data = data[::-1] + ret = btc.decode(data, 256) + ptr[0] += n + return ret + + +def read_var_int(ptr, payload): + val = payload[ptr[0]] + ptr[0] += 1 + if val < 253: + return val + return read_int(ptr, payload, 2**(val - 252)) + + +def read_var_str(ptr, payload): + l = read_var_int(ptr, payload) + ret = payload[ptr[0]: ptr[0] + l] + ptr[0] += l + return ret + + +def ip_hex_to_str(ip_hex): + # https://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses + # https://www.cypherpunk.at/onioncat_trac/wiki/OnionCat + if ip_hex[:14] == '\x00'*10 + '\xff'*2: + # ipv4 mapped ipv6 addr + return socket.inet_ntoa(ip_hex[12:]) + elif ip_hex[:6] == '\xfd\x87\xd8\x7e\xeb\x43': + return base64.b32encode(ip_hex[6:]).lower() + '.onion' + else: + return socket.inet_ntop(socket.AF_INET6, ip_hex) + + +class P2PMessageHandler(object): + def __init__(self): + self.last_message = datetime.now() + self.waiting_for_keepalive = False + self.log = (log if log else + log.getLogger('ELECTRUMPERSONALSERVER')) + + def check_keepalive(self, p2p): + if self.waiting_for_keepalive: + if (datetime.now() - self.last_message).total_seconds() < KEEPALIVE_TIMEOUT: + return + log.info('keepalive timed out, closing') + p2p.sock.close() + else: + if (datetime.now() - self.last_message).total_seconds() < KEEPALIVE_INTERVAL: + return + log.debug('sending keepalive to peer') + self.waiting_for_keepalive = True + p2p.sock.sendall(p2p.create_message('ping', '\x00'*8)) + + def handle_message(self, p2p, command, length, payload): + self.last_message = datetime.now() + self.waiting_for_keepalive = False + ptr = [0] + if command == b'version': + version = read_int(ptr, payload, 4) + services = read_int(ptr, payload, 8) + timestamp = read_int(ptr, payload, 8) + addr_recv_services = read_int(ptr, payload, 8) + addr_recv_ip = payload[ptr[0] : ptr[0]+16] + ptr[0] += 16 + addr_recv_port = read_int(ptr, payload, 2, False) + addr_trans_services = read_int(ptr, payload, 8) + addr_trans_ip = payload[ptr[0] : ptr[0]+16] + ptr[0] += 16 + addr_trans_port = read_int(ptr, payload, 2, False) + ptr[0] += 8 # skip over nonce + user_agent = read_var_str(ptr, payload) + start_height = read_int(ptr, payload, 4) + if version > RELAY_TX_VERSION: + relay = read_int(ptr, payload, 1) != 0 + else: # must check this node accepts unconfirmed transactions for the broadcast + relay = True + log.debug(('peer version message: version=%d services=0x%x' + + ' timestamp=%s user_agent=%s start_height=%d relay=%i' + + ' them=%s:%d us=%s:%d') % (version, + services, str(datetime.fromtimestamp(timestamp)), + user_agent, start_height, relay, ip_hex_to_str(addr_trans_ip) + , addr_trans_port, ip_hex_to_str(addr_recv_ip), addr_recv_port)) + p2p.sock.sendall(p2p.create_message('verack', b'')) + self.on_recv_version(p2p, version, services, timestamp, + addr_recv_services, addr_recv_ip, addr_trans_services, + addr_trans_ip, addr_trans_port, user_agent, start_height, + relay) + elif command == b'verack': + self.on_connected(p2p) + elif command == b'ping': + p2p.sock.sendall(p2p.create_message('pong', payload)) + + # optional override these in a subclass + + def on_recv_version(self, p2p, version, services, timestamp, + addr_recv_services, addr_recv_ip, addr_trans_services, + addr_trans_ip, addr_trans_port, user_agent, start_height, relay): + pass + + def on_connected(self, p2p): + pass + + def on_heartbeat(self, p2p): + pass + + +class P2PProtocol(object): + def __init__(self, p2p_message_handler, remote_hostport, + network, user_agent=DEFAULT_USER_AGENT, + socks5_hostport=("localhost", 9050), connect_timeout=30, heartbeat_interval=15): + self.log = (log if log else + log.getLogger('ELECTRUMPERSONALSERVER')) + self.p2p_message_handler = p2p_message_handler + self.network = network + self.user_agent = user_agent + self.socks5_hostport = socks5_hostport + self.heartbeat_interval = heartbeat_interval + self.connect_timeout = connect_timeout + if self.network == "testnet": + self.magic = 0x0709110b + elif self.network == "regtest": + self.magic = 0xdab5bffa + else: + self.magic = 0xd9b4bef9 + + self.closed = False + + self.remote_hostport = remote_hostport + + def run(self): + services = NODE_WITNESS + st = int(time.time()) + nonce = 0 + start_height = 0 + + netaddr = create_net_addr(ip_to_hex('0.0.0.0'), 0) + version_message = (pack('<iQQ', PROTOCOL_VERSION, services, st) + + netaddr + + netaddr + + pack('<Q', nonce) + + create_var_str(self.user_agent) + + pack('<I', start_height) + + b'\x01') + data = self.create_message('version', version_message) + while True: + log.info('connecting to bitcoin peer (magic=' + hex(self.magic) + + ') at ' + str(self.remote_hostport) + ' with proxy ' + + str(self.socks5_hostport)) + if self.socks5_hostport is None: + self.sock = socket.socket(socket.AF_INET, + socket.SOCK_STREAM) + else: + setdefaultproxy(PROXY_TYPE_SOCKS5, self.socks5_hostport[0], + self.socks5_hostport[1], True) + self.sock = socksocket() + self.sock.settimeout(self.connect_timeout) + self.sock.connect(self.remote_hostport) + self.sock.sendall(data) + break + + log.info('connected') + self.sock.settimeout(self.heartbeat_interval) + self.closed = False + try: + recv_buffer = b'' + payload_length = -1 # -1 means waiting for header + command = None + checksum = None + while not self.closed: + try: + recv_data = self.sock.recv(4096) + if not recv_data or len(recv_data) == 0: + raise EOFError() + recv_buffer += recv_data + # this is O(N^2) scaling in time, another way would be to store in a list + # and combine at the end with "".join() + # but this isnt really timing critical so didnt optimize it + + data_remaining = True + while data_remaining and not self.closed: + if payload_length == -1 and len(recv_buffer) >= HEADER_LENGTH: + net_magic, command, payload_length, checksum = unpack('<I12sI4s', recv_buffer[:HEADER_LENGTH]) + recv_buffer = recv_buffer[HEADER_LENGTH:] + if net_magic != self.magic: + log.debug('wrong MAGIC: ' + hex(net_magic)) + self.sock.close() + break + command = command.strip(b'\0') + else: + + if payload_length >= 0 and len(recv_buffer) >= payload_length: + payload = recv_buffer[:payload_length] + recv_buffer = recv_buffer[payload_length:] + if btc.bin_dbl_sha256(payload)[:4] == checksum: + self.p2p_message_handler.handle_message(self, command, + payload_length, payload) + else: + log.debug('wrong checksum, dropping message, cmd=' + command + ' payloadlen=' + str(payload_length)) + payload_length = -1 + data_remaining = True + else: + data_remaining = False + except socket.timeout: + self.p2p_message_handler.check_keepalive(self) + self.p2p_message_handler.on_heartbeat(self) + except EOFError as e: + self.closed = True + except IOError as e: + import traceback + log.debug("logging traceback from %s: \n" % + traceback.format_exc()) + self.closed = True + finally: + try: + self.sock.close() + except Exception as _: + pass + + + def close(self): + self.closed = True + + def create_message(self, command, payload): + return (pack("<I12sI", self.magic, command.encode(), len(payload)) + + btc.bin_dbl_sha256(payload)[:4] + payload) + +class P2PBroadcastTx(P2PMessageHandler): + def __init__(self, txhex): + P2PMessageHandler.__init__(self) + self.txhex = bytes.fromhex(txhex) + self.txid = btc.bin_txhash(self.txhex) + log.debug('broadcasting txid ' + str(self.txid)) + self.uploaded_tx = False + self.time_marker = datetime.now() + self.connected = False + + def on_recv_version(self, p2p, version, services, timestamp, + addr_recv_services, addr_recv_ip, addr_trans_services, + addr_trans_ip, addr_trans_port, user_agent, start_height, relay): + if not relay: + log.debug('peer not accepting unconfirmed txes, trying another') + # this happens if the other node is using blockonly=1 + p2p.close() + if not services & NODE_WITNESS: + log.debug('peer not accepting witness data, trying another') + p2p.close() + + def on_connected(self, p2p): + log.debug('sending inv') + MSG = 1 #msg_tx + inv_payload = pack('<BI', 1, MSG) + self.txid + p2p.sock.sendall(p2p.create_message('inv', inv_payload)) + self.time_marker = datetime.now() + self.uploaded_tx = False + self.connected = True + + def on_heartbeat(self, p2p): + log.debug('broadcaster heartbeat') + VERACK_TIMEOUT = 40 + GETDATA_TIMEOUT = 60 + if not self.connected: + if (datetime.now() - self.time_marker).total_seconds() < VERACK_TIMEOUT: + return + log.debug('timed out of waiting for verack') + else: + if (datetime.now() - self.time_marker).total_seconds() < GETDATA_TIMEOUT: + return + log.debug('timed out of waiting for getdata, node already has tx') + self.uploaded_tx = True + p2p.close() + + def handle_message(self, p2p, command, length, payload): + P2PMessageHandler.handle_message(self, p2p, command, length, payload) + ptr = [0] + if command == b'getdata': + count = read_var_int(ptr, payload) + for _ in range(count): + ptr[0] += 4 + hash_id = payload[ptr[0] : ptr[0] + 32] + ptr[0] += 32 + log.debug('hashid=' + hash_id.hex()) + if hash_id == self.txid: + log.debug('uploading tx') + p2p.sock.sendall(p2p.create_message('tx', self.txhex)) + self.uploaded_tx = True + p2p.close() + +def tor_broadcast_tx(txhex, tor_hostport, network, rpc): + ATTEMPTS = 8 # how many times to search for a node that accepts txes + try: + node_addrs = rpc.call("getnodeaddresses", [ATTEMPTS]) + except JsonRpcError: + log.error("BitcoinCore v0.18.0 must be running to broadcast through Tor") + return False + + node_addrs = [addr for addr in node_addrs if addr["services"] & NODE_WITNESS] + for i in range(len(node_addrs)): + remote_hostport = (node_addrs[i]["address"], node_addrs[i]["port"]) + p2p_msg_handler = P2PBroadcastTx(txhex) + p2p = P2PProtocol(p2p_msg_handler, remote_hostport=remote_hostport, + network=network, socks5_hostport=tor_hostport, heartbeat_interval=20) + try: + p2p.run() + except IOError: + continue + log.debug('uploaded={}'.format(p2p_msg_handler.uploaded_tx)) + if p2p_msg_handler.uploaded_tx: + return True + return False # never find a node that accepted unconfirms diff --git a/electrumpersonalserver/server/socks.py b/electrumpersonalserver/server/socks.py @@ -0,0 +1,410 @@ +"""SocksiPy - Python SOCKS module. +Version 1.00 + +Copyright 2006 Dan-Haim. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of Dan Haim nor the names of his contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. + + +This module provides a standard socket-like interface for Python +for tunneling connections through SOCKS proxies. + +""" + +import socket +import struct +import random + +PROXY_TYPE_SOCKS4 = 1 +PROXY_TYPE_SOCKS5 = 2 +PROXY_TYPE_HTTP = 3 + +_defaultproxy = None +_orgsocket = socket.socket + + +class ProxyError(IOError): + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + +class GeneralProxyError(ProxyError): + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + +class Socks5AuthError(ProxyError): + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + +class Socks5Error(ProxyError): + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + +class Socks4Error(ProxyError): + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + +class HTTPError(ProxyError): + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + +_generalerrors = ("success", "invalid data", "not connected", "not available", + "bad proxy type", "bad input") + +_socks5errors = ("succeeded", "general SOCKS server failure", + "connection not allowed by ruleset", "Network unreachable", + "Host unreachable", "Connection refused", "TTL expired", + "Command not supported", "Address type not supported", + "Unknown error") + +_socks5autherrors = ("succeeded", "authentication is required", + "all offered authentication methods were rejected", + "unknown username or invalid password", "unknown error") + +_socks4errors = ( + "request granted", "request rejected or failed", + "request rejected because SOCKS server cannot connect to identd on the client", + "request rejected because the client program and identd report different user-ids", + "unknown error") + + +def setdefaultproxy(proxytype=None, + addr=None, + port=None, + rdns=True, + username=str(random.randrange(10000000, 99999999)), + password=str(random.randrange(10000000, 99999999))): + """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + Sets a default proxy which all further socksocket objects will use, + unless explicitly changed. + """ + global _defaultproxy + _defaultproxy = (proxytype, addr, port, rdns, username, password) + + +class socksocket(socket.socket): + """socksocket([family[, type[, proto]]]) -> socket object + + Open a SOCKS enabled socket. The parameters are the same as + those of the standard socket init. In order for SOCKS to work, + you must specify family=AF_INET, type=SOCK_STREAM and proto=0. + """ + + def __init__(self, + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=0, + _sock=None): + _orgsocket.__init__(self, family, type, proto, _sock) + if _defaultproxy is not None: + self.__proxy = _defaultproxy + else: + self.__proxy = (None, None, None, None, None, None) + self.__proxysockname = None + self.__proxypeername = None + + def __recvall(self, bytes): + """__recvall(bytes) -> data + Receive EXACTLY the number of bytes requested from the socket. + Blocks until the required number of bytes have been received. + """ + data = b'' + while len(data) < bytes: + data = data + self.recv(bytes - len(data)) + return data + + def setproxy(self, + proxytype=None, + addr=None, + port=None, + rdns=True, + username=None, + password=None): + """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + Sets the proxy to be used. + proxytype - The type of the proxy to be used. Three types + are supported: PROXY_TYPE_SOCKS4 (including socks4a), + PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP + addr - The address of the server (IP or DNS). + port - The port of the server. Defaults to 1080 for SOCKS + servers and 8080 for HTTP proxy servers. + rdns - Should DNS queries be preformed on the remote side + (rather than the local side). The default is True. + Note: This has no effect with SOCKS4 servers. + username - Username to authenticate with to the server. + The default is no authentication. + password - Password to authenticate with to the server. + Only relevant when username is also provided. + """ + self.__proxy = (proxytype, addr, port, rdns, username, password) + + def __negotiatesocks5(self, destaddr, destport): + """__negotiatesocks5(self,destaddr,destport) + Negotiates a connection through a SOCKS5 server. + """ + # First we'll send the authentication packages we support. + if (self.__proxy[4] is not None) and (self.__proxy[5] is not None): + # The username/password details were supplied to the + # setproxy method so we support the USERNAME/PASSWORD + # authentication (in addition to the standard none). + self.sendall(b'\x05\x02\x00\x02') + else: + # No username/password were entered, therefore we + # only support connections with no authentication. + self.sendall(b'\x05\x01\x00') + # We'll receive the server's response to determine which + # method was selected + chosenauth = self.__recvall(2) + if chosenauth[0:1] != b"\x05": + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + # Check the chosen authentication method + if chosenauth[1:2] == b"\x00": + # No authentication is required + pass + elif chosenauth[1:2] == b"\x02": + # Okay, we need to perform a basic username/password + # authentication. + self.sendall(b'\x01' + bytes([len(self.__proxy[4])]) + self.__proxy[4].encode() + + bytes([len(self.__proxy[5])]) + self.__proxy[5].encode()) + authstat = self.__recvall(2) + if authstat[0:1] != b"\x01": + # Bad response + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if authstat[1:2] != b"\x00": + # Authentication failed + self.close() + raise Socks5AuthError((3, _socks5autherrors[3])) + # Authentication succeeded + else: + # Reaching here is always bad + self.close() + if chosenauth[1:2] == b"\xFF": + raise Socks5AuthError((2, _socks5autherrors[2])) + else: + raise GeneralProxyError((1, _generalerrors[1])) + # Now we can request the actual connection + req = b"\x05\x01\x00" + # If the given destination address is an IP address, we'll + # use the IPv4 address request even if remote resolving was specified. + try: + ipaddr = socket.inet_aton(destaddr) + req = req + b"\x01" + ipaddr + except socket.error: + # Well it's not an IP number, so it's probably a DNS name. + if self.__proxy[3]: + # Resolve remotely + ipaddr = None + req = req + b"\x03" + bytes([len(destaddr)]) + destaddr.encode() + else: + # Resolve locally + ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) + req = req + b"\x01" + ipaddr + req += struct.pack(">H", destport) + self.sendall(req) + # Get the response + resp = self.__recvall(4) + if resp[0:1] != b"\x05": + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + elif resp[1:2] != b"\x00": + # Connection failed + self.close() + raise Socks5Error(_socks5errors[min(9, ord(resp[1:2]))]) + # Get the bound address/port + elif resp[3:4] == b"\x01": + boundaddr = self.__recvall(4) + elif resp[3:4] == b"\x03": + resp = resp + self.recv(1) + boundaddr = self.__recvall(resp[4:5]) + else: + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + boundport = struct.unpack(">H", self.__recvall(2))[0] + self.__proxysockname = (boundaddr, boundport) + if ipaddr is not None: + self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + else: + self.__proxypeername = (destaddr, destport) + + def getproxysockname(self): + """getsockname() -> address info + Returns the bound IP address and port number at the proxy. + """ + return self.__proxysockname + + def getproxypeername(self): + """getproxypeername() -> address info + Returns the IP and port number of the proxy. + """ + return _orgsocket.getpeername(self) + + def getpeername(self): + """getpeername() -> address info + Returns the IP address and port number of the destination + machine (note: getproxypeername returns the proxy) + """ + return self.__proxypeername + + def __negotiatesocks4(self, destaddr, destport): + """__negotiatesocks4(self,destaddr,destport) + Negotiates a connection through a SOCKS4 server. + """ + # Check if the destination address provided is an IP address + rmtrslv = False + try: + ipaddr = socket.inet_aton(destaddr) + except socket.error: + # It's a DNS name. Check where it should be resolved. + if self.__proxy[3]: + ipaddr = b"\x00\x00\x00\x01" + rmtrslv = True + else: + ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) + # Construct the request packet + req = b"\x04\x01" + struct.pack(">H", destport) + ipaddr + # The username parameter is considered userid for SOCKS4 + if self.__proxy[4] is not None: + req = req + self.__proxy[4].encode() + req += b"\x00" + # DNS name if remote resolving is required + # NOTE: This is actually an extension to the SOCKS4 protocol + # called SOCKS4A and may not be supported in all cases. + if rmtrslv: + req = req + destaddr + b"\x00" + self.sendall(req) + # Get the response from the server + resp = self.__recvall(8) + if resp[0:1] != b"\x00": + # Bad data + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if resp[1:2] != b"\x5A": + # Server returned an error + self.close() + if ord(resp[1:2]) in (91, 92, 93): + self.close() + raise Socks4Error((ord(resp[1]), _socks4errors[ord(resp[1:2]) - + 90])) + else: + raise Socks4Error((94, _socks4errors[4])) + # Get the bound address/port + self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack( + ">H", resp[2:4])[0]) + if rmtrslv is not None: + self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + else: + self.__proxypeername = (destaddr, destport) + + def __negotiatehttp(self, destaddr, destport): + """__negotiatehttp(self,destaddr,destport) + Negotiates a connection through an HTTP server. + """ + # If we need to resolve locally, we do this now + if not self.__proxy[3]: + addr = socket.gethostbyname(destaddr) + else: + addr = destaddr + self.sendall("CONNECT " + addr + ":" + str(destport) + " HTTP/1.1\r\n" + + "Host: " + destaddr + "\r\n\r\n") + # We read the response until we get the string "\r\n\r\n" + resp = self.recv(1) + while resp.find(b"\r\n\r\n") == -1: + resp = resp + self.recv(1) + # We just need the first line to check if the connection + # was successful + statusline = resp.splitlines()[0].split(b" ", 2) + if statusline[0] not in ("HTTP/1.0", "HTTP/1.1"): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + try: + statuscode = int(statusline[1]) + except ValueError: + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if statuscode != 200: + self.close() + raise HTTPError((statuscode, statusline[2])) + self.__proxysockname = ("0.0.0.0", 0) + self.__proxypeername = (addr, destport) + + def connect(self, destpair): + """connect(self,despair) + Connects to the specified destination through a proxy. + destpar - A tuple of the IP/DNS address and the port number. + (identical to socket's connect). + To select the proxy server use setproxy(). + """ + # Do a minimal input check first + if (type(destpair) in + (list, tuple) == False) or (len(destpair) < 2) or ( + type(destpair[0]) != str) or (type(destpair[1]) != int): + raise GeneralProxyError((5, _generalerrors[5])) + if self.__proxy[0] == PROXY_TYPE_SOCKS5: + if self.__proxy[2] is not None: + portnum = self.__proxy[2] + else: + portnum = 1080 + _orgsocket.connect(self, (self.__proxy[1], portnum)) + self.__negotiatesocks5(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_SOCKS4: + if self.__proxy[2] is not None: + portnum = self.__proxy[2] + else: + portnum = 1080 + _orgsocket.connect(self, (self.__proxy[1], portnum)) + self.__negotiatesocks4(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_HTTP: + if self.__proxy[2] is not None: + portnum = self.__proxy[2] + else: + portnum = 8080 + _orgsocket.connect(self, (self.__proxy[1], portnum)) + self.__negotiatehttp(destpair[0], destpair[1]) + elif self.__proxy[0] is None: + _orgsocket.connect(self, (destpair[0], destpair[1])) + else: + raise GeneralProxyError((4, _generalerrors[4]))