181 lines
6.1 KiB
Python
181 lines
6.1 KiB
Python
"""
|
|
Code compilation route.
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
import uuid
|
|
import requests
|
|
from flask import Blueprint, request, jsonify
|
|
from flask_limiter import RateLimitExceeded
|
|
|
|
from services.token_service import validate_token
|
|
from extensions import limiter
|
|
|
|
compile_bp = Blueprint('compile', __name__)
|
|
|
|
COMPILER_WORKER_URL = os.environ.get('COMPILER_WORKER_URL', 'http://compiler-worker:8080/execute')
|
|
VELXIO_COMPILER_URL = os.environ.get('VELXIO_COMPILER_URL', 'http://velxio:80/api/compile/')
|
|
ANON_QUEUE_DIR = "/tmp/elemes_anon_queue"
|
|
|
|
# Ensure queue directory exists
|
|
os.makedirs(ANON_QUEUE_DIR, exist_ok=True)
|
|
|
|
def is_logged_in():
|
|
"""Check if the request has a valid student token (JSON, Form, or Cookie)."""
|
|
token = None
|
|
if request.is_json:
|
|
try:
|
|
token = request.get_json(force=True).get('token', '')
|
|
except Exception:
|
|
pass
|
|
if not token:
|
|
token = request.form.get('token', '')
|
|
if not token:
|
|
token = request.cookies.get('student_token', '')
|
|
|
|
token = str(token).strip()
|
|
return bool(token and validate_token(token))
|
|
|
|
def acquire_anon_slot():
|
|
"""Try to acquire a slot in the anonymous compilation queue."""
|
|
try:
|
|
# Cleanup stale files (> 60s)
|
|
now = time.time()
|
|
for f in os.listdir(ANON_QUEUE_DIR):
|
|
fpath = os.path.join(ANON_QUEUE_DIR, f)
|
|
try:
|
|
if now - os.path.getmtime(fpath) > 60:
|
|
os.remove(fpath)
|
|
except (OSError, IOError):
|
|
pass
|
|
|
|
# Check capacity
|
|
if len(os.listdir(ANON_QUEUE_DIR)) >= 20:
|
|
return None
|
|
|
|
slot_id = str(uuid.uuid4())
|
|
fpath = os.path.join(ANON_QUEUE_DIR, slot_id)
|
|
with open(fpath, 'w') as f:
|
|
f.write(str(now))
|
|
return fpath
|
|
except Exception:
|
|
return None
|
|
|
|
def release_anon_slot(fpath):
|
|
"""Release an acquired anonymous slot."""
|
|
if fpath and os.path.exists(fpath):
|
|
try:
|
|
os.remove(fpath)
|
|
except (OSError, IOError):
|
|
pass
|
|
|
|
@compile_bp.errorhandler(RateLimitExceeded)
|
|
def handle_rate_limit_exceeded(e):
|
|
"""Handle rate limit errors with a custom message."""
|
|
return jsonify({
|
|
'success': False,
|
|
'output': '',
|
|
'error': 'tunggu beberapa saat untuk kompilasi kembali, atau lakukan request token ke guru'
|
|
}), 429
|
|
|
|
@compile_bp.route('/compile', methods=['POST'])
|
|
@limiter.limit("1 per 2 minutes", exempt_when=is_logged_in)
|
|
def compile_code():
|
|
"""Forward code compilation to the worker service."""
|
|
try:
|
|
code = None
|
|
language = None
|
|
token = None
|
|
|
|
if request.content_type and 'application/json' in request.content_type:
|
|
try:
|
|
json_data = request.get_json(force=True)
|
|
if json_data:
|
|
code = json_data.get('code', '')
|
|
language = json_data.get('language', '')
|
|
token = json_data.get('token', '').strip()
|
|
except Exception:
|
|
pass
|
|
|
|
if not code:
|
|
code = request.form.get('code', '')
|
|
language = request.form.get('language', '')
|
|
token = request.form.get('token', '').strip()
|
|
|
|
# Authorization logic:
|
|
# 1. If token is provided, it MUST be valid.
|
|
# 2. If no token, it is anonymous access (subject to rate limits & queue).
|
|
user_is_logged_in = False
|
|
if token:
|
|
if not validate_token(token):
|
|
return jsonify({'success': False, 'output': '', 'error': 'Unauthorized: Invalid token'})
|
|
user_is_logged_in = True
|
|
|
|
# Queue logic for anonymous users
|
|
slot_fpath = None
|
|
if not user_is_logged_in:
|
|
slot_fpath = acquire_anon_slot()
|
|
if not slot_fpath:
|
|
return jsonify({
|
|
'success': False,
|
|
'output': '',
|
|
'error': 'tunggu beberapa saat untuk kompilasi kembali, atau lakukan request token ke guru'
|
|
})
|
|
|
|
try:
|
|
if not code:
|
|
return jsonify({'success': False, 'output': '', 'error': 'No code provided'})
|
|
|
|
# Forward to worker
|
|
response = requests.post(
|
|
COMPILER_WORKER_URL,
|
|
json={'code': code, 'language': language},
|
|
timeout=15
|
|
)
|
|
return jsonify(response.json())
|
|
finally:
|
|
if slot_fpath:
|
|
release_anon_slot(slot_fpath)
|
|
|
|
except requests.exceptions.RequestException as re:
|
|
return jsonify({'success': False, 'output': '', 'error': f'Compiler service unavailable: {re}'})
|
|
except Exception as e:
|
|
return jsonify({'success': False, 'output': '', 'error': f'An error occurred: {e}'})
|
|
|
|
@compile_bp.route('/velxio-compile', methods=['POST'])
|
|
@compile_bp.route('/velxio-compile/', methods=['POST'])
|
|
@limiter.limit("1 per 2 minutes", exempt_when=is_logged_in)
|
|
def velxio_compile():
|
|
"""Proxy Velxio compilation requests to the Velxio service."""
|
|
slot_fpath = None
|
|
try:
|
|
# Check authorization from cookie if not in JSON
|
|
token = request.cookies.get('student_token')
|
|
user_is_logged_in = bool(token and validate_token(token))
|
|
|
|
if not user_is_logged_in:
|
|
slot_fpath = acquire_anon_slot()
|
|
if not slot_fpath:
|
|
return jsonify({
|
|
'success': False,
|
|
'output': '',
|
|
'error': 'tunggu beberapa saat untuk kompilasi kembali, atau lakukan request token ke guru'
|
|
})
|
|
|
|
# Forward the request to Velxio
|
|
response = requests.post(
|
|
VELXIO_COMPILER_URL,
|
|
json=request.get_json(silent=True) or {},
|
|
timeout=30
|
|
)
|
|
return jsonify(response.json())
|
|
|
|
except requests.exceptions.RequestException as re:
|
|
return jsonify({'success': False, 'output': '', 'error': f'Velxio service unavailable: {re}'})
|
|
except Exception as e:
|
|
return jsonify({'success': False, 'output': '', 'error': f'An error occurred: {e}'})
|
|
finally:
|
|
if slot_fpath:
|
|
release_anon_slot(slot_fpath)
|