1import copy 2import logging 3import os 4from threading import RLock 5 6import socks 7import stem.process 8from stem import ( 9 ControllerError, 10 InvalidArguments, 11 InvalidRequest, 12 OperationFailed, 13 ProtocolError, 14 SocketClosed, 15 SocketError, 16 UnsatisfiableRequest, 17) 18from stem.connection import IncorrectSocketType 19from stem.control import Controller, Listener 20 21from sbws import settings 22from sbws.globals import ( 23 TORRC_OPTIONS_CAN_FAIL, 24 TORRC_RUNTIME_OPTIONS, 25 TORRC_STARTING_POINT, 26 fail_hard, 27) 28 29log = logging.getLogger(__name__) 30stream_building_lock = RLock() 31 32 33def attach_stream_to_circuit_listener(controller, circ_id): 34 """Returns a function that should be given to add_event_listener(). It 35 looks for newly created streams and attaches them to the given circ_id""" 36 37 def closure_stream_event_listener(st): 38 if st.status == "NEW" and st.purpose == "USER": 39 log.debug( 40 "Attaching stream %s to circ %s %s", 41 st.id, 42 circ_id, 43 circuit_str(controller, circ_id), 44 ) 45 try: 46 controller.attach_stream(st.id, circ_id) 47 # So far we never saw this error. 48 except ( 49 UnsatisfiableRequest, 50 InvalidRequest, 51 OperationFailed, 52 ) as e: 53 log.debug( 54 "Error attaching stream %s to circ %s: %s", 55 st.id, 56 circ_id, 57 e, 58 ) 59 else: 60 pass 61 62 return closure_stream_event_listener 63 64 65def add_event_listener(controller, func, event): 66 try: 67 controller.add_event_listener(func, event) 68 except ProtocolError as e: 69 log.exception("Exception trying to add event listener %s", e) 70 71 72def remove_event_listener(controller, func): 73 try: 74 controller.remove_event_listener(func) 75 except SocketClosed as e: 76 if not settings.end_event.is_set(): 77 log.debug(e) 78 else: 79 log.exception(e) 80 except ProtocolError as e: 81 log.exception("Exception trying to remove event %s", e) 82 83 84def init_controller(conf): 85 c = None 86 # If the external control port is set, use it to initialize the controller. 87 control_port = conf["tor"]["external_control_port"] 88 if control_port: 89 control_port = int(control_port) 90 # If it can not connect, the program will exit here 91 c = _init_controller_port(control_port) 92 # There is no configuration for external control socket, therefore do not 93 # attempt to connect to the control socket. 94 return c 95 96 97def is_bootstrapped(c): 98 try: 99 line = c.get_info("status/bootstrap-phase") 100 except (ControllerError, InvalidArguments, ProtocolError) as e: 101 log.exception("Error trying to check bootstrap phase %s", e) 102 return False 103 state, _, progress, *_ = line.split() 104 progress = int(progress.split("=")[1]) 105 if state == "NOTICE" and progress == 100: 106 return True 107 log.debug("Not bootstrapped. state={} progress={}".format(state, progress)) 108 return False 109 110 111def _init_controller_port(port): 112 assert isinstance(port, int) 113 try: 114 c = Controller.from_port(port=port) 115 c.authenticate() 116 except (IncorrectSocketType, SocketError): 117 fail_hard("Unable to connect to control port %s.", port) 118 # TODO: Allow for auth via more than just CookieAuthentication 119 log.info("Connected to tor via port %s", port) 120 return c 121 122 123def _init_controller_socket(socket): 124 assert isinstance(socket, str) 125 try: 126 c = Controller.from_socket_file(path=socket) 127 c.authenticate() 128 except (IncorrectSocketType, SocketError): 129 log.debug("Error initting controller socket: socket error.") 130 return None 131 except Exception as e: 132 log.exception("Error initting controller socket: %s", e) 133 return None 134 # TODO: Allow for auth via more than just CookieAuthentication 135 log.info("Connected to tor via socket %s", socket) 136 return c 137 138 139def parse_user_torrc_config(torrc, torrc_text): 140 """Parse the user configuration torrc text call `extra_lines` 141 to a dictionary suitable to use with stem and return a new torrc 142 dictionary that merges that dictionary with the existing torrc. 143 Example:: 144 145 [tor] 146 extra_lines = 147 Log debug file /tmp/tor-debug.log 148 NumCPUs 1 149 """ 150 torrc_dict = torrc.copy() 151 for line in torrc_text.split("\n"): 152 # Remove leading and trailing whitespace, if any 153 line = line.strip() 154 # Ignore blank lines 155 if len(line) < 1: 156 continue 157 # Some torrc options are only a key, some are a key value pair. 158 kv = line.split(None, 1) 159 if len(kv) > 1: 160 key, value = kv 161 else: 162 key = kv[0] 163 value = None 164 # It's really easy to add to the torrc if the key doesn't exist 165 if key not in torrc: 166 torrc_dict.update({key: value}) 167 # But if it does, we have to make a list of values. For example, say 168 # the user wants to add a SocksPort and we already have 169 # 'SocksPort auto' in the torrc. We'll go from 170 # torrc['SocksPort'] == 'auto' 171 # to 172 # torrc['SocksPort'] == ['auto', '9050'] 173 else: 174 existing_val = torrc[key] 175 if isinstance(existing_val, str): 176 torrc_dict.update({key: [existing_val, value]}) 177 else: 178 assert isinstance(existing_val, list) 179 existing_val.append(value) 180 torrc_dict.update({key: existing_val}) 181 log.debug( 182 'Adding "%s %s" to torrc with which we are launching Tor', 183 key, 184 value, 185 ) 186 return torrc_dict 187 188 189def set_torrc_starting_point(controller): 190 """Set the torrc starting point options.""" 191 for k, v in TORRC_STARTING_POINT.items(): 192 try: 193 controller.set_conf(k, v) 194 except (ControllerError, InvalidRequest, InvalidArguments) as e: 195 log.exception("Error setting option %s, %s: %s", k, v, e) 196 exit(1) 197 198 199def set_torrc_runtime_options(controller): 200 """Set torrc options at runtime.""" 201 try: 202 controller.set_options(TORRC_RUNTIME_OPTIONS) 203 # Only the first option that fails will be logged here. 204 # Just log stem's exceptions. 205 except (ControllerError, InvalidRequest, InvalidArguments) as e: 206 log.exception(e) 207 exit(1) 208 209 210def set_torrc_options_can_fail(controller): 211 """Set options that can fail, at runtime. 212 213 They can be set at launch, but since the may fail because they are not 214 supported in some Tor versions, it's easier to try one by one at runtime 215 and ignore the ones that fail. 216 """ 217 for k, v in TORRC_OPTIONS_CAN_FAIL.items(): 218 try: 219 controller.set_conf(k, v) 220 except (InvalidArguments, InvalidRequest) as error: 221 log.debug( 222 "Ignoring option not supported by this Tor version. %s", error 223 ) 224 except ControllerError as e: 225 log.exception(e) 226 exit(1) 227 228 229def launch_tor(conf): 230 os.makedirs(conf.getpath("tor", "datadir"), mode=0o700, exist_ok=True) 231 os.makedirs(conf.getpath("tor", "log"), exist_ok=True) 232 os.makedirs(conf.getpath("tor", "run_dpath"), mode=0o700, exist_ok=True) 233 # Bare minimum things, more or less 234 torrc = copy.deepcopy(TORRC_STARTING_POINT) 235 # Very important and/or common settings that we don't know until runtime 236 # The rest of the settings are in globals.py 237 torrc.update( 238 { 239 "DataDirectory": conf.getpath("tor", "datadir"), 240 "PidFile": conf.getpath("tor", "pid"), 241 "ControlSocket": conf.getpath("tor", "control_socket"), 242 "Log": [ 243 "NOTICE file {}".format( 244 os.path.join(conf.getpath("tor", "log"), "notice.log") 245 ), 246 ], 247 "CircuitBuildTimeout": conf["general"]["circuit_timeout"], 248 } 249 ) 250 251 torrc = parse_user_torrc_config(torrc, conf["tor"]["extra_lines"]) 252 # Finally launch Tor 253 try: 254 # If there is already a tor process running with the same control 255 # socket, this will exit here. 256 stem.process.launch_tor_with_config( 257 torrc, init_msg_handler=log.debug, take_ownership=True 258 ) 259 except Exception as e: 260 fail_hard("Error trying to launch tor: %s", e) 261 log.info("Started own tor.") 262 # And return a controller to it 263 cont = _init_controller_socket(conf.getpath("tor", "control_socket")) 264 # In the case it was not possible to connect to own tor socket. 265 if not cont: 266 fail_hard("Could not connect to own tor control socket.") 267 return cont 268 269 270def launch_or_connect_to_tor(conf): 271 cont = init_controller(conf) 272 if not cont: 273 cont = launch_tor(conf) 274 else: 275 if not is_torrc_starting_point_set(cont): 276 set_torrc_starting_point(cont) 277 # Set options that can fail at runtime 278 set_torrc_options_can_fail(cont) 279 # Set runtime options 280 set_torrc_runtime_options(cont) 281 log.info("Started or connected to Tor %s.", cont.get_version()) 282 return cont 283 284 285def get_socks_info(controller): 286 """Returns the first SocksPort Tor is configured to listen on, in the form 287 of an (address, port) tuple""" 288 try: 289 socks_ports = controller.get_listeners(Listener.SOCKS) 290 return socks_ports[0] 291 except SocketClosed as e: 292 if not settings.end_event.is_set(): 293 log.debug(e) 294 # This might need to return the exception if this happen in other cases 295 # than when stopping the scanner. 296 except ControllerError as e: 297 log.debug(e) 298 299 300def only_relays_with_bandwidth(controller, relays, min_bw=None, max_bw=None): 301 """ 302 Given a list of relays, only return those that optionally have above 303 **min_bw** and optionally have below **max_bw**, inclusively. If neither 304 min_bw nor max_bw are given, essentially just returns the input list of 305 relays. 306 """ 307 assert min_bw is None or min_bw >= 0 308 assert max_bw is None or max_bw >= 0 309 ret = [] 310 for relay in relays: 311 assert hasattr(relay, "consensus_bandwidth") 312 if min_bw is not None and relay.consensus_bandwidth < min_bw: 313 continue 314 if max_bw is not None and relay.consensus_bandwidth > max_bw: 315 continue 316 ret.append(relay) 317 return ret 318 319 320def circuit_str(controller, circ_id): 321 assert isinstance(circ_id, str) 322 int(circ_id) 323 try: 324 circ = controller.get_circuit(circ_id) 325 except ValueError as e: 326 log.warning( 327 "Circuit %s no longer seems to exist so can't return " 328 "a valid circuit string for it: %s", 329 circ_id, 330 e, 331 ) 332 return None 333 # exceptions raised when stopping the scanner 334 except (ControllerError, SocketClosed, socks.GeneralProxyError) as e: 335 log.debug(e) 336 return None 337 return ( 338 "[" 339 + " -> ".join(["{} ({})".format(n, fp[0:8]) for fp, n in circ.path]) 340 + "]" 341 ) 342 343 344def is_torrc_starting_point_set(tor_controller): 345 """Verify that the tor controller has the correct configuration. 346 347 When connecting to a tor controller that has not been launched by sbws, 348 it should have been configured to work with sbws. 349 350 """ 351 bad_options = False 352 torrc = TORRC_STARTING_POINT 353 for k, v in torrc.items(): 354 value_set = tor_controller.get_conf(k) 355 if v != value_set: 356 log.exception( 357 "Incorrectly configured %s, should be %s, is %s", 358 k, 359 v, 360 value_set, 361 ) 362 bad_options = True 363 if not bad_options: 364 log.info("Tor is correctly configured to work with sbws.") 365 return bad_options 366