feat(obs): nommer chaque requete, et rendre les pages d erreur audibles
OBS-005. Un 500 dans errors.log et les six lignes de app.log qui y menent n etaient relies que par leur horodatage — ce qui n est pas une relation des que le serveur traite plus d une requete a la fois. Et un utilisateur qui dit « ca a plante quand j ai clique sur enregistrer » ne donnait a personne de quoi chercher. Chaque requete recoit un identifiant, porte par toutes les lignes de journal qu elle produit, renvoye en X-Request-Id, et affiche sur la page 500 comme reference a citer. Il est **genere**, jamais lu depuis un en-tete entrant. Accepter celui du client serait pratique pour tracer a travers nginx, et permettrait aussi a n importe qui d ecrire du texte arbitraire — retours a la ligne compris — dans le fichier de journal. C est ainsi qu un journal cesse d etre une preuve. Il n y a de toute facon aucun proxy de confiance tant qu OPS-002 est ouvert. Le test correspondant assure sur l alphabet plutot qu en envoyant un retour a la ligne : le client de test de Werkzeug refuse d emettre un tel en-tete, donc l attaque ne peut meme pas etre construite par la, ce qui ne prouverait rien sur l application. **Defaut trouve en chemin, et repare.** Les cinq gabarits d erreur remplissent le bloc `content`, qui n existait que dans la branche authentifiee de la mise en page. Un visiteur deconnecte tombant sur une erreur — donc typiquement sur la page de connexion — recevait le logo, le selecteur de langue, et **aucun message**. Le code de statut etait bon, les journaux etaient bons, la page etait vide. Le <title> disait quand meme « 404 », ce qui explique en grande partie que personne ne l ait vu. Le bloc est desormais rendu dans les deux branches via self.content(), Jinja refusant deux blocs de meme nom. Un seul cote du if s execute, donc jamais de double rendu — et c est assure, pas suppose. 550 tests.
This commit is contained in:
+44
-2
@@ -14,6 +14,37 @@ import os
|
||||
import re
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
#: Value used when a record is emitted outside a request — startup, the
|
||||
#: Discord bot thread, the scheduler. Short and obviously not an id, so a
|
||||
#: grep for one never matches it by accident.
|
||||
NO_REQUEST = '-'
|
||||
|
||||
|
||||
class RequestIdFilter(logging.Filter):
|
||||
"""Stamp every record with the id of the request that produced it.
|
||||
|
||||
Without this, a 500 in errors.log and the six lines in app.log that led
|
||||
to it are related only by their timestamps, which is not a relation when
|
||||
the server is handling more than one request at a time (OBS-005).
|
||||
|
||||
The id is generated per request and never read from an inbound header.
|
||||
Accepting one would be convenient for tracing across nginx, and it would
|
||||
also let any caller write arbitrary text — newlines included — into the
|
||||
log file, which is how a log gets forged rather than read. There is no
|
||||
trusted proxy to take it from while OPS-002 is open.
|
||||
"""
|
||||
|
||||
def filter(self, record):
|
||||
record.request_id = NO_REQUEST
|
||||
try:
|
||||
from flask import g, has_request_context
|
||||
|
||||
if has_request_context():
|
||||
record.request_id = g.get('request_id', NO_REQUEST)
|
||||
except Exception: # noqa: BLE001 — logging must never be the thing that fails
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
class SensitiveDataFilter(logging.Filter):
|
||||
"""Logging filter that redacts sensitive information from log messages.
|
||||
@@ -95,10 +126,17 @@ def configure_logging(app):
|
||||
|
||||
# Create the sensitive data filter
|
||||
sensitive_filter = SensitiveDataFilter()
|
||||
request_id_filter = RequestIdFilter()
|
||||
|
||||
# Formatter with timestamp, level, module, and message
|
||||
# Formatter with timestamp, level, module, request id, and message.
|
||||
#
|
||||
# request_id comes from RequestIdFilter, which is attached to every
|
||||
# handler below. A handler that formats with this string and does not
|
||||
# carry the filter raises on its first record — so if one is ever added,
|
||||
# add the filter with it.
|
||||
formatter = logging.Formatter(
|
||||
'[%(asctime)s] %(levelname)s [%(name)s:%(lineno)d] %(message)s', datefmt='%Y-%m-%d %H:%M:%S'
|
||||
'[%(asctime)s] %(levelname)s [%(name)s:%(lineno)d] [%(request_id)s] %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -112,6 +150,7 @@ def configure_logging(app):
|
||||
error_handler.setLevel(logging.ERROR)
|
||||
error_handler.setFormatter(formatter)
|
||||
error_handler.addFilter(sensitive_filter)
|
||||
error_handler.addFilter(request_id_filter)
|
||||
app.logger.addHandler(error_handler)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -125,6 +164,7 @@ def configure_logging(app):
|
||||
auth_handler.setLevel(logging.INFO)
|
||||
auth_handler.setFormatter(formatter)
|
||||
auth_handler.addFilter(sensitive_filter)
|
||||
auth_handler.addFilter(request_id_filter)
|
||||
|
||||
# Create a named logger specifically for auth events
|
||||
auth_logger = logging.getLogger('team_tryouts.auth')
|
||||
@@ -143,6 +183,7 @@ def configure_logging(app):
|
||||
app_handler.setLevel(log_level)
|
||||
app_handler.setFormatter(formatter)
|
||||
app_handler.addFilter(sensitive_filter)
|
||||
app_handler.addFilter(request_id_filter)
|
||||
app.logger.addHandler(app_handler)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -156,6 +197,7 @@ def configure_logging(app):
|
||||
console_handler.setLevel(logging.DEBUG if debug_mode else log_level)
|
||||
console_handler.setFormatter(formatter)
|
||||
console_handler.addFilter(sensitive_filter)
|
||||
console_handler.addFilter(request_id_filter)
|
||||
app.logger.addHandler(console_handler)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user