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 }