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