Skip to content

Commit

Permalink
Bug crappy scp Fixes #13 (#14)
Browse files Browse the repository at this point in the history
* Added 'dockerpty' source to ContainerShell after updating it

* fixed typo in 'skip_container' docstring

* ContainerShell now supports SCP/SFTP-ing to *inside* the container created

* Updated dockerpty.io with unit test
  • Loading branch information
willnx committed Aug 21, 2019
1 parent 6a99b18 commit 444fef2
Show file tree
Hide file tree
Showing 19 changed files with 1,978 additions and 143 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2019.08.15
2019.08.21
68 changes: 51 additions & 17 deletions container_shell/container_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,26 @@
import sys
import atexit
import signal
import argparse
import functools
import subprocess
from pwd import getpwnam
from getpass import getuser

import docker
import requests
import dockerpty

from container_shell.lib.config import get_config
from container_shell.lib import utils, dockage
from container_shell.lib import utils, dockage, dockerpty

#pylint: disable=R0914
def main():
#pylint: disable=R0914,W0102
def main(cli_args=sys.argv[1:]):
"""Entry point logic"""
user_info = getpwnam(getuser())
username = user_info.pw_name
user_uid = user_info.pw_uid
config, using_defaults, location = get_config()
args = parse_cli(cli_args)
config, using_defaults, location = get_config(shell_command=args.command)
docker_client = docker.from_env()
logger = utils.get_logger(name=__name__,
location=config['logging'].get('location'),
Expand All @@ -32,21 +33,12 @@ def main():
if using_defaults:
logger.debug('No defined config file at %s. Using default values', location)

original_cmd = os.getenv('SSH_ORIGINAL_COMMAND', '')
if original_cmd.startswith('scp') or original_cmd.endswith('sftp-server'):
if config['config']['disable_scp']:
utils.printerr('Unable to SCP files onto this system. Forbidden.')
sys.exit(1)
else:
logger.debug('Allowing %s to SCP file. Syntax: %s', username, original_cmd)
returncode = subprocess.call(original_cmd.split())
sys.exit(returncode)

if utils.skip_container(username, config['config']['skip_users']):
logger.info('User %s accessing host environment', username)
original_cmd = os.getenv('SSH_ORIGINAL_COMMAND', args.command)
if not original_cmd:
original_cmd = os.getenv('SHELL')
proc = subprocess.Popen(original_cmd.split(), shell=True)
proc = subprocess.Popen(original_cmd.split(), shell=sys.stdout.isatty())
proc.communicate()
sys.exit(proc.returncode)

Expand Down Expand Up @@ -78,15 +70,40 @@ def main():
# on their SSH application (instead of pressing "CTL D" or typing "exit")
# will cause ContainerShell to leak containers. In other words, the
# SSH session will be gone, but the container will remain.
signal.signal(signal.SIGHUP, cleanup)
set_signal_handlers(container, logger)
try:
dockerpty.start(docker_client.api, container.id)
logger.info('Broke out of dockerpty')
except Exception as doh: #pylint: disable=W0703
logger.exception(doh)
utils.printerr("Failed to connect to PTY")
sys.exit(1)


def set_signal_handlers(container, logger):
"""Set all the OS signal handlers, so we proxy signals to the process(es)
inside the container
:Returns: None
:param container: The container created by ContainerShell
:type container: docker.models.containers.Container
:param logger: An object for writing errors/messages for debugging problems
:type logger: logging.Logger
"""
hupped = functools.partial(kill_container, container, 'SIGHUP', logger)
signal.signal(signal.SIGHUP, hupped)
interrupt = functools.partial(kill_container, container, 'SIGINT', logger)
signal.signal(signal.SIGINT, interrupt)
quit_handler = functools.partial(kill_container, container, 'SIGQUIT', logger)
signal.signal(signal.SIGQUIT, quit_handler)
abort = functools.partial(kill_container, container, 'SIGABRT', logger)
signal.signal(signal.SIGABRT, abort)
termination = functools.partial(kill_container, container, 'SIGTERM', logger)
signal.signal(signal.SIGTERM, termination)


def kill_container(container, the_signal, logger):
"""Tear down the container when ContainerShell exits
Expand Down Expand Up @@ -131,5 +148,22 @@ def kill_container(container, the_signal, logger):
logger.exception(doh)


def parse_cli(cli_args):
"""Intemperate the CLI arguments, and return a useful object
:Returns: argparse.Namespace
:param cli_args: The command line arguments supplied to container_shell
:type cli_args: List
"""
description = 'A mostly transparent proxy to an isolated shell environment.'
parser = argparse.ArgumentParser(description=description)
parser.add_argument('-c', '--command', default='',
help='Execute a specific command, then terminate.')

args = parser.parse_args(cli_args)
return args


if __name__ == '__main__':
main()
9 changes: 7 additions & 2 deletions container_shell/lib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
CONFIG_LOCATION = '/etc/container_shell/config.ini'


def get_config(location=CONFIG_LOCATION):
def get_config(shell_command='', location=CONFIG_LOCATION):
"""Read the supplied INI file, and return a usable object
:Returns: configparser.ConfigParser
:param shell_command: Override the command to run in the shell with whatever
gets supplied via the CLI.
:type shell_command: String
:param location: The location of the config.ini file
:type location: String
"""
Expand All @@ -21,6 +25,8 @@ def get_config(location=CONFIG_LOCATION):
# no config file exists. This section exists so we can communicate that
# back up the stack.
using_defaults = True
if shell_command:
config['config']['command'] = shell_command
return config, using_defaults, location


Expand All @@ -45,7 +51,6 @@ def _default():
config.set('config', 'auto_refresh', '')
config.set('config', 'skip_users', '')
config.set('config', 'create_user', 'true')
config.set('config', 'disable_scp', '')
config.set('config', 'command', '')
config.set('config', 'term_signal', 'SIGHUP')
config.set('logging', 'location', '/var/log/container_shell/messages.log')
Expand Down
8 changes: 5 additions & 3 deletions container_shell/lib/dockage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: UTF-8 -*-
"""Functions to help construct the docker container"""
import sys
import uuid

import docker
Expand All @@ -17,7 +18,7 @@ def build_args(config, username, user_uid, logger):
container_kwargs = {
'image' : config['config'].get('image'),
'hostname' : config['config'].get('hostname'),
'tty' : True,
'tty' : sys.stdout.isatty(),
'init' : True,
'stdin_open' : True,
'dns' : dns(config['dns']['servers']),
Expand Down Expand Up @@ -114,13 +115,14 @@ def container_command(username, user_uid, create_user, command, runuser, useradd
# the user, which is a safer default should a sys admin typo the config.
if create_user.lower() != 'false':
if command:
run_user = '{0} -u {1} {2}'.format(runuser, username, command)
run_user = "{0} {1} -c \"{2}\"".format(runuser, username, command)
else:
# if not a specific command, treat this as a login shell
run_user = '{0} {1} -l {2}'.format(runuser, username, command)
make_user = '{0} -m -u {1} -s /bin/bash {2} 2>/dev/null'.format(useradd, user_uid, username)

everything = "/bin/bash -c '{0} && {1}'".format(make_user, run_user)
#everything = "/bin/bash -c '{0} && {1}'".format(make_user, run_user)
everything = "/bin/bash -c '{} && {}'".format(make_user, run_user)
elif command:
everything = command
else:
Expand Down
33 changes: 33 additions & 0 deletions container_shell/lib/dockerpty/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# -*- coding: UTF-8 -*-
"""
Top-level API for dockerpty
Copyright 2014 Chris Corbyn <chris@w3style.co.uk>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

from container_shell.lib.dockerpty.pty import PseudoTerminal, RunOperation

#pylint: disable=R0913
def start(client, container, interactive=True, stdout=None, stderr=None, stdin=None, logs=None):
"""
Present the PTY of the container inside the current process.
This is just a wrapper for PseudoTerminal(client, container).start()
"""

operation = RunOperation(client, container, interactive=interactive, stdout=stdout,
stderr=stderr, stdin=stdin, logs=logs)

PseudoTerminal(client, operation).start()
Loading

0 comments on commit 444fef2

Please sign in to comment.