1import os 2import sys 3 4import stem 5from stem.descriptor.hidden_service import HiddenServiceDescriptorV3 6 7import onionbalance.common.instance 8 9from onionbalance.common import log 10 11from onionbalance.common import util 12from onionbalance.hs_v3 import manager 13 14from onionbalance.hs_v3 import stem_controller 15from onionbalance.hs_v3 import service as ob_service 16from onionbalance.hs_v3 import consensus as ob_consensus 17 18logger = log.get_logger() 19 20 21class Onionbalance(object): 22 """ 23 Onionbalance singleton that represents this onionbalance runtime. 24 25 Contains various objects that are useful to other onionbalance modules so 26 this is imported from all over the codebase. 27 """ 28 29 def __init__(self): 30 # This is kept minimal so that it's quick (it's executed at program 31 # launch because of the onionbalance singleton). The actual init work 32 # happens in init_subsystems() 33 34 # True if this onionbalance operates in a testnet (e.g. chutney) 35 self.is_testnet = False 36 37 def init_subsystems(self, args): 38 """ 39 Initialize subsystems (this is resource intensive) 40 """ 41 self.args = args 42 self.config_path = os.path.abspath(self.args.config) 43 self.config_data = self.load_config_file() 44 self.is_testnet = args.is_testnet 45 46 if self.is_testnet: 47 logger.warning("Onionbalance configured on a testnet!") 48 49 # Create stem controller and connect to the Tor network 50 self.controller = stem_controller.StemController(address=args.ip, port=args.port, socket=args.socket) 51 self.consensus = ob_consensus.Consensus() 52 53 # Initialize our service 54 self.services = self.initialize_services_from_config_data() 55 56 # Catch interesting events (like receiving descriptors etc.) 57 self.controller.add_event_listeners() 58 59 logger.warning("Onionbalance initialized (stem version: %s) (tor version: %s)!", 60 stem.__version__, self.controller.controller.get_version()) 61 logger.warning("=" * 80) 62 63 def initialize_services_from_config_data(self): 64 services = [] 65 try: 66 for service in self.config_data['services']: 67 services.append(ob_service.OnionBalanceService(service, self.config_path)) 68 except ob_service.BadServiceInit: 69 sys.exit(1) 70 71 if len(services) > 1: 72 # We don't know how to handle more than a single service right now 73 raise NotImplementedError 74 75 return services 76 77 def load_config_file(self): 78 config_data = util.read_config_data_from_file(self.config_path) 79 logger.debug("Onionbalance config data: %s", config_data) 80 81 # Do some basic validation 82 if "services" not in config_data: 83 raise ConfigError("Config file is bad. 'services' is missing. Did you make it with onionbalance-config?") 84 85 # More validation 86 for service in config_data["services"]: 87 if "key" not in service: 88 raise ConfigError("Config file is bad. 'key' is missing. Did you make it with onionbalance-config?") 89 90 if "instances" not in service: 91 raise ConfigError("Config file is bad. 'instances' is missing. Did you make it with " 92 "onionbalance-config?") 93 94 if not service["instances"]: 95 raise ConfigError("Config file is bad. No backend instances are set. Onionbalance needs at least 1.") 96 97 for instance in service["instances"]: 98 if "address" not in instance: 99 raise ConfigError("Config file is wrong. 'address' missing from instance.") 100 101 if not instance["address"]: 102 raise ConfigError("Config file is bad. Address field is not set.") 103 104 # Validate that the onion address is legit 105 try: 106 _ = HiddenServiceDescriptorV3.identity_key_from_address(instance["address"]) 107 except ValueError: 108 raise ConfigError("Cannot load instance with address: '{}'. If you are trying to run onionbalance " 109 "for v2 onions, please use the --hs-version=v2 switch".format(instance["address"])) 110 111 return config_data 112 113 def fetch_instance_descriptors(self): 114 logger.info("[*] fetch_instance_descriptors() called [*]") 115 116 # TODO: Don't do this here. Instead do it on a specialized function 117 self.controller.mark_tor_as_active() 118 119 if not self.consensus.is_live(): 120 logger.warning("No live consensus. Waiting before fetching descriptors...") 121 return 122 123 all_instances = self._get_all_instances() 124 125 onionbalance.common.instance.helper_fetch_all_instance_descriptors(self.controller.controller, 126 all_instances) 127 128 def handle_new_desc_content_event(self, desc_content_event): 129 """ 130 Parse HS_DESC_CONTENT response events for descriptor content 131 132 Update the HS instance object with the data from the new descriptor. 133 """ 134 onion_address = desc_content_event.address 135 logger.debug("Received descriptor for %s.onion from %s", 136 onion_address, desc_content_event.directory) 137 138 # Check that the HSDir returned a descriptor that is not empty 139 descriptor_text = str(desc_content_event.descriptor).encode('utf-8') 140 141 # HSDirs provide a HS_DESC_CONTENT response with either one or two 142 # CRLF lines when they do not have a matching descriptor. Using 143 # len() < 5 should ensure all empty HS_DESC_CONTENT events are matched. 144 if len(descriptor_text) < 5: 145 logger.debug("Empty descriptor received for %s.onion", onion_address) 146 return None 147 148 # OK this descriptor seems plausible: Find the instances that this 149 # descriptor belongs to: 150 for instance in self._get_all_instances(): 151 if instance.onion_address == onion_address: 152 instance.register_descriptor(descriptor_text, onion_address) 153 154 def publish_all_descriptors(self): 155 """ 156 For each service attempt to publish all descriptors 157 """ 158 logger.info("[*] publish_all_descriptors() called [*]") 159 160 if not self.consensus.is_live(): 161 logger.info("No live consensus. Waiting before publishing descriptors...") 162 return 163 164 for service in self.services: 165 service.publish_descriptors() 166 167 def _get_all_instances(self): 168 """ 169 Get all instances for all services 170 """ 171 instances = [] 172 173 for service in self.services: 174 instances.extend(service.instances) 175 176 return instances 177 178 def handle_new_status_event(self, status_event): 179 """ 180 Parse Tor status events such as "STATUS_GENERAL" 181 """ 182 # pylint: disable=no-member 183 if status_event.action == "CONSENSUS_ARRIVED": 184 logger.info("Received new consensus!") 185 self.consensus.refresh() 186 # Call all callbacks in case we just got a live consensus 187 my_onionbalance.publish_all_descriptors() 188 my_onionbalance.fetch_instance_descriptors() 189 190 def _address_is_instance(self, onion_address): 191 """ 192 Return True if 'onion_address' is one of our instances. 193 """ 194 for service in self.services: 195 for instance in service.instances: 196 if instance.has_onion_address(onion_address): 197 return True 198 return False 199 200 def _address_is_frontend(self, onion_address): 201 for service in self.services: 202 if service.has_onion_address(onion_address): 203 return True 204 return False 205 206 def handle_new_desc_event(self, desc_event): 207 """ 208 Parse HS_DESC response events 209 """ 210 action = desc_event.action 211 212 if action == "RECEIVED": 213 pass # We already log in HS_DESC_CONTENT so no need to do it here too 214 elif action == "UPLOADED": 215 logger.debug("Successfully uploaded descriptor for %s to %s", desc_event.address, desc_event.directory) 216 elif action == "FAILED": 217 if self._address_is_instance(desc_event.address): 218 logger.info("Descriptor fetch failed for instance %s from %s (%s)", 219 desc_event.address, desc_event.directory, desc_event.reason) 220 elif self._address_is_frontend(desc_event.address): 221 logger.warning("Descriptor upload failed for frontend %s to %s (%s)", 222 desc_event.address, desc_event.directory, desc_event.reason) 223 else: 224 logger.warning("Descriptor action failed for unknown service %s to %s (%s)", 225 desc_event.address, desc_event.directory, desc_event.reason) 226 elif action == "REQUESTED": 227 logger.debug("Requested descriptor for %s from %s...", desc_event.address, desc_event.directory) 228 else: 229 pass 230 231 def reload_config(self): 232 """ 233 Reload configuration and reset job scheduler 234 """ 235 236 try: 237 self.init_subsystems(self.args) 238 manager.init_scheduler(self) 239 except ConfigError as err: 240 logger.error("%s", err) 241 sys.exit(1) 242 243 244class ConfigError(Exception): 245 pass 246 247 248my_onionbalance = Onionbalance() 249