CyberTrainРуководстваКонфигурация

Конфигурация

Конфигурация и переменные окружения

ruby
# config/app.rb
Cybertrain.configure do |c|
  c.port = 3000
  c.workers = 1
end

Cybertrain::Config сначала читает переменные окружения; config/app.rb их переопределяет:

Переменная Атрибут По умолчанию
CYBERTRAIN_ENV env "development"; "production" в бинарном файле из cybertrain build (также "test")
PORT port 3000
CYBERTRAIN_DATABASE database_path storage/#{env}.sqlite3
CYBERTRAIN_SECRET_KEY_BASE secret_key_base обязательна в production; в dev/test генерируется автоматически в tmp/secret_key
SPINEL_WORKERS workers 1
CYBERTRAIN_HOST host "127.0.0.1"; 0.0.0.0 слушает на всех интерфейсах (контейнеры)
CYBERTRAIN_ALLOWED_HOSTS allowed_hosts Режим разработки принимает localhost, 127.0.0.1, [::1]; разделённые запятыми точные имена хостов расширяют этот список (без схемы, порта и подстановочных знаков). Другие хосты получают 403 до статических файлов, сессий, маршрутизации и диагностики. Это не связано с адресом прослушивания; production и test не применяют эту защиту режима разработки
CYBERTRAIN_SESSION_SAME_SITE session_same_site "Lax"; также Strict или None (None всегда добавляет Secure); любое другое значение останавливает сервер при запуске
CYBERTRAIN_SESSION_PARTITIONED session_partitioned false; 1 или true добавляет Partitioned (и Secure)
CYBERTRAIN_TRUSTED_PROXY trusted_proxy false; 1 или true делает request.client_ip (то, что засчитывает rate_limit) последним адресом X-Forwarded-For соединения с loopback — для обратного прокси на том же хосте (см. Ограничение частоты запросов); сокращение для CYBERTRAIN_TRUSTED_PROXIES=127.0.0.0/8,::1
CYBERTRAIN_TRUSTED_PROXIES trusted_proxies []; разделённые запятыми адреса IPv4/IPv6 или CIDR (172.16.0.0/12,10.0.0.0/8), для соединений с которых request.client_ip берётся из последнего адреса X-Forwarded-For, — для прокси, доступного по сети, например kamal-proxy в его контейнере; неверная запись останавливает сервер при запуске
CYBERTRAIN_MAX_PAGE max_page 10000: наибольшая страница, которую выдаёт params.page_number и принимает Relation#page (см. Постраничный вывод); значение, не являющееся числом не более чем из 18 цифр, или меньше 1 заменяется значением по умолчанию
CYBERTRAIN_FILTER_PARAMETERS filter_parameters ["password", "password_confirmation", "token", "secret", "secret_key_base", "session_key", "api_key", "authorization"]; разделённые запятыми имена заменяют список. Значение параметра, имя которого содержит одно из них (без учёта регистра ASCII: password совпадает и с user[password]), записывается как [FILTERED] в лог запросов и на страницу ошибок режима разработки; [] в config/app.rb это отключает
CYBERTRAIN_WORKER worker true; 0 или false не даёт этому процессу выполнять задачи по расписанию и фоновые задачи (см. Фоновая работа); scheduler_interval (1 с), job_poll_interval (5 с) и job_max_attempts (5) задаются в config/app.rb
CYBERTRAIN_HEALTH_PATH health_path "/up"; путь встроенной проверки работоспособности (см. Эксплуатация); пустое значение её отключает; значение без ведущего / останавливает сервер при запуске. health_worker_stale (300 с) задаётся в config/app.rb
CYBERTRAIN_HTTP_ALLOW_PRIVATE http_allow_private false; 1 или true позволяет Cybertrain::HttpClient подключаться к loopback, частным и другим непубличным адресам (см. Исходящий HTTP)
CYBERTRAIN_SMTP_HOST smtp_host ""; SMTP-сервер, через который отправляет Cybertrain::Mailer (см. Отправка электронной почты)
CYBERTRAIN_SMTP_PORT smtp_port 0: порт режима — 587 для starttls, 465 для tls, 25 для none; всё, кроме значений от 1 до 65535, считается 0
CYBERTRAIN_SMTP_USER smtp_user "": без AUTH; если задано, мейлер аутентифицируется через AUTH PLAIN
CYBERTRAIN_SMTP_PASSWORD smtp_password ""; секрет, никогда не пишется в лог, в стартовый баннер и в сообщения об ошибках
CYBERTRAIN_SMTP_FROM smtp_from "": отправитель, которого использует Mailer.deliver, когда ему не передан from:
CYBERTRAIN_SMTP_TLS smtp_tls "starttls"; также tls (неявный TLS) или none (открытый текст, учётные данные — только на loopback); обоим режимам TLS нужен require "cybertrain/net/tls"

Используйте None (вместе с CYBERTRAIN_SESSION_PARTITIONED=1) только для приложения, которое показывается во фрейме другого сайта; браузер должен обращаться к приложению по HTTPS.

