Add I2P Support
Merge https://github.com/HelloZeroNet/ZeroNet/pull/602 Signed-off-by: Marek Küthe <m.k@mk16.de>
This commit is contained in:
parent
290025958f
commit
d731fe9f91
17 changed files with 447 additions and 27 deletions
|
@ -22,7 +22,9 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - https:/
|
|||
* Password-less [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
|
||||
based authorization: Your account is protected by the same cryptography as your Bitcoin wallet
|
||||
* Built-in SQL server with P2P data synchronization: Allows easier site development and faster page load times
|
||||
* Anonymity: Full Tor network support with .onion hidden services instead of IPv4 addresses
|
||||
* Anonymity:
|
||||
* Full Tor network support with .onion hidden services instead of IPv4 addresses
|
||||
* Full I2P network support with I2P Destinations instead of IPv4 addresses
|
||||
* TLS encrypted connections
|
||||
* Automatic uPnP port opening
|
||||
* Plugin for multiuser (openproxy) support
|
||||
|
@ -132,7 +134,7 @@ https://zeronet.ipfsscan.io/
|
|||
|
||||
* File transactions are not compressed
|
||||
* No private sites
|
||||
|
||||
* ~~No more anonymous than Bittorrent~~ (built-in full Tor and I2P support added)
|
||||
|
||||
## How can I create a ZeroNet site?
|
||||
|
||||
|
|
2
Vagrantfile
vendored
2
Vagrantfile
vendored
|
@ -40,6 +40,6 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
|||
config.vm.provision "shell",
|
||||
inline: "sudo apt-get install msgpack-python python-gevent python-pip python-dev -y"
|
||||
config.vm.provision "shell",
|
||||
inline: "sudo pip install msgpack --upgrade"
|
||||
inline: "sudo pip install -r requirements.txt --upgrade"
|
||||
|
||||
end
|
||||
|
|
|
@ -11,3 +11,4 @@ websocket_client
|
|||
gevent-ws
|
||||
coincurve
|
||||
maxminddb
|
||||
i2p.socket
|
|
@ -88,6 +88,8 @@ class Config(object):
|
|||
"http://t.publictracker.xyz:6969/announce",
|
||||
"https://tracker.lilithraws.cf:443/announce",
|
||||
"https://tracker.babico.name.tr:443/announce",
|
||||
"http://opentracker.dg2.i2p/announce",
|
||||
"http://opentracker.skank.i2p/announce"
|
||||
]
|
||||
# Platform specific
|
||||
if sys.platform.startswith("win"):
|
||||
|
@ -311,6 +313,9 @@ class Config(object):
|
|||
self.parser.add_argument('--tor_use_bridges', help='Use obfuscated bridge relays to avoid Tor block', action='store_true')
|
||||
self.parser.add_argument('--tor_hs_limit', help='Maximum number of hidden services in Tor always mode', metavar='limit', type=int, default=10)
|
||||
self.parser.add_argument('--tor_hs_port', help='Hidden service port in Tor always mode', metavar='limit', type=int, default=15441)
|
||||
|
||||
self.parser.add_argument('--i2p', help='enable: Use only for I2P peers, always: Use I2P for every connection', choices=["disable", "enable", "always"], default='enable')
|
||||
self.parser.add_argument('--i2p_sam', help='I2P SAM API address', metavar='ip:port', default='127.0.0.1:7656')
|
||||
|
||||
self.parser.add_argument('--version', action='version', version='ZeroNet %s r%s' % (self.version, self.rev))
|
||||
self.parser.add_argument('--end', help='Stop multi value argument parsing', action='store_true')
|
||||
|
|
|
@ -133,6 +133,10 @@ class Connection(object):
|
|||
self.sock = socks.socksocket()
|
||||
proxy_ip, proxy_port = config.trackers_proxy.split(":")
|
||||
self.sock.set_proxy(socks.PROXY_TYPE_SOCKS5, proxy_ip, int(proxy_port))
|
||||
elif self.ip.endswith(".i2p"):
|
||||
if not self.server.i2p_manager or not self.server.i2p_manager.enabled:
|
||||
raise Exception("Can't connect to I2P addresses, no SAM API present")
|
||||
self.sock = self.server.i2p_manager.createSocket(self.ip, self.port)
|
||||
else:
|
||||
self.sock = self.createSocket()
|
||||
|
||||
|
@ -344,22 +348,27 @@ class Connection(object):
|
|||
# My handshake info
|
||||
def getHandshakeInfo(self):
|
||||
# No TLS for onion connections
|
||||
if self.ip_type == "onion":
|
||||
if self.ip_type == "onion" or self.ip_type == "i2p":
|
||||
crypt_supported = []
|
||||
elif self.ip in self.server.broken_ssl_ips:
|
||||
crypt_supported = []
|
||||
else:
|
||||
crypt_supported = CryptConnection.manager.crypt_supported
|
||||
# No peer id for onion connections
|
||||
if self.ip_type == "onion" or self.ip in config.ip_local:
|
||||
if self.ip_type == "onion" or self.ip_type == "i2p" or self.ip in config.ip_local:
|
||||
peer_id = ""
|
||||
else:
|
||||
peer_id = self.server.peer_id
|
||||
# Setup peer lock from requested onion address
|
||||
if self.handshake and self.handshake.get("target_ip", "").endswith(".onion") and self.server.tor_manager.start_onions:
|
||||
self.target_onion = self.handshake.get("target_ip").replace(".onion", "") # My onion address
|
||||
if not self.server.tor_manager.site_onions.values():
|
||||
self.server.log.warning("Unknown target onion address: %s" % self.target_onion)
|
||||
if self.handshake:
|
||||
if self.handshake.get("target_ip", "").endswith(".onion") and self.server.tor_manager.start_onions:
|
||||
self.target_onion = self.handshake.get("target_ip").replace(".onion", "") # My onion address
|
||||
if not self.server.tor_manager.site_onions.values():
|
||||
self.server.log.warning("Unknown target onion address: %s" % self.target_onion)
|
||||
elif self.handshake.get("target_ip", "").endswith(".i2p") and self.server.i2p_manager.start_dests:
|
||||
self.target_dest = self.handshake.get("target_ip").replace(".i2p", "") # My I2P Destination
|
||||
if not dest_sites.get(target_dest):
|
||||
self.server.log.error("Unknown target I2P Destination: %s" % target_dest)
|
||||
|
||||
handshake = {
|
||||
"version": config.version,
|
||||
|
@ -378,6 +387,10 @@ class Connection(object):
|
|||
handshake["onion"] = self.target_onion
|
||||
elif self.ip_type == "onion":
|
||||
handshake["onion"] = self.server.tor_manager.getOnion("global")
|
||||
elif self.target_dest:
|
||||
handshake["i2p"] = self.target_dest
|
||||
elif self.ip_type == "i2p":
|
||||
handshake["i2p"] = self.server.i2p_manager.getDest("global").base64()
|
||||
|
||||
if self.is_tracker_connection:
|
||||
handshake["tracker_connection"] = True
|
||||
|
@ -397,7 +410,7 @@ class Connection(object):
|
|||
return False
|
||||
|
||||
self.handshake = handshake
|
||||
if handshake.get("port_opened", None) is False and "onion" not in handshake and not self.is_private_ip: # Not connectable
|
||||
if handshake.get("port_opened", None) is False and "onion" not in handshake and "i2p" not in handshake and not self.is_private_ip: # Not connectable
|
||||
self.port = 0
|
||||
else:
|
||||
self.port = int(handshake["fileserver_port"]) # Set peer fileserver port
|
||||
|
@ -416,7 +429,7 @@ class Connection(object):
|
|||
if type(handshake["crypt_supported"][0]) is bytes:
|
||||
handshake["crypt_supported"] = [item.decode() for item in handshake["crypt_supported"]] # Backward compatibility
|
||||
|
||||
if self.ip_type == "onion" or self.ip in config.ip_local:
|
||||
if self.ip_type == "onion" or self.ip_type == "i2p" or self.ip in config.ip_local:
|
||||
crypt = None
|
||||
elif handshake.get("crypt"): # Recommended crypt by server
|
||||
crypt = handshake["crypt"]
|
||||
|
@ -426,13 +439,21 @@ class Connection(object):
|
|||
if crypt:
|
||||
self.crypt = crypt
|
||||
|
||||
if self.type == "in" and handshake.get("onion") and not self.ip_type == "onion": # Set incoming connection's onion address
|
||||
if self.server.ips.get(self.ip) == self:
|
||||
del self.server.ips[self.ip]
|
||||
self.setIp(handshake["onion"] + ".onion")
|
||||
self.log("Changing ip to %s" % self.ip)
|
||||
self.server.ips[self.ip] = self
|
||||
self.updateName()
|
||||
if self.type == "in":
|
||||
if handshake.get("onion") and not self.ip_type == "onion": # Set incoming connection's onion address
|
||||
if self.server.ips.get(self.ip) == self:
|
||||
del self.server.ips[self.ip]
|
||||
self.setIp(handshake["onion"] + ".onion")
|
||||
self.log("Changing ip to %s" % self.ip)
|
||||
self.server.ips[self.ip] = self
|
||||
self.updateName()
|
||||
if handshake.get("i2p") and not self.ip_type == "i2p": # Set incoming connection's I2P Destination
|
||||
if self.server.ips.get(self.ip) == self:
|
||||
del self.server.ips[self.ip]
|
||||
self.setIp(handshake["i2p"] + ".i2p")
|
||||
self.log("Changing ip to %s" % self.ip)
|
||||
self.server.ips[self.ip] = self
|
||||
self.updateName()
|
||||
|
||||
self.event_connected.set(True) # Mark handshake as done
|
||||
self.event_connected = None
|
||||
|
|
|
@ -17,6 +17,7 @@ from Config import config
|
|||
from Crypt import CryptConnection
|
||||
from Crypt import CryptHash
|
||||
from Tor import TorManager
|
||||
from I2P import I2PManager
|
||||
from Site import SiteManager
|
||||
|
||||
|
||||
|
@ -38,6 +39,10 @@ class ConnectionServer(object):
|
|||
self.peer_blacklist = SiteManager.peer_blacklist
|
||||
|
||||
self.tor_manager = TorManager(self.ip, self.port)
|
||||
if config.i2p != "disabled":
|
||||
self.i2p_manager = I2PManager(self.handleIncomingConnection)
|
||||
else:
|
||||
self.i2p_manager = None
|
||||
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
|
||||
|
@ -171,10 +176,13 @@ class ConnectionServer(object):
|
|||
|
||||
def getConnection(self, ip=None, port=None, peer_id=None, create=True, site=None, is_tracker_connection=False):
|
||||
ip_type = helper.getIpType(ip)
|
||||
has_per_site_onion = (ip.endswith(".onion") or self.port_opened.get(ip_type, None) == False) and self.tor_manager.start_onions and site
|
||||
if has_per_site_onion: # Site-unique connection for Tor
|
||||
has_per_site_onion = (((ip.endswith(".onion") or self.port_opened.get("onion", None) == False) and self.tor_manager.start_onions) or \
|
||||
((ip.endswith(".i2p") or self.port_opened.get("i2p", None) == False) and self.i2p_manager.start_dests)) and site
|
||||
if has_per_site_onion: # Site-unique connection for Tor or I2P
|
||||
if ip.endswith(".onion"):
|
||||
site_onion = self.tor_manager.getOnion(site.address)
|
||||
elif ip.endswith(".i2p"):
|
||||
site_onion = self.i2p_manager.getDest(site.address)
|
||||
else:
|
||||
site_onion = self.tor_manager.getOnion("global")
|
||||
key = ip + site_onion
|
||||
|
@ -196,7 +204,8 @@ class ConnectionServer(object):
|
|||
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:
|
||||
if (ip.endswith(".onion") and self.tor_manager.start_onions and ip.replace(".onion", "") != connection.target_onion) or \
|
||||
(ip.endswith(".i2p") and self.i2p_manager.start_dests and ip.replace(".i2p", "") != connection.target_dest):
|
||||
# For different site
|
||||
continue
|
||||
if not connection.connected and create:
|
||||
|
|
|
@ -321,6 +321,13 @@ class FileRequest(object):
|
|||
if site.addPeer(*address, source="pex"):
|
||||
added += 1
|
||||
|
||||
# Add sent i2p peers to site
|
||||
for packed_address in params.get("peers_i2p", []):
|
||||
address = helper.unpackI2PAddress(packed_address)
|
||||
got_peer_keys.append("%s:%s" % address)
|
||||
if site.addPeer(*address):
|
||||
added += 1
|
||||
|
||||
# Send back peers that is not in the sent list and connectable (not port 0)
|
||||
packed_peers = helper.packPeers(site.getConnectablePeers(params["need"], ignore=got_peer_keys, allow_private=False))
|
||||
|
||||
|
@ -335,7 +342,8 @@ class FileRequest(object):
|
|||
back = {
|
||||
"peers": packed_peers["ipv4"],
|
||||
"peers_ipv6": packed_peers["ipv6"],
|
||||
"peers_onion": packed_peers["onion"]
|
||||
"peers_onion": packed_peers["onion"],
|
||||
"peers_i2p": packed_peers["i2p"]
|
||||
}
|
||||
|
||||
self.response(back)
|
||||
|
@ -410,7 +418,7 @@ class FileRequest(object):
|
|||
"Found: %s for %s hashids in %.3fs" %
|
||||
({key: len(val) for key, val in back.items()}, len(params["hash_ids"]), time.time() - s)
|
||||
)
|
||||
self.response({"peers": back["ipv4"], "peers_onion": back["onion"], "peers_ipv6": back["ipv6"], "my": my_hashes})
|
||||
self.response({"peers": back["ipv4"], "peers_onion": back["onion"], "peers_i2p": back["i2p"], "peers_ipv6": back["ipv6"], "my": my_hashes})
|
||||
|
||||
def actionSetHashfield(self, params):
|
||||
site = self.sites.get(params["site"])
|
||||
|
|
|
@ -252,6 +252,7 @@ class FileServer(ConnectionServer):
|
|||
|
||||
if not self.port_opened["ipv4"]:
|
||||
self.tor_manager.startOnions()
|
||||
self.i2p_manager.startDests()
|
||||
|
||||
if not sites_checking:
|
||||
check_pool = gevent.pool.Pool(5)
|
||||
|
|
176
src/I2P/I2PManager.py
Normal file
176
src/I2P/I2PManager.py
Normal file
|
@ -0,0 +1,176 @@
|
|||
import logging
|
||||
|
||||
from gevent.lock import RLock
|
||||
from gevent.server import StreamServer
|
||||
from gevent.pool import Pool
|
||||
from http.client import HTTPConnection
|
||||
import urllib.request, urllib.error, urllib.parse
|
||||
|
||||
from i2p import socket
|
||||
from i2p.datatypes import Destination
|
||||
|
||||
from Config import config
|
||||
from Site import SiteManager
|
||||
from Debug import Debug
|
||||
|
||||
|
||||
class I2PHTTPConnection(HTTPConnection):
|
||||
def __init__(self, i2p_manager, site_address, *args, **kwargs):
|
||||
HTTPConnection.__init__(self, *args, **kwargs)
|
||||
self.i2p_manager = i2p_manager
|
||||
self.site_address = site_address
|
||||
self._create_connection = self._create_i2p_connection
|
||||
|
||||
def _create_i2p_connection(self, address, timeout=60,
|
||||
source_address=None):
|
||||
return self.i2p_manager.createSocket(self.site_address, *address)
|
||||
|
||||
class I2PHTTPHandler(urllib.request.HTTPHandler):
|
||||
def __init__(self, i2p_manager, site_address, *args, **kwargs):
|
||||
urllib.request.HTTPHandler.__init__(self, *args, **kwargs)
|
||||
self.i2p_manager = i2p_manager
|
||||
self.site_address = site_address
|
||||
|
||||
def http_open(self, req):
|
||||
return self.do_open(self._createI2PHTTPConnection, req)
|
||||
|
||||
def _createI2PHTTPConnection(self, *args, **kwargs):
|
||||
return I2PHTTPConnection(self.i2p_manager, self.site_address, *args, **kwargs)
|
||||
|
||||
class I2PManager:
|
||||
def __init__(self, fileserver_handler=None):
|
||||
self.dest_conns = {} # Destination: SAM connection
|
||||
self.dest_servs = {} # Destination: StreamServer
|
||||
self.site_dests = {} # Site address: Destination
|
||||
self.log = logging.getLogger("I2PManager")
|
||||
self.start_dests = None
|
||||
self.lock = RLock()
|
||||
|
||||
if config.i2p == "disable":
|
||||
self.enabled = False
|
||||
self.start_dests = False
|
||||
self.status = "Disabled"
|
||||
else:
|
||||
self.enabled = True
|
||||
self.status = "Waiting"
|
||||
|
||||
if fileserver_handler:
|
||||
self.fileserver_handler = fileserver_handler
|
||||
else:
|
||||
self.fileserver_handler = lambda self, sock, addr: None
|
||||
|
||||
self.sam_ip, self.sam_port = config.i2p_sam.split(":")
|
||||
self.sam_port = int(self.sam_port)
|
||||
|
||||
# Test SAM port
|
||||
if config.i2p != "disable":
|
||||
try:
|
||||
assert self.connect(), "No connection"
|
||||
self.log.debug("I2P SAM port %s check ok" % config.i2p_sam)
|
||||
except Exception as err:
|
||||
self.log.debug("I2P SAM port %s check error: %s" % (config.i2p_sam, err))
|
||||
self.enabled = False
|
||||
|
||||
def connect(self):
|
||||
if not self.enabled:
|
||||
return False
|
||||
self.site_dests = {}
|
||||
self.dest_conns = {}
|
||||
self.dest_servs = {}
|
||||
|
||||
self.log.debug("Connecting to %s:%s" % (self.sam_ip, self.sam_port))
|
||||
with self.lock:
|
||||
try:
|
||||
socket.checkAPIConnection((self.sam_ip, self.sam_port))
|
||||
self.status = "Connected"
|
||||
return True
|
||||
except Exception as err:
|
||||
self.status = "Error (%s)" % err
|
||||
self.log.error("I2P SAM connect error: %s" % Debug.formatException(err))
|
||||
self.enabled = False
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
for server in self.dest_servs:
|
||||
server.stop()
|
||||
self.dest_conns = {}
|
||||
self.dest_servs = {}
|
||||
|
||||
def startDests(self):
|
||||
if self.enabled:
|
||||
self.log.debug("Start Destinations")
|
||||
self.start_dests = True
|
||||
|
||||
def addDest(self, site_address=None):
|
||||
sock = socket.socket(socket.AF_I2P, socket.SOCK_STREAM,
|
||||
samaddr=(self.sam_ip, self.sam_port))
|
||||
try:
|
||||
sock.setblocking(0)
|
||||
sock.bind(None, site_address) # Transient Destination, tied to site address
|
||||
sock.listen()
|
||||
server = StreamServer(
|
||||
sock, self.fileserver_handler, spawn=Pool(1000)
|
||||
)
|
||||
server.start()
|
||||
dest = sock.getsockname()
|
||||
self.dest_conns[dest] = sock
|
||||
self.dest_servs[dest] = server
|
||||
self.status = "OK (%s Destinations running)" % len(self.dest_conns)
|
||||
SiteManager.peer_blacklist.append((dest.base64()+".i2p", 0))
|
||||
return dest
|
||||
except Exception as err:
|
||||
self.status = "SESSION CREATE error (%s)" % err
|
||||
self.log.error("I2P SESSION CREATE error: %s" % Debug.formatException(err))
|
||||
return False
|
||||
|
||||
def delDest(self, dest):
|
||||
if dest in self.dest_servs:
|
||||
self.dest_servs[dest].stop()
|
||||
del self.dest_conns[dest]
|
||||
del self.dest_servs[dest]
|
||||
self.status = "OK (%s Destinations running)" % len(self.dest_conns)
|
||||
return True
|
||||
else:
|
||||
self.status = "Tried to delete non-existent Destination"
|
||||
self.log.error("I2P error: Tried to delete non-existent")
|
||||
self.disconnect()
|
||||
return False
|
||||
|
||||
def getDest(self, site_address):
|
||||
with self.lock:
|
||||
if not self.enabled:
|
||||
return None
|
||||
if self.start_dests: # Different Destination for every site
|
||||
dest = self.site_dests.get(site_address)
|
||||
else: # Same Destination for every site
|
||||
dest = self.site_dests.get("global")
|
||||
site_address = "global"
|
||||
if not dest:
|
||||
self.site_dests[site_address] = self.addDest(site_address)
|
||||
dest = self.site_dests[site_address]
|
||||
self.log.debug("Created new Destination for %s: %s" % (site_address, dest))
|
||||
return dest
|
||||
|
||||
def getPrivateDest(self, addr):
|
||||
dest = addr if isinstance(addr, Destination) else getDest(addr)
|
||||
return self.dest_conns[dest].getPrivateDest()
|
||||
|
||||
def createSocket(self, site_address, dest, port):
|
||||
if not self.enabled:
|
||||
return False
|
||||
if dest.endswith(".i2p") and not dest.endswith(".b32.i2p"):
|
||||
dest = Destination(raw=dest[:-4], b64=True)
|
||||
self.log.debug("Creating new socket to %s:%s" %
|
||||
(dest.base32() if isinstance(dest, Destination) else dest, port))
|
||||
sock = socket.socket(socket.AF_I2P, socket.SOCK_STREAM,
|
||||
samaddr=(self.sam_ip, self.sam_port))
|
||||
sock.connect((dest, int(port)), site_address)
|
||||
return sock
|
||||
|
||||
def lookup(self, name):
|
||||
return socket.lookup(name, (self.sam_ip, self.sam_port))
|
||||
|
||||
def urlopen(self, site_address, url, timeout):
|
||||
handler = I2PHTTPHandler(self, site_address)
|
||||
opener = urllib.request.build_opener(handler)
|
||||
return opener.open(url, timeout=50)
|
1
src/I2P/__init__.py
Normal file
1
src/I2P/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from .I2PManager import I2PManager
|
|
@ -126,6 +126,8 @@ class Peer(object):
|
|||
def packMyAddress(self):
|
||||
if self.ip.endswith(".onion"):
|
||||
return helper.packOnionAddress(self.ip, self.port)
|
||||
if self.ip.endswith(".i2p"):
|
||||
return helper.packI2PAddress(self.ip, self.port)
|
||||
else:
|
||||
return helper.packAddress(self.ip, self.port)
|
||||
|
||||
|
@ -273,6 +275,8 @@ class Peer(object):
|
|||
request = {"site": site.address, "peers": packed_peers["ipv4"], "need": need_num}
|
||||
if packed_peers["onion"]:
|
||||
request["peers_onion"] = packed_peers["onion"]
|
||||
if packed_peers["i2p"]:
|
||||
request["peers_i2p"] = packed_peers["i2p"]
|
||||
if packed_peers["ipv6"]:
|
||||
request["peers_ipv6"] = packed_peers["ipv6"]
|
||||
res = self.request("pex", request)
|
||||
|
@ -299,6 +303,12 @@ class Peer(object):
|
|||
if site.addPeer(*address, source="pex"):
|
||||
added += 1
|
||||
|
||||
# Add I2P
|
||||
for peer in res.get("peers_i2p", []):
|
||||
address = helper.unpackI2PAddress(peer)
|
||||
if site.addPeer(*address):
|
||||
added += 1
|
||||
|
||||
if added:
|
||||
self.log("Added peers using pex: %s" % added)
|
||||
|
||||
|
@ -331,7 +341,7 @@ class Peer(object):
|
|||
|
||||
back = collections.defaultdict(list)
|
||||
|
||||
for ip_type in ["ipv4", "ipv6", "onion"]:
|
||||
for ip_type in ["ipv4", "ipv6", "onion", "i2p"]:
|
||||
if ip_type == "ipv4":
|
||||
key = "peers"
|
||||
else:
|
||||
|
@ -339,6 +349,8 @@ class Peer(object):
|
|||
for hash, peers in list(res.get(key, {}).items())[0:30]:
|
||||
if ip_type == "onion":
|
||||
unpacker_func = helper.unpackOnionAddress
|
||||
elif ip_type == "i2p":
|
||||
unpacker_func = helper.unpackI2PAddress
|
||||
else:
|
||||
unpacker_func = helper.unpackAddress
|
||||
|
||||
|
|
|
@ -897,6 +897,8 @@ class Site(object):
|
|||
continue # No connection
|
||||
if peer.ip.endswith(".onion") and not self.connection_server.tor_manager.enabled:
|
||||
continue # Onion not supported
|
||||
if peer.ip.endswith(".i2p") and not self.connection_server.i2p_manager.enabled:
|
||||
continue # I2P not supported
|
||||
if peer.key in ignore:
|
||||
continue # The requester has this peer
|
||||
if time.time() - peer.connection.last_recv_time > 60 * 60 * 2: # Last message more than 2 hours ago
|
||||
|
@ -937,6 +939,8 @@ class Site(object):
|
|||
need_more = need_num - len(found)
|
||||
if not self.connection_server.tor_manager.enabled:
|
||||
peers = [peer for peer in self.peers.values() if not peer.ip.endswith(".onion")]
|
||||
elif not self.connection_server.i2p_manager.enabled:
|
||||
peers = [peer for peer in self.peers.values() if not peer.ip.endswith(".i2p")]
|
||||
else:
|
||||
peers = list(self.peers.values())
|
||||
|
||||
|
@ -969,6 +973,21 @@ class Site(object):
|
|||
if not peer.connection:
|
||||
peer.connect(connection)
|
||||
back.append(peer)
|
||||
|
||||
i2p_manager = self.connection_server.i2p_manager
|
||||
for connection in self.connection_server.connections:
|
||||
if not connection.connected and time.time() - connection.start_time > 20: # Still not connected after 20s
|
||||
continue
|
||||
peer = self.peers.get("%s:%s" % (connection.ip, connection.port))
|
||||
if peer:
|
||||
if connection.ip.endswith(".i2p") and connection.target_dest and i2p_manager.start_dests:
|
||||
# Check if the connection is made with the i2p address created for the site
|
||||
valid_target_i2p = (i2p_manager.getDest(self.address), i2p_manager.getDest("global").base64())
|
||||
if connection.target_dest not in valid_target_onions:
|
||||
continue
|
||||
if not peer.connection:
|
||||
peer.connect(connection)
|
||||
back.append(peer)
|
||||
return back
|
||||
|
||||
# Cleanup probably dead peers and close connection if too much
|
||||
|
|
|
@ -39,6 +39,9 @@ class SiteAnnouncer(object):
|
|||
if not self.site.connection_server.tor_manager.enabled:
|
||||
trackers = [tracker for tracker in trackers if ".onion" not in tracker]
|
||||
|
||||
if not self.site.connection_server.i2p_manager.enabled:
|
||||
trackers = [tracker for tracker in trackers if ".i2p" not in tracker]
|
||||
|
||||
trackers = [tracker for tracker in trackers if self.getAddressParts(tracker)] # Remove trackers with unknown address
|
||||
|
||||
if "ipv6" not in self.site.connection_server.supported_ip_types:
|
||||
|
@ -67,6 +70,8 @@ class SiteAnnouncer(object):
|
|||
back.append(ip_type)
|
||||
if self.site.connection_server.tor_manager.start_onions:
|
||||
back.append("onion")
|
||||
if self.site.connection_server.i2p_manager.start_dests:
|
||||
back.append("i2p")
|
||||
return back
|
||||
|
||||
@util.Noparallel(blocking=False)
|
||||
|
|
135
src/Test/TestI2P.py
Normal file
135
src/Test/TestI2P.py
Normal file
|
@ -0,0 +1,135 @@
|
|||
import pytest
|
||||
import time
|
||||
|
||||
from File import FileServer
|
||||
|
||||
# stats.i2p
|
||||
TEST_B64 = 'Okd5sN9hFWx-sr0HH8EFaxkeIMi6PC5eGTcjM1KB7uQ0ffCUJ2nVKzcsKZFHQc7pLONjOs2LmG5H-2SheVH504EfLZnoB7vxoamhOMENnDABkIRGGoRisc5AcJXQ759LraLRdiGSR0WTHQ0O1TU0hAz7vAv3SOaDp9OwNDr9u902qFzzTKjUTG5vMTayjTkLo2kOwi6NVchDeEj9M7mjj5ySgySbD48QpzBgcqw1R27oIoHQmjgbtbmV2sBL-2Tpyh3lRe1Vip0-K0Sf4D-Zv78MzSh8ibdxNcZACmZiVODpgMj2ejWJHxAEz41RsfBpazPV0d38Mfg4wzaS95R5hBBo6SdAM4h5vcZ5ESRiheLxJbW0vBpLRd4mNvtKOrcEtyCvtvsP3FpA-6IKVswyZpHgr3wn6ndDHiVCiLAQZws4MsIUE1nkfxKpKtAnFZtPrrB8eh7QO9CkH2JBhj7bG0ED6mV5~X5iqi52UpsZ8gnjZTgyG5pOF8RcFrk86kHxAAAA'
|
||||
|
||||
@pytest.mark.usefixtures("resetSettings")
|
||||
@pytest.mark.usefixtures("resetTempSettings")
|
||||
class TestI2P:
|
||||
def testAddDest(self, i2p_manager):
|
||||
# Add
|
||||
dest = i2p_manager.addDest()
|
||||
assert dest
|
||||
assert dest in i2p_manager.dest_conns
|
||||
|
||||
# Delete
|
||||
assert i2p_manager.delDest(dest)
|
||||
assert dest not in i2p_manager.dest_conns
|
||||
|
||||
def testSignDest(self, i2p_manager):
|
||||
dest = i2p_manager.addDest()
|
||||
|
||||
# Sign
|
||||
sign = i2p_manager.getPrivateDest(dest).sign("hello")
|
||||
assert len(sign) == dest.signature_size()
|
||||
|
||||
# Verify
|
||||
assert dest.verify("hello", sign)
|
||||
assert not dest.verify("not hello", sign)
|
||||
|
||||
# Delete
|
||||
i2p_manager.delDest(dest)
|
||||
|
||||
@pytest.mark.skipif(not pytest.config.getvalue("slow"), reason="--slow not requested (takes around ~ 1min)")
|
||||
def testConnection(self, i2p_manager, file_server, site, site_temp):
|
||||
file_server.i2p_manager.start_dests = True
|
||||
dest = file_server.i2p_manager.getDest(site.address)
|
||||
assert dest
|
||||
print("Connecting to", dest.base32())
|
||||
for retry in range(5): # Wait for Destination creation
|
||||
time.sleep(10)
|
||||
try:
|
||||
connection = file_server.getConnection(dest.base64()+".i2p", 1544)
|
||||
if connection:
|
||||
break
|
||||
except Exception as err:
|
||||
continue
|
||||
assert connection.handshake
|
||||
assert not connection.handshake["peer_id"] # No peer_id for I2P connections
|
||||
|
||||
# Return the same connection without site specified
|
||||
assert file_server.getConnection(dest.base64()+".i2p", 1544) == connection
|
||||
# No reuse for different site
|
||||
assert file_server.getConnection(dest.base64()+".i2p", 1544, site=site) != connection
|
||||
assert file_server.getConnection(dest.base64()+".i2p", 1544, site=site) == file_server.getConnection(dest.base64()+".i2p", 1544, site=site)
|
||||
site_temp.address = "1OTHERSITE"
|
||||
assert file_server.getConnection(dest.base64()+".i2p", 1544, site=site) != file_server.getConnection(dest.base64()+".i2p", 1544, site=site_temp)
|
||||
|
||||
# Only allow to query from the locked site
|
||||
file_server.sites[site.address] = site
|
||||
connection_locked = file_server.getConnection(dest.base64()+".i2p", 1544, site=site)
|
||||
assert "body" in connection_locked.request("getFile", {"site": site.address, "inner_path": "content.json", "location": 0})
|
||||
assert connection_locked.request("getFile", {"site": "1OTHERSITE", "inner_path": "content.json", "location": 0})["error"] == "Invalid site"
|
||||
|
||||
def testPex(self, file_server, site, site_temp):
|
||||
# Register site to currently running fileserver
|
||||
site.connection_server = file_server
|
||||
file_server.sites[site.address] = site
|
||||
# Create a new file server to emulate new peer connecting to our peer
|
||||
file_server_temp = FileServer("127.0.0.1", 1545)
|
||||
site_temp.connection_server = file_server_temp
|
||||
file_server_temp.sites[site_temp.address] = site_temp
|
||||
# We will request peers from this
|
||||
peer_source = site_temp.addPeer("127.0.0.1", 1544)
|
||||
|
||||
# Get ip4 peers from source site
|
||||
assert peer_source.pex(need_num=10) == 1 # Need >5 to return also return non-connected peers
|
||||
assert len(site_temp.peers) == 2 # Me, and the other peer
|
||||
site.addPeer("1.2.3.4", 1555) # Add peer to source site
|
||||
assert peer_source.pex(need_num=10) == 1
|
||||
assert len(site_temp.peers) == 3
|
||||
assert "1.2.3.4:1555" in site_temp.peers
|
||||
|
||||
# Get I2P peers from source site
|
||||
site.addPeer(TEST_B64+".i2p", 1555)
|
||||
assert TEST_B64+".i2p:1555" not in site_temp.peers
|
||||
assert peer_source.pex(need_num=10) == 1 # Need >5 to return also return non-connected peers
|
||||
assert TEST_B64+".i2p:1555" in site_temp.peers
|
||||
|
||||
def testFindHash(self, i2p_manager, file_server, site, site_temp):
|
||||
file_server.ip_incoming = {} # Reset flood protection
|
||||
file_server.sites[site.address] = site
|
||||
assert file_server.i2p_manager == None
|
||||
file_server.i2p_manager = i2p_manager
|
||||
|
||||
client = FileServer("127.0.0.1", 1545)
|
||||
client.sites[site_temp.address] = site_temp
|
||||
site_temp.connection_server = client
|
||||
|
||||
# Add file_server as peer to client
|
||||
peer_file_server = site_temp.addPeer("127.0.0.1", 1544)
|
||||
|
||||
assert peer_file_server.findHashIds([1234]) == {}
|
||||
|
||||
# Add fake peer with requred hash
|
||||
fake_peer_1 = site.addPeer(TEST_B64+".i2p", 1544)
|
||||
fake_peer_1.hashfield.append(1234)
|
||||
fake_peer_2 = site.addPeer("1.2.3.5", 1545)
|
||||
fake_peer_2.hashfield.append(1234)
|
||||
fake_peer_2.hashfield.append(1235)
|
||||
fake_peer_3 = site.addPeer("1.2.3.6", 1546)
|
||||
fake_peer_3.hashfield.append(1235)
|
||||
fake_peer_3.hashfield.append(1236)
|
||||
|
||||
assert peer_file_server.findHashIds([1234, 1235]) == {
|
||||
1234: [('1.2.3.5', 1545), (TEST_B64+".i2p", 1544)],
|
||||
1235: [('1.2.3.6', 1546), ('1.2.3.5', 1545)]
|
||||
}
|
||||
|
||||
# Test my address adding
|
||||
site.content_manager.hashfield.append(1234)
|
||||
my_i2p_address = i2p_manager.getDest(site_temp.address).base64()+".i2p"
|
||||
|
||||
res = peer_file_server.findHashIds([1234, 1235])
|
||||
assert res[1234] == [('1.2.3.5', 1545), (TEST_B64+".i2p", 1544), (my_i2p_address, 1544)]
|
||||
assert res[1235] == [('1.2.3.6', 1546), ('1.2.3.5', 1545)]
|
||||
|
||||
# Reset
|
||||
file_server.i2p_manager = None
|
||||
|
||||
def testSiteDest(self, i2p_manager):
|
||||
assert i2p_manager.getDest("address1") != i2p_manager.getDest("address2")
|
||||
assert i2p_manager.getDest("address1") == i2p_manager.getDest("address1")
|
|
@ -74,6 +74,7 @@ config.debug = True
|
|||
config.debug_socket = True # Use test data for unittests
|
||||
config.verbose = True # Use test data for unittests
|
||||
config.tor = "disable" # Don't start Tor client
|
||||
config.i2p = "disable" # Don't start I2P client
|
||||
config.trackers = []
|
||||
config.data_dir = TEST_DATA_PATH # Use test data for unittests
|
||||
if "ZERONET_LOG_DIR" in os.environ:
|
||||
|
@ -126,6 +127,7 @@ from Crypt import CryptConnection
|
|||
from Crypt import CryptBitcoin
|
||||
from Ui import UiWebsocket
|
||||
from Tor import TorManager
|
||||
from I2P import I2PManager
|
||||
from Content import ContentDb
|
||||
from util import RateLimit
|
||||
from Db import Db
|
||||
|
@ -495,3 +497,13 @@ def disableLog():
|
|||
yield None # Wait until all test done
|
||||
logging.getLogger('').setLevel(logging.getLevelName(logging.CRITICAL))
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def i2p_manager():
|
||||
try:
|
||||
i2p_manager = I2PManager()
|
||||
i2p_manager.enabled = True
|
||||
assert i2p_manager.connect(), "No connection"
|
||||
i2p_manager.startDests()
|
||||
except Exception as err:
|
||||
raise pytest.skip("Test requires I2P with SAM port: %s, %s" % (config.i2p_sam, err))
|
||||
return i2p_manager
|
||||
|
|
|
@ -47,7 +47,7 @@ class UiWebsocket(object):
|
|||
self.site.page_requested = True # Dont add connection notification anymore
|
||||
import main
|
||||
file_server = main.file_server
|
||||
if not file_server.port_opened or file_server.tor_manager.start_onions is None:
|
||||
if not file_server.port_opened or (file_server.tor_manager.start_onions or file_server.i2p_manager.start_dests) is None:
|
||||
self.site.page_requested = False # Not ready yet, check next time
|
||||
else:
|
||||
try:
|
||||
|
@ -302,6 +302,8 @@ class UiWebsocket(object):
|
|||
"tor_status": file_server.tor_manager.status,
|
||||
"tor_has_meek_bridges": file_server.tor_manager.has_meek_bridges,
|
||||
"tor_use_bridges": config.tor_use_bridges,
|
||||
"i2p_enabled": file_server.i2p_manager.enabled,
|
||||
"i2p_status": file_server.i2p_manager.status,
|
||||
"ui_ip": config.ui_ip,
|
||||
"ui_port": config.ui_port,
|
||||
"version": config.version,
|
||||
|
@ -537,7 +539,7 @@ class UiWebsocket(object):
|
|||
else:
|
||||
if len(site.peers) == 0:
|
||||
import main
|
||||
if any(main.file_server.port_opened.values()) or main.file_server.tor_manager.start_onions:
|
||||
if any(main.file_server.port_opened.values()) or main.file_server.tor_manager.start_onions or main.file_server.i2p_manager.start_onions:
|
||||
if notification:
|
||||
self.cmd("notification", ["info", _["No peers found, but your content is ready to access."]])
|
||||
if callback:
|
||||
|
|
|
@ -115,7 +115,7 @@ def shellquote(*args):
|
|||
|
||||
|
||||
def packPeers(peers):
|
||||
packed_peers = {"ipv4": [], "ipv6": [], "onion": []}
|
||||
packed_peers = {"ipv4": [], "ipv6": [], "onion": [], "i2p": []}
|
||||
for peer in peers:
|
||||
try:
|
||||
ip_type = getIpType(peer.ip)
|
||||
|
@ -154,6 +154,17 @@ def packOnionAddress(onion, port):
|
|||
def unpackOnionAddress(packed):
|
||||
return base64.b32encode(packed[0:-2]).lower().decode() + ".onion", struct.unpack("H", packed[-2:])[0]
|
||||
|
||||
# Destination, port to packed (389+)-byte format
|
||||
def packI2PAddress(dest, port):
|
||||
if not isinstance(dest, Destination):
|
||||
dest = dest.replace(".i2p", "")
|
||||
dest = Destination(raw=dest, b64=True)
|
||||
return dest.serialize() + struct.pack("H", port)
|
||||
|
||||
|
||||
# From (389+)-byte format to Destination, port
|
||||
def unpackI2PAddress(packed):
|
||||
return Destination(raw=packed[0:-2]).base64() + ".i2p", struct.unpack("H", packed[-2:])[0]
|
||||
|
||||
# Get dir from file
|
||||
# Return: data/site/content.json -> data/site/
|
||||
|
|
Loading…
Reference in a new issue