[go: up one dir, main page]

File: torinfo.py

package info (click to toggle)
txtorcon 22.0.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 1,620 kB
  • sloc: python: 17,417; makefile: 229
file content (288 lines) | stat: -rw-r--r-- 9,645 bytes parent folder | download
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
# -*- coding: utf-8 -*-

from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
from __future__ import with_statement

import functools
from twisted.internet import defer

from txtorcon.interface import ITorControlProtocol


class MagicContainer(object):
    """
    This merely contains 1 or more methods or further MagicContainer
    instances; see _do_setup in TorInfo.

    Once _setup_complete() is called, this behaves differently so that
    one can get nicer access to GETINFO things from TorInfo --
    specifically dir() and so forth pretend that there are only
    methods/attributes that pertain to actual Tor GETINFO keys.

    See TorInfo.
    """

    def __init__(self, n):
        self._txtorcon_name = n
        self.attrs = {}
        self._setup = False

    def _setup_complete(self):
        self._setup = True

    def _add_attribute(self, n, v):
        self.attrs[n] = v

    def __repr__(self):
        return object.__getattribute__(self, '_txtorcon_name')

    def __getitem__(self, idx):
        return list(object.__getattribute__(self, 'attrs').items())[idx][1]

    def __len__(self):
        return len(object.__getattribute__(self, 'attrs'))

    def __dir__(self):
        return list(object.__getattribute__(self, 'attrs').keys())

    def __getattribute__(self, name):
        sup = super(MagicContainer, self)
        if sup.__getattribute__('_setup') is False:
            return sup.__getattribute__(name)

        attrs = sup.__getattribute__('attrs')
        if name == '__members__':
            return list(attrs.keys())

        else:
            if name.startswith('__'):
                return sup.__getattribute__(name)

            try:
                return attrs[name]
            except KeyError:
                if name in ['dump']:
                    return object.__getattribute__(self, name)
                raise AttributeError(name)

    def dump(self, prefix):
        prefix = prefix + '.' + object.__getattribute__(self, '_txtorcon_name')
        for x in list(object.__getattribute__(self, 'attrs').values()):
            x.dump(prefix)


class ConfigMethod(object):
    def __init__(self, info_key, protocol, takes_arg=False):
        self.info_key = info_key
        self.proto = protocol
        self.takes_arg = takes_arg

    def dump(self, prefix):
        n = self.info_key.replace('/', '.')
        n = n.replace('-', '_')
        s = '%s(%s)' % (n, 'arg' if self.takes_arg else '')
        return s

    def __call__(self, *args):
        if self.takes_arg:
            if len(args) != 1:
                raise TypeError(
                    '"%s" takes exactly one argument' % self.info_key
                )
            req = '%s/%s' % (self.info_key, str(args[0]))

        else:
            if len(args) != 0:
                raise TypeError('"%s" takes no arguments' % self.info_key)

            req = self.info_key

        def stripper(key, arg):
            # strip "keyname="
            # sometimes keyname= is followed by a newline, so final .strip()
            return arg.strip()[len(key) + 1:].strip()

        d = self.proto.get_info_raw(req)
        d.addCallback(functools.partial(stripper, req))
        return d

    def __str__(self):
        arg = ''
        if self.takes_arg:
            arg = 'arg'
        return '%s(%s)' % (self.info_key.replace('-', '_'), arg)


