import atexit import json import logging import os import time 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): 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 registerSharedAddresses(self, file_server, port_open): tracker_storage = self.getTrackerStorage() if not tracker_storage: return 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: my_tracker_address = "zero://%s:%s" % (ip, config.fileserver_port) if tracker_storage.onTrackerFound(my_tracker_address, my=True): self.logOnce("listening on public IP: %s" % my_tracker_address) self.enabled_addresses.append(my_tracker_address) if settings.get("listen_on_temporary_onion_address") and file_server.tor_manager.enabled: onion = file_server.tor_manager.getOnion(config.homepage) if onion: my_tracker_address = "zero://%s.onion:%s" % (onion, file_server.tor_manager.fileserver_port) if tracker_storage.onTrackerFound(my_tracker_address, my=True): self.logOnce("listening on temporary onion address: %s" % my_tracker_address) self.enabled_addresses.append(my_tracker_address) if settings.get("listen_on_persistent_onion_address") and file_server.tor_manager.enabled: # FIXME: not implemented pass 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.registerSharedAddresses(self, res) return res @PluginManager.registerTo("UiRequest") class UiRequestPlugin(object): def actionStatsTrackerZero(self): self.sendHeader() # Style yield """ """ 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 "
%s (added: %s, peers: %s)
" % ( str(hash_row["hash"]).encode("hex"), hash_row["date_added"], len(peer_rows) ) for peer_row in peer_rows: yield " - {ip4: <30} {onion: <30} added: {date_added}, announced: {date_announced}
".format(**dict(peer_row))