[tor-commits] [ooni-probe/master] Merge branch 'master' of https://git.torproject.org/ooni-probe

isis at torproject.org isis at torproject.org
Thu Oct 4 14:41:15 UTC 2012


commit edfaff0c38c8ad5f8772ba23721413f0878a0d35
Merge: 16c3cfe 21505f8
Author: Arturo Filastò <arturo at filasto.net>
Date:   Fri Sep 28 12:15:42 2012 +0000

    Merge branch 'master' of https://git.torproject.org/ooni-probe
    
    Conflicts:
    	.gitignore
    	ooni/assets/bridgetests.txt
    	ooni/plugins/new_bridget.py

 .gitignore                  |    4 +
 docs/bridget.md             |  102 ++++++
 ooni/assets/bridgetests.txt |   11 -
 ooni/lib/Makefile           |   36 ++
 ooni/lib/__init__.py        |   43 +---
 ooni/lib/txscapy            |    1 -
 ooni/lib/txscapy.py         |  348 ++++++++++++++++++++
 ooni/lib/txtorcon           |    1 -
 ooni/lib/txtraceroute       |    1 -
 ooni/lib/txtraceroute.py    |  752 +++++++++++++++++++++++++++++++++++++++++++
 ooni/ooniprobe.py           |   10 +-
 ooni/plugins/blocking.py    |    2 +-
 ooni/plugins/dnstamper.py   |    6 +-
 ooni/plugins/httphost.py    |    2 +-
 ooni/plugins/new_bridget.py |  239 ++++++++++----
 ooni/plugins/tcpconnect.py  |    5 +-
 ooni/plugoo/nodes.py        |    6 +-
 ooni/plugoo/reports.py      |    2 +-
 ooni/plugoo/tests.py        |    3 +-
 ooni/utils/log.py           |    1 -
 20 files changed, 1437 insertions(+), 138 deletions(-)

