add protection that shares the data with my webside

This commit is contained in:
2025-12-23 15:20:36 +01:00
parent 0b2d635fd8
commit 729e7f5fca
13 changed files with 211 additions and 60 deletions
+14
View File
@@ -0,0 +1,14 @@
class TheIPManager:
def __init__(self):
self.always_allowed_ips = set()
# Checks
def is_client_ip_always_allowed(self, client_ip:str):
return client_ip in self.always_allowed_ips
# Add
def add_always_allowed_ip(self, client_ip:str):
self.always_allowed_ips.add(client_ip)
def update_always_allowed_ip(self, client_ips:set):
self.always_allowed_ips.update(client_ips)
+2 -3
View File
@@ -1,7 +1,7 @@
from my_modules.TheIPManager import TheIPManager
from my_modules.app.logger import logger from my_modules.app.logger import logger
from dotenv import find_dotenv, load_dotenv, dotenv_values from dotenv import find_dotenv, load_dotenv, dotenv_values
from pathlib import Path
import os, asyncio import os, asyncio
async def read_dot_file(): async def read_dot_file():
@@ -17,5 +17,4 @@ WEB_DEBUG = os.getenv("WEB_DEBUG", False)
SECRET_KEY = os.getenv("FLASK_SECRET_KEY", "USE_ENV_das_ist_ein_geheimer_schlüssel_1") SECRET_KEY = os.getenv("FLASK_SECRET_KEY", "USE_ENV_das_ist_ein_geheimer_schlüssel_1")
API_GROUP = os.getenv("API_GROUP", 'NanoShare') API_GROUP = os.getenv("API_GROUP", 'NanoShare')
UPLOAD_DIR = Path("uploads") THE_IP_BOT_MANAGER = TheIPManager()
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
+1 -1
View File
@@ -8,5 +8,5 @@ formatter = Formatter(fmt="%(levelname)s %(module)s: %(message)s")
handler = AsyncStreamHandler(stream=sys.stdout) handler = AsyncStreamHandler(stream=sys.stdout)
handler.formatter = formatter handler.formatter = formatter
logger = Logger(name="my_webside_and_api", level="DEBUG" if os.getenv("WEB_DEBUG", False) == "true" else "INFO") logger = Logger(name="simple-picoshare", level="DEBUG" if os.getenv("WEB_DEBUG", False) == "true" else "INFO")
logger.handlers = [handler] logger.handlers = [handler]
+11 -4
View File
@@ -1,8 +1,7 @@
from my_modules.functions import custom_limit_key from my_modules.functions import custom_limit_key, get_my_ip_address, get_local_ip_addresses, replace_last_ip_segment, generate_all_ips
from my_modules.app.constens import SECRET_KEY, UPLOAD_DIR from my_modules.app.constens import SECRET_KEY, THE_IP_BOT_MANAGER
from my_modules.AsyncCache import AsyncCache from my_modules.AsyncCache import AsyncCache
from my_modules.app.logger import logger from my_modules.app.logger import logger
from my_modules.db.ConvexDB import ConvexDB from my_modules.db.ConvexDB import ConvexDB
from quart_session import Session from quart_session import Session
@@ -16,7 +15,6 @@ app = Quart(__name__, template_folder="../../templates/side", static_folder="../
app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 * 1024 app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 * 1024
app.secret_key = SECRET_KEY app.secret_key = SECRET_KEY
app.upload_folder = UPLOAD_DIR
# Cache, Sessions and Limiter over Valkey # Cache, Sessions and Limiter over Valkey
if os.getenv("VALKEY_HOST", None) is not None: if os.getenv("VALKEY_HOST", None) is not None:
@@ -71,6 +69,15 @@ async def init_convex():
app.convex = ConvexDB(os.getenv("CONVEX_URL")) app.convex = ConvexDB(os.getenv("CONVEX_URL"))
await app.convex.connect() await app.convex.connect()
THE_IP_BOT_MANAGER.add_always_allowed_ip('127.0.0.1')
THE_IP_BOT_MANAGER.add_always_allowed_ip(await get_my_ip_address())
local_docker_host_ip = get_local_ip_addresses()
if local_docker_host_ip:
base_ip = replace_last_ip_segment(local_docker_host_ip, 1)
all_local_ips = generate_all_ips(base_ip)
THE_IP_BOT_MANAGER.update_always_allowed_ip(all_local_ips)
@app.after_serving @app.after_serving
async def close_convex(): async def close_convex():
if app.convex: if app.convex:
+29 -15
View File
@@ -1,4 +1,4 @@
from my_modules.app.constens import SECRET_KEY from my_modules.app.constens import THE_IP_BOT_MANAGER
from my_modules.app.logger import logger from my_modules.app.logger import logger
from my_modules.app.setup import LIMITER from my_modules.app.setup import LIMITER
from my_modules.functions import get_ip from my_modules.functions import get_ip
@@ -6,7 +6,7 @@ from my_modules.functions import get_ip
from quart import jsonify, request, url_for, Response, current_app, session, abort from quart import jsonify, request, url_for, Response, current_app, session, abort
from functools import wraps from functools import wraps
from datetime import datetime from datetime import datetime
import asyncio, msgpack, json, jwt import asyncio, msgpack, json
def encode_object_default(obj): def encode_object_default(obj):
if isinstance(obj, datetime): if isinstance(obj, datetime):
@@ -24,6 +24,27 @@ async def get_auth_token():
return None return None
async def verify_token(token:str):
decoded_payload = await current_app.convex.decode_access_token_payload(access_token=token)
decoded_payload_error_state = decoded_payload.get('state', None)
if decoded_payload is None:
return {'error': "No Data from Database"}, 504
elif decoded_payload_error_state == 1:
await logger.error(decoded_payload.get('error'))
return {'error': 'Invalid access token'}, 401
elif decoded_payload_error_state == 2:
await logger.error(decoded_payload.get('error'))
return {'error': 'Wrong access token type'}, 401
elif decoded_payload_error_state == 3:
await logger.error(decoded_payload.get('error'))
return {'error': 'Refresh token not found', 'msg': 'Please login again and generate a new Token', 'url': url_for('auth_login.login')}, 403
elif decoded_payload_error_state == 4:
await logger.error(decoded_payload.get('error'))
return {'error': 'Refresh token expired'}, 401
return decoded_payload, None
# Custom decorator for token validation # Custom decorator for token validation
def token_required(func): def token_required(func):
@wraps(func) @wraps(func)
@@ -33,17 +54,10 @@ def token_required(func):
await logger.error('API Token is missing') await logger.error('API Token is missing')
return jsonify(error='Token is missing'), 400 return jsonify(error='Token is missing'), 400
try: decoded_payload, status_code = await verify_token(token)
decoded_payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256']) decoded_payload_error = decoded_payload.get('error', None)
if not await current_app.edgedb.check_if_refresh_token_exists_by_id(decoded_payload['refresh_id']): if decoded_payload_error:
await logger.error(f'API Refresh Token not found: {decoded_payload['refresh_id']}') return jsonify(decoded_payload), status_code
return jsonify(error='Refresh Token not found', msg='Please login again', url=url_for('login')), 403
except jwt.ExpiredSignatureError:
await logger.error('API Token has expired')
return jsonify(error='Token has expired'), 401
except jwt.InvalidTokenError:
await logger.error('API Token is invalid')
return jsonify(error='Token is invalid'), 401
return await func(user=decoded_payload, *args, **kwargs) return await func(user=decoded_payload, *args, **kwargs)
return wrapper return wrapper
@@ -116,8 +130,8 @@ def apply_limit(endpoint_name, limits:dict=None):
def make_key_func(endpoint): def make_key_func(endpoint):
def key_func(): def key_func():
ip = get_ip() ip = get_ip()
# if THE_IP_BOT_MANAGER.is_client_ip_always_allowed(ip): if THE_IP_BOT_MANAGER.is_client_ip_always_allowed(ip):
# return None # No key, no increment, no enforcement return None # No key, no increment, no enforcement
# Combine endpoint name and HTTP method (and client IP) into the rate-limit key # Combine endpoint name and HTTP method (and client IP) into the rate-limit key
return f":{ip}:{endpoint}:{request.method}:" return f":{ip}:{endpoint}:{request.method}:"
+20 -1
View File
@@ -1,3 +1,6 @@
from routes.handeling.errorsAndBots import maybe_a_hacker
from my_modules.app.constens import THE_IP_BOT_MANAGER
from my_modules.app.logger import logger from my_modules.app.logger import logger
from my_modules.functions import get_ip from my_modules.functions import get_ip
from my_modules.app.setup import app from my_modules.app.setup import app
@@ -14,13 +17,29 @@ async def custom_middleware():
path = request.path path = request.path
method = request.method method = request.method
db_whitelisted_or_blocked = await current_app.convex.is_ip_address_whitelisted_or_blocked(ip_address=client_ip)
# Skip allowed IPs or non-critical assets # Skip allowed IPs or non-critical assets
if ( if (
"favicon" in path db_whitelisted_or_blocked['whiteliste']
or THE_IP_BOT_MANAGER.is_client_ip_always_allowed(client_ip)
or "static" in path or "static" in path
or "favicon" in path
or "storage" in path
): ):
return return
# 2. If IP is already blocked
if db_whitelisted_or_blocked['blocked']:
await logger.error(f"[BLOCKED] {method} | {client_ip} tried {method} {path}")
await current_app.convex.increment_blocked_ip_address_access(ip_address=client_ip, method=method, path=path)
return await render_template("views/basics/blocked_access.htm", remote_addr=client_ip), 403
# 3. If path contains honeypot targets
if await current_app.convex.is_path_blocked(path=path):
await logger.warning(f"[HONEYPOT] {method} | {client_ip} accessed {path}")
return await maybe_a_hacker()
await logger.info(f"{method} | {client_ip} had accessed the Side {path}") await logger.info(f"{method} | {client_ip} had accessed the Side {path}")
@app.context_processor @app.context_processor
+42 -6
View File
@@ -1,18 +1,20 @@
from my_modules.app.setup import app, LIMITER from my_modules.app.setup import app, LIMITER
from my_modules.app.logger import logger from my_modules.app.logger import logger
from quart import request, render_template, jsonify, current_app, make_response, redirect, url_for from quart import request, render_template, jsonify, current_app, make_response
from my_modules.functions import get_ip, enforce_custom_limit from my_modules.functions import get_ip, enforce_custom_limit
@app.errorhandler(401) @app.errorhandler(401)
async def handle_unauthorized(e): async def handle_unauthorized(e):
try: if request.path.startswith("/api"):
enforce_custom_limit(LIMITER, "401", limit_count=5, window_sec=1800) return jsonify({"error": "Unauthorized Access", "message": "Gandalf has spoken: You shall not pass… until you log in."}), 401
except LookupError as e:
return await to_many_requests(e)
await logger.error(e) await logger.error(e)
return redirect(url_for('auth_login.login')) return await render_template('views/basics/error.htm',
title='Unauthorized Access',
header={'title': '401 - Unauthorized', 'message': "Gandalf has spoken: You shall not pass… until you log in."},
file={'name': '401.gif', 'alt': "Gandalf blocking the bridge You shall not pass!"},
), 401
@app.errorhandler(404) @app.errorhandler(404)
async def not_found(e): async def not_found(e):
@@ -21,13 +23,44 @@ async def not_found(e):
except LookupError as e: except LookupError as e:
return await to_many_requests(e) return await to_many_requests(e)
if request.path.startswith("/api"):
return jsonify({"error": "Page Not Found", "message": "Oops! The page you are looking for does not exist."}), 404
await logger.error(f"[404] Page Not Found: {request.path}") await logger.error(f"[404] Page Not Found: {request.path}")
await current_app.convex.increment_page_not_found_error(path=request.path, status=404)
return await render_template('views/basics/error.htm', return await render_template('views/basics/error.htm',
title='Page Not Found', title='Page Not Found',
header={'title': '404 - Page Not Found', 'message': "Oops! The page you are looking for does not exist."}, header={'title': '404 - Page Not Found', 'message': "Oops! The page you are looking for does not exist."},
file={'name': '404.webp', 'alt': "Matrix - Neo stoping the Bullets by holding his hand up"}, file={'name': '404.webp', 'alt': "Matrix - Neo stoping the Bullets by holding his hand up"},
), 404 ), 404
@app.errorhandler(418)
async def maybe_a_hacker(e=None):
try:
enforce_custom_limit(LIMITER, "BotScan", 5, 120)
except LookupError as e:
client_ip=get_ip()
await current_app.convex.increment_blocked_ip_address_access(
ip_address=client_ip,
method=request.method,
path=request.path,
)
await logger.warning(f"[HONEYPOT] Blocked {client_ip} after accessing {request.path}")
return await to_many_requests(e)
rendered = await render_template('views/basics/error.htm',
title='Oops! Something Went AWOL!',
header={'title': "418 - I'm a Teapot", 'message': f"You don't say the Magic Word. By the way, we might have your IP now, but dont worry, it's in safe hands (probably). Feel free to keep poking around, just maybe give us a sec to catch our breath."},
file={'name': 'hacker_crap.webp', 'alt': "Someone got Hacked and he says I hate this Hacker crap - Jurassic Park Movie"},
)
response = await make_response((rendered, 418))
response.headers['X-Honeypot-Triggered'] = 'true'
response.headers['X-Reason'] = 'Unauthorized access attempt'
return response
@app.errorhandler(429) @app.errorhandler(429)
async def to_many_requests(e): async def to_many_requests(e):
message = "We love your enthusiasm, but our server thought it was being DDoSed… by you. The keyboard needs a new set of keys and we need a nap. Try again soon!" message = "We love your enthusiasm, but our server thought it was being DDoSed… by you. The keyboard needs a new set of keys and we need a nap. Try again soon!"
@@ -48,6 +81,9 @@ async def internal_server_error(e):
except LookupError as e: except LookupError as e:
return await to_many_requests(e) return await to_many_requests(e)
if request.path.startswith("/api"):
return jsonify({"error": "Internal Server Error", "message": "It looks like you broke something... but don't worry, we're fixing it! In the meantime, we may or may not have logged your IP address (just kidding... or are we?). Either way, thanks for helping us find new ways to crash our system. Stay curious, hacker-friend!"}), 500
await logger.error(e) await logger.error(e)
return await render_template('views/basics/error.htm', return await render_template('views/basics/error.htm',
title='Internal Server Error', title='Internal Server Error',
+37
View File
@@ -0,0 +1,37 @@
body {
font-family: Arial, sans-serif;
background-color: var(--primary-color);
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
text-align: center;
color: var(--text-color);
}
.container {
background-color: var(--secondary-color);
padding: 40px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
max-width: 920px;
}
h1 {
color: #e74c3c;
font-size: 48px;
margin: 0;
font-family: 'Press Start 2P', cursive;
}
p {
font-size: 18px;
margin-top: 10px;
}
.blocked-ip {
font-weight: bold;
color: var(--text-muted);
}
+55
View File
@@ -0,0 +1,55 @@
:root {
--primary-color: #f0f0f0;
--secondary-color: white;
--nav-background-color: #333;
--text-color: #000;
--text-muted: #555;
--table-head-background-color: #e1e1e1;
--container-width: 580px;
--container-max-width: 903px;
--furry-button-color: #ffcaa6;
--furry-button-color-hover: #ffa974;
--furry-background-color: white;
--furry-decoration-color: #ff7f50;
--furry-list-element-color: #fff4e6;
--furry-list-element-color-hover: #ffeede;
--story-text-color: #33ff33;
--story-press-any-key-color: #ccc;
--story-background-color: radial-gradient(circle at center, #222 0%, #000 100%);
--story-button-color: #0d0d0d;
--story-button-hover-color: #003300;
--story-sidebar-background-color: black;
--story-boot-logo-color: #ffaa00;
}
@media (prefers-color-scheme: dark) {
:root {
/* --primary-color: #353535;
--secondary-color: #1f1f23; */
/* --primary-color: #2a2640;
--secondary-color: #3a3458; */
--primary-color: #1e1e2f;
--secondary-color: #2a2a3d;
--nav-background-color: #272424;
--text-color: #cec9da;
--text-muted: #999;
--table-head-background-color: var(--secondary-color);
--furry-button-color: #b67616;
--furry-list-element-color: #252231;
--furry-list-element-color-hover: var(--primary-color);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB