Сервер: пакет push на webpush-go (третья и последняя прямая зависимость),
подписка устройства, правило ADR-023 «пуш только молчащему устройству и только
один» через атомарный захват push_pending, удаление подписки на 404 и 410,
TTL сутки, urgency normal. В нагрузке только {title, body, chat} — тело
собирается из константы, плейнтекст туда не попадает даже по ошибке.
Клиент: service worker с версионированным кэшем оболочки и никогда — /api/*,
push и notificationclick, подписка на VAPID-ключ сервера, запрос разрешения
после первого отправленного сообщения, разделы настроек «уведомления»
и «установить приложение», баннер установки на iOS.
ADR-045: пуш адресован получателю — по букве ADR-023 он уходил бы и молчащему
устройству отправителя с бессмысленным заголовком из собственного ника.
ADR-047: сервер ходит на endpoint подписки, который выбирает браузер. Проверка
«только https» обходилась редиректом, а имя могло смотреть внутрь сети — теперь
запрет редиректов и проверка разрешённого адреса на уровне сокета.
ADR-048: пределы отправки — недоступный push-сервис одного аккаунта больше
не съедает пуши всего сервера.
ADR-049: явно выключенные уведомления сами не включаются обратно.
Попутно: webpush-go дописывает набивку в переданный срез, а одна нагрузка
уходила всем устройствам сообщения — гонка, пойманная go test -race.
Теперь у каждого задания своя копия.
Приёмка на боевом: подписки, hasPush, чужое устройство, Origin, оболочка
из девяти файлов, Service-Worker-Allowed. Отдельно шесть непубличных адресов
и endpoint на 3 КиБ — все отбиты.
Чеклист ручной проверки на iPhone и Android — в docs/plan.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
80 lines
3.6 KiB
Go
80 lines
3.6 KiB
Go
// Package config читает конфигурацию из переменных окружения BARE_* (ADR-022).
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// Config — всё, что сервер знает о своём окружении.
|
|
type Config struct {
|
|
Addr string // BARE_ADDR — адрес прослушивания
|
|
DB string // BARE_DB — путь к файлу SQLite
|
|
Origin string // BARE_ORIGIN — единственный допустимый Origin (ADR-021)
|
|
VAPIDPublic string // BARE_VAPID_PUBLIC
|
|
VAPIDPrivate string // BARE_VAPID_PRIVATE
|
|
VAPIDSubject string // BARE_VAPID_SUBJECT
|
|
InviteCode string // BARE_INVITE_CODE — пусто означает открытую регистрацию
|
|
|
|
// PushLocal разрешает отправку пушей на непубличные адреса. Из
|
|
// окружения не читается и в работе всегда false: сервер ходит
|
|
// только по публичным адресам (ADR-047). Поле существует ради
|
|
// тестов, где push-сервис вендора подменён сервером на 127.0.0.1.
|
|
PushLocal bool
|
|
}
|
|
|
|
// Значения по умолчанию — локальный запуск без окружения.
|
|
const (
|
|
defaultAddr = "127.0.0.1:8411"
|
|
defaultDB = "bare.db"
|
|
defaultOrigin = "http://127.0.0.1:8411"
|
|
)
|
|
|
|
// Параметры, которые сервер сообщает клиенту в GET /api/config. Это
|
|
// константы, а не переменные окружения: их значения — часть криптосистемы
|
|
// (ADR-013) и протокола (ADR-021), а не настройка машины.
|
|
const (
|
|
// KDFIterations — целевое число итераций PBKDF2 на клиенте.
|
|
KDFIterations = 1_000_000
|
|
// KDFMinIterations — нижняя граница: блоб с меньшим iter сервер не примет.
|
|
KDFMinIterations = 600_000
|
|
// KDFMaxIterations — верхняя граница (ADR-030). Сервер сам раздаёт iter
|
|
// из блоба в GET /api/kdf, и с неподъёмным значением аккаунт нельзя
|
|
// ни открыть, ни удалить: обе операции начинаются с PBKDF2.
|
|
KDFMaxIterations = 10_000_000
|
|
// MaxMessageChars — предел текста сообщения.
|
|
MaxMessageChars = 4000
|
|
)
|
|
|
|
// Load читает окружение. Незаданная переменная берёт значение по умолчанию;
|
|
// заданная пустой — ошибка: пустой адрес, путь к базе или origin неработоспособны.
|
|
func Load() (*Config, error) {
|
|
c := &Config{
|
|
Addr: env("BARE_ADDR", defaultAddr),
|
|
DB: env("BARE_DB", defaultDB),
|
|
Origin: env("BARE_ORIGIN", defaultOrigin),
|
|
VAPIDPublic: env("BARE_VAPID_PUBLIC", ""),
|
|
VAPIDPrivate: env("BARE_VAPID_PRIVATE", ""),
|
|
VAPIDSubject: env("BARE_VAPID_SUBJECT", ""),
|
|
InviteCode: env("BARE_INVITE_CODE", ""),
|
|
}
|
|
for _, v := range []struct{ key, value string }{
|
|
{"BARE_ADDR", c.Addr},
|
|
{"BARE_DB", c.DB},
|
|
{"BARE_ORIGIN", c.Origin},
|
|
} {
|
|
if v.value == "" {
|
|
return nil, fmt.Errorf("%s пуст: уберите переменную, чтобы взять значение по умолчанию, или задайте непустое", v.key)
|
|
}
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func env(key, fallback string) string {
|
|
if v, ok := os.LookupEnv(key); ok {
|
|
return strings.TrimSpace(v)
|
|
}
|
|
return fallback
|
|
}
|