Skip to content

Commit

Permalink
Merge pull request #43 from freedomofpress/cli-args
Browse files Browse the repository at this point in the history
Added CLI args and set inject home directory
  • Loading branch information
redshiftzero authored Oct 23, 2018
2 parents cb14171 + a08d7bf commit aa2c4bb
Show file tree
Hide file tree
Showing 13 changed files with 424 additions and 96 deletions.
9 changes: 4 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,15 @@ clean:
find . \( -name '*.tgz' -o -name dropin.cache \) -delete
find . | grep -E "(__pycache__)" | xargs rm -rf

TESTS ?= tests
TESTOPTS ?= -v
test: clean
xvfb-run python -m pytest
xvfb-run python -m pytest -v --cov-config .coveragerc --cov-report html --cov-report term-missing --cov=securedrop_client --cov-fail-under 100 $(TESTOPTS) $(TESTS)

pyflakes:
find . \( -name _build -o -name var -o -path ./docs -o -path \) -type d -prune -o -name '*.py' -print0 | $(XARGS) pyflakes

pycodestyle:
find . \( -name _build -o -name var \) -type d -prune -o -name '*.py' -print0 | $(XARGS) -n 1 pycodestyle --repeat --exclude=build/*,docs/*,.vscode/* --ignore=E731,E402,W504

coverage: clean
xvfb-run python -m pytest --cov-config .coveragerc --cov-report term-missing --cov=securedrop_client tests/

check: clean pycodestyle pyflakes coverage
check: clean pycodestyle pyflakes test
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ pipenv install --dev
pipenv shell
```

## Run the client

You can run the client with an ephemeral data directory:

```
./run.sh
```

If you want to persist data across restarts, you will need to run the client with:

```
./run.sh --sdc-home /path/to/my/dir/
```

## Run tests

```
Expand Down
6 changes: 0 additions & 6 deletions run.py

This file was deleted.

29 changes: 29 additions & 0 deletions run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -e

while [ -n "$1" ]; do
param="$1"
value="$2"
case $param in
--sdc-home)
SDC_HOME="$value"
shift
;;
*)
break
esac
shift
done

SDC_HOME=${SDC_HOME:-$(mktemp -d)}

echo "Running app with home directory: $SDC_HOME"

# create the database for local testing

python - << EOF
from securedrop_client.models import Base, make_engine
Base.metadata.create_all(make_engine("$SDC_HOME"))
EOF

exec python -m securedrop_client --sdc-home "$SDC_HOME" $@
3 changes: 3 additions & 0 deletions securedrop_client/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .app import run

run()
71 changes: 55 additions & 16 deletions securedrop_client/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import os
import signal
import sys
from argparse import ArgumentParser
from sqlalchemy.orm import sessionmaker
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import Qt, QTimer
Expand All @@ -29,14 +30,18 @@
from securedrop_client.logic import Client
from securedrop_client.gui.main import Window
from securedrop_client.resources import load_icon, load_css
from securedrop_client.models import engine
from securedrop_client.models import make_engine
from securedrop_client.utils import safe_mkdir


LOG_DIR = os.path.join(str(pathlib.Path.home()), '.securedrop_client')
LOG_FILE = os.path.join(LOG_DIR, 'securedrop_client.log')
DEFAULT_SDC_HOME = '~/.securedrop_client'
ENCODING = 'utf-8'


def init(sdc_home: str) -> None:
safe_mkdir(sdc_home)


def excepthook(*exc_args):
"""
This function is called in the event of a catastrophic failure.
Expand All @@ -47,18 +52,19 @@ def excepthook(*exc_args):
sys.exit(1)


def configure_logging():
def configure_logging(sdc_home: str) -> None:
"""
All logging related settings are set up by this function.
"""
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
safe_mkdir(sdc_home, 'logs')
log_file = os.path.join(sdc_home, 'logs', 'client.log')

# set logging format
log_fmt = ('%(asctime)s - %(name)s:%(lineno)d(%(funcName)s) '
'%(levelname)s: %(message)s')
formatter = logging.Formatter(log_fmt)
# define log handlers such as for rotating log files
handler = TimedRotatingFileHandler(LOG_FILE, when='midnight',
handler = TimedRotatingFileHandler(log_file, when='midnight',
backupCount=5, delay=0,
encoding=ENCODING)
handler.setFormatter(formatter)
Expand All @@ -71,7 +77,35 @@ def configure_logging():
sys.excepthook = excepthook


def run():
def configure_signal_handlers(app) -> None:
def signal_handler(*nargs) -> None:
app.quit()

for sig in [signal.SIGINT, signal.SIGTERM]:
signal.signal(sig, signal_handler)


def expand_to_absolute(value: str) -> str:
'''
Helper that expands a path to the absolute path so users can provide
arguments in the form ``~/my/dir/``.
'''
return os.path.abspath(os.path.expanduser(value))


def arg_parser() -> ArgumentParser:
parser = ArgumentParser('securedrop-client',
description='SecureDrop Journalist GUI')
parser.add_argument(
'-H', '--sdc-home',
default=DEFAULT_SDC_HOME,
type=expand_to_absolute,
help=('SecureDrop Client home directory for storing files and state. '
'(Default {})'.format(DEFAULT_SDC_HOME)))
return parser


def start_app(args, qt_args) -> None:
"""
Create all the top-level assets for the application, set things up and
run the application. Specific tasks include:
Expand All @@ -84,10 +118,11 @@ def run():
- configure the client (logic) object.
- ensure the application is setup in the default safe starting state.
"""
configure_logging()
init(args.sdc_home)
configure_logging(args.sdc_home)
logging.info('Starting SecureDrop Client {}'.format(__version__))

app = QApplication(sys.argv)
app = QApplication(qt_args)
app.setApplicationName('SecureDrop Client')
app.setDesktopFileName('org.freedomofthepress.securedrop.client')
app.setApplicationVersion(__version__)
Expand All @@ -97,19 +132,23 @@ def run():
app.setWindowIcon(load_icon(gui.icon))
app.setStyleSheet(load_css('sdclient.css'))

engine = make_engine(args.sdc_home)
Session = sessionmaker(bind=engine)
session = Session()

client = Client("http://localhost:8081/", gui, session)
client = Client("http://localhost:8081/", gui, session, args.sdc_home)
client.setup()

def signal_handler(*nargs) -> None:
app.quit()

for sig in [signal.SIGINT, signal.SIGTERM]:
signal.signal(sig, signal_handler)
configure_signal_handlers(app)
timer = QTimer()
timer.start(500)
timer.timeout.connect(lambda: None)

sys.exit(app.exec_())


def run() -> None:
args, qt_args = arg_parser().parse_known_args()
# reinsert the program's name
qt_args.insert(0, 'securedrop-client')
start_app(args, qt_args)
8 changes: 6 additions & 2 deletions securedrop_client/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import sdclientapi
import arrow
from securedrop_client import storage
from securedrop_client.utils import check_dir_permissions
from PyQt5.QtCore import QObject, QThread, pyqtSignal, QTimer


Expand Down Expand Up @@ -83,19 +84,22 @@ class Client(QObject):

finish_api_call = pyqtSignal() # Acknowledges reciept of an API call.

def __init__(self, hostname, gui, session):
def __init__(self, hostname, gui, session, home: str) -> None:
"""
The hostname, gui and session objects are used to coordinate with the
various other layers of the application: the location of the SecureDrop
proxy, the user interface and SqlAlchemy local storage respectively.
"""

check_dir_permissions(home)

super().__init__()
self.hostname = hostname # Location of the SecureDrop server.
self.gui = gui # Reference to the UI window.
self.api = None # Reference to the API for secure drop proxy.
self.session = session # Reference to the SqlAlchemy session.
self.api_thread = None # Currently active API call thread.
self.sync_flag = os.path.join(os.path.expanduser('~'), '.sdsync')
self.sync_flag = os.path.join(home, 'sync_flag')

def setup(self):
"""
Expand Down
12 changes: 4 additions & 8 deletions securedrop_client/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, backref

Base = declarative_base()

# TODO: Store this in config file, see issue #2
DB_PATH = os.path.abspath('svs.sqlite')

engine = create_engine('sqlite:///{}'.format(DB_PATH))
Base = declarative_base()
def make_engine(home: str):
db_path = os.path.join(home, 'svs.sqlite')
return create_engine('sqlite:///{}'.format(db_path))


class Source(Base):
Expand Down Expand Up @@ -104,7 +104,3 @@ def __init__(self, username):

def __repr__(self):
return "<Journalist: {}>".format(self.username)


# Populate the database.
Base.metadata.create_all(engine)
47 changes: 47 additions & 0 deletions securedrop_client/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import os


def safe_mkdir(sdc_home: str, relative_path: str=None) -> None:
'''
Safely create directories while checking permissions along the way.
'''
check_dir_permissions(sdc_home)

if not relative_path:
return

full_path = os.path.join(sdc_home, relative_path)
if not full_path == os.path.abspath(full_path):
raise ValueError('Path is not absolute: {}'.format(full_path))

path_components = split_path(relative_path)

path_so_far = sdc_home
for component in path_components:
path_so_far = os.path.join(path_so_far, component)
check_dir_permissions(path_so_far)
os.makedirs(path_so_far, 0o0700, exist_ok=True)


def check_dir_permissions(dir_path: str) -> None:
'''
Check that a directory has ``700`` as the final 3 bytes. Raises a
``RuntimeError`` otherwise.
'''
if os.path.exists(dir_path):
stat_res = os.stat(dir_path).st_mode
masked = stat_res & 0o777
if masked & 0o077:
raise RuntimeError('Unsafe permissions ({}) on {}'
.format(oct(stat_res), dir_path))


def split_path(path: str) -> list:
out = []

while path:
path, tail = os.path.split(path)
out.append(tail)

out.reverse()
return out
8 changes: 8 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import os
import pytest


@pytest.fixture(scope='function')
def safe_tmpdir(tmpdir):
os.chmod(str(tmpdir), 0o0700)
return tmpdir
Loading

0 comments on commit aa2c4bb

Please sign in to comment.