From e4073acbbbc3e261574d83e3e946836e331f0135 Mon Sep 17 00:00:00 2001 From: user Date: Mon, 7 Dec 2020 03:14:21 +0000 Subject: [PATCH] by fnordomat@github --- LICENSE.md | 2 - tool/globalist/Globalist.py | 31 -- tool/globalist/ISSUES.md | 4 - tool/globalist/README.md | 64 --- tool/globalist/globalist/__init__.py | 478 ------------------ .../__pycache__/__init__.cpython-36.pyc | Bin 9716 -> 0 bytes tool/globalist/setup.py | 17 - 7 files changed, 596 deletions(-) delete mode 100644 tool/globalist/Globalist.py delete mode 100644 tool/globalist/ISSUES.md delete mode 100644 tool/globalist/README.md delete mode 100644 tool/globalist/globalist/__init__.py delete mode 100644 tool/globalist/globalist/__pycache__/__init__.cpython-36.pyc delete mode 100644 tool/globalist/setup.py diff --git a/LICENSE.md b/LICENSE.md index 32ba4a0b..caf08aa0 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -5,7 +5,6 @@ * /addons/* -- [MIT](https://eo.wikipedia.org/wiki/MIT-permesilo) * /pdf/* -- Nekonata (Vi povas trovi ekzempleron ĉie. Dankon al aŭtoroj.) -* /tool/globalist/* (Globalist) -- [GNU GPLv3](https://eo.wikipedia.org/wiki/%C4%9Cenerala_Publika_Permesilo_de_GNU) * /tool/block_cloudflare_mitm_fx/* -- [MIT](tool/block_cloudflare_mitm_fx/LICENSE.md) * Else -- [PUBLIKA DOMINO (CC0)](https://web.archive.org/web/https://creativecommons.org/share-your-work/public-domain/cc0/) = [WTFPL](http://www.wtfpl.net/about/) @@ -23,7 +22,6 @@ CC0-permesilo permesas uzi tiujn dosierojn por iu ajn uzo, eĉ en manieroj, kiuj * /addons/* -- [MIT](https://en.wikipedia.org/wiki/MIT_License) * /pdf/* -- Unknown (You can find a copy everywhere. Thanks to authors) -* /tool/globalist/* (Globalist) -- [GNU GPLv3](https://en.wikipedia.org/wiki/GNU_General_Public_License) * /tool/block_cloudflare_mitm_fx/* -- [MIT](tool/block_cloudflare_mitm_fx/LICENSE.md) * Else -- [PUBLIC DOMAIN (CC0)](https://web.archive.org/web/https://creativecommons.org/share-your-work/public-domain/cc0/) = [WTFPL](http://www.wtfpl.net/about/) diff --git a/tool/globalist/Globalist.py b/tool/globalist/Globalist.py deleted file mode 100644 index fc21b685..00000000 --- a/tool/globalist/Globalist.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# Globalist: manage a global repo via decentral git instances -# you may peer with any number of other Globalist onions - -# Think onionshare, but with permanent onion addresses, P2P and DVCS - -# Python2/3. Dependencies: -# - stem (torsocks pip install stem / via distro) -# a recent version (>= 1.5.0) is needed for auth -# - git must be installed -# - torsocks must be installed -# - tor must be up and running and the ControlPort open - -# Use scenario: -# a) Run Tor. -# b) Run the server in the background and schedule a job for pulling from peers. -# it is a git server that listens on .onion:9418 -# it's to be expected that peers uptime will intersect with yours -# only a fraction of the time. -# c) Globalist.py creates a git, which you may use to push and pull your own changes. - -# Bugs: -# FIXME: clean up hidservauth entries on stop - -import globalist -import sys - -if __name__=='__main__': - globalist.main(args=sys.argv[1:]) diff --git a/tool/globalist/ISSUES.md b/tool/globalist/ISSUES.md deleted file mode 100644 index 80e85673..00000000 --- a/tool/globalist/ISSUES.md +++ /dev/null @@ -1,4 +0,0 @@ -version 0.0.6.2 -- HidServAuth entries are never cleaned up -- ux: -X does not disable authentication - diff --git a/tool/globalist/README.md b/tool/globalist/README.md deleted file mode 100644 index 2fa80387..00000000 --- a/tool/globalist/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# Globalist -Globalist provides distributed sharing of repositories without the need of central instances (like Microsoft GitHub). - -This is an attempt to ease the distribution of git repos, to overcome the risk of a central points of failure. - -Globalist stands for "Global List" and aims at replacing any EtherPads of more than transient value. - -It is also meant to evolve into an experimental distributed asynchronous wiki facility. - -Nodes can come and go, and network topology only depends on the peers entries in the nodes' config files. Changes that are merged by one's peers propagate by diffusion. - -The official repository can be found at https://codeberg.org/crimeflare/stop_cloudflare - -## Usage - -To use Globalist.py `python3` is needed. Either run it from globalist directory with `python3 Globalist.py` or or install it as described below. - -Per default an open tor ControlPort at 9151 without authentication is expected. You can choose another port with `-C`. -For a list of option see `--help`. - -### Create repository - -Make a new directory and put this in the file ./repo.cfg (when creating a new repository instead of cloning from a peer, the list or indeed the repo.cfg file can remain empty) - -``` -[network] -peers = -``` - -For a public repository, no authentication is needed (option -_X_). In case authentication is used, prepend the secret as follows: somebody:secret@peeroniondomainname.onion - -For each shared repo, Globalist will create one .onion service. Note that it is possible to use either bare repos or not-bare repos. - -### Clone a repository - -To clone a bare repo: - -``` -Globalist.py -bc ... -``` - -To pull once from a bare repo: - -``` -Globalist.py -bp -``` - -## To install locally - -``` -./setup.py install --user -``` - -or - -``` -torsocks pip3 install -v -e . -``` - -## To do - -set default commit messages -support signed commits -push? diff --git a/tool/globalist/globalist/__init__.py b/tool/globalist/globalist/__init__.py deleted file mode 100644 index d1326dc0..00000000 --- a/tool/globalist/globalist/__init__.py +++ /dev/null @@ -1,478 +0,0 @@ -#!/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 diff --git a/tool/globalist/globalist/__pycache__/__init__.cpython-36.pyc b/tool/globalist/globalist/__pycache__/__init__.cpython-36.pyc deleted file mode 100644 index fc0520c6102cead251f5d4810b495b750663d946..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9716 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.5.0'], - license='GPLv3' -) -