mirror of
				https://codeberg.org/crimeflare/cloudflare-tor
				synced 2025-11-04 02:17:00 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			478 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			478 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#!/usr/bin/env python3
 | 
						|
# -*- coding: utf-8 -*-
 | 
						|
 | 
						|
__version__ = "0.0.6.2"
 | 
						|
 | 
						|
try:
 | 
						|
    import ConfigParser as cp
 | 
						|
except:
 | 
						|
    import configparser as cp # python3
 | 
						|
import optparse as op
 | 
						|
import re
 | 
						|
import os
 | 
						|
import sys
 | 
						|
import json
 | 
						|
import subprocess
 | 
						|
 | 
						|
import stem
 | 
						|
from stem.control import Controller
 | 
						|
 | 
						|
# Usage:
 | 
						|
#
 | 
						|
# Make a directory.
 | 
						|
#
 | 
						|
# Put a configuration file repo.cfg listing some peers. Done.
 | 
						|
#
 | 
						|
# Initialize:
 | 
						|
#  Either a) (git init repo/) ->
 | 
						|
#    $ python Globalist.py -i
 | 
						|
#  or     b) (torsocks git clone git://example7abcdefgh.onion) ->
 | 
						|
#    $ python Globalist.py -c
 | 
						|
#
 | 
						|
# Have fun:
 | 
						|
#  Run server
 | 
						|
#    $ python Globalist.py
 | 
						|
#  Pull from peers once
 | 
						|
#    $ python Globalist.py -p
 | 
						|
#  Periodically pull, don't serve
 | 
						|
#    $ python Globalist.py -pP 1800
 | 
						|
#  Periodically pull and also serve
 | 
						|
#    $ python Globalist.py -P 1800
 | 
						|
#
 | 
						|
# That's it.
 | 
						|
 | 
						|
# One can simply check in a list of onions for open peering
 | 
						|
# as PEERS.txt ...
 | 
						|
 | 
						|
# A word of CAUTION: anyone can commit anything
 | 
						|
# and there's no mechanism for permanently blacklisting
 | 
						|
# malicious peers (although one can simply remove them
 | 
						|
# as they crop up and roll back their changes).
 | 
						|
#
 | 
						|
# A future version of Globalist.py should introduce
 | 
						|
# signed commits + reputation system, when the need arises.
 | 
						|
 | 
						|
# [network]
 | 
						|
# peers = example7abcdefgh.onion, example8abcdefgh.onion
 | 
						|
# (possibly prefixed with somebody:authkey@ ...)
 | 
						|
 | 
						|
# when using -b (bare), merge remote changes locally after
 | 
						|
# git pull origin remote/origin/master.
 | 
						|
 | 
						|
DEFAULT_CONTROLPORT = 9151
 | 
						|
 | 
						|
STATUS = {'peers': None, 'socksport': None}
 | 
						|
 | 
						|
OPTIONS = None
 | 
						|
 | 
						|
def git(command):
 | 
						|
#    print (command)
 | 
						|
    p = subprocess.Popen(["git"] + command)
 | 
						|
    return p
 | 
						|
 | 
						|
def make_exportable(path):
 | 
						|
    subprocess.Popen(["touch", os.path.abspath(os.path.join(path, "git-daemon-export-ok")) ]).wait()
 | 
						|
 | 
						|
def run_server(config, localport = 9418):
 | 
						|
    print ("Running git server on %s.onion" % config.get('onion', 'hostname'))
 | 
						|
    try:
 | 
						|
        authkey = config.get('onion', 'clientauth')
 | 
						|
        if authkey:
 | 
						|
            print ("Client auth is %s" % authkey)
 | 
						|
    except (KeyError, cp.NoOptionError) as e:
 | 
						|
        print ("No client auth")
 | 
						|
    print ("Git server local port is %d" % localport)
 | 
						|
    print ("You can now hand out this onion to prospective peers.")
 | 
						|
    print ("It will be re-used anytime Globalist starts in this directory.")
 | 
						|
 | 
						|
    what = "repo"
 | 
						|
 | 
						|
    if OPTIONS.o_bare:
 | 
						|
        make_exportable("repo.git")
 | 
						|
        what += ".git"
 | 
						|
    else:
 | 
						|
        make_exportable(os.path.join("repo",".git"))
 | 
						|
 | 
						|
    gitdaemon = git(["daemon", "--base-path=%s" % os.path.abspath("."),
 | 
						|
                     "--reuseaddr", "--verbose",
 | 
						|
    # there could be a global setting enabling write access??
 | 
						|
                     "--disable=receive-pack",
 | 
						|
                     "--listen=127.0.0.1", "--port=%d" % localport,
 | 
						|
                     os.path.abspath(what)])
 | 
						|
    output = gitdaemon.communicate()[0]
 | 
						|
    print (output)
 | 
						|
    # then background this process
 | 
						|
 | 
						|
