- в конфиг добавлены секция [auth.test_headers] и предохранитель [server] debug: заголовки входа подставляет слой транспорта, второго процесса локальный запуск больше не требует - подкоманда devtools proxy удалена целиком: всё, ради чего её поднимали, делает сам сервис - адресного предохранителя нет по решению владельца — цена названа в ADR и в модели угроз
139 lines
8.1 KiB
Go
139 lines
8.1 KiB
Go
package http
|
||
|
||
import (
|
||
"log/slog"
|
||
"net/http"
|
||
"net/netip"
|
||
)
|
||
|
||
// SubstituteIdentityHeaders — слой отладочного запуска: ставит запросу заголовки
|
||
// входа значениями из настроек.
|
||
//
|
||
// # Зачем он есть
|
||
//
|
||
// В бою заголовки ставит обратный прокси, сходивший к провайдеру. На машине
|
||
// разработчика прокси нет, а браузер заголовков не ставит — и приложение
|
||
// локально не открылось бы вовсе. Слой встаёт на место контура **внутри самого
|
||
// сервиса**: второго процесса и второго порта для этого не нужно.
|
||
//
|
||
// # Чего слой не делает
|
||
//
|
||
// Он правит заголовки запроса и ничего больше: учётной записи не заводит,
|
||
// отказов не выдаёт, в контекст не пишет. Узнавание получает запрос,
|
||
// неотличимый от пришедшего с сервера, и отлаживается поэтому боевая ветка, а
|
||
// не её отладочный двойник.
|
||
//
|
||
// # Что ему запрещено
|
||
//
|
||
// Подстановка — это настройка, которой сервис называет пришедшего сам, никого
|
||
// не спросив. Ограничений поэтому три, и каждое отсекает свой способ отдать
|
||
// чужой архив:
|
||
//
|
||
// - пустой перечень значит «подставлять нечего», и слой отдаёт цепочку
|
||
// нетронутой: звено не встаёт в неё вовсе, а запрос доходит до узнавания
|
||
// тем же, каким пришёл. Судится пустота здесь, и представление у предиката
|
||
// «подставляем ли» одно на всех — непустота самого перечня. Иначе
|
||
// включённый предохранитель при пустой имитации срезал бы `Remote-Email` у
|
||
// запроса, пришедшего без `Remote-User`;
|
||
// - подставляется только при **отсутствии** `Remote-User`. Пришедший заголовок
|
||
// не подменяется и не дополняется: узнавание отвергает запрос с двумя
|
||
// значениями, и слой, дописавший второе, превратил бы законный отказ в
|
||
// отладочный проход;
|
||
// - подставляется только с адреса из перечня доверенных, и судит адрес та же
|
||
// функция, какой судит его узнавание. Второй сверщик разошёлся бы с первым
|
||
// молча: узнавание разворачивает IPv4 в оболочке IPv6.
|
||
//
|
||
// Подставляя, слой распоряжается **всей** тройкой заголовков входа: названные
|
||
// настройками ставит их значением, не названные удаляет. Запрос без
|
||
// `Remote-User`, но с `Remote-Email` иначе собрал бы личность из двух
|
||
// источников — логин свой, почта чужая.
|
||
func SubstituteIdentityHeaders(
|
||
headers map[string]string,
|
||
trusted []netip.Prefix,
|
||
logger *slog.Logger,
|
||
) func(http.Handler) http.Handler {
|
||
// Подставлять нечего: цепочка слоёв остаётся ровно той, что в бою, —
|
||
// следующее звено возвращается как есть, лишнего звена не появляется.
|
||
if len(headers) == 0 {
|
||
return func(next http.Handler) http.Handler { return next }
|
||
}
|
||
|
||
if logger == nil {
|
||
logger = slog.Default()
|
||
}
|
||
|
||
return func(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
// Решает **наличие** заголовка, а не его значение: пустое значение
|
||
// штатно шлёт прокси там, где никого не назвал, и подстановка на
|
||
// этом месте назвала бы человеком того, кому провайдер отказал.
|
||
if len(r.Header.Values(LoginHeader)) > 0 {
|
||
next.ServeHTTP(w, r)
|
||
return
|
||
}
|
||
|
||
peer, ok := peerAddress(r.RemoteAddr)
|
||
if !ok || !isTrusted(trusted, peer) {
|
||
// Своя строка, и уровень предупреждающий. Узнавание здесь
|
||
// молчит предупреждением: заголовка нет, и оно считает случай
|
||
// штатным. Без этой строки самый частый локальный отказ —
|
||
// браузер пришёл с `::1`, а перечень называет `127.0.0.1` — не
|
||
// отличим от поломки узнавания: человек видит отказ на всём
|
||
// приложении и ни одного следа о том, почему подстановки не
|
||
// было.
|
||
//
|
||
// Подставляемых значений в строке нет: логин — ключ к чужому
|
||
// архиву, и запрет печатать его действует на подставленное
|
||
// значение наравне с пришедшим.
|
||
logger.Warn("Identity headers are not substituted for an untrusted peer",
|
||
"http.peer_addr", r.RemoteAddr,
|
||
"capability", "access", "transport", "http")
|
||
next.ServeHTTP(w, r)
|
||
return
|
||
}
|
||
|
||
// Тройка берётся из своего единственного дома, а не перечисляется
|
||
// здесь заново.
|
||
for _, name := range IdentityHeaderNames() {
|
||
if value, named := headers[name]; named {
|
||
r.Header.Set(name, value)
|
||
} else {
|
||
r.Header.Del(name)
|
||
}
|
||
}
|
||
|
||
// Уровень `INFO`, и боевой журнал он не топит по построению: строка
|
||
// пишется только тогда, когда подстановка действительно работает, а
|
||
// в бою она выключена умолчанием предохранителя. Появляется строка
|
||
// ровно в одном месте — в локальном прогоне, где стоит на каждом
|
||
// запросе и где она и нужна: ею «узнан подстановкой» отличается от
|
||
// «узнан прокси». На отладочном уровне её не видел бы никто:
|
||
// настройки под уровень журнала у сервиса нет, и он зашит `INFO`.
|
||
//
|
||
// Подставленных значений в строке нет по той же причине, по какой их
|
||
// нет в строке отказа выше.
|
||
logger.Info("Identity headers substituted from configuration",
|
||
"http.peer_addr", r.RemoteAddr,
|
||
"capability", "access", "transport", "http")
|
||
|
||
next.ServeHTTP(w, r)
|
||
})
|
||
}
|
||
}
|
||
|
||
// SubstitutedHeaderNames называет заголовки, которые слой подставит, в порядке
|
||
// объявления тройки.
|
||
//
|
||
// Нужен строке журнала при старте: перебор карты дал бы порядок, меняющийся от
|
||
// запуска к запуску, а перечисление имён по месту завело бы третий список.
|
||
func SubstitutedHeaderNames(headers map[string]string) []string {
|
||
names := make([]string, 0, len(headers))
|
||
for _, name := range IdentityHeaderNames() {
|
||
if _, named := headers[name]; named {
|
||
names = append(names, name)
|
||
}
|
||
}
|
||
|
||
return names
|
||
}
|