mirror of
https://github.com/HelloZeroNet/ZeroNet.git
synced 2023-12-14 04:33:03 +01:00
371 lines
15 KiB
Python
371 lines
15 KiB
Python
import logging
|
|
import time
|
|
import sys
|
|
import socket
|
|
from collections import defaultdict
|
|
|
|
import gevent
|
|
import msgpack
|
|
from gevent.server import StreamServer
|
|
from gevent.pool import Pool
|
|
|
|
import util
|
|
from util import helper
|
|
from Debug import Debug
|
|
from Connection import Connection
|
|
from Config import config
|
|
from Crypt import CryptConnection
|
|
from Crypt import CryptHash
|
|
from Tor import TorManager
|
|
from Site import SiteManager
|
|
|
|
|
|
class ConnectionServer(object):
|
|
def __init__(self, ip=None, port=None, request_handler=None):
|
|
if not ip:
|
|
if config.fileserver_ip_type == "ipv6":
|
|
ip = "::1"
|
|
else:
|
|
ip = "127.0.0.1"
|
|
port = 15441
|
|
self.ip = ip
|
|
self.port = port
|
|
self.last_connection_id = 1 # Connection id incrementer
|
|
self.log = logging.getLogger("ConnServer")
|
|
self.port_opened = None
|
|
self.peer_blacklist = SiteManager.peer_blacklist
|
|
|
|
self.tor_manager = TorManager(self.ip, self.port)
|
|
self.connections = [] # Connections
|
|
self.whitelist = config.ip_local # No flood protection on this ips
|
|
self.ip_incoming = {} # Incoming connections from ip in the last minute to avoid connection flood
|
|
self.broken_ssl_ips = {} # Peerids of broken ssl connections
|
|
self.ips = {} # Connection by ip
|
|
self.has_internet = True # Internet outage detection
|
|
self.supported_ip_types = ["ipv4"] # Outgoing ip_type support
|
|
if self.isIpv6Supported():
|
|
self.supported_ip_types.append("ipv6")
|
|
|
|
self.stream_server = None
|
|
self.running = False
|
|
|
|
self.stat_recv = defaultdict(lambda: defaultdict(int))
|
|
self.stat_sent = defaultdict(lambda: defaultdict(int))
|
|
self.bytes_recv = 0
|
|
self.bytes_sent = 0
|
|
self.num_recv = 0
|
|
self.num_sent = 0
|
|
|
|
self.num_incoming = 0
|
|
self.num_outgoing = 0
|
|
self.had_external_incoming = False
|
|
|
|
self.timecorrection = 0.0
|
|
|
|
# Bittorrent style peerid
|
|
self.peer_id = "-UT3530-%s" % CryptHash.random(12, "base64")
|
|
|
|
# Check msgpack version
|
|
if msgpack.version[0] == 0 and msgpack.version[1] < 4:
|
|
self.log.error(
|
|
"Error: Unsupported msgpack version: %s (<0.4.0), please run `sudo apt-get install python-pip; sudo pip install msgpack --upgrade`" %
|
|
str(msgpack.version)
|
|
)
|
|
sys.exit(0)
|
|
|
|
if request_handler:
|
|
self.handleRequest = request_handler
|
|
|
|
def start(self, check_connections=True):
|
|
self.running = True
|
|
if check_connections:
|
|
self.thread_checker = gevent.spawn(self.checkConnections)
|
|
CryptConnection.manager.loadCerts()
|
|
if config.tor != "disable":
|
|
self.tor_manager.start()
|
|
if not self.port:
|
|
self.log.info("No port found, not binding")
|
|
return False
|
|
|
|
self.log.debug("Binding to: %s:%s, (msgpack: %s), supported crypt: %s, supported ip types: %s" % (
|
|
self.ip, self.port, ".".join(map(str, msgpack.version)),
|
|
CryptConnection.manager.crypt_supported, self.supported_ip_types
|
|
))
|
|
try:
|
|
self.pool = Pool(500) # do not accept more than 500 connections
|
|
if helper.getIpType(self.ip) == "ipv6":
|
|
sock_address = (self.ip, self.port, 0, 0)
|
|
else:
|
|
sock_address = (self.ip, self.port)
|
|
|
|
self.stream_server = StreamServer(
|
|
sock_address, self.handleIncomingConnection, spawn=self.pool, backlog=100
|
|
)
|
|
except Exception, err:
|
|
self.log.info("StreamServer bind error: %s" % err)
|
|
|
|
def isIpv6Supported(self):
|
|
if helper.getIpType(self.ip) == "ipv6":
|
|
return True
|
|
|
|
# Test if we can connect to ipv6 address
|
|
ipv6_testip = "2001:19f0:6c01:e76:5400:1ff:fed6:3eca"
|
|
try:
|
|
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
|
sock.connect((ipv6_testip, 80))
|
|
local_ipv6 = sock.getsockname()[0]
|
|
if local_ipv6 == "::1":
|
|
return False
|
|
else:
|
|
return True
|
|
except Exception as err:
|
|
return False
|
|
|
|
def listen(self):
|
|
try:
|
|
self.stream_server.serve_forever()
|
|
except Exception, err:
|
|
self.log.info("StreamServer listen error: %s" % err)
|
|
|
|
def stop(self):
|
|
self.log.debug("Stopping")
|
|
self.running = False
|
|
if self.stream_server:
|
|
self.stream_server.stop()
|
|
|
|
def handleIncomingConnection(self, sock, addr):
|
|
ip, port = addr[0:2]
|
|
ip = ip.lower()
|
|
if ip.startswith("::ffff:"): # IPv6 to IPv4 mapping
|
|
ip = ip.replace("::ffff:", "", 1)
|
|
self.num_incoming += 1
|
|
|
|
if not self.had_external_incoming and not helper.isPrivateIp(ip):
|
|
self.had_external_incoming = True
|
|
|
|
# Connection flood protection
|
|
if ip in self.ip_incoming and ip not in self.whitelist:
|
|
self.ip_incoming[ip] += 1
|
|
if self.ip_incoming[ip] > 6: # Allow 6 in 1 minute from same ip
|
|
self.log.debug("Connection flood detected from %s" % ip)
|
|
time.sleep(30)
|
|
sock.close()
|
|
return False
|
|
else:
|
|
self.ip_incoming[ip] = 1
|
|
|
|
connection = Connection(self, ip, port, sock)
|
|
self.connections.append(connection)
|
|
if ip not in config.ip_local:
|
|
self.ips[ip] = connection
|
|
connection.handleIncomingConnection(sock)
|
|
|
|
def handleMessage(self, *args, **kwargs):
|
|
pass
|
|
|
|
def getConnection(self, ip=None, port=None, peer_id=None, create=True, site=None, is_tracker_connection=False):
|
|
if (ip.endswith(".onion") or self.port_opened == False) and self.tor_manager.start_onions and site: # Site-unique connection for Tor
|
|
if ip.endswith(".onion"):
|
|
site_onion = self.tor_manager.getOnion(site.address)
|
|
else:
|
|
site_onion = self.tor_manager.getOnion("global")
|
|
key = ip + site_onion
|
|
else:
|
|
key = ip
|
|
|
|
# Find connection by ip
|
|
if key in self.ips:
|
|
connection = self.ips[key]
|
|
if not peer_id or connection.handshake.get("peer_id") == peer_id: # Filter by peer_id
|
|
if not connection.connected and create:
|
|
succ = connection.event_connected.get() # Wait for connection
|
|
if not succ:
|
|
raise Exception("Connection event return error")
|
|
return connection
|
|
|
|
# Recover from connection pool
|
|
for connection in self.connections:
|
|
if connection.ip == ip:
|
|
if peer_id and connection.handshake.get("peer_id") != peer_id: # Does not match
|
|
continue
|
|
if ip.endswith(".onion") and self.tor_manager.start_onions and ip.replace(".onion", "") != connection.target_onion:
|
|
# For different site
|
|
continue
|
|
if not connection.connected and create:
|
|
succ = connection.event_connected.get() # Wait for connection
|
|
if not succ:
|
|
raise Exception("Connection event return error")
|
|
return connection
|
|
|
|
# No connection found
|
|
if create: # Allow to create new connection if not found
|
|
if port == 0:
|
|
raise Exception("This peer is not connectable")
|
|
|
|
if (ip, port) in self.peer_blacklist and not is_tracker_connection:
|
|
raise Exception("This peer is blacklisted")
|
|
|
|
try:
|
|
if (ip.endswith(".onion") or self.port_opened == False) and self.tor_manager.start_onions and site: # Lock connection to site
|
|
connection = Connection(self, ip, port, target_onion=site_onion, is_tracker_connection=is_tracker_connection)
|
|
else:
|
|
connection = Connection(self, ip, port, is_tracker_connection=is_tracker_connection)
|
|
self.num_outgoing += 1
|
|
self.ips[key] = connection
|
|
self.connections.append(connection)
|
|
connection.log("Connecting... (site: %s)" % site)
|
|
succ = connection.connect()
|
|
if not succ:
|
|
connection.close("Connection event return error")
|
|
raise Exception("Connection event return error")
|
|
|
|
except Exception, err:
|
|
connection.close("%s Connect error: %s" % (ip, Debug.formatException(err)))
|
|
raise err
|
|
|
|
if len(self.connections) > config.global_connected_limit:
|
|
gevent.spawn(self.checkMaxConnections)
|
|
|
|
return connection
|
|
else:
|
|
return None
|
|
|
|
def removeConnection(self, connection):
|
|
# Delete if same as in registry
|
|
if self.ips.get(connection.ip) == connection:
|
|
del self.ips[connection.ip]
|
|
# Site locked connection
|
|
if connection.target_onion:
|
|
if self.ips.get(connection.ip + connection.target_onion) == connection:
|
|
del self.ips[connection.ip + connection.target_onion]
|
|
# Cert pinned connection
|
|
if connection.cert_pin and self.ips.get(connection.ip + "#" + connection.cert_pin) == connection:
|
|
del self.ips[connection.ip + "#" + connection.cert_pin]
|
|
|
|
if connection in self.connections:
|
|
self.connections.remove(connection)
|
|
|
|
def checkConnections(self):
|
|
run_i = 0
|
|
while self.running:
|
|
run_i += 1
|
|
time.sleep(15) # Check every minute
|
|
self.ip_incoming = {} # Reset connected ips counter
|
|
self.broken_ssl_ips = {} # Reset broken ssl peerids count
|
|
last_message_time = 0
|
|
s = time.time()
|
|
for connection in self.connections[:]: # Make a copy
|
|
if connection.ip.endswith(".onion") or config.tor == "always":
|
|
timeout_multipler = 2
|
|
else:
|
|
timeout_multipler = 1
|
|
|
|
idle = time.time() - max(connection.last_recv_time, connection.start_time, connection.last_message_time)
|
|
if connection.last_message_time > last_message_time and not connection.is_private_ip:
|
|
# Message from local IPs does not means internet connection
|
|
last_message_time = connection.last_message_time
|
|
|
|
if connection.unpacker and idle > 30:
|
|
# Delete the unpacker if not needed
|
|
del connection.unpacker
|
|
connection.unpacker = None
|
|
|
|
elif connection.last_cmd_sent == "announce" and idle > 20: # Bootstrapper connection close after 20 sec
|
|
connection.close("[Cleanup] Tracker connection, idle: %.3fs" % idle)
|
|
|
|
if idle > 60 * 60:
|
|
# Wake up after 1h
|
|
connection.close("[Cleanup] After wakeup, idle: %.3fs" % idle)
|
|
|
|
elif idle > 20 * 60 and connection.last_send_time < time.time() - 10:
|
|
# Idle more than 20 min and we have not sent request in last 10 sec
|
|
if not connection.ping():
|
|
connection.close("[Cleanup] Ping timeout")
|
|
|
|
elif idle > 10 * timeout_multipler and connection.incomplete_buff_recv > 0:
|
|
# Incomplete data with more than 10 sec idle
|
|
connection.close("[Cleanup] Connection buff stalled")
|
|
|
|
elif idle > 10 * timeout_multipler and connection.protocol == "?": # No connection after 10 sec
|
|
connection.close(
|
|
"[Cleanup] Connect timeout: %.3fs" % idle
|
|
)
|
|
|
|
elif idle > 10 * timeout_multipler and connection.waiting_requests and time.time() - connection.last_send_time > 10 * timeout_multipler:
|
|
# Sent command and no response in 10 sec
|
|
connection.close(
|
|
"[Cleanup] Command %s timeout: %.3fs" % (connection.last_cmd_sent, time.time() - connection.last_send_time)
|
|
)
|
|
|
|
elif idle < 60 and connection.bad_actions > 40:
|
|
connection.close(
|
|
"[Cleanup] Too many bad actions: %s" % connection.bad_actions
|
|
)
|
|
|
|
elif idle > 5 * 60 and connection.sites == 0:
|
|
connection.close(
|
|
"[Cleanup] No site for connection"
|
|
)
|
|
|
|
elif run_i % 90 == 0:
|
|
# Reset bad action counter every 30 min
|
|
connection.bad_actions = 0
|
|
|
|
# Internet outage detection
|
|
if time.time() - last_message_time > max(60, 60 * 10 / max(1, float(len(self.connections)) / 50)):
|
|
# Offline: Last message more than 60-600sec depending on connection number
|
|
if self.has_internet and last_message_time:
|
|
self.has_internet = False
|
|
self.onInternetOffline()
|
|
else:
|
|
# Online
|
|
if not self.has_internet:
|
|
self.has_internet = True
|
|
self.onInternetOnline()
|
|
|
|
self.timecorrection = self.getTimecorrection()
|
|
|
|
if time.time() - s > 0.01:
|
|
self.log.debug("Connection cleanup in %.3fs" % (time.time() - s))
|
|
self.log.debug("Checkconnections ended")
|
|
|
|
@util.Noparallel(blocking=False)
|
|
def checkMaxConnections(self):
|
|
if len(self.connections) < config.global_connected_limit:
|
|
return 0
|
|
|
|
s = time.time()
|
|
num_connected_before = len(self.connections)
|
|
self.connections.sort(key=lambda connection: connection.sites)
|
|
num_closed = 0
|
|
for connection in self.connections:
|
|
idle = time.time() - max(connection.last_recv_time, connection.start_time, connection.last_message_time)
|
|
if idle > 60:
|
|
connection.close("Connection limit reached")
|
|
num_closed += 1
|
|
if num_closed > config.global_connected_limit * 0.1:
|
|
break
|
|
|
|
self.log.debug("Closed %s connections of %s after reached limit %s in %.3fs" % (
|
|
num_closed, num_connected_before, config.global_connected_limit, time.time() - s
|
|
))
|
|
return num_closed
|
|
|
|
def onInternetOnline(self):
|
|
self.log.info("Internet online")
|
|
|
|
def onInternetOffline(self):
|
|
self.had_external_incoming = False
|
|
self.log.info("Internet offline")
|
|
|
|
def getTimecorrection(self):
|
|
corrections = sorted([
|
|
connection.handshake.get("time") - connection.handshake_time + connection.last_ping_delay
|
|
for connection in self.connections
|
|
if connection.handshake.get("time") and connection.last_ping_delay
|
|
])
|
|
if len(corrections) < 6:
|
|
return 0.0
|
|
mid = len(corrections) / 2 - 1
|
|
median = (corrections[mid - 1] + corrections[mid] + corrections[mid + 1]) / 3
|
|
return median
|