<commit_before>
# test runner providing isolation/protection/security
# via Docker containers https://www.docker.io/
# comments at end of file

require_relative 'TestRunner'

class DockerTestRunner
    include TestRunner

  def initialize(dojo, bash=Bash.new)
    raise RuntimeError.new("Docker not installed") if !installed?    
    output,_ = bash.exec(stderr2stdout('docker images'))
    @image_names = DockerTestRunner.image_names(output)
  end

  def runnable?(language)
    @image_names.include?(language.image_name)
  end

  def run(sandbox, command, max_seconds)
    inner_run(sandbox, timeout(command,max_seconds), max_seconds)
  end

  def inner_run(sandbox, command, max_seconds)
    cidfile = sandbox.avatar.path + 'cidfile.txt'    
    language = sandbox.avatar.kata.language
    language_volume = "#{language.path}:#{language.path}:#{read_only}"
    sandbox_volume = "#{sandbox.path}:/sandbox:#{read_write}"
    
    docker_command =
      "docker run" +
        " -u www-data" +
        " --net=#{quoted(none)}" +
        " --cidfile=#{quoted(cidfile)}" +
        " -v #{quoted(language_volume)}" +
        " -v #{quoted(sandbox_volume)}" +
        " -w /sandbox" +
        " #{language.image_name}" +
        " /bin/bash -c" +
        " #{quoted(command)}"
        
    outer_command = timeout(docker_command,max_seconds+5)

    bash.exec("rm -f #{cidfile}")
    output,exit_status = bash.exec("#{outer_command}")
    pid,_ = bash.exec("cat #{cidfile}")
    bash.exec("docker stop #{pid} ; docker rm #{pid}")

    exit_status != fatal_error(kill) ?
        limited(clean(output),50*1024) :
        didnt_complete(max_seconds)
  end

  def self.image_names(output)
    output.split("\n")[1..-1].collect{|line| line.split[0]}.sort.uniq
  end

private

  include Cleaner

  def timeout(command,after)
    "timeout --signal=#{kill} #{after}s #{stderr2stdout(command)}"
  end

  def quoted(arg)
    '"' + arg + '"'
  end

  def fatal_error(signal)
    128 + signal
  end

  def kill
    9
  end

  def read_write
    'rw'
  end

  def read_only
    'ro'
  end

  def none
    'none'
  end
  
  def installed?
    _,exit_status = bash.exec(stderr2stdout('docker info > /dev/null'))
    exit_status === 0
  end

  def stderr2stdout(cmd)
    cmd + ' 2>&1'
  end

end


# docker run
#    " -u www-data" +
#    " --net=#{quoted('none')}" +
#    " --cidfile=#{quoted(cidfile)}" +
#    " -v #{quoted(language_volume)}" +
#    " -v #{quoted(sandbox_volume)}" +
#    " -w /sandbox" +
#    " #{language.image_name}" +
#    " /bin/bash -c" +
#    " #{quoted(command)}"
#
# -u www-data
#   The user which runs the inner_command *inside* the docker container.
#   See comments in languages/C#-NUnit/Dockerfile
#
# --net="none"
#   Turn off all networking inside the container.
#
# --cidfile=#{quoted(cidfile)}
#   I use a cidfile in the avatars folder (and not its sandbox folder)
#   to avoid  potential clash with a visible_file with the same name.
#   The cidfile must not exist before the docker command is run.
#   Thus I rm the cidfile *before* the docker run. 
#   After the docker run I retrieve the docker container's pid 
#   from the cidfile and stop and kill the container.
#   Explicitly specifying the cidfile like this (and not using the
#   docker --rm option) ensures the docker container is always killed,
#   even if the timeout occurs.
#
# -v #{quoted(language_volume)}
#   Volume mount the language's folder to the same folder inside
#   the docker container. Intermediate folders are created as necessary
#   (like mkdir -p). This provides access to supporting files which
#   were sym-linked from the animal's sandbox when the animal started.
#   Not all languages have symlinks but it's simpler to just do it anyway.
#   Mounted as a read-only volume since these support files are shared
#   by all animals in all katas that choose that language.
#   Important to quote the volume incase any paths contain spaces
#   eg languages/C++ (clang++)/
#
# -v #{quoted(sandbox_volume)}
#   Volume mount the animal's sandbox to /sandbox inside the docker 
#   container as a read-write folder. This provides isolation.
#   Important to quote the volume incase any paths contain spaces
#
# -w /sandbox
#   Working directory when the command is run is /sandbox
#   (as volume mounted in the first -v option)
#
# #{language.image_name}
#   The name of the docker image to run the inner-command inside.
#   specified in the language's manifest as its image_name.
#
# /bin/bash -c #{quoted(command)}
#   The command that is run as the docker container's "main" is always
#   './cyber-dojo.sh' which is run via bash inside a timeout.
#   I put a timeout on the outer docker-run command and not on 
#   the inner bash -c command. This is for security. If it was
#   on the inner bash -c command then a determined attacker might
#   kill the timeout but not the timed-task and thus acquire 
#   unlimited time to run any command.

<commit_msg>added bash method to DockerTestRunner<commit_after>
# test runner providing isolation/protection/security
# via Docker containers https://www.docker.io/
# comments at end of file