def makeonion(controller, config, options):
 | 
						|
    # stem docs say: provide the password here if you set one:
 | 
						|
    controller.authenticate()
 | 
						|
    # todo catch UnreadableCookieFile(
 | 
						|
 | 
						|
    onion = None
 | 
						|
 | 
						|
    extra_kwargs = {}
 | 
						|
 | 
						|
    if config.has_section('onion'):
 | 
						|
        print ("Attempting to use saved onion identity")
 | 
						|
        (keytype,key) = config.get('onion', 'key').split(':',1)
 | 
						|
 | 
						|
        if options.o_auth:
 | 
						|
            try:
 | 
						|
                print ("Attempting to use saved clientauth")
 | 
						|
                extra_kwargs['basic_auth'] =\
 | 
						|
                dict([config.get('onion', 'clientauth').split(':',1)])
 | 
						|
            except (KeyError, cp.NoOptionError) as e:
 | 
						|
                print ("No client auth present, generating one")
 | 
						|
                extra_kwargs['basic_auth'] = {'somebody': None}
 | 
						|
        else:
 | 
						|
            print ("Not using clientauth.")
 | 
						|
 | 
						|
        onion = controller.create_ephemeral_hidden_service(**extra_kwargs, ports={9418: options.a_localport}, discard_key=True, await_publication=options.o_ap, key_type=keytype, key_content=key)
 | 
						|
 | 
						|
    else:
 | 
						|
        print ("I'm afraid we don't have an identity yet, creating one")
 | 
						|
 | 
						|
        if options.o_auth:
 | 
						|
            extra_kwargs['basic_auth'] = {'somebody': None}
 | 
						|
 | 
						|
        onion = controller.create_ephemeral_hidden_service(**extra_kwargs, ports={9418: options.a_localport}, discard_key=False, await_publication=options.o_ap)
 | 
						|
 | 
						|
#    print (onion)
 | 
						|
 | 
						|
    print ("Tor controller says Onion OK")
 | 
						|
 | 
						|
    if not onion.is_ok():
 | 
						|
        raise Exception('Failed to publish onion.')
 | 
						|
    else:
 | 
						|
        # perhaps avoid overwriting when already present?
 | 
						|
        for line in onion:
 | 
						|
            if line != "OK":
 | 
						|
                k, v = line.split('=', 1)
 | 
						|
                # we only request the key if the service is new
 | 
						|
                if k == "PrivateKey":
 | 
						|
                    try:
 | 
						|
                        config.add_section('onion')
 | 
						|
                    except cp.DuplicateSectionError as e:
 | 
						|
                        pass
 | 
						|
                    config.set('onion', 'key', v)
 | 
						|
                if k == "ServiceID":
 | 
						|
                    try:
 | 
						|
                        config.add_section('onion')
 | 
						|
                    except cp.DuplicateSectionError as e:
 | 
						|
                        pass
 | 
						|
                    config.set('onion', 'hostname', v)
 | 
						|
                if k == "ClientAuth":
 | 
						|
                    try:
 | 
						|
                        config.add_section('onion')
 | 
						|
                    except cp.DuplicateSectionError as e:
 | 
						|
                        pass
 | 
						|
                    config.set('onion', 'clientauth', v)
 | 
						|
            config.write(open('repo.cfg', 'w'))
 | 
						|
 | 
						|
 | 
						|
def set_client_authentications(ls):
 | 
						|
    global OPTIONS
 | 
						|
    options = OPTIONS
 | 
						|
 | 
						|
    controller = Controller.from_port(port = options.a_controlport)
 | 
						|
    controller.authenticate()
 | 
						|
    # is there no sane way to _append_ a multi-config option in Tor????
 | 
						|
    # control protocol badly misdesigned, nobody thought of concurrent access???!?
 | 
						|
    controller.set_caching(False)
 | 
						|
 | 
						|
# except it doesn't work, the 650 message never arrives. why?
 | 
						|
#    controller.add_event_listener(my_confchanged_listener, EventType.CONF_CHANGED)
 | 
						|
