310 lines
11 KiB
Python
310 lines
11 KiB
Python
import atexit
|
|
import json
|
|
import logging
|
|
import re
|
|
import os
|
|
import time
|
|
import binascii
|
|
|
|
from util import helper
|
|
|
|
from Plugin import PluginManager
|
|
from .TrackerZeroDb import TrackerZeroDb
|
|
from Crypt import CryptRsa
|
|
from Config import config
|
|
|
|
class TrackerZero(object):
|
|
def __init__(self):
|
|
self.log = logging.getLogger("TrackerZero")
|
|
self.log_once = set()
|
|
self.enabled_addresses = []
|
|
self.config_file_path = "%s/tracker-zero.json" % config.data_dir
|
|
self.config = None
|
|
self.load()
|
|
atexit.register(self.save)
|
|
|
|
def logOnce(self, message):
|
|
if message in self.log_once:
|
|
return
|
|
self.log_once.add(message)
|
|
self.log.info(message)
|
|
|
|
def getDefaultConfig(self):
|
|
return {
|
|
"settings": {
|
|
"enable": False,
|
|
"enable_only_in_tor_always_mode": True,
|
|
"listen_on_public_ips": False,
|
|
"listen_on_temporary_onion_address": False,
|
|
"listen_on_persistent_onion_address": True
|
|
}
|
|
}
|
|
|
|
def readJSON(self, file_path, default_value):
|
|
if not os.path.isfile(file_path):
|
|
try:
|
|
self.writeJSON(file_path, default_value)
|
|
except Exception as err:
|
|
self.log.error("Error writing %s: %s" % (file_path, err))
|
|
return default_value
|
|
|
|
try:
|
|
return json.load(open(file_path))
|
|
except Exception as err:
|
|
self.log.error("Error loading %s: %s" % (file_path, err))
|
|
return default_value
|
|
|
|
def writeJSON(self, file_path, value):
|
|
helper.atomicWrite(file_path, json.dumps(value, indent=2, sort_keys=True).encode("utf8"))
|
|
|
|
def load(self):
|
|
self.config = self.readJSON(self.config_file_path, self.getDefaultConfig())
|
|
|
|
def save(self):
|
|
self.writeJSON(self.config_file_path, self.config)
|
|
|
|
def checkOnionSigns(self, onions, onion_signs, onion_sign_this):
|
|
if not onion_signs or len(onion_signs) != len(set(onions)):
|
|
return False
|
|
|
|
if time.time() - float(onion_sign_this) > 3 * 60:
|
|
return False # Signed out of allowed 3 minutes
|
|
|
|
onions_signed = []
|
|
# Check onion signs
|
|
for onion_publickey, onion_sign in onion_signs.items():
|
|
if CryptRsa.verify(onion_sign_this.encode(), onion_publickey, onion_sign):
|
|
onions_signed.append(CryptRsa.publickeyToOnion(onion_publickey))
|
|
else:
|
|
break
|
|
|
|
# Check if the same onion addresses signed as the announced onces
|
|
if sorted(onions_signed) == sorted(set(onions)):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def actionAnnounce(self, file_request, params):
|
|
if len(self.enabled_addresses) < 1:
|
|
file_request.actionUnknown("announce", params)
|
|
return
|
|
|
|
time_started = time.time()
|
|
s = time.time()
|
|
# Backward compatibility
|
|
if "ip4" in params["add"]:
|
|
params["add"].append("ipv4")
|
|
if "ip4" in params["need_types"]:
|
|
params["need_types"].append("ipv4")
|
|
|
|
hashes = params["hashes"]
|
|
|
|
all_onions_signed = self.checkOnionSigns(params.get("onions", []), params.get("onion_signs"), params.get("onion_sign_this"))
|
|
|
|
time_onion_check = time.time() - s
|
|
|
|
ip_type = helper.getIpType(file_request.connection.ip)
|
|
|
|
if ip_type == "onion" or file_request.connection.ip in config.ip_local:
|
|
is_port_open = False
|
|
elif ip_type in params["add"]:
|
|
is_port_open = True
|
|
else:
|
|
is_port_open = False
|
|
|
|
s = time.time()
|
|
# Separatley add onions to sites or at once if no onions present
|
|
i = 0
|
|
onion_to_hash = {}
|
|
for onion in params.get("onions", []):
|
|
if onion not in onion_to_hash:
|
|
onion_to_hash[onion] = []
|
|
onion_to_hash[onion].append(hashes[i])
|
|
i += 1
|
|
|
|
hashes_changed = 0
|
|
for onion, onion_hashes in onion_to_hash.items():
|
|
hashes_changed += db.peerAnnounce(
|
|
ip_type="onion",
|
|
address=onion,
|
|
port=params["port"],
|
|
hashes=onion_hashes,
|
|
onion_signed=all_onions_signed
|
|
)
|
|
time_db_onion = time.time() - s
|
|
|
|
s = time.time()
|
|
|
|
if is_port_open:
|
|
hashes_changed += db.peerAnnounce(
|
|
ip_type=ip_type,
|
|
address=file_request.connection.ip,
|
|
port=params["port"],
|
|
hashes=hashes,
|
|
delete_missing_hashes=params.get("delete")
|
|
)
|
|
time_db_ip = time.time() - s
|
|
|
|
s = time.time()
|
|
# Query sites
|
|
back = {}
|
|
peers = []
|
|
if params.get("onions") and not all_onions_signed and hashes_changed:
|
|
back["onion_sign_this"] = "%.0f" % time.time() # Send back nonce for signing
|
|
|
|
if len(hashes) > 500 or not hashes_changed:
|
|
limit = 5
|
|
order = False
|
|
else:
|
|
limit = 30
|
|
order = True
|
|
for hash in hashes:
|
|
if time.time() - time_started > 1: # 1 sec limit on request
|
|
file_request.connection.log("Announce time limit exceeded after %s/%s sites" % (len(peers), len(hashes)))
|
|
break
|
|
|
|
hash_peers = db.peerList(
|
|
hash,
|
|
address=file_request.connection.ip, onions=list(onion_to_hash.keys()), port=params["port"],
|
|
limit=min(limit, params["need_num"]), need_types=params["need_types"], order=order
|
|
)
|
|
if "ip4" in params["need_types"]: # Backward compatibility
|
|
hash_peers["ip4"] = hash_peers["ipv4"]
|
|
del(hash_peers["ipv4"])
|
|
peers.append(hash_peers)
|
|
time_peerlist = time.time() - s
|
|
|
|
back["peers"] = peers
|
|
file_request.connection.log(
|
|
"Announce %s sites (onions: %s, onion_check: %.3fs, db_onion: %.3fs, db_ip: %.3fs, peerlist: %.3fs, limit: %s)" %
|
|
(len(hashes), len(onion_to_hash), time_onion_check, time_db_onion, time_db_ip, time_peerlist, limit)
|
|
)
|
|
file_request.response(back)
|
|
|
|
|
|
def getTrackerStorage(self):
|
|
try:
|
|
if "TrackerShare" in PluginManager.plugin_manager.plugin_names:
|
|
from TrackerShare.TrackerSharePlugin import tracker_storage
|
|
return tracker_storage
|
|
elif "AnnounceShare" in PluginManager.plugin_manager.plugin_names:
|
|
from AnnounceShare.AnnounceSharePlugin import tracker_storage
|
|
return tracker_storage
|
|
except Exception as err:
|
|
self.log.error("%s" % Debug.formatException(err))
|
|
|
|
return None
|
|
|
|
def registerTrackerAddress(self, message, address, port):
|
|
_tracker_storage = self.getTrackerStorage()
|
|
if not _tracker_storage:
|
|
return
|
|
|
|
my_tracker_address = "zero://%s:%s" % (address, port)
|
|
if _tracker_storage.onTrackerFound(my_tracker_address, my=True):
|
|
self.logOnce("listening on %s: %s" % (message, my_tracker_address))
|
|
self.enabled_addresses.append("%s:%s" % (address, port))
|
|
|
|
def registerTrackerAddresses(self, file_server, port_open):
|
|
_tracker_storage = self.getTrackerStorage()
|
|
if not _tracker_storage:
|
|
return
|
|
|
|
tor_manager = file_server.tor_manager
|
|
|
|
settings = self.config.get("settings", {})
|
|
|
|
if not settings.get("enable"):
|
|
self.logOnce("Plugin loaded, but disabled by the settings")
|
|
return False
|
|
|
|
if settings.get("enable_only_in_tor_always_mode") and not config.tor == "always":
|
|
self.logOnce("Plugin loaded, but disabled from running in the modes other than 'tor = always'")
|
|
return False
|
|
|
|
self.enabled_addresses = []
|
|
|
|
if settings.get("listen_on_public_ips") and port_open and not config.tor == "always":
|
|
for ip in file_server.ip_external_list:
|
|
self.registerTrackerAddress("public IP", ip, config.fileserver_port)
|
|
|
|
if settings.get("listen_on_temporary_onion_address") and tor_manager.enabled:
|
|
onion = tor_manager.getOnion(config.homepage)
|
|
if onion:
|
|
self.registerTrackerAddress("temporary onion address", "%s.onion" % onion, tor_manager.fileserver_port)
|
|
|
|
if settings.get("listen_on_persistent_onion_address") and tor_manager.enabled:
|
|
persistent_addresses = self.config.setdefault("persistent_addresses", {})
|
|
if len(persistent_addresses) == 0:
|
|
result = tor_manager.makeOnionAndKey()
|
|
if result:
|
|
onion_address, onion_privatekey = result
|
|
persistent_addresses[onion_address] = {
|
|
"private_key": onion_privatekey
|
|
}
|
|
self.registerTrackerAddress("persistent onion address", "%s.onion" % onion_address, tor_manager.fileserver_port)
|
|
else:
|
|
for address, d in persistent_addresses.items():
|
|
private_key = d.get("private_key")
|
|
if not private_key:
|
|
continue
|
|
res = tor_manager.request(
|
|
"ADD_ONION RSA1024:%s port=%s" % (private_key, tor_manager.fileserver_port)
|
|
)
|
|
match = re.search("ServiceID=([A-Za-z0-9]+)", res)
|
|
if match:
|
|
onion_address = match.groups()[0]
|
|
if onion_address == address:
|
|
self.registerTrackerAddress("persistent onion address", "%s.onion" % onion_address, tor_manager.fileserver_port)
|
|
|
|
return len(self.enabled_addresses) > 0
|
|
|
|
if "db" not in locals().keys(): # Share during reloads
|
|
db = TrackerZeroDb()
|
|
|
|
if "tracker_zero" not in locals():
|
|
tracker_zero = TrackerZero()
|
|
|
|
|
|
|
|
@PluginManager.registerTo("FileRequest")
|
|
class FileRequestPlugin(object):
|
|
def actionAnnounce(self, params):
|
|
tracker_zero.actionAnnounce(self, params)
|
|
|
|
|
|
@PluginManager.registerTo("FileServer")
|
|
class FileServerPlugin(object):
|
|
def portCheck(self, *args, **kwargs):
|
|
res = super(FileServerPlugin, self).portCheck(*args, **kwargs)
|
|
tracker_zero.registerTrackerAddresses(self, res)
|
|
return res
|
|
|
|
|
|
@PluginManager.registerTo("UiRequest")
|
|
class UiRequestPlugin(object):
|
|
|
|
@helper.encodeResponse
|
|
def actionStatsTrackerZero(self):
|
|
self.sendHeader()
|
|
|
|
# Style
|
|
yield """
|
|
<style>
|
|
* { font-family: monospace; white-space: pre }
|
|
table td, table th { text-align: right; padding: 0px 10px }
|
|
</style>
|
|
"""
|
|
|
|
hash_rows = db.execute("SELECT * FROM hash").fetchall()
|
|
for hash_row in hash_rows:
|
|
peer_rows = db.execute(
|
|
"SELECT * FROM peer LEFT JOIN peer_to_hash USING (peer_id) WHERE hash_id = :hash_id",
|
|
{"hash_id": hash_row["hash_id"]}
|
|
).fetchall()
|
|
|
|
yield "<br>%s (added: %s, peers: %s)<br>" % (
|
|
binascii.hexlify(hash_row["hash"]).decode("utf-8"), hash_row["date_added"], len(peer_rows)
|
|
)
|
|
for peer_row in peer_rows:
|
|
yield " - {type: <6} {address: <30} {port: >5} added: {date_added}, announced: {date_announced}<br>".format(**dict(peer_row))
|