- в конфиг добавлены секция [auth.test_headers] и предохранитель [server] debug: заголовки входа подставляет слой транспорта, второго процесса локальный запуск больше не требует - подкоманда devtools proxy удалена целиком: всё, ради чего её поднимали, делает сам сервис - адресного предохранителя нет по решению владельца — цена названа в ADR и в модели угроз
137 lines
7.3 KiB
Go
137 lines
7.3 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"net/textproto"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.vakhrushev.me/av/transcriber/internal/entity"
|
|
)
|
|
|
|
// Имена ключей в отказах старта. Литералы по одному на пакет: два разошлись бы
|
|
// молча, и владелец искал бы в конфиге ключ, которого там нет.
|
|
const (
|
|
debugKey = "[server] debug"
|
|
testHeadersKey = "[auth.test_headers]"
|
|
)
|
|
|
|
// HeaderSubstitution отдаёт **что** сервис подставит запросу вместо заголовков
|
|
// обратного прокси — и этим же перечнем отвечает на «подставляет ли»: пустой
|
|
// перечень значит «не подставляет».
|
|
//
|
|
// Признака вторым значением нет намеренно. Предикат, посчитанный дважды —
|
|
// булевым ответом здесь и непустотой карты у потребителя, — разошёлся бы молча,
|
|
// и сервис либо подставлял бы молча, либо молча не подставлял. Представление
|
|
// одно: непустота карты, и судят её все одинаково — проверка старта, строка
|
|
// журнала при старте и слой подстановки.
|
|
//
|
|
// Ключи возвращаемой карты приведены к каноническому виду имени заголовка: в
|
|
// HTTP имя нечувствительно к регистру, а ключ TOML чувствителен.
|
|
//
|
|
// Перечень пуст и тогда, когда предохранитель включён, а имитация пуста: сам по
|
|
// себе признак ничего не включает.
|
|
func (c *Config) HeaderSubstitution() map[string]string {
|
|
if !c.Server.Debug || len(c.Auth.TestHeaders) == 0 {
|
|
return nil
|
|
}
|
|
|
|
headers := make(map[string]string, len(c.Auth.TestHeaders))
|
|
for name, value := range c.Auth.TestHeaders {
|
|
headers[textproto.CanonicalMIMEHeaderKey(strings.TrimSpace(name))] = value
|
|
}
|
|
|
|
return headers
|
|
}
|
|
|
|
// ValidateTestHeaders судит настройки отладочного входа на старте, до приёма
|
|
// трафика: настройка, отданная на честность выкладки, проверяется только тем,
|
|
// что чужой архив уже уехал не тому.
|
|
//
|
|
// Имена заголовков приходят доводом, а не читаются отсюда: дом у них один —
|
|
// константы транспорта, — а пакет настроек транспорта не знает и знать не
|
|
// должен. Обратное ребро сделало бы `cmd/devtools`, которому нужен один разбор
|
|
// конфига, линкующим всю поверхность HTTP.
|
|
//
|
|
// Отказов четыре, и каждый закрывает своё «не читается никак»:
|
|
//
|
|
// - имитация заполнена, предохранитель выключен. Либо человек забыл включить
|
|
// предохранитель и будет искать поломку везде, кроме одного ключа, либо
|
|
// забыл убрать имитацию из боевого файла — и тогда до открытого архива
|
|
// остаётся одно слово;
|
|
// - два ключа секции дают одно каноническое имя заголовка. `Remote-User` и
|
|
// `remote-user` для TOML — два ключа, для HTTP — одно имя, и одно из двух
|
|
// значений потерялось бы молча;
|
|
// - имитация называет имя, которого сервис не читает. Опечатка `Remote-Usr`
|
|
// иначе кончается сервисом, который никого не узнаёт, без единого следа;
|
|
// - имитация непуста, а годного логина в ней нет. Секция с одним
|
|
// `Remote-Email` подняла бы сервис, который подставит почту, удалит логин и
|
|
// не узнает никого.
|
|
//
|
|
// Значений отказы не называют: логин — ключ к чужому архиву, и запрет печатать
|
|
// его действует на подставленное значение наравне с пришедшим.
|
|
func (c *Config) ValidateTestHeaders(accepted []string, loginHeader string) error {
|
|
headers := c.HeaderSubstitution()
|
|
if len(headers) == 0 {
|
|
if len(c.Auth.TestHeaders) > 0 {
|
|
return fmt.Errorf(
|
|
"auth: секция %s заполнена, а предохранитель %s выключен: "+
|
|
"либо включите предохранитель, либо уберите имитацию",
|
|
testHeadersKey, debugKey,
|
|
)
|
|
}
|
|
|
|
// Включённый предохранитель при пустой имитации законен: сам по себе он
|
|
// ничего не включает.
|
|
return nil
|
|
}
|
|
|
|
// Два ключа, различающиеся только регистром, дали бы одно имя заголовка и
|
|
// одно значение — второе потерялось бы молча.
|
|
if len(headers) != len(c.Auth.TestHeaders) {
|
|
return fmt.Errorf(
|
|
"auth: в секции %s два ключа называют один заголовок: "+
|
|
"имя заголовка нечувствительно к регистру, и одно из значений потерялось бы молча",
|
|
testHeadersKey,
|
|
)
|
|
}
|
|
|
|
known := make(map[string]bool, len(accepted))
|
|
for _, name := range accepted {
|
|
known[textproto.CanonicalMIMEHeaderKey(name)] = true
|
|
}
|
|
|
|
unknown := make([]string, 0, len(headers))
|
|
for name := range headers {
|
|
if !known[name] {
|
|
unknown = append(unknown, name)
|
|
}
|
|
}
|
|
if len(unknown) > 0 {
|
|
// Порядок перебора карты свой у каждого прогона, а отказ старта читает
|
|
// человек: без сортировки один и тот же конфиг давал бы разный текст.
|
|
sort.Strings(unknown)
|
|
return fmt.Errorf(
|
|
"auth: секция %s называет заголовок, которого сервис не читает: %s; принимаются %s",
|
|
testHeadersKey, strings.Join(unknown, ", "), strings.Join(accepted, ", "),
|
|
)
|
|
}
|
|
|
|
login, named := headers[textproto.CanonicalMIMEHeaderKey(loginHeader)]
|
|
if !named {
|
|
return fmt.Errorf(
|
|
"auth: секция %s не называет ключа %s: сервис подставил бы всё прочее и не узнал бы никого",
|
|
testHeadersKey, loginHeader,
|
|
)
|
|
}
|
|
if _, ok := entity.AcceptProviderLogin(login); !ok {
|
|
return fmt.Errorf(
|
|
"auth: значение ключа %s в секции %s не годится в логин: "+
|
|
"пустое, из одних пробельных знаков, длиннее %d знаков либо с управляющими знаками",
|
|
loginHeader, testHeadersKey, entity.MaxProviderLoginLength,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|