[Pkg-privacy-commits] [onionbalance] 33/117: Add OnionBalance config file and instance key generator

Donncha O'Cearbahill donncha-guest at moszumanska.debian.org
Wed Dec 16 23:18:43 UTC 2015


This is an automated email from the git hooks/post-receive script.

donncha-guest pushed a commit to branch debian/sid
in repository onionbalance.

commit ab71f7cf6a82b329f41b48f4d9ba1667506abf37
Author: Donncha O'Cearbhaill <donncha at donncha.is>
Date:   Tue Jun 23 20:09:38 2015 +0100

    Add OnionBalance config file and instance key generator
---
 onionbalance/settings.py | 228 ++++++++++++++++++++++++++++++++++++++++++++++-
 onionbalance/util.py     |  23 +++++
 2 files changed, 248 insertions(+), 3 deletions(-)

diff --git a/onionbalance/settings.py b/onionbalance/settings.py
index 21cb02d..7dbe0ce 100644
--- a/onionbalance/settings.py
+++ b/onionbalance/settings.py
@@ -6,8 +6,13 @@ Implements the generation and loading of configuration files.
 
 import os
 import sys
+import argparse
+import getpass
+import logging
+from builtins import input, range
 
 import yaml
+import Crypto.PublicKey
 
 from onionbalance import config
 from onionbalance import util
@@ -47,7 +52,7 @@ def initialize_services(controller, services_config):
         if not service_key:
             logger.error("Private key %s could not be loaded.",
                          service.get("key"))
-            sys.exit(0)
+            sys.exit(1)
         else:
             # Successfully imported the private key
             onion_address = util.calc_onion_address(service_key)
@@ -80,9 +85,226 @@ def initialize_services(controller, services_config):
         ))
 
 
+def parse_cmd_args():
+    """
+    Parses and returns command line arguments for config generator
+    """
+
+    parser = argparse.ArgumentParser(
+        description="%s generates config files and keys for OnionBalance "
+        "instances and managment servers. Calling without any options will"
+        "initiate an interactive mode." % sys.argv[0])
+
+    parser.add_argument("--key", type=str, default=None,
+                        help="RSA private key for the master onion service.")
+
+    parser.add_argument("-p", "--password", type=str, default=None,
+                        help="Optional password which can be used to encrypt"
+                        "the master service private key.")
+
+    parser.add_argument("-n", type=int, default=2, dest="num_instances",
+                        help="Number of instances to generate (default: "
+                        "%(default)s).")
+
+    parser.add_argument("-t", "--tag", type=str, default='srv',
+                        help="Prefix name for the service instances "
+                        "(default: %(default)s).")
+
+    parser.add_argument("--output", type=str, default='config/',
+                        help="Directory to store generate config files. "
+                        "The directory will be created if it does not "
+                        "already exist.")
+
+    parser.add_argument("--no-interactive", action='store_true',
+                        help="Try to run automatically without prompting for"
+                        "user input.")
+
+    parser.add_argument("-v", type=str, default="info", dest='verbosity',
+                        help="Minimum verbosity level for logging. Available "
+                        "in ascending order: debug, info, warning, error, "
+                        "critical).  The default is info.")
+
+    parser.add_argument("--service-port-line", type=str,
+                        default="HiddenServicePort 80 127.0.0.1:80",
+                        help="Port line for each instance's torrc file "
+                        "(default: %(default)s).")
+
+    # .. todo:: Add option to specify HS host and port for instance torrc
+
+    return parser.parse_args()
+
+
 def generate_config():
     """
     Entry point for interactive config file generation.
     """