Другие атрибуты с фиксированными переопределяемыми значениями по умолчанию: views_root ("app/views"), public_root ("public"), layout ("layouts/application"), log_level (:info), session_cookie_name, session_max_age (2 недели), session_secure (true в production, что помечает cookie сессии как Secure; false в остальных окружениях, хотя SameSite=None и Partitioned всё равно добавляют Secure), pool_size (4), static_files/csrf (true), csrf_exempt_prefixes ([]: префиксы путей, например "/api/", чьи не-GET-запросы пропускают проверку CSRF, — для эндпоинта, который программы вызывают с собственными учётными данными; каждый должен начинаться и заканчиваться на / и не быть просто /, иначе запуск завершается сбоем; префикс сопоставляется с путём в том виде, в каком он отправлен, поэтому percent-кодированное написание всё равно проверяется; ни один другой маршрут не должен совпадать с путём под этим префиксом, а освобождённый контроллер не должен полагаться на сессию для аутентификации, поскольку сессия там всё равно загружается), max_body_bytes (10 МиБ: на более длинное тело запроса, в том виде, в каком оно отправлено, отвечается 413 до его чтения; увеличьте для больших загрузок; распакованный размер тела в gzip ограничивается отдельно через request.decoded_body(limit)), max_render_depth (12 одновременно открытых отрисовок: страница и её партиалы; увеличьте для партиалов, которым обоснованно нужна более глубокая рекурсия; значение должно быть не меньше 1, иначе запуск завершается сбоем). Примечание к обновлению: вложенность отрисовки, прежде неограниченная, теперь по умолчанию ограничена 12 (страница — глубина 1, поэтому партиалы могут вкладываться на 11 уровней; макет отрисовывается после страницы и во вложенность не входит), и приложение с более глубоким деревом (комментарии в виде веток, меню категорий) получает ошибку шаблона, пока не задаст max_render_depth. Загрузка файлов: max_upload_bytes (5 МиБ на загруженный файл; не должен превышать max_body_bytes), max_multipart_parts (128 частей, включая текстовые поля, не более 4096), upload_root ("storage/uploads", куда пишет Cybertrain::BlobStore; создаётся при первой загрузке, а не при запуске).

Заголовки безопасности: каждый ответ, который пишет сервер (действия, middleware статических файлов, страницы ошибок и страница диагностики режима разработки, собственные 400/411/413/500 сервера и HEAD), несёт X-Content-Type-Options: nosniff и X-Frame-Options: SAMEORIGIN, если только ответ уже не задаёт заголовок с таким именем (в любом регистре): Response#to_http добавляет их после собственных заголовков ответа и только при их отсутствии, поэтому response.set_header("X-Frame-Options", "DENY") в действии отправляет ровно ваше значение, один раз. Переключателя в Config, чтобы отключить их для всего приложения, намеренно нет; встраиваемый виджет переопределяет заголовок в собственных ответах (заголовок Content-Security-Policy: frame-ancestors ... в современных браузерах имеет приоритет над X-Frame-Options). Из-за nosniff браузеры понимают Content-Type буквально: middleware статических файлов знает обычные расширения public/ (html, css, js/mjs, изображения, включая webp и avif, шрифты, wasm, pdf, аудио и видео, json, csv, xml), а всё остальное отдаётся как application/octet-stream, то есть на скачивание; добавьте расширение в Cybertrain::Static::EXTENSIONS, если приложению нужно показывать в браузере другое. Обратный прокси, который сам отдаёт public/, обходит эти заголовки (см. docs/deploy.en.md). Cybertrain::Test::Client вызывает приложение внутри процесса и никогда не сериализует ответ, поэтому эти два значения по умолчанию в нём не видны; проверяйте их на response.to_http.

Фильтруемые параметры: строка Started GET "/login?..." в логе запросов и страница ошибок режима разработки (её строка запроса и дамп params и тела JSON) показывают значение каждого параметра, названного в config.filter_parameters, как [FILTERED], как это делает filter_parameters в Rails, поэтому ?token= в URL или пароль, отправленный в форму, которая не прошла проверку, не появляется в логе запросов фреймворка и на странице ошибок режима разработки. Заменяются только значения: остальная часть строки запроса пишется в лог как отправлена (управляющие символы экранируются). Список сопоставляется по подстроке без учёта регистра; расширьте его в config/app.rb (c.filter_parameters = c.filter_parameters + ["otp", "ssn"]) или замените через CYBERTRAIN_FILTER_PARAMETERS. Значения, которые действие пишет в лог само, не фильтруются; там спрашивайте Cybertrain::ParameterFilter.filter?(name) или ParameterFilter.filter_query(text).

Развёртывание: cybertrain build создаёт dist/: бинарный файл dist/NAME со встроенными представлениями, dist/public/ (статические ресурсы; или пусть их отдаёт обратный прокси), а также dist/storage/ (по умолчанию файл SQLite) и dist/tmp/. Запускайте его из dist/ с заданной CYBERTRAIN_SECRET_KEY_BASE; это production, если CYBERTRAIN_ENV не говорит иного, а ./NAME migrate применяет вкомпилированные в него миграции. Бинарный файл говорит только на обычном HTTP/1.1; для TLS и HTTP/2 поставьте перед ним nginx/Caddy (в production cookie сессии имеет флаг Secure, поэтому отдавайте приложение по HTTPS). В production исключение даёт ответ 500, а голый ответ с ошибкой (текстовый 404 маршрутизатора, head :not_found, render plain:) заменяется на public/<status>.html, если такой файл существует; ошибки, которые действие отрисовывает как HTML или JSON, проходят без изменений. SIGTERM прекращает приём соединений, даёт уже выполняющимся запросам завершиться (с ответом Connection: close; простаивающие keep-alive-соединения закрываются сразу) и выходит, когда они закончатся, или по истечении drain_timeout сервера (10 с). Цикл слежения и пересборки никогда не работает в production. docs/deploy.en.md (на японском: docs/deploy.md) проводит через весь процесс: cybertrain new, сервер Ubuntu, systemd, Caddy с HTTPS, повторные развёртывания, откаты и резервные копии.

Kamal: cybertrain new также записывает то, что нужно Kamal 2: Dockerfile, который выполняет cybertrain setup и cybertrain build на этапе сборки в Ubuntu и копирует dist/ в облегчённый этап того же выпуска (миграции при запуске, затем обслуживание на 0.0.0.0:3000), .dockerignore, config/deploy.yml (kamal-proxy с Let's Encrypt перед приложением, проверка работоспособности по public/up.html, файл SQLite на томе в /app/storage) и .kamal/secrets (KAMAL_REGISTRY_PASSWORD и CYBERTRAIN_SECRET_KEY_BASE из окружения). Заполните значения, помеченные CHANGE ME, сделайте коммит и выполните kamal setup; подробности — в docs/deploy-kamal.md (часть 8 учебника по развёртыванию). examples/blog содержит ту же конфигурацию, собранную с фреймворком из этой рабочей копии.

Настройки приложения

Собственные настройки приложения (публичный базовый URL, переключатель функции, интервал, SMTP-хост) объявляются в том же Config с типом, переменной окружения и значением по умолчанию:

ruby
# 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)

Каждое значение читается один раз, когда config/app.rb выполняется при запуске. Пустая или незаданная переменная означает значение по умолчанию. Булево значение принимает 1/true или 0/false; целое — десятичные цифры (с необязательным ведущим -) в пределах min..max (обе границы необязательны). Любое другое значение заменяется значением по умолчанию, и в лог пишется строка [WARN] об этом. Чтение необъявленного имени или имени, объявленного с другим типом, возбуждает ArgumentError, как и повторное объявление имени. Тесты меняют значение через set_string_setting, set_int_setting и set_bool_setting. ./NAME help перечисляет все объявленные настройки с их переменными и значениями по умолчанию, а стартовый баннер выводит текущие значения (=> Settings: base_url="" worker=true ...); настройка с secret: true в обоих местах показывается только по имени, как [FILTERED]. Аксессора Cybertrain.config.base_url нет: в Spinel нет define_method, поэтому читатели — это типизированные методы, принимающие имя.

Часовые пояса

Spinel не поставляется с базой данных tz, а установка ENV["TZ"] для преобразования времени меняет его для всех потоков процесса. Cybertrain::TimeZone вместо этого читает системные файлы часовых поясов: /usr/share/zoneinfo/<name> или $TZDIR/<name>, если задан TZDIR (в облегчённом образе контейнера установите пакет tzdata). Он разбирает TZif версий 1–4 (RFC 8536), включая правило POSIX TZ, на которое «облегчённый» файл опирается после своего последнего перехода, и кэширует каждый пояс на всё время жизни процесса (копирование при записи, поэтому поиск из нескольких воркеров не берёт блокировку).

Всё выражается целыми секундами эпохи Unix. Локальная эпоха — это показание настенных часов, считаемое так, будто это UTC, поэтому арифметика настенного времени — это простое сложение, а TimeZone.civil_to_epoch / TimeZone.epoch_to_civil переводят между ней и [year, month, day, hour, minute, second]:

ruby
tokyo = Cybertrain::TimeZone.find("Asia/Tokyo")
tokyo.found?                         # false для неизвестного имени: тогда пояс ведёт себя как UTC
tokyo.utc_offset_at(Time.now.to_i)   # => 32400
tokyo.abbreviation_at(Time.now.to_i) # => "JST"

# Ближайшие 09:00 в Токио:
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

На переходе на летнее время и обратно local_to_utc детерминирован: настенное время, которое пропускает переход вперёд (02:30 в день перевода часов вперёд в США), читается со смещением до перехода, поэтому попадает после него (03:30 EDT); настенное время, которое повторяет переход назад, даёт более ранний момент. find принимает только относительное имя из символов A-Za-z0-9_+-./ без пустых сегментов и сегментов . или .., поэтому имя пояса, взятое из профиля пользователя, не может добраться до файла вне каталога поясов; неизвестный или некорректный пояс не кэшируется, а "UTC" не нужен файл. Хелперов форматирования для шаблонов (l(time, zone:)) пока нет.