1# Copyright 2012-2019, Damian Johnson and The Tor Project 2# See LICENSE for licensing information 3 4import io 5import re 6 7import stem 8import stem.control 9import stem.descriptor.router_status_entry 10import stem.prereq 11import stem.response 12import stem.util 13import stem.version 14 15from stem.util import connection, log, str_tools, tor_tools 16 17# Matches keyword=value arguments. This can't be a simple "(.*)=(.*)" pattern 18# because some positional arguments, like circuit paths, can have an equal 19# sign. 20 21KW_ARG = re.compile('^(.*) ([A-Za-z0-9_]+)=(\\S*)$') 22QUOTED_KW_ARG = re.compile('^(.*) ([A-Za-z0-9_]+)="(.*)"$') 23CELL_TYPE = re.compile('^[a-z0-9_]+$') 24PARSE_NEWCONSENSUS_EVENTS = True 25 26# TODO: We can remove the following when we drop python2.6 support. 27 28INT_TYPE = int if stem.prereq.is_python_3() else long 29 30 31class Event(stem.response.ControlMessage): 32 """ 33 Base for events we receive asynchronously, as described in section 4.1 of the 34 `control-spec 35 <https://gitweb.torproject.org/torspec.git/tree/control-spec.txt>`_. 36 37 :var str type: event type 38 :var list positional_args: positional arguments of the event 39 :var dict keyword_args: key/value arguments of the event 40 """ 41 42 _POSITIONAL_ARGS = () # attribute names for recognized positional arguments 43 _KEYWORD_ARGS = {} # map of 'keyword => attribute' for recognized attributes 44 _QUOTED = () # positional arguments that are quoted 45 _OPTIONALLY_QUOTED = () # positional arguments that may or may not be quoted 46 _SKIP_PARSING = False # skip parsing contents into our positional_args and keyword_args 47 _VERSION_ADDED = stem.version.Version('0.1.1.1-alpha') # minimum version with control-spec V1 event support 48 49 def _parse_message(self): 50 if not str(self).strip(): 51 raise stem.ProtocolError('Received a blank tor event. Events must at the very least have a type.') 52 53 self.type = str(self).split()[0] 54 self.positional_args = [] 55 self.keyword_args = {} 56 57 # if we're a recognized event type then translate ourselves into that subclass 58 59 if self.type in EVENT_TYPE_TO_CLASS: 60 self.__class__ = EVENT_TYPE_TO_CLASS[self.type] 61 62 if not self._SKIP_PARSING: 63 self._parse_standard_attr() 64 65 self._parse() 66 67 def __hash__(self): 68 return stem.util._hash_attr(self, 'arrived_at', parent = stem.response.ControlMessage, cache = True) 69 70 def _parse_standard_attr(self): 71 """ 72 Most events are of the form... 73 650 *( positional_args ) *( key "=" value ) 74 75 This parses this standard format, populating our **positional_args** and 76 **keyword_args** attributes and creating attributes if it's in our event's 77 **_POSITIONAL_ARGS** and **_KEYWORD_ARGS**. 78 """ 79 80 # Tor events contain some number of positional arguments followed by 81 # key/value mappings. Parsing keyword arguments from the end until we hit 82 # something that isn't a key/value mapping. The rest are positional. 83 84 content = str(self) 85 86 while True: 87 match = QUOTED_KW_ARG.match(content) 88 89 if not match: 90 match = KW_ARG.match(content) 91 92 if match: 93 content, keyword, value = match.groups() 94 self.keyword_args[keyword] = value 95 else: 96 break 97 98 # Setting attributes for the fields that we recognize. 99 100 self.positional_args = content.split()[1:] 101 positional = list(self.positional_args) 102 103 for attr_name in self._POSITIONAL_ARGS: 104 attr_value = None 105 106 if positional: 107 if attr_name in self._QUOTED or (attr_name in self._OPTIONALLY_QUOTED and positional[0].startswith('"')): 108 attr_values = [positional.pop(0)] 109 110 if not attr_values[0].startswith('"'): 111 raise stem.ProtocolError("The %s value should be quoted, but didn't have a starting quote: %s" % (attr_name, self)) 112 113 while True: 114 if not positional: 115 raise stem.ProtocolError("The %s value should be quoted, but didn't have an ending quote: %s" % (attr_name, self)) 116 117 attr_values.append(positional.pop(0)) 118 119 if attr_values[-1].endswith('"'): 120 break 121 122 attr_value = ' '.join(attr_values)[1:-1] 123 else: 124 attr_value = positional.pop(0) 125 126 setattr(self, attr_name, attr_value) 127 128 for controller_attr_name, attr_name in self._KEYWORD_ARGS.items(): 129 setattr(self, attr_name, self.keyword_args.get(controller_attr_name)) 130 131 def _iso_timestamp(self, timestamp): 132 """ 133 Parses an iso timestamp (ISOTime2Frac in the control-spec). 134 135 :param str timestamp: timestamp to parse 136 137 :returns: **datetime** with the parsed timestamp 138 139 :raises: :class:`stem.ProtocolError` if timestamp is malformed 140 """ 141 142 if timestamp is None: 143 return None 144 145 try: 146 return str_tools._parse_iso_timestamp(timestamp) 147 except ValueError as exc: 148 raise stem.ProtocolError('Unable to parse timestamp (%s): %s' % (exc, self)) 149 150 # method overwritten by our subclasses for special handling that they do 151 def _parse(self): 152 pass 153 154 def _log_if_unrecognized(self, attr, attr_enum): 155 """ 156 Checks if an attribute exists in a given enumeration, logging a message if 157 it isn't. Attributes can either be for a string or collection of strings 158 159 :param str attr: name of the attribute to check 160 :param stem.util.enum.Enum enum: enumeration to check against 161 """ 162 163 attr_values = getattr(self, attr) 164 165 if attr_values: 166 if stem.util._is_str(attr_values): 167 attr_values = [attr_values] 168 169 for value in attr_values: 170 if value not in attr_enum: 171 log_id = 'event.%s.unknown_%s.%s' % (self.type.lower(), attr, value) 172 unrecognized_msg = "%s event had an unrecognized %s (%s). Maybe a new addition to the control protocol? Full Event: '%s'" % (self.type, attr, value, self) 173 log.log_once(log_id, log.INFO, unrecognized_msg) 174 175 176class AddrMapEvent(Event): 177 """ 178 Event that indicates a new address mapping. 179 180 The ADDRMAP event was one of the first Control Protocol V1 events and was 181 introduced in tor version 0.1.1.1-alpha. 182 183 .. versionchanged:: 1.1.0 184 Added the cached attribute. 185 186 :var str hostname: address being resolved 187 :var str destination: destination of the resolution, this is usually an ip, 188 but could be a hostname if TrackHostExits is enabled or **NONE** if the 189 resolution failed 190 :var datetime expiry: expiration time of the resolution in local time 191 :var str error: error code if the resolution failed 192 :var datetime utc_expiry: expiration time of the resolution in UTC 193 :var bool cached: **True** if the resolution will be kept until it expires, 194 **False** otherwise or **None** if undefined 195 """ 196 197 _POSITIONAL_ARGS = ('hostname', 'destination', 'expiry') 198 _KEYWORD_ARGS = { 199 'error': 'error', 200 'EXPIRES': 'utc_expiry', 201 'CACHED': 'cached', 202 } 203 _OPTIONALLY_QUOTED = ('expiry') 204 205 def _parse(self): 206 if self.destination == '<error>': 207 self.destination = None 208 209 if self.expiry is not None: 210 if self.expiry == 'NEVER': 211 self.expiry = None 212 else: 213 try: 214 self.expiry = stem.util.str_tools._parse_timestamp(self.expiry) 215 except ValueError: 216 raise stem.ProtocolError('Unable to parse date in ADDRMAP event: %s' % self) 217 218 if self.utc_expiry is not None: 219 self.utc_expiry = stem.util.str_tools._parse_timestamp(self.utc_expiry) 220 221 if self.cached is not None: 222 if self.cached == 'YES': 223 self.cached = True 224 elif self.cached == 'NO': 225 self.cached = False 226 else: 227 raise stem.ProtocolError("An ADDRMAP event's CACHED mapping can only be 'YES' or 'NO': %s" % self) 228 229 230class AuthDirNewDescEvent(Event): 231 """ 232 Event specific to directory authorities, indicating that we just received new 233 descriptors. The descriptor type contained within this event is unspecified 234 so the descriptor contents are left unparsed. 235 236 The AUTHDIR_NEWDESCS event was introduced in tor version 0.1.1.10-alpha and 237 removed in 0.3.2.1-alpha. (:spec:`6e887ba`) 238 239 .. deprecated:: 1.6.0 240 Tor dropped this event as of version 0.3.2.1. (:spec:`6e887ba`) 241 242 :var stem.AuthDescriptorAction action: what is being done with the descriptor 243 :var str message: explanation of why we chose this action 244 :var str descriptor: content of the descriptor 245 """ 246 247 _SKIP_PARSING = True 248 _VERSION_ADDED = stem.version.Requirement.EVENT_AUTHDIR_NEWDESCS 249 250 def _parse(self): 251 lines = str(self).split('\n') 252 253 if len(lines) < 5: 254 raise stem.ProtocolError("AUTHDIR_NEWDESCS events must contain lines for at least the type, action, message, descriptor, and terminating 'OK'") 255 elif lines[-1] != 'OK': 256 raise stem.ProtocolError("AUTHDIR_NEWDESCS doesn't end with an 'OK'") 257 258 # TODO: For stem 2.0.0 we should consider changing 'descriptor' to a 259 # ServerDescriptor instance. 260 261 self.action = lines[1] 262 self.message = lines[2] 263 self.descriptor = '\n'.join(lines[3:-1]) 264 265 266class BandwidthEvent(Event): 267 """ 268 Event emitted every second with the bytes sent and received by tor. 269 270 The BW event was one of the first Control Protocol V1 events and was 271 introduced in tor version 0.1.1.1-alpha. 272 273 :var int read: bytes received by tor that second 274 :var int written: bytes sent by tor that second 275 """ 276 277 _POSITIONAL_ARGS = ('read', 'written') 278 279 def _parse(self): 280 if not self.read: 281 raise stem.ProtocolError('BW event is missing its read value') 282 elif not self.written: 283 raise stem.ProtocolError('BW event is missing its written value') 284 elif not self.read.isdigit() or not self.written.isdigit(): 285 raise stem.ProtocolError("A BW event's bytes sent and received should be a positive numeric value, received: %s" % self) 286 287 self.read = INT_TYPE(self.read) 288 self.written = INT_TYPE(self.written) 289 290 291class BuildTimeoutSetEvent(Event): 292 """ 293 Event indicating that the timeout value for a circuit has changed. This was 294 first added in tor version 0.2.2.7. 295 296 The BUILDTIMEOUT_SET event was introduced in tor version 0.2.2.7-alpha. 297 298 :var stem.TimeoutSetType set_type: way in which the timeout is changing 299 :var int total_times: circuit build times tor used to determine the timeout 300 :var int timeout: circuit timeout value in milliseconds 301 :var int xm: Pareto parameter Xm in milliseconds 302 :var float alpha: Pareto parameter alpha 303 :var float quantile: CDF quantile cutoff point 304 :var float timeout_rate: ratio of circuits that have time out 305 :var int close_timeout: duration to keep measurement circuits in milliseconds 306 :var float close_rate: ratio of measurement circuits that are closed 307 """ 308 309 _POSITIONAL_ARGS = ('set_type',) 310 _KEYWORD_ARGS = { 311 'TOTAL_TIMES': 'total_times', 312 'TIMEOUT_MS': 'timeout', 313 'XM': 'xm', 314 'ALPHA': 'alpha', 315 'CUTOFF_QUANTILE': 'quantile', 316 'TIMEOUT_RATE': 'timeout_rate', 317 'CLOSE_MS': 'close_timeout', 318 'CLOSE_RATE': 'close_rate', 319 } 320 _VERSION_ADDED = stem.version.Requirement.EVENT_BUILDTIMEOUT_SET 321 322 def _parse(self): 323 # convert our integer and float parameters 324 325 for param in ('total_times', 'timeout', 'xm', 'close_timeout'): 326 param_value = getattr(self, param) 327 328 if param_value is not None: 329 try: 330 setattr(self, param, int(param_value)) 331 except ValueError: 332 raise stem.ProtocolError('The %s of a BUILDTIMEOUT_SET should be an integer: %s' % (param, self)) 333 334 for param in ('alpha', 'quantile', 'timeout_rate', 'close_rate'): 335 param_value = getattr(self, param) 336 337 if param_value is not None: 338 try: 339 setattr(self, param, float(param_value)) 340 except ValueError: 341 raise stem.ProtocolError('The %s of a BUILDTIMEOUT_SET should be a float: %s' % (param, self)) 342 343 self._log_if_unrecognized('set_type', stem.TimeoutSetType) 344 345 346class CircuitEvent(Event): 347 """ 348 Event that indicates that a circuit has changed. 349 350 The fingerprint or nickname values in our 'path' may be **None** if the 351 VERBOSE_NAMES feature isn't enabled. The option was first introduced in tor 352 version 0.1.2.2, and on by default after 0.2.2.1. 353 354 The CIRC event was one of the first Control Protocol V1 events and was 355 introduced in tor version 0.1.1.1-alpha. 356 357 .. versionchanged:: 1.4.0 358 Added the socks_username and socks_password attributes which is used for 359 `stream isolation 360 <https://gitweb.torproject.org/torspec.git/tree/proposals/171-separate-streams.txt>`_. 361 362 :var str id: circuit identifier 363 :var stem.CircStatus status: reported status for the circuit 364 :var tuple path: relays involved in the circuit, these are 365 **(fingerprint, nickname)** tuples 366 :var tuple build_flags: :data:`~stem.CircBuildFlag` attributes 367 governing how the circuit is built 368 :var stem.CircPurpose purpose: purpose that the circuit is intended for 369 :var stem.HiddenServiceState hs_state: status if this is a hidden service circuit 370 :var str rend_query: circuit's rendezvous-point if this is hidden service related 371 :var datetime created: time when the circuit was created or cannibalized 372 :var stem.CircClosureReason reason: reason for the circuit to be closed 373 :var stem.CircClosureReason remote_reason: remote side's reason for the circuit to be closed 374 :var str socks_username: username for using this circuit 375 :var str socks_password: password for using this circuit 376 """ 377 378 _POSITIONAL_ARGS = ('id', 'status', 'path') 379 _KEYWORD_ARGS = { 380 'BUILD_FLAGS': 'build_flags', 381 'PURPOSE': 'purpose', 382 'HS_STATE': 'hs_state', 383 'REND_QUERY': 'rend_query', 384 'TIME_CREATED': 'created', 385 'REASON': 'reason', 386 'REMOTE_REASON': 'remote_reason', 387 'SOCKS_USERNAME': 'socks_username', 388 'SOCKS_PASSWORD': 'socks_password', 389 } 390 391 def _parse(self): 392 self.path = tuple(stem.control._parse_circ_path(self.path)) 393 self.created = self._iso_timestamp(self.created) 394 395 if self.build_flags is not None: 396 self.build_flags = tuple(self.build_flags.split(',')) 397 398 if not tor_tools.is_valid_circuit_id(self.id): 399 raise stem.ProtocolError("Circuit IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.id, self)) 400 401 self._log_if_unrecognized('status', stem.CircStatus) 402 self._log_if_unrecognized('build_flags', stem.CircBuildFlag) 403 self._log_if_unrecognized('purpose', stem.CircPurpose) 404 self._log_if_unrecognized('hs_state', stem.HiddenServiceState) 405 self._log_if_unrecognized('reason', stem.CircClosureReason) 406 self._log_if_unrecognized('remote_reason', stem.CircClosureReason) 407 408 def _compare(self, other, method): 409 # sorting circuit events by their identifier 410 411 if not isinstance(other, CircuitEvent): 412 return False 413 414 my_id = getattr(self, 'id') 415 their_id = getattr(other, 'id') 416 417 return method(my_id, their_id) if my_id != their_id else method(hash(self), hash(other)) 418 419 def __gt__(self, other): 420 return self._compare(other, lambda s, o: s > o) 421 422 def __ge__(self, other): 423 return self._compare(other, lambda s, o: s >= o) 424 425 426class CircMinorEvent(Event): 427 """ 428 Event providing information about minor changes in our circuits. This was 429 first added in tor version 0.2.3.11. 430 431 The CIRC_MINOR event was introduced in tor version 0.2.3.11-alpha. 432 433 :var str id: circuit identifier 434 :var stem.CircEvent event: type of change in the circuit 435 :var tuple path: relays involved in the circuit, these are 436 **(fingerprint, nickname)** tuples 437 :var tuple build_flags: :data:`~stem.CircBuildFlag` attributes 438 governing how the circuit is built 439 :var stem.CircPurpose purpose: purpose that the circuit is intended for 440 :var stem.HiddenServiceState hs_state: status if this is a hidden service circuit 441 :var str rend_query: circuit's rendezvous-point if this is hidden service related 442 :var datetime created: time when the circuit was created or cannibalized 443 :var stem.CircPurpose old_purpose: prior purpose for the circuit 444 :var stem.HiddenServiceState old_hs_state: prior status as a hidden service circuit 445 """ 446 447 _POSITIONAL_ARGS = ('id', 'event', 'path') 448 _KEYWORD_ARGS = { 449 'BUILD_FLAGS': 'build_flags', 450 'PURPOSE': 'purpose', 451 'HS_STATE': 'hs_state', 452 'REND_QUERY': 'rend_query', 453 'TIME_CREATED': 'created', 454 'OLD_PURPOSE': 'old_purpose', 455 'OLD_HS_STATE': 'old_hs_state', 456 } 457 _VERSION_ADDED = stem.version.Requirement.EVENT_CIRC_MINOR 458 459 def _parse(self): 460 self.path = tuple(stem.control._parse_circ_path(self.path)) 461 self.created = self._iso_timestamp(self.created) 462 463 if self.build_flags is not None: 464 self.build_flags = tuple(self.build_flags.split(',')) 465 466 if not tor_tools.is_valid_circuit_id(self.id): 467 raise stem.ProtocolError("Circuit IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.id, self)) 468 469 self._log_if_unrecognized('event', stem.CircEvent) 470 self._log_if_unrecognized('build_flags', stem.CircBuildFlag) 471 self._log_if_unrecognized('purpose', stem.CircPurpose) 472 self._log_if_unrecognized('hs_state', stem.HiddenServiceState) 473 self._log_if_unrecognized('old_purpose', stem.CircPurpose) 474 self._log_if_unrecognized('old_hs_state', stem.HiddenServiceState) 475 476 477class ClientsSeenEvent(Event): 478 """ 479 Periodic event on bridge relays that provides a summary of our users. 480 481 The CLIENTS_SEEN event was introduced in tor version 0.2.1.10-alpha. 482 483 :var datetime start_time: time in UTC that we started collecting these stats 484 :var dict locales: mapping of country codes to a rounded count for the number of users 485 :var dict ip_versions: mapping of ip protocols to a rounded count for the number of users 486 """ 487 488 _KEYWORD_ARGS = { 489 'TimeStarted': 'start_time', 490 'CountrySummary': 'locales', 491 'IPVersions': 'ip_versions', 492 } 493 _VERSION_ADDED = stem.version.Requirement.EVENT_CLIENTS_SEEN 494 495 def _parse(self): 496 if self.start_time is not None: 497 self.start_time = stem.util.str_tools._parse_timestamp(self.start_time) 498 499 if self.locales is not None: 500 locale_to_count = {} 501 502 for entry in self.locales.split(','): 503 if '=' not in entry: 504 raise stem.ProtocolError("The CLIENTS_SEEN's CountrySummary should be a comma separated listing of '<locale>=<count>' mappings: %s" % self) 505 506 locale, count = entry.split('=', 1) 507 508 if len(locale) != 2: 509 raise stem.ProtocolError("Locales should be a two character code, got '%s': %s" % (locale, self)) 510 elif not count.isdigit(): 511 raise stem.ProtocolError('Locale count was non-numeric (%s): %s' % (count, self)) 512 elif locale in locale_to_count: 513 raise stem.ProtocolError("CountrySummary had multiple mappings for '%s': %s" % (locale, self)) 514 515 locale_to_count[locale] = int(count) 516 517 self.locales = locale_to_count 518 519 if self.ip_versions is not None: 520 protocol_to_count = {} 521 522 for entry in self.ip_versions.split(','): 523 if '=' not in entry: 524 raise stem.ProtocolError("The CLIENTS_SEEN's IPVersions should be a comma separated listing of '<protocol>=<count>' mappings: %s" % self) 525 526 protocol, count = entry.split('=', 1) 527 528 if not count.isdigit(): 529 raise stem.ProtocolError('IP protocol count was non-numeric (%s): %s' % (count, self)) 530 531 protocol_to_count[protocol] = int(count) 532 533 self.ip_versions = protocol_to_count 534 535 536class ConfChangedEvent(Event): 537 """ 538 Event that indicates that our configuration changed, either in response to a 539 SETCONF or RELOAD signal. 540 541 The CONF_CHANGED event was introduced in tor version 0.2.3.3-alpha. 542 543 .. deprecated:: 1.7.0 544 Deprecated the *config* attribute. Some tor configuration options (like 545 ExitPolicy) can have multiple values, so a simple 'str => str' mapping 546 meant that we only provided the last. 547 548 .. versionchanged:: 1.7.0 549 Added the changed and unset attributes. 550 551 :var dict changed: mapping of configuration options to a list of their new 552 values 553 :var list unset: configuration options that have been unset 554 """ 555 556 _SKIP_PARSING = True 557 _VERSION_ADDED = stem.version.Requirement.EVENT_CONF_CHANGED 558 559 def _parse(self): 560 self.changed = {} 561 self.unset = [] 562 self.config = {} # TODO: remove in stem 2.0 563 564 # Skip first and last line since they're the header and footer. For 565 # instance... 566 # 567 # 650-CONF_CHANGED 568 # 650-ExitNodes=caerSidi 569 # 650-ExitPolicy 570 # 650-MaxCircuitDirtiness=20 571 # 650 OK 572 573 for line in str(self).splitlines()[1:-1]: 574 if '=' in line: 575 key, value = line.split('=', 1) 576 self.changed.setdefault(key, []).append(value) 577 else: 578 key, value = line, None 579 self.unset.append(key) 580 581 self.config[key] = value 582 583 584class DescChangedEvent(Event): 585 """ 586 Event that indicates that our descriptor has changed. 587 588 The DESCCHANGED event was introduced in tor version 0.1.2.2-alpha. 589 """ 590 591 _VERSION_ADDED = stem.version.Requirement.EVENT_DESCCHANGED 592 593 594class GuardEvent(Event): 595 """ 596 Event that indicates that our guard relays have changed. The 'endpoint' could 597 be either a... 598 599 * fingerprint 600 * 'fingerprint=nickname' pair 601 602 The derived 'endpoint_*' attributes are generally more useful. 603 604 The GUARD event was introduced in tor version 0.1.2.5-alpha. 605 606 :var stem.GuardType guard_type: purpose the guard relay is for 607 :var str endpoint: relay that the event concerns 608 :var str endpoint_fingerprint: endpoint's finterprint 609 :var str endpoint_nickname: endpoint's nickname if it was provided 610 :var stem.GuardStatus status: status of the guard relay 611 """ 612 613 _VERSION_ADDED = stem.version.Requirement.EVENT_GUARD 614 _POSITIONAL_ARGS = ('guard_type', 'endpoint', 'status') 615 616 def _parse(self): 617 self.endpoint_fingerprint = None 618 self.endpoint_nickname = None 619 620 try: 621 self.endpoint_fingerprint, self.endpoint_nickname = \ 622 stem.control._parse_circ_entry(self.endpoint) 623 except stem.ProtocolError: 624 raise stem.ProtocolError("GUARD's endpoint doesn't match a ServerSpec: %s" % self) 625 626 self._log_if_unrecognized('guard_type', stem.GuardType) 627 self._log_if_unrecognized('status', stem.GuardStatus) 628 629 630class HSDescEvent(Event): 631 """ 632 Event triggered when we fetch a hidden service descriptor that currently isn't in our cache. 633 634 The HS_DESC event was introduced in tor version 0.2.5.2-alpha. 635 636 .. versionadded:: 1.2.0 637 638 .. versionchanged:: 1.3.0 639 Added the reason attribute. 640 641 .. versionchanged:: 1.5.0 642 Added the replica attribute. 643 644 .. versionchanged:: 1.7.0 645 Added the index attribute. 646 647 :var stem.HSDescAction action: what is happening with the descriptor 648 :var str address: hidden service address 649 :var stem.HSAuth authentication: service's authentication method 650 :var str directory: hidden service directory servicing the request 651 :var str directory_fingerprint: hidden service directory's finterprint 652 :var str directory_nickname: hidden service directory's nickname if it was provided 653 :var str descriptor_id: descriptor identifier 654 :var stem.HSDescReason reason: reason the descriptor failed to be fetched 655 :var int replica: replica number the descriptor involves 656 :var str index: computed index of the HSDir the descriptor was uploaded to or fetched from 657 """ 658 659 _VERSION_ADDED = stem.version.Requirement.EVENT_HS_DESC 660 _POSITIONAL_ARGS = ('action', 'address', 'authentication', 'directory', 'descriptor_id') 661 _KEYWORD_ARGS = {'REASON': 'reason', 'REPLICA': 'replica', 'HSDIR_INDEX': 'index'} 662 663 def _parse(self): 664 self.directory_fingerprint = None 665 self.directory_nickname = None 666 667 if self.directory != 'UNKNOWN': 668 try: 669 self.directory_fingerprint, self.directory_nickname = \ 670 stem.control._parse_circ_entry(self.directory) 671 except stem.ProtocolError: 672 raise stem.ProtocolError("HS_DESC's directory doesn't match a ServerSpec: %s" % self) 673 674 if self.replica is not None: 675 if not self.replica.isdigit(): 676 raise stem.ProtocolError('HS_DESC event got a non-numeric replica count (%s): %s' % (self.replica, self)) 677 678 self.replica = int(self.replica) 679 680 self._log_if_unrecognized('action', stem.HSDescAction) 681 self._log_if_unrecognized('authentication', stem.HSAuth) 682 683 684class HSDescContentEvent(Event): 685 """ 686 Provides the content of hidden service descriptors we fetch. 687 688 The HS_DESC_CONTENT event was introduced in tor version 0.2.7.1-alpha. 689 690 .. versionadded:: 1.4.0 691 692 :var str address: hidden service address 693 :var str descriptor_id: descriptor identifier 694 :var str directory: hidden service directory servicing the request 695 :var str directory_fingerprint: hidden service directory's finterprint 696 :var str directory_nickname: hidden service directory's nickname if it was provided 697 :var stem.descriptor.hidden_service.HiddenServiceDescriptorV2 descriptor: descriptor that was retrieved 698 """ 699 700 _VERSION_ADDED = stem.version.Requirement.EVENT_HS_DESC_CONTENT 701 _POSITIONAL_ARGS = ('address', 'descriptor_id', 'directory') 702 703 def _parse(self): 704 if self.address == 'UNKNOWN': 705 self.address = None 706 707 self.directory_fingerprint = None 708 self.directory_nickname = None 709 710 try: 711 self.directory_fingerprint, self.directory_nickname = \ 712 stem.control._parse_circ_entry(self.directory) 713 except stem.ProtocolError: 714 raise stem.ProtocolError("HS_DESC_CONTENT's directory doesn't match a ServerSpec: %s" % self) 715 716 # skip the first line (our positional arguments) and last ('OK') 717 718 desc_content = str_tools._to_bytes('\n'.join(str(self).splitlines()[1:-1])) 719 self.descriptor = None 720 721 if desc_content: 722 self.descriptor = list(stem.descriptor.hidden_service._parse_file(io.BytesIO(desc_content)))[0] 723 724 725class LogEvent(Event): 726 """ 727 Tor logging event. These are the most visible kind of event since, by 728 default, tor logs at the NOTICE :data:`~stem.Runlevel` to stdout. 729 730 The logging events were some of the first Control Protocol V1 events 731 and were introduced in tor version 0.1.1.1-alpha. 732 733 :var stem.Runlevel runlevel: runlevel of the logged message 734 :var str message: logged message 735 """ 736 737 _SKIP_PARSING = True 738 739 def _parse(self): 740 self.runlevel = self.type 741 self._log_if_unrecognized('runlevel', stem.Runlevel) 742 743 # message is our content, minus the runlevel and ending "OK" if a 744 # multi-line message 745 746 self.message = str(self)[len(self.runlevel) + 1:].rstrip('\nOK') 747 748 749class NetworkStatusEvent(Event): 750 """ 751 Event for when our copy of the consensus has changed. This was introduced in 752 tor version 0.1.2.3. 753 754 The NS event was introduced in tor version 0.1.2.3-alpha. 755 756 :var list desc: :class:`~stem.descriptor.router_status_entry.RouterStatusEntryV3` for the changed descriptors 757 """ 758 759 _SKIP_PARSING = True 760 _VERSION_ADDED = stem.version.Requirement.EVENT_NS 761 762 def _parse(self): 763 content = str(self).lstrip('NS\n').rstrip('\nOK') 764 765 # TODO: For stem 2.0.0 consider changing 'desc' to 'descriptors' to match 766 # our other events. 767 768 self.desc = list(stem.descriptor.router_status_entry._parse_file( 769 io.BytesIO(str_tools._to_bytes(content)), 770 False, 771 entry_class = stem.descriptor.router_status_entry.RouterStatusEntryV3, 772 )) 773 774 775class NetworkLivenessEvent(Event): 776 """ 777 Event for when the network becomes reachable or unreachable. 778 779 The NETWORK_LIVENESS event was introduced in tor version 0.2.7.2-alpha. 780 781 .. versionadded:: 1.5.0 782 783 :var str status: status of the network ('UP', 'DOWN', or possibly other 784 statuses in the future) 785 """ 786 787 _VERSION_ADDED = stem.version.Requirement.EVENT_NETWORK_LIVENESS 788 _POSITIONAL_ARGS = ('status',) 789 790 791class NewConsensusEvent(Event): 792 """ 793 Event for when we have a new consensus. This is similar to 794 :class:`~stem.response.events.NetworkStatusEvent`, except that it contains 795 the whole consensus so anything not listed is implicitly no longer 796 recommended. 797 798 The NEWCONSENSUS event was introduced in tor version 0.2.1.13-alpha. 799 800 .. versionchanged:: 1.6.0 801 Added the consensus_content attribute. 802 803 .. deprecated:: 1.6.0 804 In Stem 2.0 we'll remove the desc attribute, so this event only provides 805 the unparsed consensus. Callers can then parse it if they'd like. To drop 806 parsing before then you can set... 807 808 :: 809 810 stem.response.events.PARSE_NEWCONSENSUS_EVENTS = False 811 812 :var str consensus_content: consensus content 813 :var list desc: :class:`~stem.descriptor.router_status_entry.RouterStatusEntryV3` for the changed descriptors 814 """ 815 816 _SKIP_PARSING = True 817 _VERSION_ADDED = stem.version.Requirement.EVENT_NEWCONSENSUS 818 819 def _parse(self): 820 self.consensus_content = str(self).lstrip('NEWCONSENSUS\n').rstrip('\nOK') 821 822 # TODO: For stem 2.0.0 consider changing 'desc' to 'descriptors' to match 823 # our other events. 824 825 if PARSE_NEWCONSENSUS_EVENTS: 826 self.desc = list(stem.descriptor.router_status_entry._parse_file( 827 io.BytesIO(str_tools._to_bytes(self.consensus_content)), 828 False, 829 entry_class = stem.descriptor.router_status_entry.RouterStatusEntryV3, 830 )) 831 else: 832 self.desc = None 833 834 835class NewDescEvent(Event): 836 """ 837 Event that indicates that a new descriptor is available. 838 839 The fingerprint or nickname values in our 'relays' may be **None** if the 840 VERBOSE_NAMES feature isn't enabled. The option was first introduced in tor 841 version 0.1.2.2, and on by default after 0.2.2.1. 842 843 The NEWDESC event was one of the first Control Protocol V1 events and was 844 introduced in tor version 0.1.1.1-alpha. 845 846 :var tuple relays: **(fingerprint, nickname)** tuples for the relays with 847 new descriptors 848 """ 849 850 def _parse(self): 851 self.relays = tuple([stem.control._parse_circ_entry(entry) for entry in str(self).split()[1:]]) 852 853 854class ORConnEvent(Event): 855 """ 856 Event that indicates a change in a relay connection. The 'endpoint' could be 857 any of several things including a... 858 859 * fingerprint 860 * nickname 861 * 'fingerprint=nickname' pair 862 * address:port 863 864 The derived 'endpoint_*' attributes are generally more useful. 865 866 The ORCONN event was one of the first Control Protocol V1 events and was 867 introduced in tor version 0.1.1.1-alpha. Its id attribute was added in 868 version 0.2.5.2-alpha. 869 870 .. versionchanged:: 1.2.0 871 Added the id attribute. 872 873 :var str id: connection identifier 874 :var str endpoint: relay that the event concerns 875 :var str endpoint_fingerprint: endpoint's finterprint if it was provided 876 :var str endpoint_nickname: endpoint's nickname if it was provided 877 :var str endpoint_address: endpoint's address if it was provided 878 :var int endpoint_port: endpoint's port if it was provided 879 :var stem.ORStatus status: state of the connection 880 :var stem.ORClosureReason reason: reason for the connection to be closed 881 :var int circ_count: number of established and pending circuits 882 """ 883 884 _POSITIONAL_ARGS = ('endpoint', 'status') 885 _KEYWORD_ARGS = { 886 'REASON': 'reason', 887 'NCIRCS': 'circ_count', 888 'ID': 'id', 889 } 890 891 def _parse(self): 892 self.endpoint_fingerprint = None 893 self.endpoint_nickname = None 894 self.endpoint_address = None 895 self.endpoint_port = None 896 897 try: 898 self.endpoint_fingerprint, self.endpoint_nickname = \ 899 stem.control._parse_circ_entry(self.endpoint) 900 except stem.ProtocolError: 901 if ':' not in self.endpoint: 902 raise stem.ProtocolError("ORCONN endpoint is neither a relay nor 'address:port': %s" % self) 903 904 address, port = self.endpoint.rsplit(':', 1) 905 906 if not connection.is_valid_port(port): 907 raise stem.ProtocolError("ORCONN's endpoint location's port is invalid: %s" % self) 908 909 self.endpoint_address = address 910 self.endpoint_port = int(port) 911 912 if self.circ_count is not None: 913 if not self.circ_count.isdigit(): 914 raise stem.ProtocolError('ORCONN event got a non-numeric circuit count (%s): %s' % (self.circ_count, self)) 915 916 self.circ_count = int(self.circ_count) 917 918 if self.id and not tor_tools.is_valid_connection_id(self.id): 919 raise stem.ProtocolError("Connection IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.id, self)) 920 921 self._log_if_unrecognized('status', stem.ORStatus) 922 self._log_if_unrecognized('reason', stem.ORClosureReason) 923 924 925class SignalEvent(Event): 926 """ 927 Event that indicates that tor has received and acted upon a signal being sent 928 to the process. As of tor version 0.2.4.6 the only signals conveyed by this 929 event are... 930 931 * RELOAD 932 * DUMP 933 * DEBUG 934 * NEWNYM 935 * CLEARDNSCACHE 936 937 The SIGNAL event was introduced in tor version 0.2.3.1-alpha. 938 939 :var stem.Signal signal: signal that tor received 940 """ 941 942 _POSITIONAL_ARGS = ('signal',) 943 _VERSION_ADDED = stem.version.Requirement.EVENT_SIGNAL 944 945 def _parse(self): 946 # log if we recieved an unrecognized signal 947 expected_signals = ( 948 stem.Signal.RELOAD, 949 stem.Signal.DUMP, 950 stem.Signal.DEBUG, 951 stem.Signal.NEWNYM, 952 stem.Signal.CLEARDNSCACHE, 953 ) 954 955 self._log_if_unrecognized('signal', expected_signals) 956 957 958class StatusEvent(Event): 959 """ 960 Notification of a change in tor's state. These are generally triggered for 961 the same sort of things as log messages of the NOTICE level or higher. 962 However, unlike :class:`~stem.response.events.LogEvent` these contain well 963 formed data. 964 965 The STATUS_GENERAL, STATUS_CLIENT, STATUS_SERVER events were introduced 966 in tor version 0.1.2.3-alpha. 967 968 :var stem.StatusType status_type: category of the status event 969 :var stem.Runlevel runlevel: runlevel of the logged message 970 :var str action: activity that caused this message 971 :var dict arguments: attributes about the event 972 """ 973 974 _POSITIONAL_ARGS = ('runlevel', 'action') 975 _VERSION_ADDED = stem.version.Requirement.EVENT_STATUS 976 977 def _parse(self): 978 if self.type == 'STATUS_GENERAL': 979 self.status_type = stem.StatusType.GENERAL 980 elif self.type == 'STATUS_CLIENT': 981 self.status_type = stem.StatusType.CLIENT 982 elif self.type == 'STATUS_SERVER': 983 self.status_type = stem.StatusType.SERVER 984 else: 985 raise ValueError("BUG: Unrecognized status type (%s), likely an EVENT_TYPE_TO_CLASS addition without revising how 'status_type' is assigned." % self.type) 986 987 # Just an alias for our parent class' keyword_args since that already 988 # parses these for us. Unlike our other event types Tor commonly supplies 989 # arbitrary key/value pairs for these, so making an alias here to better 990 # draw attention that the StatusEvent will likely have them. 991 992 self.arguments = self.keyword_args 993 994 self._log_if_unrecognized('runlevel', stem.Runlevel) 995 996 997class StreamEvent(Event): 998 """ 999 Event that indicates that a stream has changed. 1000 1001 The STREAM event was one of the first Control Protocol V1 events and was 1002 introduced in tor version 0.1.1.1-alpha. 1003 1004 :var str id: stream identifier 1005 :var stem.StreamStatus status: reported status for the stream 1006 :var str circ_id: circuit that the stream is attached to, this is **None** of 1007 the stream is unattached 1008 :var str target: destination of the stream 1009 :var str target_address: destination address (ip, hostname, or '(Tor_internal)') 1010 :var int target_port: destination port 1011 :var stem.StreamClosureReason reason: reason for the stream to be closed 1012 :var stem.StreamClosureReason remote_reason: remote side's reason for the stream to be closed 1013 :var stem.StreamSource source: origin of the REMAP request 1014 :var str source_addr: requester of the connection 1015 :var str source_address: requester address (ip or hostname) 1016 :var int source_port: requester port 1017 :var stem.StreamPurpose purpose: purpose for the stream 1018 """ 1019 1020 _POSITIONAL_ARGS = ('id', 'status', 'circ_id', 'target') 1021 _KEYWORD_ARGS = { 1022 'REASON': 'reason', 1023 'REMOTE_REASON': 'remote_reason', 1024 'SOURCE': 'source', 1025 'SOURCE_ADDR': 'source_addr', 1026 'PURPOSE': 'purpose', 1027 } 1028 1029 def _parse(self): 1030 if self.target is None: 1031 raise stem.ProtocolError("STREAM event didn't have a target: %s" % self) 1032 else: 1033 if ':' not in self.target: 1034 raise stem.ProtocolError("Target location must be of the form 'address:port': %s" % self) 1035 1036 address, port = self.target.rsplit(':', 1) 1037 1038 if not connection.is_valid_port(port, allow_zero = True): 1039 raise stem.ProtocolError("Target location's port is invalid: %s" % self) 1040 1041 self.target_address = address 1042 self.target_port = int(port) 1043 1044 if self.source_addr is None: 1045 self.source_address = None 1046 self.source_port = None 1047 else: 1048 if ':' not in self.source_addr: 1049 raise stem.ProtocolError("Source location must be of the form 'address:port': %s" % self) 1050 1051 address, port = self.source_addr.rsplit(':', 1) 1052 1053 if not connection.is_valid_port(port, allow_zero = True): 1054 raise stem.ProtocolError("Source location's port is invalid: %s" % self) 1055 1056 self.source_address = address 1057 self.source_port = int(port) 1058 1059 # spec specifies a circ_id of zero if the stream is unattached 1060 1061 if self.circ_id == '0': 1062 self.circ_id = None 1063 1064 self._log_if_unrecognized('reason', stem.StreamClosureReason) 1065 self._log_if_unrecognized('remote_reason', stem.StreamClosureReason) 1066 self._log_if_unrecognized('purpose', stem.StreamPurpose) 1067 1068 1069class StreamBwEvent(Event): 1070 """ 1071 Event (emitted approximately every second) with the bytes sent and received 1072 by the application since the last such event on this stream. 1073 1074 The STREAM_BW event was introduced in tor version 0.1.2.8-beta. 1075 1076 .. versionchanged:: 1.6.0 1077 Added the time attribute. 1078 1079 :var str id: stream identifier 1080 :var int written: bytes sent by the application 1081 :var int read: bytes received by the application 1082 :var datetime time: time when the measurement was recorded 1083 """ 1084 1085 _POSITIONAL_ARGS = ('id', 'written', 'read', 'time') 1086 _VERSION_ADDED = stem.version.Requirement.EVENT_STREAM_BW 1087 1088 def _parse(self): 1089 if not tor_tools.is_valid_stream_id(self.id): 1090 raise stem.ProtocolError("Stream IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.id, self)) 1091 elif not self.written: 1092 raise stem.ProtocolError('STREAM_BW event is missing its written value') 1093 elif not self.read: 1094 raise stem.ProtocolError('STREAM_BW event is missing its read value') 1095 elif not self.read.isdigit() or not self.written.isdigit(): 1096 raise stem.ProtocolError("A STREAM_BW event's bytes sent and received should be a positive numeric value, received: %s" % self) 1097 1098 self.read = INT_TYPE(self.read) 1099 self.written = INT_TYPE(self.written) 1100 self.time = self._iso_timestamp(self.time) 1101 1102 1103class TransportLaunchedEvent(Event): 1104 """ 1105 Event triggered when a pluggable transport is launched. 1106 1107 The TRANSPORT_LAUNCHED event was introduced in tor version 0.2.5.0-alpha. 1108 1109 .. versionadded:: 1.1.0 1110 1111 :var str type: 'server' or 'client' 1112 :var str name: name of the pluggable transport 1113 :var str address: IPv4 or IPv6 address where the transport is listening for 1114 connections 1115 :var int port: port where the transport is listening for connections 1116 """ 1117 1118 _POSITIONAL_ARGS = ('type', 'name', 'address', 'port') 1119 _VERSION_ADDED = stem.version.Requirement.EVENT_TRANSPORT_LAUNCHED 1120 1121 def _parse(self): 1122 if self.type not in ('server', 'client'): 1123 raise stem.ProtocolError("Transport type should either be 'server' or 'client': %s" % self) 1124 1125 if not connection.is_valid_ipv4_address(self.address) and \ 1126 not connection.is_valid_ipv6_address(self.address): 1127 raise stem.ProtocolError("Transport address isn't a valid IPv4 or IPv6 address: %s" % self) 1128 1129 if not connection.is_valid_port(self.port): 1130 raise stem.ProtocolError('Transport port is invalid: %s' % self) 1131 1132 self.port = int(self.port) 1133 1134 1135class ConnectionBandwidthEvent(Event): 1136 """ 1137 Event emitted every second with the bytes sent and received by tor on a 1138 per-connection basis. 1139 1140 The CONN_BW event was introduced in tor version 0.2.5.2-alpha. 1141 1142 .. versionadded:: 1.2.0 1143 1144 .. versionchanged:: 1.6.0 1145 Renamed 'type' attribute to 'conn_type' so it wouldn't be override parent 1146 class attribute with the same name. 1147 1148 :var str id: connection identifier 1149 :var stem.ConnectionType conn_type: connection type 1150 :var int read: bytes received by tor that second 1151 :var int written: bytes sent by tor that second 1152 """ 1153 1154 _KEYWORD_ARGS = { 1155 'ID': 'id', 1156 'TYPE': 'conn_type', 1157 'READ': 'read', 1158 'WRITTEN': 'written', 1159 } 1160 1161 _VERSION_ADDED = stem.version.Requirement.EVENT_CONN_BW 1162 1163 def _parse(self): 1164 if not self.id: 1165 raise stem.ProtocolError('CONN_BW event is missing its id') 1166 elif not self.conn_type: 1167 raise stem.ProtocolError('CONN_BW event is missing its connection type') 1168 elif not self.read: 1169 raise stem.ProtocolError('CONN_BW event is missing its read value') 1170 elif not self.written: 1171 raise stem.ProtocolError('CONN_BW event is missing its written value') 1172 elif not self.read.isdigit() or not self.written.isdigit(): 1173 raise stem.ProtocolError("A CONN_BW event's bytes sent and received should be a positive numeric value, received: %s" % self) 1174 elif not tor_tools.is_valid_connection_id(self.id): 1175 raise stem.ProtocolError("Connection IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.id, self)) 1176 1177 self.read = INT_TYPE(self.read) 1178 self.written = INT_TYPE(self.written) 1179 1180 self._log_if_unrecognized('conn_type', stem.ConnectionType) 1181 1182 1183class CircuitBandwidthEvent(Event): 1184 """ 1185 Event emitted every second with the bytes sent and received by tor on a 1186 per-circuit basis. 1187 1188 The CIRC_BW event was introduced in tor version 0.2.5.2-alpha. 1189 1190 .. versionadded:: 1.2.0 1191 1192 .. versionchanged:: 1.6.0 1193 Added the time attribute. 1194 1195 .. versionchanged:: 1.7.0 1196 Added the delivered_read, delivered_written, overhead_read, and 1197 overhead_written attributes. 1198 1199 :var str id: circuit identifier 1200 :var int read: bytes received by tor that second 1201 :var int written: bytes sent by tor that second 1202 :var int delivered_read: user payload received by tor that second 1203 :var int delivered_written: user payload sent by tor that second 1204 :var int overhead_read: padding so read cells will have a fixed length 1205 :var int overhead_written: padding so written cells will have a fixed length 1206 :var datetime time: time when the measurement was recorded 1207 """ 1208 1209 _KEYWORD_ARGS = { 1210 'ID': 'id', 1211 'READ': 'read', 1212 'WRITTEN': 'written', 1213 'DELIVERED_READ': 'delivered_read', 1214 'DELIVERED_WRITTEN': 'delivered_written', 1215 'OVERHEAD_READ': 'overhead_read', 1216 'OVERHEAD_WRITTEN': 'overhead_written', 1217 'TIME': 'time', 1218 } 1219 1220 _VERSION_ADDED = stem.version.Requirement.EVENT_CIRC_BW 1221 1222 def _parse(self): 1223 if not self.id: 1224 raise stem.ProtocolError('CIRC_BW event is missing its id') 1225 elif not self.read: 1226 raise stem.ProtocolError('CIRC_BW event is missing its read value') 1227 elif not self.written: 1228 raise stem.ProtocolError('CIRC_BW event is missing its written value') 1229 elif not self.read.isdigit(): 1230 raise stem.ProtocolError("A CIRC_BW event's bytes received should be a positive numeric value, received: %s" % self) 1231 elif not self.written.isdigit(): 1232 raise stem.ProtocolError("A CIRC_BW event's bytes sent should be a positive numeric value, received: %s" % self) 1233 elif self.delivered_read and not self.delivered_read.isdigit(): 1234 raise stem.ProtocolError("A CIRC_BW event's delivered bytes received should be a positive numeric value, received: %s" % self) 1235 elif self.delivered_written and not self.delivered_written.isdigit(): 1236 raise stem.ProtocolError("A CIRC_BW event's delivered bytes sent should be a positive numeric value, received: %s" % self) 1237 elif self.overhead_read and not self.overhead_read.isdigit(): 1238 raise stem.ProtocolError("A CIRC_BW event's overhead bytes received should be a positive numeric value, received: %s" % self) 1239 elif self.overhead_written and not self.overhead_written.isdigit(): 1240 raise stem.ProtocolError("A CIRC_BW event's overhead bytes sent should be a positive numeric value, received: %s" % self) 1241 elif not tor_tools.is_valid_circuit_id(self.id): 1242 raise stem.ProtocolError("Circuit IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.id, self)) 1243 1244 self.time = self._iso_timestamp(self.time) 1245 1246 for attr in ('read', 'written', 'delivered_read', 'delivered_written', 'overhead_read', 'overhead_written'): 1247 value = getattr(self, attr) 1248 1249 if value: 1250 setattr(self, attr, INT_TYPE(value)) 1251 1252 1253class CellStatsEvent(Event): 1254 """ 1255 Event emitted every second with a count of the number of cells types broken 1256 down by the circuit. **These events are only emitted if TestingTorNetwork is 1257 set.** 1258 1259 The CELL_STATS event was introduced in tor version 0.2.5.2-alpha. 1260 1261 .. versionadded:: 1.2.0 1262 1263 :var str id: circuit identifier 1264 :var str inbound_queue: inbound queue identifier 1265 :var str inbound_connection: inbound connection identifier 1266 :var dict inbound_added: mapping of added inbound cell types to their count 1267 :var dict inbound_removed: mapping of removed inbound cell types to their count 1268 :var dict inbound_time: mapping of inbound cell types to the time they took to write in milliseconds 1269 :var str outbound_queue: outbound queue identifier 1270 :var str outbound_connection: outbound connection identifier 1271 :var dict outbound_added: mapping of added outbound cell types to their count 1272 :var dict outbound_removed: mapping of removed outbound cell types to their count 1273 :var dict outbound_time: mapping of outbound cell types to the time they took to write in milliseconds 1274 """ 1275 1276 _KEYWORD_ARGS = { 1277 'ID': 'id', 1278 'InboundQueue': 'inbound_queue', 1279 'InboundConn': 'inbound_connection', 1280 'InboundAdded': 'inbound_added', 1281 'InboundRemoved': 'inbound_removed', 1282 'InboundTime': 'inbound_time', 1283 'OutboundQueue': 'outbound_queue', 1284 'OutboundConn': 'outbound_connection', 1285 'OutboundAdded': 'outbound_added', 1286 'OutboundRemoved': 'outbound_removed', 1287 'OutboundTime': 'outbound_time', 1288 } 1289 1290 _VERSION_ADDED = stem.version.Requirement.EVENT_CELL_STATS 1291 1292 def _parse(self): 1293 if self.id and not tor_tools.is_valid_circuit_id(self.id): 1294 raise stem.ProtocolError("Circuit IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.id, self)) 1295 elif self.inbound_queue and not tor_tools.is_valid_circuit_id(self.inbound_queue): 1296 raise stem.ProtocolError("Queue IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.inbound_queue, self)) 1297 elif self.inbound_connection and not tor_tools.is_valid_connection_id(self.inbound_connection): 1298 raise stem.ProtocolError("Connection IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.inbound_connection, self)) 1299 elif self.outbound_queue and not tor_tools.is_valid_circuit_id(self.outbound_queue): 1300 raise stem.ProtocolError("Queue IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.outbound_queue, self)) 1301 elif self.outbound_connection and not tor_tools.is_valid_connection_id(self.outbound_connection): 1302 raise stem.ProtocolError("Connection IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.outbound_connection, self)) 1303 1304 self.inbound_added = _parse_cell_type_mapping(self.inbound_added) 1305 self.inbound_removed = _parse_cell_type_mapping(self.inbound_removed) 1306 self.inbound_time = _parse_cell_type_mapping(self.inbound_time) 1307 self.outbound_added = _parse_cell_type_mapping(self.outbound_added) 1308 self.outbound_removed = _parse_cell_type_mapping(self.outbound_removed) 1309 self.outbound_time = _parse_cell_type_mapping(self.outbound_time) 1310 1311 1312class TokenBucketEmptyEvent(Event): 1313 """ 1314 Event emitted when refilling an empty token bucket. **These events are only 1315 emitted if TestingTorNetwork is set.** 1316 1317 The TB_EMPTY event was introduced in tor version 0.2.5.2-alpha. 1318 1319 .. versionadded:: 1.2.0 1320 1321 :var stem.TokenBucket bucket: bucket being refilled 1322 :var str id: connection identifier 1323 :var int read: time in milliseconds since the read bucket was last refilled 1324 :var int written: time in milliseconds since the write bucket was last refilled 1325 :var int last_refill: time in milliseconds the bucket has been empty since last refilled 1326 """ 1327 1328 _POSITIONAL_ARGS = ('bucket',) 1329 _KEYWORD_ARGS = { 1330 'ID': 'id', 1331 'READ': 'read', 1332 'WRITTEN': 'written', 1333 'LAST': 'last_refill', 1334 } 1335 1336 _VERSION_ADDED = stem.version.Requirement.EVENT_TB_EMPTY 1337 1338 def _parse(self): 1339 if self.id and not tor_tools.is_valid_connection_id(self.id): 1340 raise stem.ProtocolError("Connection IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.id, self)) 1341 elif not self.read.isdigit(): 1342 raise stem.ProtocolError("A TB_EMPTY's READ value should be a positive numeric value, received: %s" % self) 1343 elif not self.written.isdigit(): 1344 raise stem.ProtocolError("A TB_EMPTY's WRITTEN value should be a positive numeric value, received: %s" % self) 1345 elif not self.last_refill.isdigit(): 1346 raise stem.ProtocolError("A TB_EMPTY's LAST value should be a positive numeric value, received: %s" % self) 1347 1348 self.read = int(self.read) 1349 self.written = int(self.written) 1350 self.last_refill = int(self.last_refill) 1351 1352 self._log_if_unrecognized('bucket', stem.TokenBucket) 1353 1354 1355def _parse_cell_type_mapping(mapping): 1356 """ 1357 Parses a mapping of the form... 1358 1359 key1:value1,key2:value2... 1360 1361 ... in which keys are strings and values are integers. 1362 1363 :param str mapping: value to be parsed 1364 1365 :returns: dict of **str => int** mappings 1366 1367 :rasies: **stem.ProtocolError** if unable to parse the mapping 1368 """ 1369 1370 if mapping is None: 1371 return None 1372 1373 results = {} 1374 1375 for entry in mapping.split(','): 1376 if ':' not in entry: 1377 raise stem.ProtocolError("Mappings are expected to be of the form 'key:value', got '%s': %s" % (entry, mapping)) 1378 1379 key, value = entry.rsplit(':', 1) 1380 1381 if not CELL_TYPE.match(key): 1382 raise stem.ProtocolError("Key had invalid characters, got '%s': %s" % (key, mapping)) 1383 elif not value.isdigit(): 1384 raise stem.ProtocolError("Values should just be integers, got '%s': %s" % (value, mapping)) 1385 1386 results[key] = int(value) 1387 1388 return results 1389 1390 1391EVENT_TYPE_TO_CLASS = { 1392 'ADDRMAP': AddrMapEvent, 1393 'AUTHDIR_NEWDESCS': AuthDirNewDescEvent, 1394 'BUILDTIMEOUT_SET': BuildTimeoutSetEvent, 1395 'BW': BandwidthEvent, 1396 'CELL_STATS': CellStatsEvent, 1397 'CIRC': CircuitEvent, 1398 'CIRC_BW': CircuitBandwidthEvent, 1399 'CIRC_MINOR': CircMinorEvent, 1400 'CLIENTS_SEEN': ClientsSeenEvent, 1401 'CONF_CHANGED': ConfChangedEvent, 1402 'CONN_BW': ConnectionBandwidthEvent, 1403 'DEBUG': LogEvent, 1404 'DESCCHANGED': DescChangedEvent, 1405 'ERR': LogEvent, 1406 'GUARD': GuardEvent, 1407 'HS_DESC': HSDescEvent, 1408 'HS_DESC_CONTENT': HSDescContentEvent, 1409 'INFO': LogEvent, 1410 'NETWORK_LIVENESS': NetworkLivenessEvent, 1411 'NEWCONSENSUS': NewConsensusEvent, 1412 'NEWDESC': NewDescEvent, 1413 'NOTICE': LogEvent, 1414 'NS': NetworkStatusEvent, 1415 'ORCONN': ORConnEvent, 1416 'SIGNAL': SignalEvent, 1417 'STATUS_CLIENT': StatusEvent, 1418 'STATUS_GENERAL': StatusEvent, 1419 'STATUS_SERVER': StatusEvent, 1420 'STREAM': StreamEvent, 1421 'STREAM_BW': StreamBwEvent, 1422 'TB_EMPTY': TokenBucketEmptyEvent, 1423 'TRANSPORT_LAUNCHED': TransportLaunchedEvent, 1424 'WARN': LogEvent, 1425 1426 # accounting for a bug in tor 0.2.0.22 1427 'STATUS_SEVER': StatusEvent, 1428} 1429