This file is indexed.

/usr/share/pyshared/stem/util/conf.py is in python-stem 1.1.0-1.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
# Copyright 2011-2013, Damian Johnson and The Tor Project
# See LICENSE for licensing information

"""
Handlers for text configuration files. Configurations are simple string to
string mappings, with the configuration files using the following rules...

* the key/value is separated by a space
* anything after a "#" is ignored as a comment
* excess whitespace is trimmed
* empty lines are ignored
* multi-line values can be defined by following the key with lines starting
  with a '|'

For instance...

::

  # This is my sample config
  user.name Galen
  user.password yabba1234 # here's an inline comment
  user.notes takes a fancy to pepperjack cheese
  blankEntry.example

  msg.greeting
  |Multi-line message exclaiming of the
  |wonder and awe that is pepperjack!

... would be loaded as...

::

  config = {
    "user.name": "Galen",
    "user.password": "yabba1234",
    "user.notes": "takes a fancy to pepperjack cheese",
    "blankEntry.example": "",
    "msg.greeting": "Multi-line message exclaiming of the\\nwonder and awe that is pepperjack!",
  }

Configurations are managed via the :class:`~stem.util.conf.Config` class. The
:class:`~stem.util.conf.Config` can be be used directly with its
:func:`~stem.util.conf.Config.get` and :func:`~stem.util.conf.Config.set`
methods, but usually modules will want a local dictionary with just the
configurations that it cares about.

To do this use the :func:`~stem.util.conf.config_dict` function. For example...

::

  import getpass
  from stem.util import conf, connection

  def config_validator(key, value):
    if key == "timeout":
      # require at least a one second timeout
      return max(1, value)
    elif key == "endpoint":
      if not connection.is_valid_ipv4_address(value):
        raise ValueError("'%s' isn't a valid IPv4 address" % value)
    elif key == "port":
      if not connection.is_valid_port(value):
        raise ValueError("'%s' isn't a valid port" % value)
    elif key == "retries":
      # negative retries really don't make sense
      return max(0, value)

  CONFIG = conf.config_dict("ssh_login", {
    "username": getpass.getuser(),
    "password": "",
    "timeout": 10,
    "endpoint": "263.12.8.0",
    "port": 22,
    "reconnect": False,
    "retries": 3,
  }, config_validator)

There's several things going on here so lets take it step by step...

* The :func:`~stem.util.conf.config_dict` provides a dictionary that's bound
  to a given configuration. If the "ssh_proxy_config" configuration changes
  then so will the contents of CONFIG.

* The dictionary we're passing to :func:`~stem.util.conf.config_dict` provides
  two important pieces of information: default values and their types. See the
  Config's :func:`~stem.util.conf.Config.get` method for how these type
  inferences work.

* The config_validator is a hook we're adding to make sure CONFIG only gets
  values we think are valid. In this case it ensures that our timeout value
  is at least one second, and rejects endpoints or ports that are invalid.

Now lets say our user has the following configuration file...

::

  username waddle_doo
  password jabberwocky
  timeout -15
  port 9000000
  retries lots
  reconnect true
  logging debug

... and we load it as follows...

::

  >>> from from stem.util import conf
  >>> our_config = conf.get_config("ssh_login")
  >>> our_config.load("/home/atagar/user_config")
  >>> print CONFIG
  {
    "username": "waddle_doo",
    "password": "jabberwocky",
    "timeout": 1,
    "endpoint": "263.12.8.0",
    "port": 22,
    "reconnect": True,
    "retries": 3,
  }

Here's an expanation of what happened...

* the username, password, and reconnect attributes took the values in the
  configuration file

* the 'config_validator' we added earlier allows for a minimum timeout of one
  and rejected the invalid port (with a log message)

* we weren't able to convert the retries' "lots" value to an integer so it kept
  its default value and logged a warning

* the user didn't supply an endpoint so that remained unchanged

* our CONFIG didn't have a 'logging' attribute so it was ignored

**Module Overview:**

::

  config_dict - provides a dictionary that's kept in sync with our config
  get_config - singleton for getting configurations
  parse_enum_csv - helper funcion for parsing confguration entries for enums

  Config - Custom configuration
    |- load - reads a configuration file
    |- save - writes the current configuration to a file
    |- clear - empties our loaded configuration contents
    |- add_listener - notifies the given listener when an update occurs
    |- clear_listeners - removes any attached listeners
    |- keys - provides keys in the loaded configuration
    |- set - sets the given key/value pair
    |- unused_keys - provides keys that have never been requested
    |- get - provides the value for a given key, with type inference
    +- get_value - provides the value for a given key as a string
"""

import threading

from stem.util import log

CONFS = {}  # mapping of identifier to singleton instances of configs


