Compare commits

...

6 Commits

Author SHA1 Message Date
182e8b9a94 Beta version, working PoC 2021-02-15 13:51:48 +01:00
e122d5a452 init script and mock data/passwords files 2021-02-15 12:50:14 +01:00
15a80a883c update 2021-02-15 12:38:46 +01:00
4130d113ba doc 2021-02-15 12:37:59 +01:00
e3e7b7b5f7 custom status possible 2021-02-15 10:43:25 +01:00
fcf90f9f81 url args working 2021-02-15 10:37:35 +01:00
14 changed files with 211 additions and 231 deletions

View File

@ -0,0 +1 @@
production_eleves

View File

@ -1,25 +1,30 @@
FROM python:3-alpine
#TODO as an educational env, we sould use debian or centos. more like debian ? A dockerfile each ?
FROM python:3
#TODO as an educational env, we sould use debian or centos. more like debian? A dockerfile each?
RUN apk update && apk add gcc linux-headers build-base nginx openssh
RUN pip install uwsgi
RUN apt update && apt install -y gcc nginx openssh-server
RUN pip3 install uwsgi
WORKDIR /usr/share/app
WORKDIR /app
RUN addgroup eleve
# Python app
COPY python_app/* ./
COPY python_app/ ./python_app
ENV UID=33
ENV MOUNT=/
ENV TZ=Europe/Paris
RUN MKDIR /tmp/uwsgi
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"]
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 ["sh", "-c", "echo lol"]
# SSH server
RUN mkdir /run/sshd
# Nginx server
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
COPY ./nginx/favicon.ico ./
# Entrypoint
COPY ./entrypoint.sh ./entrypoint.sh

View File

@ -1,60 +1,71 @@
# Python, web and SSH sandbox
For educational purpose only! None of this software is industry grade quality.
**For educational purpose only! None of this software is tested, optimized nor secured. It is actually unsecure on purpose**
This is a very experimental tool, **it may be working**. Any suggestion or PR is welcome.
This repo got several parts :
# How to use it?
## Install docker
CF the interweb TODO
## A python script
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 :
- /m1/f1 -> execute the f1 function from modules/m1.py
- /path/to/m2/f2 -> execute the f2 function from modules/path/to/m2.py
## Run it
While in this file directory, open a terminal and run:
```
docker run -it --name pythonsandbox --rm --network host -v "$(pwd)"/production_eleves:/app/python_app/modules -v "$(pwd)"/config:/app/config adrianamaglio/pythonsandbox
```
(Logs will flow in your terminal, CTRL+C will stop the process).
## SSH server
Allow student to connect via SSH or SFTP to add python files.
create a file named `users.txt`, then passwords and accounts will be generated by `entrypoint.sh`
TODO:
- install and configure the server
- configure chroot
- create the homes in modules directory
## Initialize it
The directory `config` must contain a `users.txt` containing one username per line, or a `passwords.txt` file, containing `username=password` lines.
If you do not provide a password file, it will be generated from user file.
The password file is the database from which users/passwords are created in the system.
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!
## A docker image
## Use it
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.
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.
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
```
docker build . -t pythonsandbox
docker build . -t adrianamaglio/pythonsandbox
```
or pull it
```
TODO: send image to hub
```
## Run the docker image
```
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
```
## Volumes
- `/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.
## Roadmap
## Environment variables
None used, do watever you want
# How does it works?
## Example
With the files under `./app/modules` you can get the following URLs :
- http://localhost/mod1/func1_1
- http://localhost/mod1/func1_2
- http://localhost/myriem/mod2/func2_1
## A python script
It run with uwsgi (CF dockerfile CMD line) and load python modules files according to URL.
For instance, when you HTTP-get `/test/my_function` the function `my_function` is executed from the file `test.py`.
###Default behavior:
- if `test` is a directory, we will try to load default function `index` from file `test/my_function.py`
- 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

5
test-python-ssh/config/init.sh Executable file
View File

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

View File

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

View File

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

View File

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

View File

@ -1,29 +1,70 @@
#!/bin/sh
# Check we got users
if [ ! -f 'users.txt' ] ; then
echo "Missing file users.txt"
exit -1
HOME_BASE="/app/python_app/modules"
USERS_LIST="/app/config/users.txt"
PASSWD_LIST="/app/config/passwords.txt"
CUSTOM_SCRIPT="/app/config/init.sh"
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
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
function genPassowrd () {
genPassowrd () {
tr -dc A-Za-z0-9 </dev/urandom | head -c $1
}
if [ ! -f 'passwords.txt' ] ; then
for user in $(cat users.txt) ; do
echo $user $(genPassowrd 10) >> passwords.txt
if [ ! -f $PASSWD_LIST ] ; then
for user in $(cat "$USERS_LIST") ; do
echo "$user$separator$(genPassowrd 10)" >> $PASSWD_LIST
done
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 -c '/etc/nginx/nginx.conf' &
nginx -c '/etc/nginx/nginx.conf'
# SSH server
#TODO
/usr/sbin/sshd -E /dev/stderr
# Start watever the container should be doing
# TODO start it as www-data
$@
/bin/sh -c "$*"

View File

@ -16,8 +16,8 @@ http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
types_hash_max_size 2048;
types_hash_bucket_size 128;
types_hash_max_size 2048;
types_hash_bucket_size 128;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
@ -30,13 +30,17 @@ server {
listen 80;
listen [::]:80;
root /usr/share/app/modules/
location / {
index index.html main.py;
try_files $uri $uri/ =404;
}
root /usr/share/app/python_app/modules/;
location ~ favicon.ico {
root /usr/share/app/;
}
location ~ \.py {
location / {
# index index.html main.py;
# try_files $uri $uri/ =404;
# }
#location ~ \.py {
include uwsgi_params;
#uwsgi_param PATH_INFO "$1";
#uwsgi_param SCRIPT_NAME /;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,116 +0,0 @@
# $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