Compare commits

..

No commits in common. "182e8b9a94ed016b96ec03a1ddd0d2766a842d3d" and "9d453878604ff457af83d1d799ee2598623a30de" have entirely different histories.

14 changed files with 230 additions and 210 deletions

View File

@ -1 +0,0 @@
production_eleves

View File

@ -1,30 +1,25 @@
FROM python:3 FROM python:3-alpine
#TODO as an educational env, we sould use debian or centos. more like debian? A dockerfile each? #TODO as an educational env, we sould use debian or centos. more like debian ? A dockerfile each ?
RUN apt update && apt install -y gcc nginx openssh-server RUN apk update && apk add gcc linux-headers build-base nginx openssh
RUN pip3 install uwsgi RUN pip install uwsgi
WORKDIR /app WORKDIR /usr/share/app
RUN addgroup eleve
# Python app # Python app
COPY python_app/ ./python_app COPY python_app/* ./
ENV UID=33 ENV UID=33
ENV MOUNT=/ ENV MOUNT=/
ENV TZ=Europe/Paris
RUN mkdir /tmp/uwsgi RUN MKDIR /tmp/uwsgi
CMD ["uwsgi", "-s", "/tmp/uwsgi/uwsgi.sock", "--chown-socket", "${UID}", "--manage-script-name", "--mount", "${MOUNT}=python_app.main:application", "--http-timeout", "10", "--master", "--hook-master-start", "'unix_signal:15 gracefully_kill_them_all'", "--need-app", "--die-on-term", "--show-config", "--log-master", "--strict", "--vacuum", "--single-interpreter"] CMD ["uwsgi", "--chown-socket", "$UID", "-s", "/tmp/uwsgi/uwsgi.sock", "--manage-script-name", "--mount", "$MOUNT=main:prod_app", "--http-timeout", "10", "--master", "--hook-master-start", "unix_signal:15gracefully_kill_them_all", "--need-app", "--die-on-term", "--show-config", "--log-master", "--strict", "--vacuum", "--single-interpreter"]
#CMD ["sh", "-c", "echo lol"]
# SSH server # SSH server
RUN mkdir /run/sshd
# Nginx server # Nginx server
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./nginx/favicon.ico ./
# Entrypoint # Entrypoint
COPY ./entrypoint.sh ./entrypoint.sh COPY ./entrypoint.sh ./entrypoint.sh

View File

@ -1,71 +1,60 @@
# Python, web and SSH sandbox # Python, web and SSH sandbox
**For educational purpose only! None of this software is tested, optimized nor secured. It is actually unsecure on purpose** For educational purpose only! None of this software is industry grade quality.
This is a very experimental tool, **it may be working**. Any suggestion or PR is welcome.
# How to use it? This repo got several parts :
## Install docker
CF the interweb TODO
## Run it ## A python script
While in this file directory, open a terminal and run: It run with uwsgi, see the dockerfile CMD line. You will need to change it to make it listen on a port.
``` Used to execute any python script in the `module` directory given a certain URL :
docker run -it --name pythonsandbox --rm --network host -v "$(pwd)"/production_eleves:/app/python_app/modules -v "$(pwd)"/config:/app/config adrianamaglio/pythonsandbox - /m1/f1 -> execute the f1 function from modules/m1.py
``` - /path/to/m2/f2 -> execute the f2 function from modules/path/to/m2.py
(Logs will flow in your terminal, CTRL+C will stop the process).
## Initialize it ## SSH server
The directory `config` must contain a `users.txt` containing one username per line, or a `passwords.txt` file, containing `username=password` lines. Allow student to connect via SSH or SFTP to add python files.
If you do not provide a password file, it will be generated from user file. create a file named `users.txt`, then passwords and accounts will be generated by `entrypoint.sh`
The password file is the database from which users/passwords are created in the system. TODO:
APermitRootLogindditionnaly, you can add a file named `./config/init.sh` which will be executed (as root) before starting the servers. It is usefull for debuging and customisation purposes! - install and configure the server
- configure chroot
- create the homes in modules directory
## Use it ## A docker image
You can now ssh into your localhost (and others computer on the same network can ssh into your host).
Usernames and passwords are the one provided in the password file.
Students home directories are then listed in the `production_eleves` directory.
## Debug it
You can open a shell in the container anytime by running this command on the docker host:
```
docker exec -it pythonsandbox bash
```
# The docker image
To bundle everything in one place. To bundle everything in one place.
This docker image is not a pretty one, we should split those services into several containers. This docker image is not a pretty one, we should split those services into several containers.
But that would be harder to run, so forget that. But that would be harder to run, so forget that.
Also, as this is poorly tested, the docker system make sure the environment is stable. Also, as this is poorly tested, the docker system make sure the environment is stable.
# Instructions
## Install docker
CF the interweb TODO
## Build the docker image ## Build the docker image
``` ```
docker build . -t adrianamaglio/pythonsandbox docker build . -t pythonsandbox
``` ```
or pull it or pull it
``` ```
TODO: send image to hub TODO: send image to hub
``` ```
## Volumes ## Run the docker image
- `/app/modules` is where python scripts will be executed on some URL calls. ```
- `/app/config` is the location of the user, password and init files. docker run -it --network host --name pythonsandbox pythonsandbox
```
Or if you want to save student work outside of the container:
```
docker run -it --network host --name pythonsandbox -v "$(pwd)"/app/modules:/usr/share/app/modules pythonsandbox
```
And with user list file
```
docker run -it --network host --name pythonsandbox -v "$(pwd)"/app/modules:/usr/share/app/modules -v "$(pwd)"/app/users.txt:/usr/share/app/users.txt pythonsandbox
```
## Environment variables ## Roadmap
None used, do watever you want
# How does it works?
## A python script ## Example
It run with uwsgi (CF dockerfile CMD line) and load python modules files according to URL. With the files under `./app/modules` you can get the following URLs :
For instance, when you HTTP-get `/test/my_function` the function `my_function` is executed from the file `test.py`. - http://localhost/mod1/func1_1
###Default behavior: - http://localhost/mod1/func1_2
- if `test` is a directory, we will try to load default function `index` from file `test/my_function.py` - http://localhost/myriem/mod2/func2_1
- if `test/my_function` is a directory, we will try to load default function `index` from file `test/my_function/main.py`
If you dont like this default behavior, just dont use main and index names.
### Arguments
GET arguments (the ones in URL), are passed as function parameter (only if the parameters name matches the arguments name).
## SSH server
Allow student to connect via SSH or SFTP to add python files and play with bash.
## NGINX HTTP server
For more flexibility with HTTP

View File

@ -1,5 +0,0 @@
#!/bin/bash
echo INIIIIT
# set root passwd
echo -e "root\nroot" | passwd

View File

@ -1,2 +0,0 @@
218-amine=TPA6eMfztS
218-chems=osjMQQ8rXd

View File

@ -1,2 +0,0 @@
218-amine
218-chems

View File

@ -1,8 +0,0 @@
version: '3'
services:
app:
build: .
volumes:
- ./config:/app/config
- ./production_eleves:/app/python_app/modules
network_mode: "host"

View File

@ -1,70 +1,29 @@
#!/bin/sh #!/bin/sh
HOME_BASE="/app/python_app/modules" # Check we got users
USERS_LIST="/app/config/users.txt" if [ ! -f 'users.txt' ] ; then
PASSWD_LIST="/app/config/passwords.txt" echo "Missing file users.txt"
CUSTOM_SCRIPT="/app/config/init.sh" exit -1
separator="=" # Must be ascii for cut
forbidden_chars=". /"
# Check we got user list
if [ ! -f "$USERS_LIST" ] && [ ! -f "$PASSWD_LIST" ] ; then
echo "Les fichiers des utilisateurs ou des passwords nont pas étés trouvées."
exit 1
fi fi
for c in $forbidden_chars ; do
for file in "$USERS_LIST" "$PASSWD_LIST" ; do
if [ -n "$(cat "$USERS_LIST" | grep -F $c)" ] ; then
echo "Le fichier « $file » ne doit pas contenir le caractère « $c » !"
exit 1
fi
done
done
# Generate passwords if not done yet # Generate passwords if not done yet
genPassowrd () { function genPassowrd () {
tr -dc A-Za-z0-9 </dev/urandom | head -c $1 tr -dc A-Za-z0-9 </dev/urandom | head -c $1
} }
if [ ! -f $PASSWD_LIST ] ; then
for user in $(cat "$USERS_LIST") ; do if [ ! -f 'passwords.txt' ] ; then
echo "$user$separator$(genPassowrd 10)" >> $PASSWD_LIST for user in $(cat users.txt) ; do
echo $user $(genPassowrd 10) >> passwords.txt
done done
fi fi
# Create users, home dirs, change passwords and home owners
for line in $(cat $PASSWD_LIST) ; do
name="$(echo "$line" | cut -d "$separator" -f 1)"
pass="$(echo "$line" | cut -d "$separator" -f 2)"
home="$HOME_BASE/$name"
mkdir -p "$home"
#useradd --home-dir "$home" --no-user-group -G eleve --shell /bin/bash --root "$home" "$name"
useradd --home-dir "$home" --no-user-group -G eleve --shell /bin/bash "$name"
echo "$pass\n$pass" | passwd "$name" &> /dev/null
chown "$name":eleve "$home"
done
# Allow SSH as root
if [ -z "$(grep '^PermitRootLogin yes' /etc/ssh/sshd_config)" ] ; then
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
fi
echo "\nFin de la préparation des utilisateurs.\n"
# Custom script
if [ -f "$CUSTOM_SCRIPT" ] ; then
if [ ! -x "$CUSTOM_SCRIPT" ] ; then
chmod +x "$CUSTOM_SCRIPT"
fi
"$CUSTOM_SCRIPT"
fi
# Nginx # Nginx
nginx -c '/etc/nginx/nginx.conf' nginx -c '/etc/nginx/nginx.conf' &
# SSH server # SSH server
/usr/sbin/sshd -E /dev/stderr #TODO
# Start watever the container should be doing # Start watever the container should be doing
/bin/sh -c "$*" # TODO start it as www-data
$@

View File

@ -30,17 +30,13 @@ server {
listen 80; listen 80;
listen [::]:80; listen [::]:80;
root /usr/share/app/python_app/modules/; root /usr/share/app/modules/
location ~ favicon.ico { location / {
root /usr/share/app/; index index.html main.py;
try_files $uri $uri/ =404;
} }
location / { location ~ \.py {
# index index.html main.py;
# try_files $uri $uri/ =404;
# }
#location ~ \.py {
include uwsgi_params; include uwsgi_params;
#uwsgi_param PATH_INFO "$1"; #uwsgi_param PATH_INFO "$1";
#uwsgi_param SCRIPT_NAME /; #uwsgi_param SCRIPT_NAME /;

View File

@ -4,91 +4,70 @@ import importlib
# Get function args # Get function args
import inspect import inspect
# check file properties
import os
# The directory where student work will be # The directory where student work will be
# not a real path, do not use ./ or stuff like this # not a real path, do not use ./ or stuff like this
BASE_MODULE_PATH = 'python_app/modules' BASE_MODULE_PATH = 'modules'
if BASE_MODULE_PATH != '': if BASE_MODULE_PATH != '':
BASE_MODULE_PATH += '/' BASE_MODULE_PATH += '/'
def application(env, start_response): def application(env, start_response):
""" Cette fonction est appellée à chaque requête HTTP et doit exécuter le bon code python. """ # Some hard-coded paths
if env['PATH_INFO'] == '/favicon.ico':
path = env['PATH_INFO'][1:] # removing first slash return file_content('favicon.ico')
# slash stuff if env['PATH_INFO'] == '/':
if path.endswith('/'): return index()
path = path[:-1]
path_elements = tuple(d for d in path.split('/') if d != '')
path_minus_one = '/'.join(path_elements[:-1])
# Find which python module and function will be called # Find which python module and function will be called
if os.path.isfile(BASE_MODULE_PATH + path_minus_one): elements = env['PATH_INFO'].split('/')[1:] # Removing the first empty element
path = path_minus_one path = ''
function = path_elements[-1] module = 'main'
elif os.path.isfile(BASE_MODULE_PATH + path):
function = 'index' function = 'index'
elif os.path.isdir(BASE_MODULE_PATH + path): if len(elements) == 1:
path += '/main' module = elements[0]
function = 'index' elif len(elements) == 2:
else: module = elements[0]
return htmlresp(404, 'Le dossier <em>{}</em> na pas été trouvé.'.format(path), start_response) function = elements[1]
elif len(elements) > 2:
path = '/'.join(elements[0:-2])
module = elements[-2]
# Module full path function = elements[-1]
module_path = BASE_MODULE_PATH + path if path != '':
path += '/'
module_path = BASE_MODULE_PATH + path + module
module_path = module_path.replace('/', '.') module_path = module_path.replace('/', '.')
# Import the function # Import the function
try: try:
m = importlib.import_module(module_path) m = importlib.import_module(module_path)
importlib.reload(m)
except ModuleNotFoundError: except ModuleNotFoundError:
#print('Le fichier {} na pas été trouvé. {}'.format(module_path, str(path_elements))) print('Le fichier {} na pas été trouvé.'.format(module_path))
return htmlresp(404, 'Le fichier <em>{}</em> na pas été trouvé.'.format(path), start_response) return htmlresp(404, 'Le fichier {} na pas été trouvé'.format(path + module), start_response)
# Find which parameters the function needs # Find which parameters the function needs
params = {}
try: try:
f = getattr(m,function) f = getattr(m,function)
# Call the function with the rigth attributes # TODO get http parameters and give them to the function
#print(inspect.signature(f))
except AttributeError: except AttributeError:
return htmlresp(404, 'La fonction <em>{}</em> na pas été trouvée dans {}'.format(function, module_path), start_response) return htmlresp(404, 'La fonction {} na pas été trouvée'.format(function), start_response)
# Pass url parameters to the function # Call the function with the rigth attributes
try: return [bytes(str(f(*params)), 'utf8')]
#print(inspect.getargspec(f))
#params = {p:(d if d is not None else '') for p,d in zip(inspect.getargspec(f).args, inspect.getargspec(f).defaults)}
expected_params = inspect.getargspec(f).args
params = {}
# TODO POST params?
for item in env['QUERY_STRING'].split('&'):
if item == '':
continue
k,v = tuple(item.split('='))
if k in expected_params:
params[k] = v
except Exception:
return htmlresp(400, 'La fonction <em>{}</em> demande les arguments suivants : {}. On a uniquement {}.'.format(function, params, ), start_response)
try:
response = f(**params)
if type(response) is tuple:
return htmlresp(response[0], str(response[1]), start_response, False)
else:
return htmlresp(200, str(response), start_response, False)
except Exception as e:
return htmlresp(400, 'Erreur à lexécution de la fonction <em>{}</em>.<pre style="color:#d90303;">{}</pre>'.format(function, e), start_response)
def htmlresp(code, message, start_response, squelette=True): def index ():
""" Cette fonction crée le squelette HTML minimal """ return file_content('passwords.txt')
def htmlresp(code, message, start_response):
html = '<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>{}</body></html>' html = '<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>{}</body></html>'
return resp(code, [('Content-Type','text/html')], html.format(message) if squelette else message, start_response) return resp(code, [('Content-Type','text/html')], html.format(message), start_response)
def resp(code, headers, message, start_response): def resp(code, headers, message, start_response):
""" Cette fonction permet de faire une réponse HTTP """
start_response(str(code), headers) start_response(str(code), headers)
return bytes(message, 'utf8') return bytes(message, 'utf8')
def file_content (filename):
with open(filename, mode='rb') as file:
return file.read()

View File

@ -1,3 +0,0 @@
def index(lol, mdr=True):
""" Main entrypoint """
return 'Bienvenue ! lol:{} mdr:{} '.format(lol,mdr)

View File

@ -0,0 +1,5 @@
def func1_1():
return "Bonjour de func1_1"
def func1_2():
return "Bonjour de func1_2"

View File

@ -0,0 +1,2 @@
218.amine
218.chems

View File

@ -0,0 +1,116 @@
# $OpenBSD: sshd_config,v 1.103 2018/04/09 20:41:22 tj Exp $
# This is the sshd server system-wide configuration file. See
# sshd_config(5) for more information.
# This sshd was compiled with PATH=/usr/local/sbin:/usr/local/bin:/usr/bin
# The strategy used for options in the default sshd_config shipped with
# OpenSSH is to specify options with their default value where
# possible, but leave them commented. Uncommented options override the
# default value.
#Port 22
#AddressFamily any
#ListenAddress 0.0.0.0
#ListenAddress ::
#HostKey /etc/ssh/ssh_host_rsa_key
#HostKey /etc/ssh/ssh_host_ecdsa_key
#HostKey /etc/ssh/ssh_host_ed25519_key
# Ciphers and keying
#RekeyLimit default none
# Logging
#SyslogFacility AUTH
#LogLevel INFO
# Authentication:
#LoginGraceTime 2m
#PermitRootLogin prohibit-password
#StrictModes yes
#MaxAuthTries 6
#MaxSessions 10
#PubkeyAuthentication yes
# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2
# but this is overridden so installations will only check .ssh/authorized_keys
AuthorizedKeysFile .ssh/authorized_keys
#AuthorizedPrincipalsFile none
#AuthorizedKeysCommand none
#AuthorizedKeysCommandUser nobody
# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts
#HostbasedAuthentication no
# Change to yes if you don't trust ~/.ssh/known_hosts for
# HostbasedAuthentication
#IgnoreUserKnownHosts no
# Don't read the user's ~/.rhosts and ~/.shosts files
#IgnoreRhosts yes
# To disable tunneled clear text passwords, change to no here!
#PasswordAuthentication yes
#PermitEmptyPasswords no
# Change to no to disable s/key passwords
ChallengeResponseAuthentication no
# Kerberos options
#KerberosAuthentication no
#KerberosOrLocalPasswd yes
#KerberosTicketCleanup yes
#KerberosGetAFSToken no
# GSSAPI options
#GSSAPIAuthentication no
#GSSAPICleanupCredentials yes
# Set this to 'yes' to enable PAM authentication, account processing,
# and session processing. If this is enabled, PAM authentication will
# be allowed through the ChallengeResponseAuthentication and
# PasswordAuthentication. Depending on your PAM configuration,
# PAM authentication via ChallengeResponseAuthentication may bypass
# the setting of "PermitRootLogin without-password".
# If you just want the PAM account and session checks to run without
# PAM authentication, then enable this but set PasswordAuthentication
# and ChallengeResponseAuthentication to 'no'.
UsePAM yes
#AllowAgentForwarding yes
#AllowTcpForwarding yes
#GatewayPorts no
#X11Forwarding no
#X11DisplayOffset 10
#X11UseLocalhost yes
#PermitTTY yes
PrintMotd no # pam does that
#PrintLastLog yes
#TCPKeepAlive yes
#PermitUserEnvironment no
#Compression delayed
#ClientAliveInterval 0
#ClientAliveCountMax 3
#UseDNS no
#PidFile /run/sshd.pid
#MaxStartups 10:30:100
#PermitTunnel no
#ChrootDirectory none
#VersionAddendum none
# no default banner path
#Banner none
# override default of no subsystems
Subsystem sftp /usr/lib/ssh/sftp-server
# Example of overriding settings on a per-user basis
#Match User anoncvs
# X11Forwarding no
# AllowTcpForwarding no
# PermitTTY no
# ForceCommand cvs server