1# Copyright 2011-2019, Damian Johnson and The Tor Project
2# See LICENSE for licensing information
3
4"""
5Tor versioning information and requirements for its features. These can be
6easily parsed and compared, for instance...
7
8::
9
10  >>> from stem.version import get_system_tor_version, Requirement
11  >>> my_version = get_system_tor_version()
12  >>> print(my_version)
13  0.2.1.30
14  >>> my_version >= Requirement.TORRC_CONTROL_SOCKET
15  True
16
17**Module Overview:**
18
19::
20
21  get_system_tor_version - gets the version of our system's tor installation
22
23  Version - Tor versioning information
24
25.. data:: Requirement (enum)
26
27  Enumerations for the version requirements of features.
28
29  .. deprecated:: 1.6.0
30     Requirement entries belonging to tor versions which have been obsolete for
31     at least six months will be removed when we break backward compatibility
32     in the 2.x stem release.
33
34  ===================================== ===========
35  Requirement                           Description
36  ===================================== ===========
37  **AUTH_SAFECOOKIE**                   SAFECOOKIE authentication method
38  **DESCRIPTOR_COMPRESSION**            `Expanded compression support for ZSTD and LZMA <https://gitweb.torproject.org/torspec.git/commit/?id=1cb56afdc1e55e303e3e6b69e90d983ee217d93f>`_
39  **DORMANT_MODE**                      **DORMANT** and **ACTIVE** :data:`~stem.Signal`
40  **DROPGUARDS**                        DROPGUARDS requests
41  **EVENT_AUTHDIR_NEWDESCS**            AUTHDIR_NEWDESC events
42  **EVENT_BUILDTIMEOUT_SET**            BUILDTIMEOUT_SET events
43  **EVENT_CIRC_MINOR**                  CIRC_MINOR events
44  **EVENT_CLIENTS_SEEN**                CLIENTS_SEEN events
45  **EVENT_CONF_CHANGED**                CONF_CHANGED events
46  **EVENT_DESCCHANGED**                 DESCCHANGED events
47  **EVENT_GUARD**                       GUARD events
48  **EVENT_HS_DESC_CONTENT**             HS_DESC_CONTENT events
49  **EVENT_NETWORK_LIVENESS**            NETWORK_LIVENESS events
50  **EVENT_NEWCONSENSUS**                NEWCONSENSUS events
51  **EVENT_NS**                          NS events
52  **EVENT_SIGNAL**                      SIGNAL events
53  **EVENT_STATUS**                      STATUS_GENERAL, STATUS_CLIENT, and STATUS_SERVER events
54  **EVENT_STREAM_BW**                   STREAM_BW events
55  **EVENT_TRANSPORT_LAUNCHED**          TRANSPORT_LAUNCHED events
56  **EVENT_CONN_BW**                     CONN_BW events
57  **EVENT_CIRC_BW**                     CIRC_BW events
58  **EVENT_CELL_STATS**                  CELL_STATS events
59  **EVENT_TB_EMPTY**                    TB_EMPTY events
60  **EVENT_HS_DESC**                     HS_DESC events
61  **EXTENDCIRCUIT_PATH_OPTIONAL**       EXTENDCIRCUIT queries can omit the path if the circuit is zero
62  **FEATURE_EXTENDED_EVENTS**           'EXTENDED_EVENTS' optional feature
63  **FEATURE_VERBOSE_NAMES**             'VERBOSE_NAMES' optional feature
64  **GETINFO_CONFIG_TEXT**               'GETINFO config-text' query
65  **GETINFO_GEOIP_AVAILABLE**           'GETINFO ip-to-country/ipv4-available' query and its ipv6 counterpart
66  **GETINFO_MICRODESCRIPTORS**          'GETINFO md/all' query
67  **GETINFO_UPTIME**                    'GETINFO uptime' query
68  **HIDDEN_SERVICE_V3**                 Support for v3 hidden services
69  **HSFETCH**                           HSFETCH requests
70  **HSFETCH_V3**                        HSFETCH for version 3 hidden services
71  **HSPOST**                            HSPOST requests
72  **ADD_ONION**                         ADD_ONION and DEL_ONION requests
73  **ADD_ONION_BASIC_AUTH**              ADD_ONION supports basic authentication
74  **ADD_ONION_NON_ANONYMOUS**           ADD_ONION supports non-anonymous mode
75  **ADD_ONION_MAX_STREAMS**             ADD_ONION support for MaxStreamsCloseCircuit
76  **LOADCONF**                          LOADCONF requests
77  **MICRODESCRIPTOR_IS_DEFAULT**        Tor gets microdescriptors by default rather than server descriptors
78  **SAVECONF_FORCE**                    Added the 'FORCE' flag to SAVECONF
79  **TAKEOWNERSHIP**                     TAKEOWNERSHIP requests
80  **TORRC_CONTROL_SOCKET**              'ControlSocket <path>' config option
81  **TORRC_PORT_FORWARDING**             'PortForwarding' config option
82  **TORRC_DISABLE_DEBUGGER_ATTACHMENT** 'DisableDebuggerAttachment' config option
83  **TORRC_VIA_STDIN**                   Allow torrc options via 'tor -f -' (:trac:`13865`)
84  ===================================== ===========
85"""
86
87import os
88import re
89
90import stem.prereq
91import stem.util
92import stem.util.enum
93import stem.util.system
94
95if stem.prereq._is_lru_cache_available():
96  from functools import lru_cache
97else:
98  from stem.util.lru_cache import lru_cache
99
100# cache for the get_system_tor_version function
101VERSION_CACHE = {}
102
103VERSION_PATTERN = re.compile(r'^([0-9]+)\.([0-9]+)\.([0-9]+)(\.[0-9]+)?(-\S*)?(( \(\S*\))*)$')
104
105
106def get_system_tor_version(tor_cmd = 'tor'):
107  """
108  Queries tor for its version. This is os dependent, only working on linux,
109  osx, and bsd.
110
111  :param str tor_cmd: command used to run tor
112
113  :returns: :class:`~stem.version.Version` provided by the tor command
114
115  :raises: **IOError** if unable to query or parse the version
116  """
117
118  if tor_cmd not in VERSION_CACHE:
119    version_cmd = '%s --version' % tor_cmd
120
121    try:
122      version_output = stem.util.system.call(version_cmd)
123    except OSError as exc:
124      # make the error message nicer if this is due to tor being unavialable
125
126      if 'No such file or directory' in str(exc):
127        if os.path.isabs(tor_cmd):
128          exc = "Unable to check tor's version. '%s' doesn't exist." % tor_cmd
129        else:
130          exc = "Unable to run '%s'. Maybe tor isn't in your PATH?" % version_cmd
131
132      raise IOError(exc)
133
134    for line in version_output:
135      # output example:
136      # Oct 21 07:19:27.438 [notice] Tor v0.2.1.30. This is experimental software. Do not rely on it for strong anonymity. (Running on Linux i686)
137      # Tor version 0.2.1.30.
138
139      if line.startswith('Tor version ') and line.endswith('.'):
140        try:
141          version_str = line[12:-1]
142          VERSION_CACHE[tor_cmd] = Version(version_str)
143          break
144        except ValueError as exc:
145          raise IOError(exc)
146
147    if tor_cmd not in VERSION_CACHE:
148      raise IOError("'%s' didn't provide a parseable version:\n\n%s" % (version_cmd, '\n'.join(version_output)))
149
150  return VERSION_CACHE[tor_cmd]
151
152
153@lru_cache()
154def _get_version(version_str):
155  return Version(version_str)
156
157
158class Version(object):
159  """
160  Comparable tor version. These are constructed from strings that conform to
161  the 'new' style in the `tor version-spec
162  <https://gitweb.torproject.org/torspec.git/tree/version-spec.txt>`_,
163  such as "0.1.4" or "0.2.2.23-alpha (git-7dcd105be34a4f44)".
164
165  .. versionchanged:: 1.6.0
166     Added all_extra parameter.
167
168  :var int major: major version
169  :var int minor: minor version
170  :var int micro: micro version
171  :var int patch: patch level (**None** if undefined)
172  :var str status: status tag such as 'alpha' or 'beta-dev' (**None** if undefined)
173  :var str extra: first extra information without its parentheses such as
174    'git-8be6058d8f31e578' (**None** if undefined)
175  :var list all_extra: all extra information entries, without their parentheses
176  :var str git_commit: git commit id (**None** if it wasn't provided)
177
178  :param str version_str: version to be parsed
179
180  :raises: **ValueError** if input isn't a valid tor version
181  """
182
183  def __init__(self, version_str):
184    self.version_str = version_str
185    version_parts = VERSION_PATTERN.match(version_str)
186
187    if version_parts:
188      major, minor, micro, patch, status, extra_str, _ = version_parts.groups()
189
190      # The patch and status matches are optional (may be None) and have an extra
191      # proceeding period or dash if they exist. Stripping those off.
192
193      if patch:
194        patch = int(patch[1:])
195
196      if status:
197        status = status[1:]
198
199      self.major = int(major)
200      self.minor = int(minor)
201      self.micro = int(micro)
202      self.patch = patch
203      self.status = status
204      self.all_extra = [entry[1:-1] for entry in extra_str.strip().split()] if extra_str else []
205      self.extra = self.all_extra[0] if self.all_extra else None
206      self.git_commit = None
207
208      for extra in self.all_extra:
209        if extra and re.match('^git-[0-9a-f]{16}$', extra):
210          self.git_commit = extra[4:]
211          break
212    else:
213      raise ValueError("'%s' isn't a properly formatted tor version" % version_str)
214
215  def __str__(self):
216    """
217    Provides the string used to construct the version.
218    """
219
220    return self.version_str
221
222  def _compare(self, other, method):
223    """
224    Compares version ordering according to the spec.
225    """
226
227    if not isinstance(other, Version):
228      return False
229
230    for attr in ('major', 'minor', 'micro', 'patch'):
231      my_version = getattr(self, attr)
232      other_version = getattr(other, attr)
233
234      if my_version is None:
235        my_version = 0
236
237      if other_version is None:
238        other_version = 0
239
240      if my_version != other_version:
241        return method(my_version, other_version)
242
243    # According to the version spec...
244    #
245    #   If we *do* encounter two versions that differ only by status tag, we
246    #   compare them lexically as ASCII byte strings.
247
248    my_status = self.status if self.status else ''
249    other_status = other.status if other.status else ''
250
251    return method(my_status, other_status)
252
253  def __hash__(self):
254    return stem.util._hash_attr(self, 'major', 'minor', 'micro', 'patch', 'status', cache = True)
255
256  def __eq__(self, other):
257    return self._compare(other, lambda s, o: s == o)
258
259  def __ne__(self, other):
260    return not self == other
261
262  def __gt__(self, other):
263    """
264    Checks if this version meets the requirements for a given feature. We can
265    be compared to either a :class:`~stem.version.Version` or
266    :class:`~stem.version._VersionRequirements`.
267    """
268
269    if isinstance(other, _VersionRequirements):
270      for rule in other.rules:
271        if rule(self):
272          return True
273
274      return False
275
276    return self._compare(other, lambda s, o: s > o)
277
278  def __ge__(self, other):
279    if isinstance(other, _VersionRequirements):
280      for rule in other.rules:
281        if rule(self):
282          return True
283
284      return False
285
286    return self._compare(other, lambda s, o: s >= o)
287
288
289class _VersionRequirements(object):
290  """
291  Series of version constraints that can be compared to. For instance, this
292  allows for comparisons like 'if I'm greater than version X in the 0.2.2
293  series, or greater than version Y in the 0.2.3 series'.
294
295  This is a logical 'or' of the series of rules.
296  """
297
298  def __init__(self):
299    self.rules = []
300
301  def greater_than(self, version, inclusive = True):
302    """
303    Adds a constraint that we're greater than the given version.
304
305    :param stem.version.Version version: version we're checking against
306    :param bool inclusive: if comparison is inclusive or not
307    """
308
309    if inclusive:
310      self.rules.append(lambda v: version <= v)
311    else:
312      self.rules.append(lambda v: version < v)
313
314  def less_than(self, version, inclusive = True):
315    """
316    Adds a constraint that we're less than the given version.
317
318    :param stem.version.Version version: version we're checking against
319    :param bool inclusive: if comparison is inclusive or not
320    """
321
322    if inclusive:
323      self.rules.append(lambda v: version >= v)
324    else:
325      self.rules.append(lambda v: version > v)
326
327  def in_range(self, from_version, to_version, from_inclusive = True, to_inclusive = False):
328    """
329    Adds constraint that we're within the range from one version to another.
330
331    :param stem.version.Version from_version: beginning of the comparison range
332    :param stem.version.Version to_version: end of the comparison range
333    :param bool from_inclusive: if comparison is inclusive with the starting version
334    :param bool to_inclusive: if comparison is inclusive with the ending version
335    """
336
337    def new_rule(v):
338      if from_inclusive and to_inclusive:
339        return from_version <= v <= to_version
340      elif from_inclusive:
341        return from_version <= v < to_version
342      else:
343        return from_version < v < to_version
344
345    self.rules.append(new_rule)
346
347
348safecookie_req = _VersionRequirements()
349safecookie_req.in_range(Version('0.2.2.36'), Version('0.2.3.0'))
350safecookie_req.greater_than(Version('0.2.3.13'))
351
352Requirement = stem.util.enum.Enum(
353  ('AUTH_SAFECOOKIE', safecookie_req),
354  ('DESCRIPTOR_COMPRESSION', Version('0.3.1.1-alpha')),
355  ('DORMANT_MODE', Version('0.4.0.1-alpha')),
356  ('DROPGUARDS', Version('0.2.5.1-alpha')),
357  ('EVENT_AUTHDIR_NEWDESCS', Version('0.1.1.10-alpha')),
358  ('EVENT_BUILDTIMEOUT_SET', Version('0.2.2.7-alpha')),
359  ('EVENT_CIRC_MINOR', Version('0.2.3.11-alpha')),
360  ('EVENT_CLIENTS_SEEN', Version('0.2.1.10-alpha')),
361  ('EVENT_CONF_CHANGED', Version('0.2.3.3-alpha')),
362  ('EVENT_DESCCHANGED', Version('0.1.2.2-alpha')),
363  ('EVENT_GUARD', Version('0.1.2.5-alpha')),
364  ('EVENT_HS_DESC_CONTENT', Version('0.2.7.1-alpha')),
365  ('EVENT_NS', Version('0.1.2.3-alpha')),
366  ('EVENT_NETWORK_LIVENESS', Version('0.2.7.2-alpha')),
367  ('EVENT_NEWCONSENSUS', Version('0.2.1.13-alpha')),
368  ('EVENT_SIGNAL', Version('0.2.3.1-alpha')),
369  ('EVENT_STATUS', Version('0.1.2.3-alpha')),
370  ('EVENT_STREAM_BW', Version('0.1.2.8-beta')),
371  ('EVENT_TRANSPORT_LAUNCHED', Version('0.2.5.0-alpha')),
372  ('EVENT_CONN_BW', Version('0.2.5.2-alpha')),
373  ('EVENT_CIRC_BW', Version('0.2.5.2-alpha')),
374  ('EVENT_CELL_STATS', Version('0.2.5.2-alpha')),
375  ('EVENT_TB_EMPTY', Version('0.2.5.2-alpha')),
376  ('EVENT_HS_DESC', Version('0.2.5.2-alpha')),
377  ('EXTENDCIRCUIT_PATH_OPTIONAL', Version('0.2.2.9')),
378  ('FEATURE_EXTENDED_EVENTS', Version('0.2.2.1-alpha')),
379  ('FEATURE_VERBOSE_NAMES', Version('0.2.2.1-alpha')),
380  ('GETINFO_CONFIG_TEXT', Version('0.2.2.7-alpha')),
381  ('GETINFO_GEOIP_AVAILABLE', Version('0.3.2.1-alpha')),
382  ('GETINFO_MICRODESCRIPTORS', Version('0.3.5.1-alpha')),
383  ('GETINFO_UPTIME', Version('0.3.5.1-alpha')),
384  ('HIDDEN_SERVICE_V3', Version('0.3.3.1-alpha')),
385  ('HSFETCH', Version('0.2.7.1-alpha')),
386  ('HSFETCH_V3', Version('0.4.1.1-alpha')),
387  ('HSPOST', Version('0.2.7.1-alpha')),
388  ('ADD_ONION', Version('0.2.7.1-alpha')),
389  ('ADD_ONION_BASIC_AUTH', Version('0.2.9.1-alpha')),
390  ('ADD_ONION_NON_ANONYMOUS', Version('0.2.9.3-alpha')),
391  ('ADD_ONION_MAX_STREAMS', Version('0.2.7.2-alpha')),
392  ('LOADCONF', Version('0.2.1.1')),
393  ('MICRODESCRIPTOR_IS_DEFAULT', Version('0.2.3.3')),
394  ('SAVECONF_FORCE', Version('0.3.1.1-alpha')),
395  ('TAKEOWNERSHIP', Version('0.2.2.28-beta')),
396  ('TORRC_CONTROL_SOCKET', Version('0.2.0.30')),
397  ('TORRC_PORT_FORWARDING', Version('0.2.3.1-alpha')),
398  ('TORRC_DISABLE_DEBUGGER_ATTACHMENT', Version('0.2.3.9')),
399  ('TORRC_VIA_STDIN', Version('0.2.6.3-alpha')),
400)
401