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 }