# SETEVENTS conf_changed
 | 
						|
 | 
						|
    hsa = controller.get_conf_map('hidservauth') 
 | 
						|
 | 
						|
    for authpair in ls:
 | 
						|
        if authpair['auth'] and len(authpair['auth']):
 | 
						|
            hsa['hidservauth'].append('%s.onion %s' % (authpair['onion'], authpair['auth']))
 | 
						|
 | 
						|
    hsa['hidservauth'] = list(set(hsa['hidservauth']))
 | 
						|
 | 
						|
    controller.set_conf('hidservauth', hsa['hidservauth'])
 | 
						|
    controller.close()
 | 
						|
 | 
						|
 | 
						|
def getpeers(config):
 | 
						|
    if STATUS['peers']:
 | 
						|
        return STATUS['peers']
 | 
						|
 | 
						|
    if config.has_section('network'):
 | 
						|
        peerslist = config.get('network', 'peers').split(',')
 | 
						|
        peers = []
 | 
						|
        authpairs = []
 | 
						|
 | 
						|
        for peerentry in peerslist:
 | 
						|
 | 
						|
            # extract what looks like an onion identifier
 | 
						|
            try:
 | 
						|
                authpair = re.findall('(?:(somebody:[A-Za-z0-9+/]{22})@)?([a-z2-8]{16})', peerentry)[0]
 | 
						|
 | 
						|
                userpass = authpair[0].split(":",1)
 | 
						|
                if not userpass or not len(userpass)==2:
 | 
						|
                    userpass = (None, None)
 | 
						|
 | 
						|
                authpairs += [{'auth':userpass[1],
 | 
						|
                               'user':userpass[0], # somebody
 | 
						|
                               'onion':authpair[1]}]
 | 
						|
                peers += [authpair[1]]
 | 
						|
 | 
						|
            except Exception as e:
 | 
						|
                print (e)
 | 
						|
 | 
						|
        set_client_authentications(authpairs)
 | 
						|
 | 
						|
        STATUS['peers'] = peers
 | 
						|
 | 
						|
        return peers
 | 
						|
 | 
						|
    else:
 | 
						|
        STATUS['peers'] = []
 | 
						|
 | 
						|
        return []
 | 
						|
 | 
						|
def clone(config):
 | 
						|
    peers = getpeers(config)
 | 
						|
 | 
						|
    # FIXME: when the first fails, we should move on to the next..
 | 
						|
 | 
						|
    what  = "git://%s.onion/repo" % peers[0]
 | 
						|
    where = "repo"
 | 
						|
    how   = []
 | 
						|
 | 
						|
    if OPTIONS.o_bare:
 | 
						|
        what  += ".git"
 | 
						|
        where += ".git"
 | 
						|
        how   = ["--bare", "--mirror"]
 | 
						|
 | 
						|
    cloneproc = subprocess.Popen(["torsocks", "-P", str(STATUS['socksport']), "git", "clone"] + how + [what, where])
 | 
						|
    if cloneproc.wait() != 0:
 | 
						|
        print ("Error cloning, exiting.")
 | 
						|
        return -1
 | 
						|
    else:
 | 
						|
        make_exportable(where)
 | 
						|
 | 
						|
    # Make a local editable repo
 | 
						|
    try:
 | 
						|
        git(["clone", "repo", "repo.git"]).wait()
 | 
						|
    except:
 | 
						|
        print ("Failed to export repository, try to remove 'repo.git'.")
 | 
						|
 | 
						|
    processes = []
 | 
						|
    for peer in peers[1:]:
 | 
						|
        processes.append([peer, subprocess.Popen(["torsocks", "-P", STATUS['socksport'], "git", "-C", os.path.abspath("repo"), "pull", "git://%s.onion/repo" % peer])])
 | 
						|
    for (peer,proc) in processes:
 | 
						|
        if proc.wait() != 0:
 | 
						|
            print ("Error with %s" % peer)
 | 
						|
 | 
						|
def pull(config):
 | 
						|
    peers = getpeers(config)
 | 
						|
 | 
						|
    print ("Pulling from %s" % peers)
 | 
						|
 | 
						|
    processes = []
 | 
						|
    for peer in peers:
 | 
						|
        processes.append([peer, subprocess.Popen(["torsocks", "-P", STATUS['socksport'], "git", "-C", os.path.abspath("repo"), "pull", "git://%s.onion/repo" % peer])])
 | 
						|
    for (peer,proc) in processes:
 | 
						|
        if proc.wait() != 0:
 | 
						|
            print ("Error with %s" % peer)
 | 
						|
 | 
						|
