This file is indexed.

/usr/share/arm/util/conf.py is in tor-arm 1.4.5.0-1.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
"""
This provides handlers for specially formatted configuration files. Entries are
expected to consist of simple key/value pairs, and anything after "#" is
stripped as a comment. Excess whitespace is trimmed and empty lines are
ignored. 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 chese
blankEntry.example

would be loaded as four entries (the last one's value being an empty string).
If a key's defined multiple times then the last instance of it is used.
"""

import threading

from util import log

CONFS = {}  # mapping of identifier to singleton instances of configs
CONFIG = {"log.configEntryNotFound": None,
          "log.configEntryTypeError": log.NOTICE}

def loadConfig(config):
  config.update(CONFIG)

def getConfig(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.
  
  Arguments:
    handle - unique identifier used to access this config instance
  """
  
  if not handle in CONFS: CONFS[handle] = Config()
  return CONFS[handle]

class Config():
  """
  Handler for easily working with custom configurations, providing persistence
  to and from files. All operations are thread safe.
  
  Parameters:
    path        - location from which configurations are saved and loaded
    contents    - mapping of current key/value pairs
    rawContents - last read/written config (initialized to an empty string)
  """
  
  def __init__(self):
    """
    Creates a new configuration instance.
    """
    
    self.path = None        # location last loaded from
    self.contents = {}      # configuration key/value pairs
    self.contentsLock = threading.RLock()
    self.requestedKeys = set()
    self.rawContents = []   # raw contents read from configuration file
  
  def getValue(self, key, default=None, multiple=False):
    """
    This provides the currently value associated with a given key. If no such
    key exists then this provides the default.
    
    Arguments:
      key      - config setting to be fetched
      default  - value provided if no such key exists
      multiple - provides back a list of all values if true, otherwise this
                 returns the last loaded configuration value
    """
    
    self.contentsLock.acquire()
    
    if key in self.contents:
      val = self.contents[key]
      if not multiple: val = val[-1]
      self.requestedKeys.add(key)
    else:
      msg = "config entry '%s' not found, defaulting to '%s'" % (key, str(default))
      log.log(CONFIG["log.configEntryNotFound"], msg)
      val = default
    
    self.contentsLock.release()
    
    return val
  
  def get(self, key, default=None):
    """
    Fetches the given configuration, using the key and default value to hint
    the type it should be. Recognized types are:
    - logging runlevel if key starts with "log."
    - boolean if default is a boolean (valid values are 'true' and 'false',
      anything else provides the default)
    - integer or float if default is a number (provides default if fails to
      cast)
    - list of all defined values default is a list
    - mapping of all defined values (key/value split via "=>") if the default
      is a dict
    
    Arguments:
      key      - config setting to be fetched
      default  - value provided if no such key exists
    """
    
    isMultivalue = isinstance(default, list) or isinstance(default, dict)
    val = self.getValue(key, default, isMultivalue)
    if val == default: return val
    
    if key.startswith("log."):
      if val.upper() == "NONE": val = None
      elif val.upper() in log.Runlevel.values(): val = val.upper()
      else:
        msg = "Config entry '%s' is expected to be a runlevel" % key
        if default != None: msg += ", defaulting to '%s'" % default
        log.log(CONFIG["log.configEntryTypeError"], msg)
        val = default
    elif isinstance(default, bool):
      if val.lower() == "true": val = True
      elif val.lower() == "false": val = False
      else:
        msg = "Config entry '%s' is expected to be a boolean, defaulting to '%s'" % (key, str(default))
        log.log(CONFIG["log.configEntryTypeError"], msg)
        val = default
    elif isinstance(default, int):
      try: val = int(val)
      except ValueError:
        msg = "Config entry '%s' is expected to be an integer, defaulting to '%i'" % (key, default)
        log.log(CONFIG["log.configEntryTypeError"], msg)
        val = default
    elif isinstance(default, float):
      try: val = float(val)
      except ValueError:
        msg = "Config entry '%s' is expected to be a float, defaulting to '%f'" % (key, default)
        log.log(CONFIG["log.configEntryTypeError"], msg)
        val = default
    elif isinstance(default, list):
      pass # nothing special to do (already a list)
    elif isinstance(default, dict):
      valMap = {}
      for entry in val:
        if "=>" in entry:
          entryKey, entryVal = entry.split("=>", 1)
          valMap[entryKey.strip()] = entryVal.strip()
        else:
          msg = "Ignoring invalid %s config entry (expected a mapping, but \"%s\" was missing \"=>\")" % (key, entry)
          log.log(CONFIG["log.configEntryTypeError"], msg)
      val = valMap
    
    return val
  
  def getStrCSV(self, key, default = None, count = None):
    """
    Fetches the given key as a comma separated value. This provides back a list
    with the stripped values.
    
    Arguments:
      key     - config setting to be fetched
      default - value provided if no such key exists or doesn't match the count
      count   - if set, then a TypeError is logged (and default returned) if
                the number of elements doesn't match the count
    """
    
    confValue = self.getValue(key)
    if confValue == None: return default
    else:
      confComp = [entry.strip() for entry in confValue.split(",")]
      
      # check if the count doesn't match
      if count != None and len(confComp) != count:
        msg = "Config entry '%s' is expected to be %i comma separated values" % (key, count)
        if default != None and (isinstance(default, list) or isinstance(default, tuple)):
          defaultStr = ", ".join([str(i) for i in default])
          msg += ", defaulting to '%s'" % defaultStr
        
        log.log(CONFIG["log.configEntryTypeError"], msg)
        return default
      
      return confComp
  
  def getIntCSV(self, key, default = None, count = None, minValue = None, maxValue = None):
    """
    Fetches the given comma separated value, logging a TypeError (and returning
    the default) if the values arne't ints or aren't constrained to the given
    bounds.
    
    Arguments:
      key      - config setting to be fetched
      default  - value provided if no such key exists, doesn't match the count,
                 values aren't all integers, or doesn't match the bounds
      count    - checks that the number of values matches this if set
      minValue - checks that all values are over this if set
      maxValue - checks that all values are less than this if set
    """
    
    confComp = self.getStrCSV(key, default, count)
    if confComp == default: return default
    
    # validates the input, setting the errorMsg if there's a problem
    errorMsg = None
    baseErrorMsg = "Config entry '%s' is expected to %%s" % key
    if default != None and (isinstance(default, list) or isinstance(default, tuple)):
      defaultStr = ", ".join([str(i) for i in default])
      baseErrorMsg += ", defaulting to '%s'" % defaultStr
    
    for val in confComp:
      if not val.isdigit():
        errorMsg = baseErrorMsg % "only have integer values"
        break
      else:
        if minValue != None and int(val) < minValue:
          errorMsg = baseErrorMsg % "only have values over %i" % minValue
          break
        elif maxValue != None and int(val) > maxValue:
          errorMsg = baseErrorMsg % "only have values less than %i" % maxValue
          break
    
    if errorMsg:
      log.log(CONFIG["log.configEntryTypeError"], errorMsg)
      return default
    else: return [int(val) for val in confComp]

  def update(self, confMappings, limits = {}):
    """
    Revises a set of key/value mappings to reflect the current configuration.
    Undefined values are left with their current values.
    
    Arguments:
      confMappings - configuration key/value mappings to be revised
      limits       - mappings of limits on numeric values, expected to be of
                     the form "configKey -> min" or "configKey -> (min, max)"
    """
    
    for entry in confMappings.keys():
      val = self.get(entry, confMappings[entry])
      
      if entry in limits and (isinstance(val, int) or isinstance(val, float)):
        if isinstance(limits[entry], tuple):
          val = max(val, limits[entry][0])
          val = min(val, limits[entry][1])
        else: val = max(val, limits[entry])
      
      confMappings[entry] = val
  
  def getKeys(self):
    """
    Provides all keys in the currently loaded configuration.
    """
    
    return self.contents.keys()
  
  def getUnusedKeys(self):
    """
    Provides the set of keys that have never been requested.
    """
    
    return set(self.getKeys()).difference(self.requestedKeys)
  
  def set(self, key, value):
    """
    Stores the given configuration value.
    
    Arguments:
      key   - config key to be set
      value - config value to be set
    """
    
    self.contentsLock.acquire()
    self.contents[key] = [value]
    self.contentsLock.release()
  
  def clear(self):
    """
    Drops all current key/value mappings.
    """
    
    self.contentsLock.acquire()
    self.contents.clear()
    self.contentsLock.release()
  
  def load(self, path):
    """
    Reads in the contents of the given path, adding its configuration values
    and overwriting any that already exist. If the file's empty then this
    doesn't do anything. Other issues (like having insufficient permissions or
    if the file doesn't exist) result in an IOError.
    
    Arguments:
      path - file path to be loaded
    """
    
    configFile = open(path, "r")
    self.rawContents = configFile.readlines()
    configFile.close()
    
    self.contentsLock.acquire()
    
    for line in self.rawContents:
      # strips any commenting or excess whitespace
      commentStart = line.find("#")
      if commentStart != -1: line = line[:commentStart]
      line = line.strip()
      
      # parse the key/value pair
      if line and " " in line:
        key, value = line.split(" ", 1)
        value = value.strip()
        
        if key in self.contents: self.contents[key].append(value)
        else: self.contents[key] = [value]
    
    self.path = path
    self.contentsLock.release()
  
  def save(self, saveBackup=True):
    """
    Writes the contents of the current configuration. If a configuration file
    already exists then merges as follows:
    - comments and file contents not in this config are left unchanged
    - lines with duplicate keys are stripped (first instance is kept)
    - existing entries are overwritten with their new values, preserving the
      positioning of in-line comments if able
    - config entries not in the file are appended to the end in alphabetical
      order
    
    If problems arise in writing (such as an unset path or insufficient
    permissions) result in an IOError.
    
    Arguments:
      saveBackup - if true and a file already exists then it's saved (with
                   '.backup' appended to its filename)
    """
    
    pass # TODO: implement when persistence is needed