require_relative 'TestRunner'

class DockerTestRunner
    include TestRunner

  def initialize(dojo, bash=Bash.new)
    @bash = bash
    raise RuntimeError.new("Docker not installed") if !installed?    
    output,_ = bash(stderr2stdout('docker images'))
    @image_names = DockerTestRunner.image_names(output)
  end

  def runnable?(language)
    @image_names.include?(language.image_name)
  end

  def run(sandbox, command, max_seconds)
    inner_run(sandbox, timeout(command,max_seconds), max_seconds)
  end

  def inner_run(sandbox, command, max_seconds)
    cidfile = sandbox.avatar.path + 'cidfile.txt'    
    language = sandbox.avatar.kata.language
    language_volume = "#{language.path}:#{language.path}:#{read_only}"
    sandbox_volume = "#{sandbox.path}:/sandbox:#{read_write}"
    
    docker_command =
      "docker run" +
        " -u www-data" +
        " --net=#{quoted(none)}" +
        " --cidfile=#{quoted(cidfile)}" +
        " -v #{quoted(language_volume)}" +
        " -v #{quoted(sandbox_volume)}" +
        " -w /sandbox" +
        " #{language.image_name}" +
        " /bin/bash -c" +
        " #{quoted(command)}"
        
    outer_command = timeout(docker_command,max_seconds+5)

    bash("rm -f #{cidfile}")
    output,exit_status = bash("#{outer_command}")
    pid,_ = bash("cat #{cidfile}")
    bash("docker stop #{pid} ; docker rm #{pid}")

    exit_status != fatal_error(kill) ?
        limited(clean(output),50*1024) :
        didnt_complete(max_seconds)
  end

  def self.image_names(output)
    output.split("\n")[1..-1].collect{|line| line.split[0]}.sort.uniq
  end

private

  include Cleaner

  def bash(command)
    @bash.exec(command)
  end

  def installed?
    _,exit_status = bash(stderr2stdout('docker info > /dev/null'))
    exit_status === 0
  end
  
  def timeout(command,after)
    "timeout --signal=#{kill} #{after}s #{stderr2stdout(command)}"
  end

  def quoted(arg)
    '"' + arg + '"'
  end

  def fatal_error(signal)
    128 + signal
  end

  def kill
    9
  end

  def read_write
    'rw'
  end

  def read_only
    'ro'
  end

  def none
    'none'
  end
  
end


# docker run
#    " -u www-data" +
#    " --net=#{quoted('none')}" +
#    " --cidfile=#{quoted(cidfile)}" +
#    " -v #{quoted(language_volume)}" +
#    " -v #{quoted(sandbox_volume)}" +
#    " -w /sandbox" +
#    " #{language.image_name}" +
#    " /bin/bash -c" +
#    " #{quoted(command)}"
#
# -u www-data
#   The user which runs the inner_command *inside* the docker container.
#   See comments in languages/C#-NUnit/Dockerfile
#
# --net="none"
#   Turn off all networking inside the container.
#
# --cidfile=#{quoted(cidfile)}
#   I use a cidfile in the avatars folder (and not its sandbox folder)
#   to avoid  potential clash with a visible_file with the same name.
#   The cidfile must not exist before the docker command is run.
#   Thus I rm the cidfile *before* the docker run. 
#   After the docker run I retrieve the docker container's pid 
#   from the cidfile and stop and kill the container.
#   Explicitly specifying the cidfile like this (and not using the
#   docker --rm option) ensures the docker container is always killed,
#   even if the timeout occurs.
#
# -v #{quoted(language_volume)}
#   Volume mount the language's folder to the same folder inside
#   the docker container. Intermediate folders are created as necessary
#   (like mkdir -p). This provides access to supporting files which
#   were sym-linked from the animal's sandbox when the animal started.
#   Not all languages have symlinks but it's simpler to just do it anyway.
#   Mounted as a read-only volume since these support files are shared
#   by all animals in all katas that choose that language.
#   Important to quote the volume incase any paths contain spaces
#   eg languages/C++ (clang++)/
#
# -v #{quoted(sandbox_volume)}
#   Volume mount the animal's sandbox to /sandbox inside the docker 
#   container as a read-write folder. This provides isolation.
#   Important to quote the volume incase any paths contain spaces
#
# -w /sandbox
#   Working directory when the command is run is /sandbox
#   (as volume mounted in the first -v option)
#
# #{language.image_name}
#   The name of the docker image to run the inner-command inside.
#   specified in the language's manifest as its image_name.
#
# /bin/bash -c #{quoted(command)}
#   The command that is run as the docker container's "main" is always
#   './cyber-dojo.sh' which is run via bash inside a timeout.
#   I put a timeout on the outer docker-run command and not on 
#   the inner bash -c command. This is for security. If it was
#   on the inner bash -c command then a determined attacker might
#   kill the timeout but not the timed-task and thus acquire 
#   unlimited time to run any command.


{'fraction_non_alphanumeric': 0.11513622603430877, 'fraction_numerical': 0.003935418768920283, 'mean_word_length': 3.1853885135135136, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 6, 'https://': 2, 'lorem ipsum': 0, 'www.': 8, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5735631', 'n_tokens_mistral': 3310, 'n_tokens_neox': 3161, 'n_words': 1033}