class _SyncListener(object):
  def __init__(self, config_dict, interceptor):
    self.config_dict = config_dict
    self.interceptor = interceptor

  def update(self, config, key):
    if key in self.config_dict:
      new_value = config.get(key, self.config_dict[key])

      if new_value == self.config_dict[key]:
        return  # no change

      if self.interceptor:
        interceptor_value = self.interceptor(key, new_value)

        if interceptor_value:
          new_value = interceptor_value

      self.config_dict[key] = new_value


def config_dict(handle, conf_mappings, handler = None):
  """
  Makes a dictionary that stays synchronized with a configuration.

  This takes a dictionary of 'config_key => default_value' mappings and
  changes the values to reflect our current configuration. This will leave
  the previous values alone if...

  * we don't have a value for that config_key
  * we can't convert our value to be the same type as the default_value

  If a handler is provided then this is called just prior to assigning new
  values to the config_dict. The handler function is expected to accept the
  (key, value) for the new values and return what we should actually insert
  into the dictionary. If this returns None then the value is updated as
  normal.

  For more information about how we convert types see our
  :func:`~stem.util.conf.Config.get` method.

  **The dictionary you get from this is manged by the
  :class:`~stem.util.conf.Config` class and should be treated as being
  read-only.**

  :param str handle: unique identifier for a config instance
  :param dict conf_mappings: config key/value mappings used as our defaults
  :param functor handler: function referred to prior to assigning values
  """

  selected_config = get_config(handle)
  selected_config.add_listener(_SyncListener(conf_mappings, handler).update)
  return conf_mappings


def get_config(handle):
  """
  Singleton constructor for configuration file instances. If a configuration
  already exists for the handle then it's returned. Otherwise a fresh instance
  is constructed.

  :param str handle: unique identifier used to access this config instance
  """

  if not handle in CONFS:
    CONFS[handle] = Config()

  return CONFS[handle]


def parse_enum(key, value, enumeration):
  """
  Provides the enumeration value for a given key. This is a case insensitive
  lookup and raises an exception if the enum key doesn't exist.

  :param str key: configuration key being looked up
  :param str value: value to be parsed
  :param stem.util.enum.Enum enumeration: enumeration the values should be in

  :returns: enumeration value

  :raises: **ValueError** if the **value** isn't among the enumeration keys
  """

  return parse_enum_csv(key, value, enumeration, 1)[0]


def parse_enum_csv(key, value, enumeration, count = None):
  """
  Parses a given value as being a comma separated listing of enumeration keys,
  returning the corresponding enumeration values. This is intended to be a
  helper for config handlers. The checks this does are case insensitive.

  The **count** attribute can be used to make assertions based on the number of
  values. This can be...

  * None to indicate that there's no restrictions.
  * An int to indicate that we should have this many values.
  * An (int, int) tuple to indicate the range that values can be in. This range
    is inclusive and either can be None to indicate the lack of a lower or
    upper bound.

  :param str key: configuration key being looked up
  :param str value: value to be parsed
  :param stem.util.enum.Enum enumeration: enumeration the values should be in
  :param int,tuple count: validates that we have this many items

  :returns: list with the enumeration values

  :raises: **ValueError** if the count assertion fails or the **value** entries
    don't match the enumeration keys
  """

  values = [val.upper().strip() for val in value.split(',')]

  if values == ['']:
    return []

  if count is None:
    pass  # no count validateion checks to do
  elif isinstance(count, int):
    if len(values) != count:
      raise ValueError("Config entry '%s' is expected to be %i comma separated values, got '%s'" % (key, count, value))
  elif isinstance(count, tuple) and len(count) == 2:
    minimum, maximum = count

    if minimum is not None and len(values) < minimum:
      raise ValueError("Config entry '%s' must have at least %i comma separated values, got '%s'" % (key, minimum, value))

    if maximum is not None and len(values) > maximum:
      raise ValueError("Config entry '%s' can have at most %i comma separated values, got '%s'" % (key, maximum, value))
  else:
    raise ValueError("The count must be None, an int, or two value tuple. Got '%s' (%s)'" % (count, type(count)))

  result = []
  enum_keys = [k.upper() for k in enumeration.keys()]
  enum_values = list(enumeration)

  for val in values:
    if val in enum_keys:
      result.append(enum_values[enum_keys.index(val)])
    else:
      raise ValueError("The '%s' entry of config entry '%s' wasn't in the enumeration (expected %s)" % (val, key, ', '.join(enum_keys)))

  return result


