Compare commits

..

No commits in common. "fee6fdec2bad704813f772dedec1da5f6a6f4f0d" and "fe0e36796aab997a8431a527bea0758ee4c31572" have entirely different histories.

12 changed files with 32 additions and 453 deletions

View File

@ -1,11 +1,8 @@
FROM python:3.11-slim
# Create a non-root user for security
RUN useradd --create-home --shell /bin/bash app
# Install gcc compiler for C code compilation and other production dependencies
# Install gcc compiler for C code compilation
RUN apt-get update && \
apt-get install -y gcc build-essential curl && \
apt-get install -y gcc build-essential && \
rm -rf /var/lib/apt/lists/*
# Set working directory
@ -18,12 +15,8 @@ RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Change ownership to the app user
RUN chown -R app:app /app
USER app
# Expose port 5000
EXPOSE 5000
# Run the application with Gunicorn in production mode using config file
CMD ["gunicorn", "--config", "gunicorn.conf.py", "app:app"]
# Run the application with Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "1", "app:app"]

185
README.md
View File

@ -70,38 +70,6 @@ The system uses Markdown files for content management. There are two main types
The content directory is located at the parent directory level, outside of the `elemes/` directory. This allows for easy linking and sharing of content between different instances of the LMS.
### Environment Configuration
The application uses environment variables for configuration. Create a `.env` file at the parent directory level (same level as the `elemes/` directory) to configure the application. The `podman-compose.yml` file is configured to read environment variables from `../.env`.
Example `.env` file:
```
# Production Environment Configuration
# Flask Configuration
FLASK_ENV=production
FLASK_DEBUG=0
SECRET_KEY=your-super-secret-key-here-change-this-in-production
# Application Configuration
CONTENT_DIR=content
STATIC_DIR=static
TEMPLATES_DIR=templates
TOKENS_FILE=tokens.csv
# Server Configuration
HOST=0.0.0.0
PORT=5000
# Security Configuration
MAX_CONTENT_LENGTH=1024 * 1024 # 1MB max upload size
WTF_CSRF_ENABLED=true
# Logging Configuration
LOG_LEVEL=INFO
LOG_FILE=/var/log/lms-c/app.log
```
### Home Page (`home.md`)
The home page serves as the main landing page and lesson directory. Here's the template structure:
@ -424,121 +392,13 @@ This LMS can be used as a submodule in another repository. To set it up:
3. Add your lesson files to the content directory
4. Create an environment configuration file at the root level:
```bash
touch .env
```
5. Run the LMS from the elemes directory:
4. Run the LMS from the elemes directory:
```bash
cd elemes
podman-compose -f podman-compose.yml up --build
```
The content directory and environment configuration at the root level will be automatically mounted to the application container.
## Production Deployment
For production deployment, additional considerations and configurations are required to ensure security, performance, and reliability.
### Production Environment Setup
1. **Environment Variables**: Use the following environment variables for production:
- `FLASK_ENV=production` - Sets the environment to production
- `FLASK_DEBUG=0` - Disables debug mode
- `PORT=5000` - Specifies the port to run the application on
2. **Security Considerations**:
- Run the container as a non-root user (already configured in Dockerfile)
- Limit resource usage with podman/docker resource constraints
- Use a reverse proxy (like Nginx) in front of the application
- Implement proper HTTPS with SSL/TLS certificates
3. **Performance Optimizations**:
- Use the multi-worker Gunicorn configuration (already configured)
- Set appropriate worker processes based on your server's CPU cores
- Configure proper caching mechanisms if needed
### Production Deployment with Podman
To deploy in a production environment:
1. Build the production image:
```bash
podman build -t lms-c:prod .
```
2. Run with production settings:
```bash
podman run -d \
--name lms-c-prod \
-p 80:5000 \
-v /path/to/production/content:/app/content \
-v /path/to/production/static:/app/static \
-v /path/to/production/tokens.csv:/app/tokens.csv \
--restart=unless-stopped \
--memory=512m \
--cpus=1 \
lms-c:prod
```
### Production Deployment with Podman Compose
Create a production-specific compose file (`podman-compose.prod.yml`):
```yaml
version: '3.8'
services:
lms-c:
build: .
ports:
- "80:5000"
volumes:
- /path/to/production/content:/app/content
- /path/to/production/static:/app/static
- /path/to/production/templates:/app/templates
- /path/to/production/tokens.csv:/app/tokens.csv
env_file:
- /path/to/production/.env
restart: unless-stopped
mem_limit: 512m
cpus: 1.0
```
Run with:
```bash
podman-compose -f podman-compose.prod.yml up -d --build
```
### Reverse Proxy Configuration
For production, it's recommended to put a reverse proxy like Nginx in front of the application:
Example Nginx configuration:
```
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
### SSL/HTTPS Setup
For HTTPS, you can use Let's Encrypt with Certbot:
```bash
sudo certbot --nginx -d your-domain.com
```
This will automatically configure SSL certificates for your domain.
The content directory at the root level will be automatically mounted to the application container.
## Security Considerations
@ -556,12 +416,8 @@ This will automatically configure SSL certificates for your domain.
- `tokens.csv`: Student progress tracking file
- `generate_tokens.py`: Script to generate tokens CSV file
- `Dockerfile`: Container configuration
- `podman-compose.yml`: Podman Compose configuration with env_file support
- `podman-compose.yml`: Podman Compose configuration
- `requirements.txt`: Python dependencies
- `test/`: Directory containing test scripts and load testing tools
- `test/test_production.sh`: Production environment test script
- `test/locustfile.py`: Load testing script using Locust
- `test/podman-compose.locust.yml`: Podman Compose configuration for load testing
## Development
@ -571,27 +427,12 @@ To access the running container for development:
podman exec -it <container_name> /bin/bash
```
## Testing
The project includes comprehensive test scripts:
1. **Production Environment Tests**: Run the production test script to verify all functionality:
```bash
./test/test_production.sh
```
2. **Load Testing**: Use the included Locust configuration for load testing:
```bash
podman-compose -f test/podman-compose.locust.yml up --build
```
## Troubleshooting
- If you get permission errors, make sure your user has access to Podman
- If the application doesn't start, check that port 5000 is available
- If code compilation fails, verify that the gcc compiler is available in the container
- If progress tracking isn't working, ensure the tokens.csv file is properly formatted
- If environment variables aren't being loaded, verify that the .env file is in the correct location and properly formatted
## Stopping the Application
@ -721,11 +562,11 @@ The system includes support for load testing using Locust to simulate multiple c
4. **Alternative: Run Locust Directly**
- Install Locust on the load testing machine:
```bash
pip install -r test/requirements.txt
pip install -r locust/requirements.txt
```
- Run Locust directly:
```bash
cd test
cd locust
locust -f locustfile.py --host http://<LMS_SERVER_IP>:5000
```
@ -746,19 +587,3 @@ Monitor the LMS server's resource usage (CPU, memory, disk I/O) during load test
- Compilation performance under load
- Database performance (if one is added in the future)
- Container resource limits
The included `locustfile.py` simulates the following user behaviors:
- Browsing the home page
- Viewing lessons
- Compiling C code
- Validating student tokens
- Logging in with tokens
- Tracking student progress
### Monitoring Performance
Monitor the LMS server's resource usage (CPU, memory, disk I/O) during load testing to identify potential bottlenecks. Pay attention to:
- Response times for API requests
- Compilation performance under load
- Database performance (if one is added in the future)
- Container resource limits

View File

@ -18,7 +18,7 @@ else
podman exec -it lms-c-container /bin/bash
else
echo "Building and starting container..."
podman build -t lms-c . && podman run -d -p 5000:5000 --name lms-c-container -v ../content:/app/content -v ./static:/app/static -v ./templates:/app/templates -v ./tokens.csv:/app/tokens.csv -e FLASK_ENV=production -e FLASK_DEBUG=0 lms-c
podman build -t lms-c . && podman run -d -p 5000:5000 --name lms-c-container -v ../content:/app/content -v ./static:/app/static -v ./templates:/app/templates -v ./tokens.csv:/app/tokens.csv lms-c
sleep 5 # Wait for the application to start
podman exec -it lms-c-container /bin/bash
fi

29
app.py
View File

@ -9,7 +9,6 @@ import subprocess
import tempfile
import markdown
from flask import Flask, render_template, request, jsonify, send_from_directory
from flask_talisman import Talisman
import glob
import csv
import uuid
@ -17,29 +16,11 @@ from datetime import datetime
app = Flask(__name__)
# Load configuration from environment variables with defaults
CONTENT_DIR = os.environ.get('CONTENT_DIR', 'content')
STATIC_DIR = os.environ.get('STATIC_DIR', 'static')
TEMPLATES_DIR = os.environ.get('TEMPLATES_DIR', 'templates')
TOKENS_FILE = os.environ.get('TOKENS_FILE', 'tokens.csv')
# Security configuration using Talisman
Talisman(app,
force_https=False, # Set to True if using SSL
strict_transport_security=True,
strict_transport_security_max_age=31536000,
frame_options='DENY',
content_security_policy={
'default-src': "'self'",
'script-src': "'self' 'unsafe-inline' https://cdnjs.cloudflare.com",
'style-src': "'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com",
'img-src': "'self' data: https:",
'font-src': "'self' https://cdn.jsdelivr.net",
'connect-src': "'self'",
'frame-ancestors': "'none'",
},
referrer_policy='strict-origin-when-cross-origin'
)
# Configuration
CONTENT_DIR = 'content'
STATIC_DIR = 'static'
TEMPLATES_DIR = 'templates'
TOKENS_FILE = 'tokens.csv'
def get_lessons():
"""Get all lesson files from the content directory"""

View File

@ -1,38 +0,0 @@
# Production Environment Configuration
#
# tailscale Configuration
ELEMES_HOST=change-to-lms-topic
TS_AUTHKEY=your-super-secret-key-here-change-this-in-production
# Flask Configuration
FLASK_ENV=production
FLASK_DEBUG=0
SECRET_KEY=your-super-secret-key-here-change-this-in-production
# Application Configuration
CONTENT_DIR=content
STATIC_DIR=static
TEMPLATES_DIR=templates
TOKENS_FILE=tokens.csv
# Server Configuration
HOST=0.0.0.0
PORT=5000
# Security Configuration
MAX_CONTENT_LENGTH=1024 * 1024 # 1MB max upload size
WTF_CSRF_ENABLED=true
# Logging Configuration
LOG_LEVEL=INFO
LOG_FILE=/var/log/lms-c/app.log
# Database Configuration (if using one in future)
# DATABASE_URL=postgresql://user:password@localhost/dbname
# External Services (if applicable)
# SMTP_SERVER=smtp.example.com
# SMTP_PORT=587
# SMTP_USERNAME=user@example.com
# SMTP_PASSWORD=password

View File

@ -1,19 +0,0 @@
{
"TCP": {
"443": {
"HTTPS": true
}
},
"Web": {
"${TS_CERT_DOMAIN}:443": {
"Handlers": {
"/": {
"Proxy": "http://elemes:5000"
}
}
}
},
"AllowFunnel": {
"${TS_CERT_DOMAIN}:443": true
}
}

View File

@ -1,38 +0,0 @@
# Gunicorn configuration file for production
# Server socket
bind = "0.0.0.0:5000"
backlog = 2048
# Worker processes
workers = 4
worker_class = "sync"
worker_connections = 1000
timeout = 120
keepalive = 5
max_requests = 1000
max_requests_jitter = 100
preload_app = True
# Security
limit_request_line = 4094
limit_request_fields = 100
limit_request_field_size = 8190
# Logging
accesslog = "-"
errorlog = "-"
loglevel = "info"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
# Process naming
proc_name = 'lms-c'
# Server mechanics
user = 'app'
group = 'app'
tmp_upload_dir = None
# SSL (uncomment and configure if using SSL)
# keyfile = '/path/to/keyfile'
# certfile = '/path/to/certfile'

View File

@ -1,38 +1,15 @@
version: '3.8'
services:
elemes:
lms-c:
build: .
container_name: elemes
ports:
- "5000:5000"
volumes:
- ../content:/app/content
- ./static:/app/static
- ./templates:/app/templates
- ../tokens_siswa.csv:/app/tokens.csv
env_file:
- ../.env
command: gunicorn --config gunicorn.conf.py app:app
elemes-ts:
image: docker.io/tailscale/tailscale:latest
container_name: elemes-ts
hostname: ${ELEMES_HOST}
- ./tokens.csv:/app/tokens.csv
environment:
- TS_AUTHKEY=${TS_AUTHKEY}
- TS_SERVE_CONFIG=/config/sinau-c-tail.json
- TS_STATE_DIR=/var/lib/tailscale
- TS_USERSPACE=true
volumes:
- ./state:/var/lib/tailscale
- ../config:/config
- /dev/net/tun:/dev/net/tun
cap_add:
- net_admin
- sys_module
restart: unless-stopped
networks:
main_network:
drive: bridge
network_mode: service:elemes-ts
- FLASK_ENV=development
command: python app.py

View File

@ -1,4 +1,3 @@
Flask==2.3.3
markdown==3.5
gunicorn==21.2.0
flask-talisman==1.1.0

View File

@ -5,19 +5,19 @@
echo "Starting C Programming Learning Management System..."
# Check if container is already running
if [ "$(podman ps -q -f name=elemes-container)" ]; then
if [ "$(podman ps -q -f name=lms-c-container)" ]; then
echo "Container is already running. Access the application at http://localhost:5000"
exit 0
fi
# Check if container exists but is stopped
if [ "$(podman ps -aq -f name=elemes-container)" ]; then
if [ "$(podman ps -aq -f name=lms-c-container)" ]; then
echo "Starting existing container..."
podman start elemes-container
podman start lms-c-container
else
# Build and run the container
echo "Building and starting container..."
podman-compose up --build -d
podman build -t lms-c . && podman run -d -p 5000:5000 --name lms-c-container -v ../content:/app/content -v ./static:/app/static -v ./templates:/app/templates -v ./tokens.csv:/app/tokens.csv lms-c
fi
echo "Application is now running. Access at http://localhost:5000"

View File

@ -5,6 +5,6 @@
echo "Stopping C Programming Learning Management System..."
# Stop and remove the container
podman-compose down
podman stop lms-c-container && podman rm lms-c-container
echo "Application has been stopped."

View File

@ -1,101 +0,0 @@
#!/bin/bash
# Test script for LMS-C Production Environment
echo "Starting LMS-C Production Environment Tests..."
# Start the application
echo "Starting the application..."
cd ..
./start.sh
cd test
# Wait for the application to start
echo "Waiting for application to start..."
sleep 10
# Test 1: Check if the home page loads and displays lessons
echo "Test 1: Checking home page and lesson display..."
HOME_PAGE_CHECK=$(curl -s http://localhost:5000/ | grep -i "Available Lessons" | wc -l)
if [ $HOME_PAGE_CHECK -gt 0 ]; then
echo "✓ Test 1 PASSED: Home page displays lessons correctly"
else
echo "✗ Test 1 FAILED: Home page does not display lessons"
fi
# Test 2: Check if specific lesson pages load
echo "Test 2: Checking lesson page loading..."
LESSON_PAGE_CHECK=$(curl -s http://localhost:5000/lesson/welcome.md | grep -i "C Programming Learning System" | wc -l)
if [ $LESSON_PAGE_CHECK -gt 0 ]; then
echo "✓ Test 2 PASSED: Lesson pages load correctly"
else
echo "✗ Test 2 FAILED: Lesson pages do not load"
fi
# Test 3: Test the code compilation API
echo "Test 3: Testing code compilation API..."
COMPILATION_TEST=$(curl -s -X POST http://localhost:5000/compile \
-H "Content-Type: application/json" \
-d '{"code":"#include <stdio.h>\n\nint main() {\n printf(\"Hello, World!\\n\");\n return 0;\n}"}')
# Check if the response contains success: true and the expected output
if echo "$COMPILATION_TEST" | grep -q '"success":true' && echo "$COMPILATION_TEST" | grep -q "Hello, World!"; then
echo "✓ Test 3 PASSED: Code compilation API works correctly"
else
echo "✗ Test 3 FAILED: Code compilation API not working"
echo " Response: $COMPILATION_TEST"
fi
# Test 4: Test with a more complex C program
echo "Test 4: Testing complex code compilation..."
COMPLEX_CODE='{
"code": "#include <stdio.h>\nint main() {\n int i, sum = 0;\n for (i = 1; i <= 5; i++) {\n sum += i;\n }\n printf(\"Sum of 1 to 5 is: %d\\n\", sum);\n return 0;\n}"
}'
COMPLEX_TEST=$(curl -s -X POST http://localhost:5000/compile \
-H "Content-Type: application/json" \
-d "$COMPLEX_CODE")
if echo "$COMPLEX_TEST" | grep -q '"success":true' && echo "$COMPLEX_TEST" | grep -q "Sum of 1 to 5"; then
echo "✓ Test 4 PASSED: Complex code compilation works correctly"
else
echo "✗ Test 4 FAILED: Complex code compilation not working"
echo " Response: $COMPLEX_TEST"
fi
# Test 5: Check if all expected lesson files are accessible
echo "Test 5: Checking lesson accessibility..."
LESSON_FILES=("welcome.md" "hello_world.md" "introduction_to_c.md" "arrays.md")
ALL_LESSONS_ACCESSIBLE=true
for lesson in "${LESSON_FILES[@]}"; do
lesson_check=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:5000/lesson/$lesson")
if [ "$lesson_check" -ne 200 ]; then
echo "✗ Lesson $lesson returned HTTP $lesson_check"
ALL_LESSONS_ACCESSIBLE=false
fi
done
if [ "$ALL_LESSONS_ACCESSIBLE" = true ]; then
echo "✓ Test 5 PASSED: All test lessons are accessible"
else
echo "✗ Test 5 FAILED: Some lessons are not accessible"
fi
# Test 6: Check for errors in container logs
echo "Test 6: Checking container logs for errors..."
LOG_ERRORS=$(podman logs elemes_lms-c_1 2>&1 | grep -i error | wc -l)
if [ $LOG_ERRORS -eq 0 ]; then
echo "✓ Test 6 PASSED: No errors found in container logs"
else
echo "✗ Test 6 FAILED: Errors found in container logs"
podman logs elemes_lms-c_1 | grep -i error
fi
# Stop the application
echo "Stopping the application..."
cd ..
./stop.sh
cd test
echo "All tests completed!"