import os import json from flask import Flask, request, abort # type: ignore from dotenv import load_dotenv load_dotenv() def create_app(): # Set folders to parent directory since app logic is now in a subfolder template_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'templates')) static_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'static')) app = Flask(__name__, template_folder=template_dir, static_folder=static_dir) app.secret_key = os.getenv('SECRET_KEY', 'dev_key') # --- ๐Ÿ›ก๏ธ ๋ณด์•ˆ ์‹ค๋“œ & ๋กœ๊น… ์„ค์ • --- import logging from logging.handlers import RotatingFileHandler log_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'logs')) os.makedirs(log_dir, exist_ok=True) file_handler = RotatingFileHandler( os.path.join(log_dir, 'app.log'), maxBytes=10*1024*1024, # 10MB backupCount=5, encoding='utf-8' ) file_handler.setFormatter(logging.Formatter( '[%(asctime)s] %(levelname)s in %(module)s: %(message)s' )) app.logger.addHandler(file_handler) app.logger.setLevel(logging.INFO) app.logger.info('๐Ÿš€ ๋‡Œ์‚ฌ๋ฃŒ ์„œ๋ฒ„ ๊ธฐ๋™ - ๋กœ๊น… ์‹œ์Šคํ…œ ๊ฐ€๋™') @app.errorhandler(403) def forbidden(e): return "Forbidden: Suspicious activity detected. Your IP has been logged.", 403 @app.before_request def unified_logger(): # ํด๋ผ์ด์–ธํŠธ IP (Cloudflare ๋“ฑ์„ ๊ฑฐ์น  ๊ฒฝ์šฐ X-Forwarded-For ํ™•์ธ) ip = request.headers.get('X-Forwarded-For', request.remote_addr) path = request.path method = request.method params = request.query_string.decode('utf-8') # 1. ๋ณด์•ˆ ์‹ค๋“œ: ๋ฉ”์ธ/๋กœ๊ทธ์ธ ํŽ˜์ด์ง€์— ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋ถ™์€ ๊ฒฝ์šฐ ์ฆ‰์‹œ ์ฐจ๋‹จ if path.rstrip('/') in ['', '/login'] and params: log_msg = f"[SHIELD] Blocked: [{ip}] {method} {path}?{params}" app.logger.warning(log_msg) abort(403) # 2. ํŠธ๋ž˜ํ”ฝ ๋กœ๊น… (์ •์  ํŒŒ์ผ ์ œ์™ธ) if not path.startswith('/static/'): log_msg = f"ACCESS: [{ip}] {method} {path}" if params: log_msg += f"?{params}" app.logger.info(log_msg) upload_folder = os.path.abspath(os.path.join(static_dir, 'uploads')) os.makedirs(upload_folder, exist_ok=True) app.config['UPLOAD_FOLDER'] = upload_folder # Load config.json config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'config.json')) if os.path.exists(config_path): with open(config_path, 'r', encoding='utf-8') as f: cfg = json.load(f) app.config['UPLOAD_SECURITY'] = cfg.get('upload_security', {}) else: app.config['UPLOAD_SECURITY'] = {'allowed_extensions': [], 'blocked_extensions': []} # Initialize DB schema from .database import init_db init_db() # Session and Security configurations app.config.update( SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SAMESITE='Lax', SESSION_COOKIE_SECURE=os.getenv('SESSION_COOKIE_SECURE', 'False').lower() == 'true', PERMANENT_SESSION_LIFETIME=3600 # 60 minutes (1 hour) session ) @app.after_request def add_security_headers(response): """๋ณด์•ˆ ๊ฐ•ํ™”๋ฅผ ์œ„ํ•œ HTTP ํ—ค๋” ์ถ”๊ฐ€""" response.headers['X-Content-Type-Options'] = 'nosniff' response.headers['X-Frame-Options'] = 'SAMEORIGIN' response.headers['X-XSS-Protection'] = '1; mode=block' # Content Security Policy (Toast UI ๋ฐ ์™ธ๋ถ€ CDN ํ—ˆ์šฉ) # ์šด์˜ ํ™˜๊ฒฝ์— ๋งž์ถฐ ์ ์ง„์ ์œผ๋กœ ๊ฐ•ํ™” ๊ฐ€๋Šฅ csp = ( "default-src 'self'; " "script-src 'self' 'unsafe-inline' https://uicdn.toast.com https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://static.cloudflareinsights.com https://d3js.org; " "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://uicdn.toast.com; " "font-src 'self' https://fonts.gstatic.com; " "img-src 'self' data: blob:; " "connect-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://cloudflareinsights.com https://d3js.org;" ) response.headers['Content-Security-Policy'] = csp return response # Register modular blueprints from .routes import register_blueprints register_blueprints(app) return app