diff --cc .gitignore
index 6b1b9ce,686e319..0458e37
--- a/.gitignore
+++ b/.gitignore
@@@ -8,4 -8,4 +8,8 @@@ proxy-lists/italy-http-ips.tx
  private/*
  /ooni/plugins/dropin.cache
  oonib/oonibackend.conf
 -ooni/lib/txtorcon
++<<<<<<< HEAD
 +ooni/assets/*
++=======
++ooni/lib/txtorcon
++>>>>>>> 21505f84aef5d60c7e138590a1a40e3df773d680
diff --cc ooni/ooniprobe.py
index 86f4e2d,539c2ac..ec529d6
mode 100644,100755..100644
--- a/ooni/ooniprobe.py
+++ b/ooni/ooniprobe.py
diff --cc ooni/plugins/new_bridget.py
index 673cacc,0000000..c85caeb
mode 100644,000000..100644
--- a/ooni/plugins/new_bridget.py
+++ b/ooni/plugins/new_bridget.py
@@@ -1,421 -1,0 +1,528 @@@
 +#!/usr/bin/env python
 +# -*- encoding: utf-8 -*-
 +#
 +#  +-----------+
 +#  |  BRIDGET  |
 +#  |        +----------------------------------------------+
 +#  +--------| Use a slave Tor process to test making a Tor |
 +#           | connection to a list of bridges or relays.   |
 +#           +----------------------------------------------+
 +#
 +# :authors: Arturo Filasto, Isis Lovecruft
 +# :licence: see included LICENSE
 +# :version: 0.1.0-alpha
 +
 +from __future__             import with_statement
- from zope.interface         import implements
++from os                     import getcwd
++from os.path                import isfile
++from os.path                import join as pj
 +from twisted.python         import usage
 +from twisted.plugin         import IPlugin
 +from twisted.internet       import defer, error, reactor
++from zope.interface         import implements
 +
 +import random
 +import sys
 +
- try:
-     from ooni.lib.txtorcon  import CircuitListenerMixin, IStreamAttacher
- except:
-     print "BridgeT requires txtorcon: https://github.com/meejah/txtorcon.git"
-     print "Your copy of OONI should have it included, if you're seeing this"
-     print "message, please file a bug report."
-     log.msg ("Bridget: Unable to import from ooni.lib.txtorcon")
- 
 +from ooni.utils             import log
 +from ooni.plugoo.tests      import ITest, OONITest
 +from ooni.plugoo.assets     import Asset
 +
 +
- class BridgetArgs(usage.Options):
++def portCheck(number):
++    number = int(number)
++    if number not in range(1024, 65535):
++        raise ValueError("Port out of range")
 +
-     def portCheck(number):
-         number = int(number)
-         if number not in range(1024, 65535):
-             raise ValueError("Port out of range")
-     portCheck.coerceDoc = "Ports must be between 1024 and 65535"
++portCheckAllowed     = "must be between 1024 and 65535."
++sockCheck, ctrlCheck = portCheck, portCheck
++sockCheck.coerceDoc  = "Port to use for Tor's SocksPort, " + portCheckAllowed
++ctrlCheck.coerceDoc  = "Port to use for Tor's ControlPort, " + portCheckAllowed
 +
++
++class BridgetArgs(usage.Options):
 +    optParameters = [
 +        ['bridges', 'b', None,
-          'List of bridges to scan <IP>:<ORport>'],
++         'File listing bridge IP:ORPorts to test'],
 +        ['relays', 'f', None,
-          'List of relays to scan <IP>'],
-         ['socks', 's', 9049, portCheck,
-          'Tor SocksPort to use'],
-         ['control', 'c', 9052, portCheck,
-          'Tor ControlPort to use'],
-         ['tor-path', 'p', None,
++         'File listing relay IPs to test'],
++        ['socks', 's', 9049, None, portCheck],
++        ['control', 'c', 9052, None, portCheck],
++        ['torpath', 'p', None,
 +         'Path to the Tor binary to use'],
-         ['data-dir', 'd', None,
++        ['datadir', 'd', None,
 +         'Tor DataDirectory to use'],
 +        ['transport', 't', None,
 +         'Tor ClientTransportPlugin'],
 +        ['resume', 'r', 0,
 +         'Resume at this index']]
-     optFlags = [['random', 'x', 'Randomize control and socks ports']]
++    optFlags = [
++        ['random', 'x', 'Use random ControlPort and SocksPort']]
 +
 +    def postOptions(self):
-         ## We can't test pluggable transports without bridges
 +        if self['transport'] and not self['bridges']:
 +            e = "Pluggable transport requires the bridges option"
 +            raise usage.UsageError, e
-         ## We can't use random and static port simultaneously
 +        if self['socks'] and self['control']:
 +            if self['random']:
 +                e = "Unable to use random and specific ports simultaneously"
 +                raise usageError, e
 +
 +class CustomCircuit(CircuitListenerMixin):
 +    implements(IStreamAttacher)
 +
 +    from txtorcon.interface import IRouterContainer, ICircuitContainer
 +
 +    def __init__(self, state):
 +        self.state = state
 +        self.waiting_circuits = []
 +
 +    def waiting_on(self, circuit):
 +        for (circid, d) in self.waiting_circuits:
 +            if circuit.id == circid:
 +                return true
 +        return False
 +
 +    def circuit_extend(self, circuit, router):
 +        "ICircuitListener"
 +        if circuit.purpose != 'GENERAL':
 +            return
 +        if self.waiting_on(circuit):
 +            log.msg("Circuit %d (%s)" % (circuit.id, router.id_hex))
 +
 +    def circuit_built(self, circuit):
 +        "ICircuitListener"
 +        if circuit.purpose != 'GENERAL':
 +            return
 +
 +        log.msg("Circuit %s built ..." % circuit.id)
 +        log.msg("Full path of %s: %s" % (circuit.id, circuit.path))
 +
 +        for (circid, d) in self.waiting_circuits:
 +            if circid == circuit.id:
 +                self.waiting_circuits.remove(circid, d)
 +                d.callback(circuit)
 +
 +    def circuit_failed(self, circuit, reason):
 +        if self.waiting_on(circuit):
 +            log.msg("A circuit we requested %s failed for reason %s" %
 +                    (circuit.id, reason))
 +            circid, d = None, None
 +            for x in self.waiting_circuits:
 +                if x[0] == circuit.id:
 +                    circid, d, stream_cc = x
 +            if d is None:
 +                raise Exception("Expected to find circuit.")
 +
 +            self.waiting_circuits.remove((circid, d))
 +            log.msg("Trying to build a circuit for %s" % circid)
 +            self.request_circuit_build(d)
 +
 +    def check_circuit_route(self, circuit, router):
 +        if router in circuit.path:
 +            #router.update() ## XXX can i use without args? no.
 +            TorInfo.dump(self)
 +
 +    def request_circuit_build(self, deferred):
 +        entries = self.state.entry_guards.value()
 +        relays  = self.state.routers.values()
 +
 +        log.msg("We have these nodes listed as entry guards:")
 +        log.msg("%s" % entries)
 +        log.msg("We have these nodes listed as relays:")
 +        log.msg("%s" % relays)
 +
 +        path = [random.choice(entries),
 +                random.choice(relays),
 +                random.choice(relays)]
 +
 +        log.msg("Requesting a circuit: %s"
 +                % '-->'.join(map(lambda x: x.location.countrycode, path)))
 +
 +        class AppendWaiting:
 +            def __init__(self, attacher, deferred):
 +                self.attacher = attacher
 +                self.d        = deferred
 +
 +            def __call__(self, circuit):
 +                """
 +                Return from build_circuit is a Circuit, however, we want to
 +                wait until it is built before we can issue an attach on it and
 +                callback to the Deferred we issue here.
 +                """
 +                log.msg("Circuit %s is in progress ..." % circuit.id)
 +                self.attacher.waiting_circuits.append((circuit.id, self.d))
 +
 +        return self.state.build_circuit(path).addCallback(AppendWaiting(self, deferred_to_callback)).addErrback(log.err)
 +
 +class BridgetAsset(Asset):
 +    """
-     Class for parsing bridge assets so that they can be commented out.
++    Class for parsing bridget Assets ignoring commented out lines.
 +    """
 +    def __init__(self, file=None):
 +        self = Asset.__init__(self, file)
 +
 +    def parse_line(self, line):
 +        if line.startswith('#'):
 +            return
 +        else:
 +            return line.replace('\n','')
 +
 +class BridgetTest(OONITest):
 +    """
 +    XXX fill me in
 +
 +    :ivar config:
 +        An :class:`ooni.lib.txtorcon.TorConfig` instance.
 +    :ivar relay_list:
 +        A list of all provided relays to test. We have to do this because
 +        txtorcon.TorState().entry_guards won't build a custom circuit if the
 +        first hop isn't in the torrc's EntryNodes.
 +    :ivar bridge_list:
 +        A list of all provided bridges to test.
 +    :ivar socks_port:
 +        Integer for Tor's SocksPort.
 +    :ivar control_port:
 +        Integer for Tor's ControlPort.
 +    :ivar plug_transport:
 +        String defining the Tor's ClientTransportPlugin, for testing
 +        a bridge's pluggable transport functionality.
 +    :ivar tor_binary:
 +        Path to the Tor binary to use, e.g. \'/usr/sbin/tor\'
 +    """
 +    implements(IPlugin, ITest)
 +
 +    shortName = "newbridget"
 +    description = "bridget"
 +    requirements = None
 +    options = BridgetArgs
 +    blocking = False
 +
 +    def initialize(self):
 +        """
 +        Extra initialization steps. We only want one child Tor process
 +        running, so we need to deal with the creation of TorConfig() only
 +        once, before the experiment runs.
 +        """
 +        self.socks_port     = 9049
 +        self.control_port   = 9052
 +        self.tor_binary     = '/usr/sbin/tor'
 +        self.data_directory = None
 +
 +        if self.local_options:
-             try:
-                 from ooni.lib.txtorcon import TorConfig
-             except:
-                 e = "Could not import TorConfig class from txtorcon!"
-                 raise ImportError, e
++            options = self.local_options
 +
-             options             = self.local_options
-             self.config         = TorConfig()
- 
-             ## Don't run the experiment if we don't have anything to test
 +            if not options['bridges'] and not options['relays']:
 +                self.suicide = True
++                return
++
++            try:
++                from ooni.lib.txtorcon import TorConfig
++            except ImportError:
++                log.msg ("Bridget: Unable to import from ooni.lib.txtorcon")
++                wd, tx = getcwd(), 'lib/txtorcon/torconfig.py'
++                chk = pj(wd,tx) if wd.endswith('ooni') else pj(wd,'ooni/'+tx)
++                try:
++                    assert isfile(chk)
++                except:
++                    log.msg("Error: Some OONI libraries are missing!")
++                    log.msg("Please go to /ooni/lib/ and do \"make all\"")
++
++            self.config = TorConfig()
 +
 +            if options['bridges']:
 +                self.config.UseBridges = 1
 +
 +            if options['relays']:
 +                ## Stupid hack for testing only relays:
 +                ## Tor doesn't use EntryNodes when UseBridges is enabled, but
 +                ## config.state.entry_guards needs to include the first hop to
 +                ## build a custom circuit.
 +                self.config.EntryNodes = ','.join(relay_list)
 +
 +            if options['socks']:
 +                self.socks_port = options['socks']
 +
 +            if options['control']:
 +                self.control_port = options['control']
 +
 +            if options['random']:
 +                log.msg("Using randomized ControlPort and SocksPort ...")
 +                self.socks_port   = random.randint(1024, 2**16)
 +                self.control_port = random.randint(1024, 2**16)
 +
-             if options['tor-path']:
-                 self.tor_binary = options['tor-path']
++            if options['torpath']:
++                self.tor_binary = options['torpath']
 +
-             if options['data-dir']:
-                 self.config.DataDirectory = options['data-dir']
++            if options['datadir']:
++                self.config.DataDirectory = options['datadir']
 +
 +            if options['transport']:
 +                ## ClientTransportPlugin transport socks4|socks5 IP:PORT
 +                ## ClientTransportPlugin transport exec path-to-binary [options]
 +                if not options['bridges']:
 +                    e = "You must use the bridge option to test a transport."
 +                    raise usage.UsageError("%s" % e)
 +
 +                log.msg("Using pluggable transport ...")
 +                ## XXX fixme there's got to be a better way to check the exec
 +                assert type(options['transport']) is str
 +                self.config.ClientTransportPlugin = options['transport']
 +
 +            self.config.SocksPort   = self.socks_port
 +            self.config.ControlPort = self.control_port
 +            self.config.save()
 +
 +    def load_assets(self):
 +        """
 +        Load bridges and/or relays from files given in user options. Bridges
 +        should be given in the form IP:ORport. We don't want to load these as
 +        assets, because it's inefficient to start a Tor process for each one.
 +        """
 +        assets           = {}
 +        self.bridge_list = []
 +        self.relay_list  = []
 +
 +        ## XXX fix me
 +        ## we should probably find a more memory nice way to load addresses,
 +        ## in case the files are really large
 +        if self.local_options:
-             if self.local_options['bridges']:
-                 log.msg("Loading bridge information from %s ..."
-                         % self.local_options['bridges'])
-                 with open(self.local_options['bridges']) as bridge_file:
-                     for line in bridge_file.readlines():
++            def make_asset_list(opt, lst):
++                log.msg("Loading information from %s ..." % opt)
++                with open(opt) as opt_file:
++                    for line in opt_file.readlines():
 +                        if line.startswith('#'):
 +                            continue
 +                        else:
-                             self.bridge_list.append(line.replace('\n',''))
-                 assets.update({'bridges': self.bridge_list})
++                            lst.append(line.replace('\n',''))
 +
++            if self.local_options['bridges']:
++                make_asset_list(self.local_options['bridges'],
++                                self.bridge_list)
++                assets.update({'bridges': self.bridge_list})
 +            if self.local_options['relays']:
-                 log.msg("Loading relay information from %s  ..."
-                         % self.local_options['relays'])
-                 with open(options['relays']) as relay_file:
-                     for line in relay_file.readlines():
-                         if line.startswith('#'):
-                             continue
-                         else:
-                             self.relay_list.append(line.replace('\n',''))
++                make_asset_list(self.local_options['relays'],
++                                self.relay_list)
 +                assets.update({'relays': self.relay_list})
 +        return assets
 +
 +    def experiment(self, args):
 +        """
 +        XXX fill me in
 +
 +        :param args:
 +            The :class:`ooni.plugoo.asset.Asset <Asset>` line currently being
 +            used.
 +        :meth launch_tor:
 +            Returns a Deferred which callbacks with a
 +            :class:`ooni.lib.txtorcon.torproto.TorProcessProtocol
 +            <TorProcessProtocol>` connected to the fully-bootstrapped Tor;
 +            this has a :class:`ooni.lib.txtorcon.torcontol.TorControlProtocol
 +            <TorControlProtocol>` instance as .protocol.
 +        """
-         from ooni.lib.txtorcon import TorProtocolFactory, TorConfig, TorState
-         from ooni.lib.txtorcon import DEFAULT_VALUE, launch_tor
++        try:
++            from ooni.lib.txtorcon import CircuitListenerMixin, IStreamAttacher
++            from ooni.lib.txtorcon import TorProtocolFactory, TorConfig, TorState
++            from ooni.lib.txtorcon import DEFAULT_VALUE, launch_tor
++        except ImportError:
++            log.msg("Error: Unable to import from ooni.lib.txtorcon")
++            wd, tx = getcwd(), 'lib/txtorcon/torconfig.py'
++            chk = pj(wd,tx) if wd.endswith('ooni') else pj(wd,'ooni/'+tx)
++            try:
++                assert isfile(chk)
++            except:
++                log.msg("Error: Some OONI libraries are missing!")
++                log.msg("       Please go to /ooni/lib/ and do \"make all\"")
++                return sys.exit()
 +
 +        def bootstrap(ctrl):
 +            """
 +            Launch a Tor process with the TorConfig instance returned from
 +            initialize().
 +            """
 +            conf = TorConfig(ctrl)
 +            conf.post_bootstrap.addCallback(setup_done).addErrback(setup_fail)
 +            log.msg("Tor process connected, bootstrapping ...")
 +
 +        def reconf_controller(conf, bridge):
 +            ## if bridges and relays, use one bridge then build a circuit
 +            ## from three relays
 +            conf.Bridge = bridge
 +            ## XXX do we need a SIGHUP to restart?
 +
 +            ## XXX see txtorcon.TorControlProtocol.add_event_listener we
 +            ## may not need full CustomCircuit class
 +
 +            ## if bridges only, try one bridge at a time, but don't build
 +            ## circuits, just return
 +            ## if relays only, build circuits from relays
 +
 +        def reconf_fail(args):
 +            log.msg("Reconfiguring Tor config with args %s failed" % args)
 +            reactor.stop()
 +
 +        def setup_fail(args):
 +            log.msg("Setup Failed.")
 +            report.update({'failed': args})
 +            reactor.stop()
 +
 +        def setup_done(proto):
 +            log.msg("Setup Complete: %s" % proto)
 +            state = TorState(proto.tor_protocol)
 +            state.post_bootstrap.addCallback(state_complete).addErrback(setup_fail)
 +            report.update({'success': args})
 +
 +        def updates(prog, tag, summary):
 +            log.msg("%d%%: %s" % (prog, summary))
 +
 +        if len(args) == 0:
-             log.msg("Bridget needs lists of bridges and/or relays to test!")
++            log.msg("Bridget can't run without bridges or relays to test!")
 +            log.msg("Exiting ...")
-             d = sys.exit()
-             return d
- 
++            return sys.exit()
 +        else:
++
++            class CustomCircuit(CircuitListenerMixin):
++                implements(IStreamAttacher)
++
++                from txtorcon.interface import IRouterContainer
++                from txtorcon.interface import ICircuitContainer
++
++                def __init__(self, state):
++                    self.state = state
++                    self.waiting_circuits = []
++
++                def waiting_on(self, circuit):
++                    for (circid, d) in self.waiting_circuits:
++                        if circuit.id == circid:
++                            return true
++                    return False
++
++                def circuit_extend(self, circuit, router):
++                    "ICircuitListener"
++                    if circuit.purpose != 'GENERAL':
++                        return
++                    if self.waiting_on(circuit):
++                        log.msg("Circuit %d (%s)"
++                                % (circuit.id, router.id_hex))
++
++                def circuit_built(self, circuit):
++                    "ICircuitListener"
++                    if circuit.purpose != 'GENERAL':
++                        return
++                    log.msg("Circuit %s built ..." % circuit.id)
++                    log.msg("Full path of %s: %s" % (circuit.id, circuit.path))
++                    for (circid, d) in self.waiting_circuits:
++                        if circid == circuit.id:
++                            self.waiting_circuits.remove(circid, d)
++                            d.callback(circuit)
++
++                def circuit_failed(self, circuit, reason):
++                    if self.waiting_on(circuit):
++                        log.msg("A circuit we requested %s failed for reason %s"
++                                % (circuit.id, reason))
++                        circid, d = None, None
++                        for x in self.waiting_circuits:
++                            if x[0] == circuit.id:
++                                circid, d, stream_cc = x
++                        if d is None:
++                            raise Exception("Expected to find circuit.")
++
++                        self.waiting_circuits.remove((circid, d))
++                        log.msg("Trying to build a circuit for %s" % circid)
++                        self.request_circuit_build(d)
++
++                def check_circuit_route(self, circuit, router):
++                    if router in circuit.path:
++                        #router.update() ## XXX can i use without args? no.
++                        TorInfo.dump(self)
++
++                def request_circuit_build(self, deferred):
++                    entries = self.state.entry_guards.value()
++                    relays  = self.state.routers.values()
++                    log.msg("We have these nodes listed as entry guards:")
++                    log.msg("%s" % entries)
++                    log.msg("We have these nodes listed as relays:")
++                    log.msg("%s" % relays)
++                    path = [random.choice(entries),
++                            random.choice(relays),
++                            random.choice(relays)]
++                    log.msg("Requesting a circuit: %s"
++                            % '-->'.join(map(lambda x: x.location.countrycode,
++                                             path)))
++
++                    class AppendWaiting:
++                        def __init__(self, attacher, deferred):
++                            self.attacher = attacher
++                            self.d        = deferred
++
++                        def __call__(self, circuit):
++                            """
++                            Return from build_circuit is a Circuit, however,
++                            we want to wait until it is built before we can
++                            issue an attach on it and callback to the Deferred
++                            we issue here.
++                            """
++                            log.msg("Circuit %s is in progress ..." % circuit.id)
++                            self.attacher.waiting_circuits.append((circuit.id,
++                                                                   self.d))
++
++                    fin = self.state.build_circuit(path)
++                    fin.addCallback(AppendWaiting(self, deferred_to_callback))
++                    fin.addErrback(log.err)
++                    return fin
++
++
 +            if len(self.bridge_list) >= 1:
 +                for bridge in self.bridge_list:
 +                    try:
-                         print "BRIDGE IS %s" % bridge
++                        log.msg("Current Bridge: %s" % bridge)
 +                        reconf_controller(self.config, bridge)
 +                    except:
 +                        reconf_fail(bridge)
 +
 +            log.msg("Bridget: initiating test ... ")
 +            log.msg("Using the following as our torrc:\n%s"
 +                    % self.config.create_torrc())
 +            report = {'tor_config': self.config.config}
 +            log.msg("Starting Tor ...")
 +
 +            ## :return: a Deferred which callbacks with a TorProcessProtocol
 +            ##          connected to the fully-bootstrapped Tor; this has a
 +            ##          txtorcon.TorControlProtocol instance as .protocol.
 +            d = launch_tor(self.config,
 +                           reactor,
 +                           progress_updates=updates,
 +                           tor_binary=self.tor_binary)
 +            d.addCallback(bootstrap, self.config)
 +            d.addErrback(setup_fail)
 +            ## now build circuits
 +
 +            #print "Tor process ID: %s" % d.transport.pid
 +            return d
 +
 +## So that getPlugins() can register the Test:
 +bridget = BridgetTest(None, None, None)
 +
 +## ISIS' NOTES
 +## -----------
 +## self.config.save() only needs to be called if Tor is already running.
 +##
- ## need to add transport type to torrc Bridge line:
- ##       Bridge <transport> IP:ORPort <fingerprint>
++## to test gid, uid, and euid:
++## with open('/proc/self/state') as uidfile:
++##     print uidfile.read(1000)
 +##
 +## TODO:
 +##       o  add option for any kwarg=arg self.config setting
 +##       o  cleanup documentation
++##       x  add DataDirectory option
 +##       o  check if bridges are public relays
 +##       o  take bridge_desc file as input, also be able to give same
 +##          format as output
- ##       o  change the stupid name
++##       o  Add assychronous timout for deferred, so that we don't wait
++##          forever for bridges that don't work.
++##       o  Add mechanism for testing through another host
 +##
 +## FIX:
- ##     data directory is not found, or permissions aren't right
++##       o  DataDirectory is not found, or permissions aren't right
++##       o  Bridge line needs generation of transport properties
++##              Bridge <transport> IP:ORPort <fingerprint>





More information about the tor-commits mailing list