class TorInfo(object):
    """Implements some attribute magic over top of TorControlProtocol so
    that all the available GETINFO values are gettable in a little
    easier fashion. Dashes are replaced by underscores (since dashes
    aren't valid in method/attribute names for Python). Some of the
    magic methods will take a single string argument if the
    corresponding Tor GETINFO would take one (in 'GETINFO info/names'
    it will end with '/*', and the same in torspec). In either case,
    the method returns a Deferred which will callback with the
    requested value, always a string.

    For example (see also examples/tor_info.py):

        proto = TorControlProtocol()
        #...
        def cb(arg):
            print arg
        info = TorInfo(proto)
        info.traffic.written().addCallback(cb)
        info.ip_to_country('8.8.8.8').addCallback(cb)

    For interactive use -- or even checking things progammatically --
    TorInfo pretends it only has attributes that coorespond to valid
    GETINFO calls.  So for example, dir(info) will only return all the
    currently valid top-level things. In the above example this might
    be ['traffic', 'ip_to_country'] (of course in practice this is a
    much longer list). And "dir(info.traffic)" might return ['read',
    'written']

    For something similar to this for configuration (GETCONF, SETCONF)
    see TorConfig which is quite a lot more complicated (internally)
    since you can set config items.

    NOTE that 'GETINFO config/*' is not supported as it's the only
    case that's not a leaf, but theoretically a method.

    """

    def __init__(self, control, errback=None):
        self._setup = False
        self.attrs = {}
        '''After _setup is True, these are all we show as attributes.'''

        self.protocol = ITorControlProtocol(control)
        self.errback = errback

        self.post_bootstrap = defer.Deferred()
        if self.protocol.post_bootstrap:
            self.protocol.post_bootstrap.addCallback(self.bootstrap)

        else:
            self.bootstrap()

    def _add_attribute(self, n, v):
        self.attrs[n] = v

    # iterator protocol

    def __getitem__(self, idx):
        sup = super(TorInfo, self)
        if sup.__getattribute__('_setup') is True:
            return list(object.__getattribute__(self, 'attrs').items())[idx][1]
        raise TypeError("No __getitem__ until we've setup.")

    def __len__(self):
        sup = super(TorInfo, self)
        if sup.__getattribute__('_setup') is True:
            return len(object.__getattribute__(self, 'attrs'))
        raise TypeError("No length until we're setup.")

    # change our attribute behavior based on the value of _setup

    def __dir__(self):
        sup = super(TorInfo, self)
        if sup.__getattribute__('_setup') is True:
            return list(sup.__getattribute__('attrs').keys())
        return list(sup.__getattribute__('__dict__').keys())

    def __getattribute__(self, name):
        sup = super(TorInfo, self)
        if sup.__getattribute__('_setup') is False:
            return sup.__getattribute__(name)

        attrs = sup.__getattribute__('attrs')
        # are there other "special" attributes we need to consider..?
        if name == '__members__':
            return list(attrs.keys())
        if name == '__class__':
            return sup.__class__

        else:
            try:
                return attrs[name]

            except KeyError:
                if name == 'dump':
                    return object.__getattribute__(self, name)

        raise AttributeError(name)

    def bootstrap(self, *args):
        d = self.protocol.get_info_raw("info/names")
        d.addCallback(self._do_setup)
        if self.errback:
            d.addErrback(self.errback)
        d.addCallback(self._setup_complete)
        return d

    def dump(self):
        for x in object.__getattribute__(self, 'attrs').values():
            x.dump('')

    def _do_setup(self, data):
        # FIXME figure out why network-status doesn't work (get
        # nothing back from Tor it seems, although stem does get an
        # answer). this is a space-separated list of ~2500 OR id's;
        # could it be that LineReceiver can't handle it?
        added_magic = []
        for line in data.split('\n'):
            if line == "info/names=" or line.strip() == '':
                continue

            (name, documentation) = line.split(' ', 1)
            # FIXME think about this -- this is the only case where
            # there's something that's a directory
            # (i.e. MagicContainer) AND needs to be a ConfigMethod as
            # well...but doesn't really seem very useful. Somewhat
            # simpler to not support this case for now...
            if name == 'config/*':
                continue

            if name.endswith('/*'):
                # this takes an arg, so make a method
                bits = name[:-2].split('/')
                takes_arg = True

            else:
                bits = name.split('/')
                takes_arg = False

            mine = self
            for bit in bits[:-1]:
                bit = bit.replace('-', '_')
                if bit in mine.attrs:
                    mine = mine.attrs[bit]
                    if not isinstance(mine, MagicContainer):
                        raise RuntimeError(
                            "Already had something: %s for %s" % (bit, name)
                        )

                else:
                    c = MagicContainer(bit)
                    added_magic.append(c)
                    mine._add_attribute(bit, c)
                    mine = c
            n = bits[-1].replace('-', '_')
            if n in mine.attrs:
                raise RuntimeError(
                    "Already had something: %s for %s" % (n, name)
                )
            mine._add_attribute(n, ConfigMethod('/'.join(bits),
                                                self.protocol, takes_arg))

        for c in added_magic:
            c._setup_complete()
        return None

    def _setup_complete(self, *args):
        pb = self.post_bootstrap
        self._setup = True
        pb.callback(self)