def fetch(config):
 | 
						|
    peers = getpeers(config)
 | 
						|
    print ("Fetching from %s" % peers)
 | 
						|
    processes = []
 | 
						|
    for peer in peers:
 | 
						|
        processes.append([peer, subprocess.Popen(["torsocks", "-P", STATUS['socksport'], "git", "-C", os.path.abspath("repo.git"), "fetch", "git://%s.onion/repo.git" % peer, '+refs/heads/*:refs/remotes/origin/*'])])
 | 
						|
 | 
						|
    for (peer,proc) in processes:
 | 
						|
        if proc.wait() != 0:
 | 
						|
            print ("Error with %s" % peer)
 | 
						|
 | 
						|
def init(config):
 | 
						|
    global OPTIONS # not needed for read access btw
 | 
						|
    options = OPTIONS
 | 
						|
 | 
						|
    print ("Initializing ...")
 | 
						|
 | 
						|
    if options.o_bare:
 | 
						|
        git(["init", "repo.git", "--bare"]).wait()
 | 
						|
        # Make a local editable repo
 | 
						|
        git(["clone", "repo.git", "repo"]).wait()
 | 
						|
 | 
						|
    else:
 | 
						|
        git(["init", "repo"]).wait()
 | 
						|
 | 
						|
    print ("Initialized")
 | 
						|
 | 
						|
def main(args=[]):
 | 
						|
    # OptionParser is capable of printing a helpscreen
 | 
						|
    opt = op.OptionParser()
 | 
						|
 | 
						|
    opt.add_option("-V", "--version", dest="o_version", action="store_true",
 | 
						|
                   default=False, help="print version number")
 | 
						|
 | 
						|
    opt.add_option("-i", "--init", dest="o_init", action="store_true",
 | 
						|
                   default=False, help="make new empty repo")
 | 
						|
 | 
						|
    opt.add_option("-b", "--bare", dest="o_bare", action="store_true",
 | 
						|
                   default=False, help="use bare repos and fetch, not pull")
 | 
						|
 | 
						|
    opt.add_option("-c", "--clone", dest="o_clone", action="store_true",
 | 
						|
                   default=False, help="clone repo from 1st peer")
 | 
						|
 | 
						|
    opt.add_option("-p", "--pull", dest="o_pull", action="store_true",
 | 
						|
                   default=False, help="pull / fetch from peers and don't serve")
 | 
						|
 | 
						|
    opt.add_option("-P", "--periodically-pull", dest="a_pull", action="store",
 | 
						|
                   type="int", default=None, metavar="PERIOD",
 | 
						|
                   help="pull / fetch from peers every n seconds")
 | 
						|
 | 
						|
    opt.add_option("-L", "--local", dest="a_localport", action="store", type="int",
 | 
						|
                   default=9418, metavar="PORT", help="local port for git daemon")
 | 
						|
 | 
						|
    opt.add_option("-C", "--control-port", dest="a_controlport", action="store", type="int",
 | 
						|
                   default=9151,  metavar="PORT", help="Tor controlport")
 | 
						|
 | 
						|
#    opt.add_option("-CP", "--control-password", dest="a_controlpassword", action="store", type="int",
 | 
						|
#                   default="", help="Tor Control Password")
 | 
						|
 | 
						|
#    opt.add_option("-CC", "--control-cookie", dest="a_controlcookie", action="store", type="int",
 | 
						|
