Configuration et variables d’environnement
# config/app.rb
Cybertrain.configure do |c|
c.port = 3000
c.workers = 1
end
Cybertrain::Config lit d’abord l’environnement ; config/app.rb a le
dernier mot :
| Variable | Attribut | Par défaut |
|---|---|---|
CYBERTRAIN_ENV |
env |
"development" ; "production" dans un binaire issu de cybertrain build (aussi "test") |
PORT |
port |
3000 |
CYBERTRAIN_DATABASE |
database_path |
storage/#{env}.sqlite3 |
CYBERTRAIN_SECRET_KEY_BASE |
secret_key_base |
requis en production ; en dev/test, un secret est généré automatiquement dans tmp/secret_key |
SPINEL_WORKERS |
workers |
1 |
CYBERTRAIN_HOST |
host |
"127.0.0.1" ; 0.0.0.0 écoute sur toutes les interfaces (conteneurs) |
CYBERTRAIN_ALLOWED_HOSTS |
allowed_hosts |
Le développement accepte localhost, 127.0.0.1, [::1] ; des noms d’hôte exacts séparés par des virgules étendent cette liste (sans schéma, port ni joker). Les autres hôtes reçoivent une 403 avant les fichiers statiques, les sessions, le routage ou les diagnostics. C’est distinct de l’adresse d’écoute ; la production et le test n’appliquent pas cette protection propre au développement |
CYBERTRAIN_SESSION_SAME_SITE |
session_same_site |
"Lax" ; aussi Strict ou None (None ajoute toujours Secure) ; toute autre valeur arrête le serveur au démarrage |
CYBERTRAIN_SESSION_PARTITIONED |
session_partitioned |
false ; 1 ou true ajoute Partitioned (et Secure) |
CYBERTRAIN_TRUSTED_PROXY |
trusted_proxy |
false ; 1 ou true fait de request.client_ip (ce que compte rate_limit) la dernière adresse de X-Forwarded-For d’une connexion venant du loopback, pour un reverse proxy sur le même hôte (voir Limitation du débit) ; raccourci de CYBERTRAIN_TRUSTED_PROXIES=127.0.0.0/8,::1 |
CYBERTRAIN_TRUSTED_PROXIES |
trusted_proxies |
[] ; des adresses IPv4/IPv6 ou des CIDR séparés par des virgules (172.16.0.0/12,10.0.0.0/8) dont les connexions ont leur request.client_ip tiré de la dernière adresse de X-Forwarded-For, pour un proxy atteint par un réseau, comme kamal-proxy dans son conteneur ; une entrée invalide arrête le serveur au démarrage |
CYBERTRAIN_MAX_PAGE |
max_page |
10000 : la plus grande page que donne params.page_number et qu’accepte Relation#page (voir Pagination) ; une valeur qui n’est pas un nombre d’au plus 18 chiffres, ou inférieure à 1, donne la valeur par défaut |
CYBERTRAIN_FILTER_PARAMETERS |
filter_parameters |
["password", "password_confirmation", "token", "secret", "secret_key_base", "session_key", "api_key", "authorization"] ; des noms séparés par des virgules remplacent la liste. Un paramètre dont le nom contient l’un d’eux (sans tenir compte de la casse ASCII : password correspond aussi à user[password]) voit sa valeur écrite [FILTERED] dans le journal des requêtes et sur la page d’erreur de développement ; [] dans config/app.rb désactive ce filtrage |
CYBERTRAIN_WORKER |
worker |
true ; 0 ou false empêche ce processus d’exécuter les tâches planifiées et les jobs (voir Tâches de fond) ; scheduler_interval (1 s), job_poll_interval (5 s) et job_max_attempts (5) se règlent dans config/app.rb |
CYBERTRAIN_HEALTH_PATH |
health_path |
"/up" ; le chemin du contrôle de santé intégré (voir Exploitation) ; une valeur vide le désactive ; une valeur sans / initial arrête le serveur au démarrage. health_worker_stale (300 s) se règle dans config/app.rb |
CYBERTRAIN_HTTP_ALLOW_PRIVATE |
http_allow_private |
false ; 1 ou true permet à Cybertrain::HttpClient de se connecter au loopback, aux adresses privées et aux autres adresses non publiques (voir HTTP sortant) |
CYBERTRAIN_SMTP_HOST |
smtp_host |
"" ; le serveur SMTP par lequel envoie Cybertrain::Mailer (voir Envoyer des e-mails) |
CYBERTRAIN_SMTP_PORT |
smtp_port |
0 : le port du mode, 587 pour starttls, 465 pour tls, 25 pour none ; toute valeur hors de 1 à 65535 vaut 0 |
CYBERTRAIN_SMTP_USER |
smtp_user |
"" : pas d’AUTH ; s’il est défini, le mailer s’authentifie avec AUTH PLAIN |
CYBERTRAIN_SMTP_PASSWORD |
smtp_password |
"" ; un secret, jamais journalisé, jamais dans la bannière de démarrage ni dans un message d’erreur |
CYBERTRAIN_SMTP_FROM |
smtp_from |
"" : l’expéditeur qu’utilise Mailer.deliver quand from: n’est pas fourni |
CYBERTRAIN_SMTP_TLS |
smtp_tls |
"starttls" ; aussi tls (TLS implicite) ou none (texte en clair, identifiants vers le loopback uniquement) ; les deux modes TLS demandent require "cybertrain/net/tls" |
N’utilisez None (avec CYBERTRAIN_SESSION_PARTITIONED=1) que pour une
application affichée dans un cadre d’un autre site ; le navigateur doit
atteindre l’application en HTTPS.
Autres attributs aux valeurs par défaut fixes et modifiables : views_root
("app/views"), public_root ("public"), layout
("layouts/application"), log_level (:info), session_cookie_name,
session_max_age (2 semaines), session_secure (true en production, ce qui
marque le cookie de session Secure ; false ailleurs, mais
SameSite=None et Partitioned ajoutent Secure de toute façon),
pool_size (4), static_files/csrf (true), csrf_exempt_prefixes
([] : des préfixes de chemin comme "/api/" dont les requêtes autres que
GET sautent la vérification CSRF, pour un point d’accès que des programmes
appellent avec un identifiant qui leur est propre ; chacun doit commencer
et finir par / et ne pas être / seul, sinon le démarrage échoue ; le
préfixe est comparé au chemin tel qu’envoyé, donc une graphie encodée en
pourcent reste vérifiée ; aucune autre route ne doit correspondre à un
chemin sous ce préfixe, et le contrôleur exempté ne doit pas compter sur la
session pour l’authentification, puisque la session y est tout de même
chargée), max_body_bytes (10 Mio : un corps de requête plus long,
tel qu’envoyé, reçoit une 413 avant d’être lu ; augmentez-le pour les gros
envois de fichiers ; la taille décompressée d’un corps gzip est bornée
séparément par request.decoded_body(limit)), max_render_depth (12 rendus
ouverts à la fois : la page et ses partiels ; augmentez-le pour des
partiels qui s’appellent récursivement plus profondément à juste titre ; il
doit valoir au moins 1, sinon le démarrage échoue). Note de mise à
niveau : l’imbrication des rendus, illimitée auparavant, est désormais
plafonnée à 12 par défaut (la page est à la profondeur 1, les partiels peuvent
donc s’imbriquer sur 11 niveaux ; le layout est rendu après la page et ne
s’imbrique pas), et une application avec un arbre plus profond (commentaires en
fil, menu de catégories) obtient une erreur de template tant qu’elle ne définit
pas max_render_depth. Envois de fichiers : max_upload_bytes
(5 Mio par fichier envoyé ; ne doit pas dépasser max_body_bytes),
max_multipart_parts (128 parties, champs texte compris, au plus 4096),
upload_root ("storage/uploads", où écrit Cybertrain::BlobStore ; créé
au premier envoi, pas au démarrage).
En-têtes de sécurité : chaque réponse qu’écrit le serveur (actions,
middleware des fichiers statiques, pages d’erreur et page de diagnostic de
développement, les 400/411/413/500 propres au serveur, et HEAD) porte
X-Content-Type-Options: nosniff et X-Frame-Options: SAMEORIGIN, sauf si la
réponse définit déjà un en-tête de ce nom (quelle que soit la casse) :
Response#to_http les ajoute après les en-têtes propres à la réponse et
seulement s’ils manquent, donc
response.set_header("X-Frame-Options", "DENY") dans une action envoie
exactement votre valeur, une seule fois. Il n’y a délibérément pas de réglage
Config pour les désactiver dans toute l’application ; un widget
intégrable remplace l’en-tête sur ses propres réponses (un en-tête
Content-Security-Policy: frame-ancestors ... l’emporte sur X-Frame-Options
dans les navigateurs actuels). À cause de nosniff, les navigateurs prennent
Content-Type au pied de la lettre : le middleware des fichiers statiques
connaît les extensions habituelles de public/ (html, css, js/mjs, images dont
webp et avif, polices, wasm, pdf, audio et vidéo, json, csv, xml), et tout le
reste est servi en application/octet-stream, c’est-à-dire en
téléchargement ; ajoutez l’extension à Cybertrain::Static::EXTENSIONS si
une application en a besoin d’une autre affichée directement. Un reverse proxy
qui sert lui-même public/ contourne ces en-têtes (voir
docs/deploy.en.md). Cybertrain::Test::Client appelle
l’application dans le même processus et ne sérialise jamais la réponse : il
ne montre donc pas ces deux valeurs par défaut ; vérifiez-les sur
response.to_http.
Paramètres filtrés : la ligne Started GET "/login?..." du journal des
requêtes et la page d’erreur de développement (sa ligne de requête, et son
affichage des params et d’un corps JSON) montrent la valeur de chaque paramètre
nommé dans config.filter_parameters sous la forme [FILTERED], comme le fait
le filter_parameters de Rails, de sorte qu’un ?token= dans une URL ou un mot
de passe posté à un formulaire qui échoue n’apparaît ni dans le journal des
requêtes du framework ni sur la page d’erreur de développement. Seules les
valeurs sont remplacées : le reste de la chaîne de requête est journalisé
tel qu’envoyé (caractères de contrôle échappés). La liste compare par
sous-chaîne, sans tenir compte de la casse ; étendez-la dans
config/app.rb (c.filter_parameters = c.filter_parameters + ["otp", "ssn"])
ou remplacez-la avec CYBERTRAIN_FILTER_PARAMETERS. Les valeurs qu’une action
journalise elle-même ne sont pas filtrées ; interrogez-y
Cybertrain::ParameterFilter.filter?(name) ou
ParameterFilter.filter_query(text).
Déploiement : cybertrain build produit dist/ : le binaire
dist/NAME avec les vues intégrées, dist/public/ (ressources statiques, ou
laissez un reverse proxy le servir), ainsi que dist/storage/ (le fichier
SQLite par défaut) et dist/tmp/. Lancez-le depuis dist/ avec
CYBERTRAIN_SECRET_KEY_BASE défini ; il est en production sauf si
CYBERTRAIN_ENV indique autre chose, et ./NAME
migrate applique les migrations compilées dans le binaire. Le binaire ne parle
que du HTTP/1.1 simple ; placez nginx/Caddy devant pour le TLS et HTTP/2
(le cookie de session est Secure en production, servez donc l’application en
HTTPS). En production, une exception renvoie une 500, et une réponse d’erreur
brute (la 404 en texte brut du routeur, head :not_found, render plain:) est
remplacée par public/<status>.html quand ce fichier existe ; les erreurs
qu’une action rend en HTML ou en JSON passent telles quelles. SIGTERM cesse
d’accepter des connexions, laisse se terminer les requêtes déjà en cours
(auxquelles il est répondu avec Connection: close ; les connexions
keep-alive inactives sont fermées immédiatement) et quitte quand elles sont
terminées, ou après le drain_timeout du serveur (10 s). La boucle de
surveillance et de recompilation ne tourne jamais en production.
docs/deploy.en.md (en japonais :
docs/deploy.md) en décrit tout le parcours :
cybertrain new, un serveur Ubuntu, systemd, Caddy avec HTTPS, les
redéploiements, les retours arrière et les sauvegardes.
Kamal : cybertrain new écrit aussi ce dont
Kamal 2 a besoin : un Dockerfile qui exécute
cybertrain setup et cybertrain build dans une étape de build Ubuntu et
copie dist/ dans une étape allégée de la même version (migration au
démarrage, puis service sur 0.0.0.0:3000), un .dockerignore,
config/deploy.yml (kamal-proxy avec Let’s Encrypt en façade, le contrôle de
santé sur public/up.html, le fichier SQLite sur un volume en
/app/storage) et .kamal/secrets (KAMAL_REGISTRY_PASSWORD et
CYBERTRAIN_SECRET_KEY_BASE tirés de l’environnement). Renseignez les valeurs
marquées CHANGE ME, committez, puis lancez kamal setup ;
docs/deploy-kamal.md donne les détails (partie 8 du
tutoriel de déploiement).
examples/blog embarque la même
configuration, construite avec le framework de cette copie du dépôt.
Réglages de l’application
Les réglages propres à une application (une URL de base publique, un
interrupteur de fonctionnalité, un intervalle, un hôte SMTP) sont déclarés sur
le même Config, avec un type, une variable d’environnement et une valeur par
défaut :
# config/app.rb
Cybertrain.configure do |c|
c.setting_string :base_url, env: "MYAPP_BASE_URL", default: ""
c.setting_string :smtp_password, env: "MYAPP_SMTP_PASSWORD", default: "", secret: true
c.setting_bool :worker, env: "MYAPP_WORKER", default: true
c.setting_int :prune_interval, env: "MYAPP_PRUNE_INTERVAL", default: 3600, min: 1, max: 9_999_999
end
Cybertrain.config.string_setting(:base_url)
Cybertrain.config.bool_setting(:worker)
Cybertrain.config.int_setting(:prune_interval)
Chaque valeur est lue une seule fois, quand config/app.rb s’exécute au
démarrage. Une variable vide ou non définie signifie la valeur par défaut. Un
booléen accepte 1/true ou 0/false ; un entier accepte des chiffres
décimaux (avec un - initial facultatif) compris dans min..max (tous deux
facultatifs). Toute autre valeur se rabat sur la valeur par défaut et
journalise une ligne [WARN] qui le signale. Lire un nom non déclaré, ou
déclaré avec un autre type, lève ArgumentError, tout comme déclarer deux fois
un même nom. Les tests changent une valeur avec set_string_setting,
set_int_setting et set_bool_setting. ./NAME help liste chaque réglage
déclaré avec sa variable et sa valeur par défaut, et la bannière de démarrage
affiche les valeurs courantes (=> Settings: base_url="" worker=true ...) ;
un réglage secret: true n’apparaît dans les deux que par son nom, sous la
forme [FILTERED]. Il n’y a pas d’accesseur Cybertrain.config.base_url :
Spinel n’a pas de define_method, les lecteurs sont donc des méthodes typées
qui prennent le nom.
Fuseaux horaires
Spinel n’embarque pas de base de données tz, et définir ENV["TZ"] pour
convertir une heure la change pour tous les threads du processus.
Cybertrain::TimeZone lit plutôt les fichiers de fuseaux du système :
/usr/share/zoneinfo/<name>, ou $TZDIR/<name> quand TZDIR est défini
(installez le paquet tzdata dans une image de conteneur allégée). Il analyse
les versions 1 à 4 de TZif (RFC 8536), y compris la règle TZ POSIX dont dépend
un fichier « slim » après sa dernière transition, et met chaque
fuseau en cache pour toute la durée de vie du processus (copie à l’écriture,
donc les recherches depuis plusieurs workers ne prennent aucun verrou).
Tout est exprimé en secondes epoch Integer. Une epoch locale est une lecture
d’horloge murale comptée comme si c’était de l’UTC, de sorte que
l’arithmétique sur l’heure murale est une simple addition, et que
TimeZone.civil_to_epoch / TimeZone.epoch_to_civil passent de cette valeur à
[year, month, day, hour, minute, second] et inversement :
tokyo = Cybertrain::TimeZone.find("Asia/Tokyo")
tokyo.found? # false pour un nom inconnu : le fuseau se comporte alors comme UTC
tokyo.utc_offset_at(Time.now.to_i) # => 32400
tokyo.abbreviation_at(Time.now.to_i) # => "JST"
# Le prochain 09:00 à Tokyo :
now = Time.now.to_i
today = Cybertrain::TimeZone.epoch_to_civil(tokyo.utc_to_local(now))
run = tokyo.local_to_utc(Cybertrain::TimeZone.civil_to_epoch(today[0], today[1], today[2], 9, 0, 0))
run = tokyo.local_to_utc(Cybertrain::TimeZone.civil_to_epoch(today[0], today[1], today[2] + 1, 9, 0, 0)) if run <= now
Lors d’un changement d’heure, local_to_utc est déterministe : une heure
murale qu’un saut en avant fait sauter (02:30 le jour du passage à l’heure d’été
aux États-Unis) est lue avec le décalage d’avant le saut, et tombe donc après
lui (03:30 EDT) ; une heure murale qu’un recul répète donne l’instant le
plus tôt. find n’accepte qu’un nom relatif composé de A-Za-z0-9_+-./ sans
segment vide, . ou .., de sorte qu’un nom de fuseau tiré du profil d’un
utilisateur ne peut pas atteindre un fichier hors du répertoire des
fuseaux ; un fuseau inconnu ou mal formé n’est pas mis en cache, et "UTC"
n’a besoin d’aucun fichier. Les helpers de formatage pour les templates
(l(time, zone:)) n’existent pas encore.