9c731d6e67
Build and Push Docker Container / build-and-push (push) Failing after 51s
- Register quart_common wide-event logging during app setup so every HTTP request emits one canonical structured event. - Replace the inline security middleware with reusable quart_common security middleware wiring and move skip path configuration into app constants. - Add NanoShare-specific wide-event context for health checks, auth/error handlers, file list/edit/delete/serve flows and upload outcomes. - Rename runtime logging/project metadata from simple-picoshare to nanoshare where it is emitted in service context. - Update my_helpers and quart_common submodules for Convex/wide-event integration and reusable security middleware support. - Add NanoShare middleware tests covering safe user context, client IP enrichment, missing Convex handling and Convex security lookup failures.
203 lines
8.7 KiB
Python
203 lines
8.7 KiB
Python
from my_modules.app.constens import (
|
||
BLOCKED_IPS_ACCESSING_TIMES,
|
||
BLOCKED_IPS_STORED_TIMEFRAME,
|
||
)
|
||
from my_modules.app.setup import app, LIMITER
|
||
from my_modules.app.logger import logger
|
||
|
||
from quart import request, render_template, jsonify, current_app, make_response
|
||
from my_modules.functions import (
|
||
get_ip,
|
||
enforce_custom_limit,
|
||
get_request_context,
|
||
)
|
||
from quart_common.web.env import is_development_environment
|
||
from quart_common.web.wide_event import add_wide_event_context
|
||
|
||
IGNORED_404_PATHS = [
|
||
"/.well-known/",
|
||
]
|
||
|
||
IGNORE_CONTAIN_404_PATHS = [
|
||
"/.htaccess",
|
||
]
|
||
|
||
FEATURE_FLAG_DISABLED_PREFIX = "feature_flag_disabled:"
|
||
|
||
AUTH_STATUS_TITLES = {
|
||
400: '400 - OAuth Time Paradox',
|
||
401: '401 - Not Authenticated',
|
||
403: '403 - Access Forbidden',
|
||
500: '500 - Auth Reactor Meltdown',
|
||
504: '504 - Auth Gateway Timeout',
|
||
}
|
||
|
||
AUTH_STATUS_MESSAGES = {
|
||
400: (
|
||
'The fox courier dropped your login form in a puddle before it reached the gatekeeper. '
|
||
'Please send it again with all required fields so the checkpoint can read it.'
|
||
),
|
||
401: (
|
||
'The fox guards checked your badge and it did not pass the sniff test this round. '
|
||
'Please sign in again so they can issue a fresh one.'
|
||
),
|
||
403: (
|
||
'The fox guards found your badge valid, but this den is still off-limits for your current role. '
|
||
'If you should have access, ask an admin to update your permissions.'
|
||
),
|
||
500: (
|
||
'The auth engine coughed up a spark and the fox mechanics are tightening bolts right now. '
|
||
'Please try again in a moment while they get the reactor stable.'
|
||
),
|
||
504: (
|
||
'The fox guards are still waiting for the auth mothership to answer the walkie-talkie. '
|
||
'Please try again in a moment before they start howling at the server rack.'
|
||
),
|
||
}
|
||
|
||
async def auth_error(message:str, status_code:int=400):
|
||
context = get_request_context()
|
||
if status_code in AUTH_STATUS_MESSAGES:
|
||
funny_message = AUTH_STATUS_MESSAGES[status_code]
|
||
else:
|
||
funny_message = (
|
||
'The fox guards tripped over a cable while checking your badge. '
|
||
'Authentication failed. Please try again or contact an administrator.'
|
||
)
|
||
|
||
add_wide_event_context(auth={"operation_status": "error"}, error={"type": "AuthenticationError", "message": message})
|
||
logger.error(f"[AUTH:{status_code}] {message}")
|
||
|
||
if context and context.path.startswith("/api"):
|
||
return jsonify({"error": "Authentication Error", "message": funny_message}), status_code
|
||
|
||
return await render_template('views/basics/error.htm',
|
||
title='Authentication Error',
|
||
header={'title': AUTH_STATUS_TITLES.get(status_code, f'{status_code} - Authentication Error'), 'message': funny_message},
|
||
file={'name': 'auth_error.webp', 'alt': 'A monitor flashes unauthorized access in blinking red warning text'},
|
||
), status_code
|
||
|
||
@app.errorhandler(401)
|
||
async def handle_unauthorized(e):
|
||
context = get_request_context()
|
||
add_wide_event_context(auth={"operation_status": "unauthorized"}, error={"type": type(e).__name__, "message": str(e)})
|
||
if context.path.startswith("/api"):
|
||
return jsonify({"error": "Unauthorized Access", "message": "Gandalf has spoken: You shall not pass… until you log in."}), 401
|
||
|
||
logger.error(e)
|
||
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)
|
||
async def not_found(e):
|
||
try:
|
||
enforce_custom_limit(LIMITER, "404")
|
||
except LookupError as e:
|
||
return await to_many_requests(e)
|
||
|
||
context = get_request_context()
|
||
error_description = str(getattr(e, "description", ""))
|
||
is_feature_flag_disabled_404 = error_description.startswith(FEATURE_FLAG_DISABLED_PREFIX)
|
||
|
||
if (
|
||
not is_development_environment()
|
||
and not is_feature_flag_disabled_404
|
||
and context.path not in IGNORED_404_PATHS
|
||
and not any(p in context.path for p in IGNORE_CONTAIN_404_PATHS)
|
||
):
|
||
await current_app.convex.increment_page_not_found_error(
|
||
path=context.path, status=404
|
||
)
|
||
|
||
add_wide_event_context(error={"type": "NotFound", "message": str(e)})
|
||
logger.error(f"[404] Page Not Found: {context.path}")
|
||
|
||
if context.path.startswith("/api"):
|
||
return jsonify({"error": "Page Not Found", "message": "Oops! The page you are looking for does not exist."}), 404
|
||
|
||
return await render_template('views/basics/error.htm',
|
||
title='Page Not Found',
|
||
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"},
|
||
), 404
|
||
|
||
@app.errorhandler(418)
|
||
async def maybe_a_hacker(e=None):
|
||
try:
|
||
enforce_custom_limit(LIMITER, "BotScan", BLOCKED_IPS_ACCESSING_TIMES, BLOCKED_IPS_STORED_TIMEFRAME)
|
||
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,
|
||
)
|
||
add_wide_event_context(security={"blocked": True, "block_reason": "honeypot_rate_limit"})
|
||
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 don’t 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"},
|
||
)
|
||
|
||
add_wide_event_context(security={"blocked": True, "block_reason": "honeypot"})
|
||
response = await make_response((rendered, 418))
|
||
response.headers['X-Honeypot-Triggered'] = 'true'
|
||
response.headers['X-Reason'] = 'Unauthorized access attempt'
|
||
|
||
return response
|
||
|
||
@app.errorhandler(429)
|
||
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!"
|
||
|
||
context = get_request_context()
|
||
add_wide_event_context(rate_limit={"limited": True}, error={"type": type(e).__name__, "message": str(e)})
|
||
if context.path.startswith("/api") or context.path.endswith('/auth/userinfo') or context.path.endswith('/auth/refresh'):
|
||
return jsonify({"error": "Too Many Requests - YOU SHALL NOT PASS (for now)", "message": message}), 429
|
||
|
||
return await render_template('views/basics/error.htm',
|
||
title='Too Many Requests',
|
||
header={'title': '429 - YOU SHALL NOT PASS (for now)', 'message': message},
|
||
file={'name': '429_JimCarrey.gif', 'alt': "Jim Carrey Tips very fast on a computer keyboard"},
|
||
), 429
|
||
|
||
@app.errorhandler(500)
|
||
async def internal_server_error(e):
|
||
try:
|
||
enforce_custom_limit(LIMITER, "500")
|
||
except LookupError as e:
|
||
return await to_many_requests(e)
|
||
|
||
context = get_request_context()
|
||
add_wide_event_context(error={"type": type(e).__name__, "message": str(e)})
|
||
if context.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
|
||
|
||
logger.error(e)
|
||
return await render_template('views/basics/error.htm',
|
||
title='Internal Server Error',
|
||
header={'title': '500 - 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!"},
|
||
file={'name': '500.webp', 'alt': "Astronaut jumping and clicking on random Buttons as a red alert gone off - They is a Text on the Image saying: Why don't shit Work!?!"},
|
||
), 500
|
||
|
||
@app.errorhandler(504)
|
||
async def database_server_error(e):
|
||
try:
|
||
enforce_custom_limit(LIMITER, "504")
|
||
except LookupError as e:
|
||
return await to_many_requests(e)
|
||
|
||
add_wide_event_context(error={"type": type(e).__name__, "message": str(e)})
|
||
logger.error(e)
|
||
return await render_template('views/basics/error.htm',
|
||
title='Database Error',
|
||
header={'title': '504 - Database Error', 'message': "It looks like something is broke on our end... but don't worry, we're fixing it! Either way, thanks for helping us find new ways to crash our system. Stay curious, hacker-friend!"},
|
||
file={'name': '504.gif', 'alt': "Hex Code running over a screen and ends with Error"},
|
||
), 504
|