-    return None
-    logger.info("Config generation")
+
+    logger.info("Beginning OnionBalance config generation.")
+
+    # Parse initial command line options
+    args = parse_cmd_args()
+
+    # If CLI options have been provided, don't enter interactive mode
+    # Crude check to see if any options beside --verbosity are set.
+    verbose = True if '-v' in sys.argv else False
+
+    if ((len(sys.argv) > 1 and not verbose) or len(sys.argv) > 3 or
+            args.no_interactive):
+        interactive = False
+        logger.info("Entering non-interactive mode.")
+    else:
+        interactive = True
+        logger.info("No command line arguments found, entering interactive "
+                    "mode.")
+
+    logger.setLevel(logging.__dict__[args.verbosity.upper()])
+
+    # Check if output directory exists, if not try create it
+    output_path = None
+    if interactive:
+        output_path = input("Enter path to store generated config "
+                            "[{}]: ".format(os.path.abspath(args.output)))
+    output_path = output_path or args.output
+    try:
+        util.try_make_dir(output_path)
+    except OSError:
+        logger.exception("Problem encountered when trying to create the "
+                         "output directory %s.", os.path.abspath(output_path))
+    else:
+        logger.debug("Created the output directory '%s'.",
+                     os.path.abspath(output_path))
+
+    # The output directory should be empty to avoid having conflict keys
+    # or config files.
+    if not util.is_directory_empty(output_path):
+        logger.error("The specified output directory is not empty. Please "
+                     "delete any files and folders or specify another output "
+                     "directory.")
+        sys.exit(1)
+
+    # Load master key if specified
+    key_path = None
+    if interactive:
+        # Read key path from user
+        key_path = input("Enter path to master service private key "
+                         "(Leave empty to generate a key): ")
+    key_path = args.key or key_path
+    if key_path:
+        if not os.path.isfile(key_path):
+            logger.error("The specified master service private key '%s' "
+                         "could not be found. Please confirm the path and "
+                         "file permissions are correct.", key_path)
+            sys.exit(1)
+        else:
+            # Try load the specified private key file
+            master_key = util.key_decrypt_prompt(key_path)
+            if not master_key:
+                logger.error("The specified master private key %s could not "
+                             "be loaded.", os.path.abspath(master_key))
+                sys.exit(1)
+            else:
+                master_onion_address = util.calc_onion_address(master_key)
+                logger.info("Successfully loaded a master key for service "
+                            "%s.onion.", master_onion_address)
+
+    else:
+        # No key specified, begin generating a new one.
+        master_key = Crypto.PublicKey.RSA.generate(1024)
+        master_onion_address = util.calc_onion_address(master_key)
+        logger.debug("Created a new master key for service %s.onion.",
+                     master_onion_address)
+
+    # Finished loading/generating master key, now try generate keys for
+    # each service instance
+    num_instances = None
+    if interactive:
+        num_instances = input("Number of instance services to create "
+                              "[{}]: ".format(args.num_instances))
+        # Cast to int if a number was specified
+        try:
+            num_instances = int(num_instances)
+        except ValueError:
+            num_instances = None
+    num_instances = num_instances or args.num_instances
+    logger.debug("Creating %d service instances.", num_instances)
+
+    tag = None
+    if interactive:
+        tag = input("Provide a tag name to group these instances "
+                    "[{}]: ".format(args.tag))
+    tag = tag or args.tag
+
+    torrc_port_line = None
+    if interactive:
+        torrc_port_line = input("Specify a HiddenServicePort option for each "
+                                "instance's torrc file [{}]: ".format(
+                                    args.service_port_line))
+    torrc_port_line = torrc_port_line or args.service_port_line
+
+    instances = []
+    for i in range(0, num_instances):
+        instance_key = Crypto.PublicKey.RSA.generate(1024)
+        instance_address = util.calc_onion_address(instance_key)
+        logger.debug("Created a key for instance %s.onion.",
+                     instance_address)
+        instances.append((instance_address, instance_key))
+
+    # Write master service key to directory
+    master_passphrase = None
+    if interactive:
+        master_passphrase = getpass.getpass(
+            "Provide an optional password to encrypt the master private "
+            "key (Not encrypted if no password is specified): ")
+    master_passphrase = master_passphrase or args.password
+
+    master_dir = os.path.join(output_path, 'master')
+    util.try_make_dir(master_dir)
+    master_key_file = os.path.join(master_dir,
+                                   '{}.key'.format(master_onion_address))
+    with open(master_key_file, "wb") as key_file:
+        key_file.write(master_key.exportKey(passphrase=master_passphrase))
+        logger.debug("Successfully wrote master key to file %s.",
+                     os.path.abspath(master_key_file))
+
+    # Create YAML OnionBalance settings file for these instances
+    service_data = {'key': '{}.key'.format(master_onion_address)}
+    service_data['instances'] = [{'address': address,
+                                  'name': '{}{}'.format(tag, i+1)} for
+                                 i, (address, _) in enumerate(instances)]
+    settings_data = {'services': [service_data]}
+    config_yaml = yaml.dump(settings_data, default_flow_style=False)
+
+    config_file_path = os.path.join(master_dir, 'config.yaml')
+    with open(config_file_path, "w") as config_file:
+        config_file.write(u"# OnionBalance Config File\n")
+        config_file.write(config_yaml)
+        logger.info("Wrote master service config file '%s'.",
+                    os.path.abspath(config_file_path))
+
+    # Try generate config files for each service instance
+    for i, (instance_address, instance_key) in enumerate(instances):
+        # Create a numbered directory for instance
+        instance_dir = os.path.join(output_path, '{}{}'.format(tag, i+1))
+        util.try_make_dir(os.path.join(instance_dir,
+                                       '{}'.format(instance_address)))
+        instance_key_file = os.path.join(instance_dir,
+                                         '{}'.format(instance_address),
+                                         'private_key')
+        with open(instance_key_file, "wb") as key_file:
+            key_file.write(instance_key.exportKey())
+            logger.debug("Successfully wrote key for instance %s.onion to "
+                         "file.", instance_address)
+
+        # Write torrc file for each instance
+        instance_torrc = os.path.join(instance_dir, 'instance_torrc')
+        with open(instance_torrc, "w") as torrc_file:
+            torrc_file.write("SocksPort 0\n")
+            torrc_file.write("HiddenServiceDir {}\n".format(instance_address))
+            torrc_file.write("{}\n".format(torrc_port_line))
+
+    # Output final status message
+    logger.info("Done! Successfully generated an OnionBalance config and %d "
+                "instance keys for service %s.onion.",
+                num_instances, master_onion_address)
+
+    sys.exit(0)
diff --git a/onionbalance/util.py b/onionbalance/util.py
index 8f4d571..c305acb 100644
--- a/onionbalance/util.py
+++ b/onionbalance/util.py
@@ -5,6 +5,7 @@ import datetime
 import getpass
 import base64
 import binascii
+import os
 
 # import Crypto.Util
 import Crypto.PublicKey
@@ -116,3 +117,25 @@ def key_decrypt_prompt(key_file, retries=3):
 
     # No private key was imported
     raise ValueError("Could not import RSA key.")
+
+
+def try_make_dir(path):
+    """
+    Try to create a directory (including any parent directories)
+    """
+    try:
+        os.makedirs(path)
+    except OSError:
+        if not os.path.isdir(path):
+            raise
+
+
+def is_directory_empty(path):
+    """
+    Check if a directory contains any files or directories.
+    """
+    for dirpath, dirnames, files in os.walk(path):
+        if files or dirnames:
+            return False
+        else:
+            return True

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-privacy/packages/onionbalance.git



More information about the Pkg-privacy-commits mailing list