#!/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