class Config(object):
  """
  Handler for easily working with custom configurations, providing persistence
  to and from files. All operations are thread safe.

  **Example usage:**

  User has a file at '/home/atagar/myConfig' with...

  ::

    destination.ip 1.2.3.4
    destination.port blarg

    startup.run export PATH=$PATH:~/bin
    startup.run alias l=ls

  And they have a script with...

  ::

    from stem.util import conf

    # Configuration values we'll use in this file. These are mappings of
    # configuration keys to the default values we'll use if the user doesn't
    # have something different in their config file (or it doesn't match this
    # type).

    ssh_config = conf.config_dict("ssh_login", {
      "login.user": "atagar",
      "login.password": "pepperjack_is_awesome!",
      "destination.ip": "127.0.0.1",
      "destination.port": 22,
      "startup.run": [],
    })

    # Makes an empty config instance with the handle of 'ssh_login'. This is
    # a singleton so other classes can fetch this same configuration from
    # this handle.

    user_config = conf.get_config("ssh_login")

    # Loads the user's configuration file, warning if this fails.

    try:
      user_config.load("/home/atagar/myConfig")
    except IOError as exc:
      print "Unable to load the user's config: %s" % exc

    # This replace the contents of ssh_config with the values from the user's
    # config file if...
    #
    # * the key is present in the config file
    # * we're able to convert the configuration file's value to the same type
    #   as what's in the mapping (see the Config.get() method for how these
    #   type inferences work)
    #
    # For instance in this case...
    #
    # * the login values are left alone because they aren't in the user's
    #   config file
    #
    # * the 'destination.port' is also left with the value of 22 because we
    #   can't turn "blarg" into an integer
    #
    # The other values are replaced, so ssh_config now becomes...
    #
    # {"login.user": "atagar",
    #  "login.password": "pepperjack_is_awesome!",
    #  "destination.ip": "1.2.3.4",
    #  "destination.port": 22,
    #  "startup.run": ["export PATH=$PATH:~/bin", "alias l=ls"]}
    #
    # Information for what values fail to load and why are reported to
    # 'stem.util.log'.
  """

  def __init__(self):
    self._path = None        # location we last loaded from or saved to
    self._contents = {}      # configuration key/value pairs
    self._listeners = []     # functors to be notified of config changes

    # used for accessing _contents
    self._contents_lock = threading.RLock()

    # keys that have been requested (used to provide unused config contents)
    self._requested_keys = set()

  def load(self, path = None):
    """
    Reads in the contents of the given path, adding its configuration values
    to our current contents.

    :param str path: file path to be loaded, this uses the last loaded path if
      not provided

    :raises:
      * **IOError** if we fail to read the file (it doesn't exist, insufficient
        permissions, etc)
      * **ValueError** if no path was provided and we've never been provided one
    """

    if path:
      self._path = path
    elif not self._path:
      raise ValueError("Unable to load configuration: no path provided")

    with open(self._path, "r") as config_file:
      read_contents = config_file.readlines()

    with self._contents_lock:
      while read_contents:
        line = read_contents.pop(0)

        # strips any commenting or excess whitespace
        comment_start = line.find("#")

        if comment_start != -1:
          line = line[:comment_start]

        line = line.strip()

        # parse the key/value pair
        if line:
          try:
            key, value = line.split(" ", 1)
            value = value.strip()
          except ValueError:
            log.debug("Config entry '%s' is expected to be of the format 'Key Value', defaulting to '%s' -> ''" % (line, line))
            key, value = line, ""

          if not value:
            # this might be a multi-line entry, try processing it as such
            multiline_buffer = []

            while read_contents and read_contents[0].lstrip().startswith("|"):
              content = read_contents.pop(0).lstrip()[1:]  # removes '\s+|' prefix
              content = content.rstrip("\n")           # trailing newline
              multiline_buffer.append(content)

            if multiline_buffer:
              self.set(key, "\n".join(multiline_buffer), False)
              continue

          self.set(key, value, False)

  def save(self, path = None):
    """
    Saves configuration contents to disk. If a path is provided then it
    replaces the configuration location that we track.

    :param str path: location to be saved to

    :raises: **ValueError** if no path was provided and we've never been provided one
    """

    if path:
      self._path = path
    elif not self._path:
      raise ValueError("Unable to save configuration: no path provided")

    with self._contents_lock:
      with open(self._path, 'w') as output_file:
        for entry_key in sorted(self.keys()):
          for entry_value in self.get_value(entry_key, multiple = True):
            # check for multi line entries
            if "\n" in entry_value:
              entry_value = "\n|" + entry_value.replace("\n", "\n|")

            output_file.write('%s %s\n' % (entry_key, entry_value))

  def clear(self):
    """
    Drops the configuration contents and reverts back to a blank, unloaded
    state.
    """

    with self._contents_lock:
      self._contents.clear()
      self._requested_keys = set()

  def add_listener(self, listener, backfill = True):
    """
    Registers the function to be notified of configuration updates. Listeners
    are expected to be functors which accept (config, key).

    :param functor listener: function to be notified when our configuration is changed
    :param bool backfill: calls the function with our current values if **True**
    """

    with self._contents_lock:
      self._listeners.append(listener)

      if backfill:
        for key in self.keys():
          listener(self, key)

  def clear_listeners(self):
    """
    Removes all attached listeners.
    """

    self._listeners = []

  def keys(self):
    """
    Provides all keys in the currently loaded configuration.

    :returns: **list** if strings for the configuration keys we've loaded
    """

    return self._contents.keys()

  def unused_keys(self):
    """
    Provides the configuration keys that have never been provided to a caller
    via :func:`~stem.util.conf.config_dict` or the
    :func:`~stem.util.conf.Config.get` and
    :func:`~stem.util.conf.Config.get_value` methods.

    :returns: **set** of configuration keys we've loaded but have never been requested
    """

    return set(self.keys()).difference(self._requested_keys)

  def set(self, key, value, overwrite = True):
    """
    Appends the given key/value configuration mapping, behaving the same as if
    we'd loaded this from a configuration file.

    :param str key: key for the configuration mapping
    :param str,list value: value we're setting the mapping to
    :param bool overwrite: replaces the previous value if **True**, otherwise
      the values are appended
    """

    with self._contents_lock:
      if isinstance(value, str):
        if not overwrite and key in self._contents:
          self._contents[key].append(value)
        else:
          self._contents[key] = [value]

        for listener in self._listeners:
          listener(self, key)
      elif isinstance(value, (list, tuple)):
        if not overwrite and key in self._contents:
          self._contents[key] += value
        else:
          self._contents[key] = value

        for listener in self._listeners:
          listener(self, key)
      else:
        raise ValueError("Config.set() only accepts str, list, or tuple. Provided value was a '%s'" % type(value))

  def get(self, key, default = None):
    """
    Fetches the given configuration, using the key and default value to
    determine the type it should be. Recognized inferences are:

    * **default is a boolean => boolean**

      * values are case insensitive
      * provides the default if the value isn't "true" or "false"

    * **default is an integer => int**

      * provides the default if the value can't be converted to an int

    * **default is a float => float**

      * provides the default if the value can't be converted to a float

    * **default is a list => list**

      * string contents for all configuration values with this key

    * **default is a tuple => tuple**

      * string contents for all configuration values with this key

    * **default is a dictionary => dict**

      * values without "=>" in them are ignored
      * values are split into key/value pairs on "=>" with extra whitespace
        stripped

    :param str key: config setting to be fetched
    :param default object: value provided if no such key exists or fails to be converted

    :returns: given configuration value with its type inferred with the above rules
    """

    is_multivalue = isinstance(default, (list, tuple, dict))
    val = self.get_value(key, default, is_multivalue)

    if val == default:
      return val  # don't try to infer undefined values

    if isinstance(default, bool):
      if val.lower() == "true":
        val = True
      elif val.lower() == "false":
        val = False
      else:
        log.debug("Config entry '%s' is expected to be a boolean, defaulting to '%s'" % (key, str(default)))
        val = default
    elif isinstance(default, int):
      try:
        val = int(val)
      except ValueError:
        log.debug("Config entry '%s' is expected to be an integer, defaulting to '%i'" % (key, default))
        val = default
    elif isinstance(default, float):
      try:
        val = float(val)
      except ValueError:
        log.debug("Config entry '%s' is expected to be a float, defaulting to '%f'" % (key, default))
        val = default
    elif isinstance(default, list):
      pass  # nothing special to do (already a list)
    elif isinstance(default, tuple):
      val = tuple(val)
    elif isinstance(default, dict):
      valMap = {}
      for entry in val:
        if "=>" in entry:
          entryKey, entryVal = entry.split("=>", 1)
          valMap[entryKey.strip()] = entryVal.strip()
        else:
          log.debug("Ignoring invalid %s config entry (expected a mapping, but \"%s\" was missing \"=>\")" % (key, entry))
      val = valMap

    return val

  def get_value(self, key, default = None, multiple = False):
    """
    This provides the current value associated with a given key.

    :param str key: config setting to be fetched
    :param object default: value provided if no such key exists
    :param bool multiple: provides back a list of all values if **True**,
      otherwise this returns the last loaded configuration value

    :returns: **str** or **list** of string configuration values associated
      with the given key, providing the default if no such key exists
    """

    with self._contents_lock:
      if key in self._contents:
        self._requested_keys.add(key)

        if multiple:
          return self._contents[key]
        else:
          return self._contents[key][-1]
      else:
        message_id = "stem.util.conf.missing_config_key_%s" % key
        log.log_once(message_id, log.TRACE, "config entry '%s' not found, defaulting to '%s'" % (key, default))
        return default