#                   default="", help="Tor Control Cookie")
 | 
						|
 | 
						|
    opt.add_option("-a", "--await", dest="o_ap", action="store_true",
 | 
						|
                   default=False, help="await publication of .onion in DHT before proceeding")
 | 
						|
 | 
						|
    opt.add_option("-x", "--auth", action="store_true", default=True,
 | 
						|
                   dest="o_auth", help="enable authentication (private)")
 | 
						|
 | 
						|
    opt.add_option("-X", "--no-auth", action="store_false", default=True,
 | 
						|
                   dest="o_auth", help="disable authentication (not private)")
 | 
						|
 | 
						|
    (options, args) = opt.parse_args(args)
 | 
						|
 | 
						|
    global OPTIONS
 | 
						|
    OPTIONS = options
 | 
						|
 | 
						|
    if options.o_version:
 | 
						|
        print (__version__)
 | 
						|
        return 0
 | 
						|
 | 
						|
    if options.o_auth and stem.__version__ < '1.5.0':
 | 
						|
        sys.stderr.write ("stem version >=1.5.0 required for auth\n")
 | 
						|
        return 1
 | 
						|
 | 
						|
    if not options.a_controlport:
 | 
						|
        options.a_controlport = DEFAULT_CONTROLPORT
 | 
						|
 | 
						|
    # Extract socksport via c.get_conf and use this (-P in torsocks)
 | 
						|
    # TODO implement authentication token / cookie
 | 
						|
    controller = Controller.from_port(port = options.a_controlport)
 | 
						|
    controller.authenticate()
 | 
						|
    if controller.get_conf('SocksPort'):
 | 
						|
        STATUS['socksport'] = controller.get_conf('SocksPort').split(" ",1)[0]
 | 
						|
    else:
 | 
						|
        STATUS['socksport'] = 9050
 | 
						|
    controller.close()
 | 
						|
 | 
						|
    config = cp.ConfigParser()
 | 
						|
    cfgfile = None
 | 
						|
    try:
 | 
						|
        cfgfile = open('repo.cfg')
 | 
						|
    except FileNotFoundError as e:
 | 
						|
        print("Trying to make file repo.cfg")
 | 
						|
        try:
 | 
						|
            os.mknod("repo.cfg")
 | 
						|
            os.chmod("repo.cfg", 0o600)
 | 
						|
            cfgfile = open('repo.cfg')
 | 
						|
        except Exception as e:
 | 
						|
            print (e)
 | 
						|
            return 1
 | 
						|
 | 
						|
    config.readfp(cfgfile)
 | 
						|
 | 
						|
    try:
 | 
						|
        os.stat("repo.git")
 | 
						|
        if not options.o_bare:
 | 
						|
            print ("repo.git exists, setting -b implicitly")
 | 
						|
            # TODO -B to override
 | 
						|
        options.o_bare = True
 | 
						|
 | 
						|
    except FileNotFoundError as e:
 | 
						|
        if not options.o_init and not options.o_clone and options.o_bare:
 | 
						|
            print ("./repo.git/ does not exist, try -ib or -cb")
 | 
						|
            return 1
 | 
						|
 | 
						|
    try:
 | 
						|
        os.stat("repo")
 | 
						|
    except FileNotFoundError as e:
 | 
						|
        if not options.o_init and not options.o_clone and not options.o_bare:
 | 
						|
            print("./repo/ does not exist, try -i or -c")
 | 
						|
            return 1
 | 
						|
 | 
						|
    except Exception as e:
 | 
						|
        print (e)
 | 
						|
        return 1
 | 
						|
 | 
						|
    if options.o_init:
 | 
						|
        init(config)
 | 
						|
 | 
						|
    peers = getpeers(config)
 | 
						|
 | 
						|
    if options.o_clone:
 | 
						|
        if not len(peers):
 | 
						|
            print ("No peers, can't clone. Please enter a peer in repo.cfg")
 | 
						|
        clone(config)
 | 
						|
        return 1
 | 
						|
 | 
						|
    threads = []
 | 
						|
 | 
						|
    if options.a_pull:
 | 
						|
        if not len(peers):
 | 
						|
            print ("No peers, not starting pulling task.")
 | 
						|
 | 
						|
        else:
 | 
						|
            import threading
 | 
						|
            from   datetime import timedelta as td
 | 
						|
            from   datetime import datetime
 | 
						|
            
 | 
						|
            class T:
 | 
						|
                def __init__(self):
 | 
						|
                    self.last = datetime.now()
 | 
						|
                    
 | 
						|
                def run(self):
 | 
						|
                    if options.o_bare:
 | 
						|
                        fetch(config)
 | 
						|
                    else:
 | 
						|
                        pull(config)
 | 
						|
                    threading.Timer(options.a_pull, T.run, args=(self,)).start()
 | 
						|
                    
 | 
						|
            task = T()
 | 
						|
 | 
						|
            t = threading.Thread(target=T.run, args=(task,))
 | 
						|
            t . setDaemon(True)
 | 
						|
            threads.append(t)
 | 
						|
            t.start()
 | 
						|
 | 
						|
    # It's either pull(once) or serve. It's no problem running pull from
 | 
						|
    # another console while the server is up. It's no problem specifying
 | 
						|
    # periodic pull with either.
 | 
						|
 | 
						|
    if options.o_pull and not options.a_pull:
 | 
						|
        if options.o_bare:
 | 
						|
            fetch(config)
 | 
						|
        else:
 | 
						|
            pull(config)
 | 
						|
 | 
						|
    elif not options.o_pull:
 | 
						|
        controller = Controller.from_port(port = options.a_controlport)
 | 
						|
        makeonion(controller, config, options)
 | 
						|
        run_server(config, localport = options.a_localport)
 | 
						|
        controller.close()
 | 
						|
 | 
						|
    for t in threads:
 | 
						|
        t.join()
 | 
						|
 | 
						|
# TODO: should only generate a clientauth on a previously unauthenticated repo if requested by command line option
 |