diff --git a/.gitignore b/.gitignore index d10d749..01f9cb9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,2 @@ -.venv -.env -db.sqlite -flask_session -__pycache__ -app.log \ No newline at end of file +build/ +.vscode/ \ No newline at end of file diff --git a/dockerfile b/dockerfile deleted file mode 100644 index 36948e9..0000000 --- a/dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM python:alpine - -# Set the working directory -WORKDIR /app - -# Copy the requirements file into the container -COPY requirements.txt . - -# Install the required packages -RUN pip install --no-cache-dir -r requirements.txt -RUN pip install gunicorn - -# Copy the rest of the application code into the container -COPY src src -COPY templates templates -COPY static static - -# Expose the port the app runs on -EXPOSE 5000 - -# run the application -ENTRYPOINT [ "gunicorn", "-b", ":5000", "--access-logfile", "-", "--error-logfile", "-", "src.wsgi:app" ] \ No newline at end of file diff --git a/makefile b/makefile new file mode 100644 index 0000000..a65aaad --- /dev/null +++ b/makefile @@ -0,0 +1,51 @@ +# Directories +INCLUDE_DIR = inc +SRC_DIR = src +BUILD_DIR = build + +# Compiler and linker settings +CXX = g++ +LIBS = +CXXFLAGS = -std=c++17 -I $(INCLUDE_DIR) + +# Source and object files +SRC = $(wildcard $(SRC_DIR)/*.cpp) + +# Target executable +UNAME := $(shell uname -s) +BUILD_DIR := $(BUILD_DIR)/$(UNAME) +OBJ_DIR := $(BUILD_DIR)/objs +BIN = $(BUILD_DIR)/main + +# Object files corresponding to the source files (now in obj directory) +OBJS = $(addprefix $(OBJ_DIR)/, $(addsuffix .o, $(basename $(notdir $(SRC))))) + +# development target with debugging +dev: CXXFLAGS += -g -Wall -Wformat +dev: all + +# Release target +release: CXXFLAGS += -O3 +release: all + +# Create directories for build output +dirs: + @mkdir -p $(BUILD_DIR) + @mkdir -p $(OBJ_DIR) + +# Clear build directory +clear: + @find $(OBJ_DIR) -type f -name '*.o' -exec rm -f {} + + +# Pattern rule for source files in src directory +$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp + $(CXX) $(CXXFLAGS) -c -o $@ $< + +all: dirs clear $(BIN) + @echo Build complete + +$(BIN): $(OBJS) + $(CXX) -o $@ $^ $(CXXFLAGS) $(LIBS) + +clean: + rm -rf $(BUILD_DIR) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index bcf2451..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -#psycopg2-binary -python-dotenv -flask-session -requests -flask \ No newline at end of file diff --git a/run.sh b/run.sh deleted file mode 100755 index 461da21..0000000 --- a/run.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -[ ! -f .env ] || export $(grep -v '^#' .env | xargs) - -flask --app src.wsgi.py --debug run \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..d1fd9d0 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,14 @@ +#define CROW_STATIC_DIRECTORY "../static" +#include + +int main() { + crow::SimpleApp app; + + CROW_ROUTE(app, "/")([] { + return "hello world"; + }); + + app.port(8080).multithreaded().run(); +} + +// note: replace mustache with jinja \ No newline at end of file diff --git a/src/routes/error_handlers.py b/src/routes/error_handlers.py deleted file mode 100644 index 34e055e..0000000 --- a/src/routes/error_handlers.py +++ /dev/null @@ -1,48 +0,0 @@ -# Imports -from flask import Blueprint, render_template -from os import getenv as env -import logging - -import src.routes.snake as snake - - -# Create blueprint -bp = Blueprint( - 'error_handlers', - __name__, - template_folder=env('TEMPLATE_FOLDER', default='../templates'), - static_folder=env('STATIC_FOLDER', default='../static') -) - - -# Create logger -log = logging.getLogger(__name__) - - -# Route for 500 error -@bp.route('/500') -@bp.app_errorhandler(500) -def internal_server_error(error=None): - if error is not None: - log.error("Internal server error: %s", error) - return render_template('errors/500.html'), 500 - - -# Route for 404 error -@bp.route('/404') -@bp.app_errorhandler(404) -def not_found(error=None): - if error is not None: - log.warning("Page not found: %s", error) - scores = snake.get_leaderboard() - token = snake.generate_start_token() - return render_template('errors/404.html', scores=scores, token=token, cap_key=env('CAP_KEY', default='')), 404 if error is not None else 200 - - -# Route for 400 error -@bp.route('/400') -@bp.app_errorhandler(400) -def bad_request(error=None): - if error is not None: - log.warning("Bad request: %s", error) - return render_template('errors/400.html', error=error), 400 \ No newline at end of file diff --git a/src/routes/generic.py b/src/routes/generic.py deleted file mode 100644 index cf350dd..0000000 --- a/src/routes/generic.py +++ /dev/null @@ -1,65 +0,0 @@ -# Imports -from flask import Blueprint, render_template, request, abort, send_file -from os import getenv as env -import logging, os - - -# Create blueprint -bp = Blueprint( - 'generic', - __name__, - template_folder=env('TEMPLATE_FOLDER', default='../templates'), - static_folder=env('STATIC_FOLDER', default='../static') -) - - -# Create logger -log = logging.getLogger(__name__) - - -# Route for index page -@bp.route('/') -def index(): - return render_template('index.html') - - -# Route for favicon -@bp.route('/favicon.ico') -def favicon(): - return send_file('../static/content/other/favicon.ico') - - -# Route for robots.txt -@bp.route('/robots.txt') -def robots(): - return send_file('../static/content/other/robots.txt') - - -# Route for sitemap.xml -@bp.route('/sitemap.xml') -def sitemap(): - return send_file('../static/content/other/sitemap.xml') - - -# Catch-all route for generic pages -@bp.route('/') -def catch_all(filename): - try: return render_template(f'pages/{filename if filename.endswith(".html") else filename + ".html"}') - except Exception as e: - # If the template is not found, check if it is a directory - os_path = os.path.join(bp.template_folder, 'pages', filename)[3:] - if os.path.isdir(os_path): - # walk through the directory and find all files - pages = [] - for root, dirs, files_in_dir in os.walk(os_path): - for file in files_in_dir: - pages.append(os.path.relpath(os.path.join(root, file), os_path)) - for dir in dirs: - pages.append(os.path.relpath(os.path.join(root, dir), os_path) + '/') - - # If it is a directory, render a directory page - if not filename.endswith('/'): filename += '/' - return render_template('bases/directory.html', directory=filename, pages=pages) - - # If it is a file, return a 404 error - abort(404, f"Template '{filename}' not found: {e}") \ No newline at end of file diff --git a/src/routes/snake.py b/src/routes/snake.py deleted file mode 100644 index beb49f6..0000000 --- a/src/routes/snake.py +++ /dev/null @@ -1,146 +0,0 @@ -# Imports -from flask import Blueprint, abort, request, redirect -from os import urandom, getenv as env -import src.utils.database as database -import src.utils.cap as cap -import logging, datetime, threading, time - - -# Create blueprint -bp = Blueprint( - 'snake', - __name__, - template_folder=env('TEMPLATE_FOLDER', default='../templates'), - static_folder=env('STATIC_FOLDER', default='../static') -) - - -# Create logger -log = logging.getLogger(__name__) - - -# Create database instance -db = database.Database(db_name=env('DB_NAME', default='db.sqlite')) -db.execute('CREATE TABLE IF NOT EXISTS snake_scores (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, score INTEGER)') -db.execute('''CREATE TABLE IF NOT EXISTS snake_tokens ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - token TEXT UNIQUE NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - ip TEXT UNIQUE NOT NULL -)''') - -# Input validation function -def valid_length(value, min_length=1, max_length=100): - if not isinstance(value, str): - return False - return min_length <= len(value) <= max_length - - -def valid_score(score, game_token): - start_time = db.execute('SELECT created_at FROM snake_tokens WHERE token = ?', (game_token,)).fetchone() - if not start_time: - log.error("Game token not found.") - return False - - start_time = datetime.datetime.fromisoformat(start_time[0]) - current_time = datetime.datetime.now() - elapsed_time = (current_time - start_time).total_seconds() - - if elapsed_time < score / 10 * 3 + 10: # assuming that each point takes 3 seconds to achieve and 10 seconds to start the game and do captcha - log.error("Score is too high for the elapsed time.") - return False - - if score <= 0 or score > 10000: # Arbitrary upper limit for scores - log.error("Score is out of valid range.") - return False - - if score % 10 != 0: - log.error("Score is not a multiple of 10.") - return False - - # delete the token after score validation - db.execute('DELETE FROM snake_tokens WHERE token = ?', (game_token,)) - log.info(f"Score {score} validated successfully for token {game_token}.") - - return True - - -# Route for score submission -@bp.route('/snake/submit', methods=['POST']) -def submit_score(): - name = request.form.get('name') - score = request.form.get('score') - captcha_token = request.form.get('cap-token') - game_token = request.form.get('game_token') - - if not cap.verify_captcha(captcha_token): - log.error("Captcha verification failed.") - abort(400, "Captcha verification failed") - - if not name or not score or not captcha_token or not game_token: - log.error("Name, score, captcha token, or game token is missing.") - abort(400, "Missing required fields") - - if not valid_length(name, min_length=3, max_length=15): - log.error("Invalid name length.") - abort(400, "Name must be between 3 and 15 characters long.") - - if not valid_score(int(score), game_token): - log.error("Invalid score.") - abort(400, "Score not vilid, so either you are trying to cheat the leaderboard or something is seriously wrong.") - - try: - db.execute('INSERT INTO snake_scores (name, score) VALUES (?, ?)', (name, int(score))) - db.execute('DELETE FROM snake_tokens WHERE token = ?', (game_token,)) - log.info(f"Score submitted: {name} - {score}") - return redirect('/404') - - except Exception as e: - log.error(f"Database error: {e}") - abort(500, "Internal server error while submitting score.") - - -# Generate a unique game token -def generate_start_token(): - """Generate a unique start token for the game.""" - token = urandom(16).hex() - ip = request.headers.get('X-Forwarded-For', request.remote_addr) - - ip_token = db.execute('SELECT token FROM snake_tokens WHERE ip = ?', (ip,)).fetchone() - if ip_token: - log.info(f"Token already exists for IP: {ip}, reusing token.") - return ip_token[0] - - log.info(f"Generated start token: {token}") - db.execute('INSERT INTO snake_tokens (token, ip) VALUES (?, ?)', (token, ip)) - return token - - -# Get leaderboard scores -def get_leaderboard(): - """Fetch scores from the leaderboard.""" - try: - scores = db.execute('SELECT name, score FROM snake_scores ORDER BY score DESC').fetchall() - leaderboard = [{'position': i + 1, 'name': score[0], 'score': score[1]} for i, score in enumerate(scores)] - log.info("Leaderboard fetched successfully.") - return leaderboard - except Exception as e: - log.error(f"Error fetching leaderboard: {e}") - return [] - - -# Clear all tokens older than 1 hour -def clear_old_tokens(): - while True: - try: - one_hour_ago = datetime.datetime.now() - datetime.timedelta(hours=1) - db.execute('DELETE FROM snake_tokens WHERE created_at < ?', (one_hour_ago,)) - log.info("Old tokens cleared.") - except Exception as e: - log.error(f"Error clearing old tokens: {e}") - time.sleep(3600) # Run every hour - - -# Start the token clearing thread -token_thread = threading.Thread(target=clear_old_tokens, daemon=True) -token_thread.start() \ No newline at end of file diff --git a/src/utils/cap.py b/src/utils/cap.py deleted file mode 100644 index 456e7c5..0000000 --- a/src/utils/cap.py +++ /dev/null @@ -1,42 +0,0 @@ -# Imports -from os import getenv as env -import requests, logging - - -# Create logger -log = logging.getLogger(__name__) - - -# Function to verify CAPTCHA response -def verify_captcha(token: str) -> bool: - """ - Verify the CAP response token with the CAP server. - - Args: - token (str): The CAP response token to verify. - - Returns: - bool: True if the token is valid, False otherwise. - """ - if not token: - return False - - try: - response = requests.post( - f"https://cap.alfieking.dev/{env('CAP_KEY', default='')}/siteverify", - json={ - 'secret': env('CAP_SECRET', default=''), - 'response': token, - }, - timeout=10 - ) - - response.raise_for_status() - if response.status_code != 200: - log.error("CAPTCHA verification failed with status code: %s", response.status_code) - return False - return response.json().get('success', False) - - except Exception as e: - log.error("Error verifying CAPTCHA: %s", e) - return False \ No newline at end of file diff --git a/src/utils/database.py b/src/utils/database.py deleted file mode 100644 index c8a36a2..0000000 --- a/src/utils/database.py +++ /dev/null @@ -1,18 +0,0 @@ -# Imports -import sqlite3 - -# Database class -class Database: - def __init__(self, db_name='db.sqlite'): - self.connection = sqlite3.connect(db_name, check_same_thread=False) - self.cursor = self.connection.cursor() - - def execute(self, query, params=None): - if params is None: - params = [] - self.cursor.execute(query, params) - self.connection.commit() - return self.cursor - - def close(self): - self.connection.close() \ No newline at end of file diff --git a/src/wsgi.py b/src/wsgi.py deleted file mode 100644 index 47d4970..0000000 --- a/src/wsgi.py +++ /dev/null @@ -1,58 +0,0 @@ -# Imports -from flask import Flask -from flask_session import Session - -from dotenv import load_dotenv -from os import getenv as env, listdir -import logging, importlib - - -# Load env -load_dotenv() - - -# Create console log handler -console_log = logging.StreamHandler() -console_log.setFormatter(logging.Formatter("\033[1;32m%(asctime)s\033[0m - \033[1;34m%(levelname)s\033[0m - \033[1;31m%(name)s\033[0m - %(message)s")) -console_log.setLevel(logging.INFO) - -# Create file log handler -file_log = logging.FileHandler(env('LOG_FILE', default='app.log'), mode=env('LOG_MODE', default='a')) -file_log.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s")) -file_log.setLevel(logging.DEBUG) - -# Add handlers to the logger -log = logging.getLogger() -log.setLevel(logging.DEBUG) -log.addHandler(console_log) -log.addHandler(file_log) -log.info("Logging initialized") - - -# Create flask app -app = Flask( - __name__, - template_folder=env('TEMPLATE_FOLDER', default='../templates'), - static_folder=env('STATIC_FOLDER', default='../static') -) - -# Configure sessions -app.config["SESSION_PERMANENT"] = True -app.config["SESSION_TYPE"] = "filesystem" -Session(app) - - -# Load routes -routes_dir = env('ROUTES_DIR', default='src/routes') -for filename in listdir(routes_dir): - if not filename.endswith('.py') and filename.startswith('__'): - continue - - module_name = f"{routes_dir.replace('/', '.')}.{filename[:-3]}" - try: - module = importlib.import_module(module_name) - if hasattr(module, 'bp'): - app.register_blueprint(module.bp) - log.info(f"Registered blueprint: {module_name}") - except Exception as e: - log.error(f"Failed to register blueprint {module_name}: {e}") \ No newline at end of file diff --git a/templates/bases/base.html b/templates/bases/base.jinja similarity index 100% rename from templates/bases/base.html rename to templates/bases/base.jinja diff --git a/templates/bases/directory.html b/templates/bases/directory.jinja similarity index 100% rename from templates/bases/directory.html rename to templates/bases/directory.jinja diff --git a/templates/errors/400.html b/templates/errors/400.jinja similarity index 100% rename from templates/errors/400.html rename to templates/errors/400.jinja diff --git a/templates/errors/404.html b/templates/errors/404.jinja similarity index 100% rename from templates/errors/404.html rename to templates/errors/404.jinja diff --git a/templates/errors/500.html b/templates/errors/500.jinja similarity index 100% rename from templates/errors/500.html rename to templates/errors/500.jinja diff --git a/templates/index.html b/templates/index.jinja similarity index 100% rename from templates/index.html rename to templates/index.jinja diff --git a/templates/pages/events/crittersmk.html b/templates/pages/events/crittersmk.jinja similarity index 100% rename from templates/pages/events/crittersmk.html rename to templates/pages/events/crittersmk.jinja diff --git a/templates/pages/events/paws-n-pistons.html b/templates/pages/events/paws-n-pistons.jinja similarity index 100% rename from templates/pages/events/paws-n-pistons.html rename to templates/pages/events/paws-n-pistons.jinja diff --git a/templates/pages/toaster.html b/templates/pages/toaster.jinja similarity index 100% rename from templates/pages/toaster.html rename to templates/pages/toaster.jinja