- пришедшего называет заголовок Remote-User от прокси, и верят ему только с адреса из перечня trusted_proxies; своего входа у сервиса не осталось — ни корня /auth, ни кук, ни срока сессии, ни секрета клиента в конфиге и в базе - учётная запись заводится первым обращением: EnsureUser в пакете хранилища, шаг схемы 202608220001 с колонкой provider_login и снятыми правилами users - cmd/oidcstub заменён на cmd/devtools с подкомандой proxy; заодно закрыт унаследованный DL3066 — пользователь образа назван числом
127 lines
6.2 KiB
Go
127 lines
6.2 KiB
Go
// Command devtools — оснастка разработчика: то, что нужно для локального
|
|
// прогона и никогда не едет в боевой образ.
|
|
//
|
|
// Пакет один на все такие инструменты, а не по пакету на инструмент. Причина
|
|
// счётная: каждый отдельный пакет стоит четырёх мест — строка сборки образа,
|
|
// «Деплой» в устройстве, «Команды» в памятке, README, — и забытая строка сборки
|
|
// тихо кладёт инструмент разработчика в боевой образ. Один пакет платит эти
|
|
// четыре места **однажды**, сколько бы подкоманд в нём ни завелось.
|
|
//
|
|
// Сегодня подкоманда одна — `proxy`. Заведение владельца панели придёт
|
|
// подкомандой `admin` вместе с задачей о запуске одной командой.
|
|
//
|
|
// Имена заголовков берутся **константами транспорта**, а не литералами: они
|
|
// нормативны, и второй список разошёлся бы с первым молча — локальный вход
|
|
// перестал бы узнавать кого бы то ни было, а искали бы поломку в сервисе.
|
|
//
|
|
// Вывод идёт stdlib-логом в поток ошибок, а не `slog`: его читает человек в
|
|
// терминале, в сбор он не едет. Изъятие названо строкой в конвенции журнала.
|
|
//
|
|
// В образ пакет не едет: ступень сборки называет `./cmd/transcriber` поимённо.
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"os"
|
|
|
|
httpcontroller "git.vakhrushev.me/av/transcriber/internal/controller/http"
|
|
)
|
|
|
|
func main() {
|
|
log.SetFlags(0)
|
|
|
|
if len(os.Args) < 2 {
|
|
usage()
|
|
os.Exit(2)
|
|
}
|
|
|
|
switch os.Args[1] {
|
|
case "proxy":
|
|
runProxy(os.Args[2:])
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "неизвестная подкоманда: %s\n\n", os.Args[1])
|
|
usage()
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
func usage() {
|
|
fmt.Fprint(os.Stderr, `Оснастка разработчика.
|
|
|
|
Подкоманды:
|
|
proxy подставной обратный прокси: ставит заголовок и шлёт запрос сервису
|
|
|
|
`)
|
|
}
|
|
|
|
// runProxy поднимает подставной обратный прокси.
|
|
//
|
|
// Зачем он нужен: сервис узнаёт пришедшего по заголовку, который на сервере
|
|
// ставит Caddy, сходив к Authelia. На машине разработчика ни того ни другого
|
|
// нет, а браузер заголовков не ставит — значит приложение локально не
|
|
// открывалось бы вовсе. Прокси встаёт на их место: слушает свой порт, ставит
|
|
// заголовок и переправляет запрос сервису.
|
|
//
|
|
// Проверок он не делает никаких — ни пароля, ни группы, ни срока. Это его
|
|
// назначение, а не упущение: пускать он должен всякого, кто до него дошёл, а
|
|
// сам он слушает петлевой адрес.
|
|
func runProxy(args []string) {
|
|
flags := flag.NewFlagSet("proxy", flag.ExitOnError)
|
|
listen := flags.String("listen", "127.0.0.1:9000", "адрес, на котором слушать")
|
|
target := flags.String("target", "http://127.0.0.1:8080", "адрес сервиса")
|
|
login := flags.String("user", "dev", "логин, которым называть пришедшего")
|
|
name := flags.String("name", "Разработчик", "имя, пригодное к показу")
|
|
email := flags.String("email", "", "адрес почты; пустой не ставится вовсе")
|
|
if err := flags.Parse(args); err != nil {
|
|
os.Exit(2)
|
|
}
|
|
|
|
// Слушаем петлевой адрес по умолчанию, и это часть назначения: прокси
|
|
// пускает всякого, кто до него дошёл, и в чужой сети становится открытым
|
|
// входом в сервис.
|
|
upstream, err := url.Parse(*target)
|
|
if err != nil {
|
|
log.Fatalf("адрес сервиса не читается: %v", err)
|
|
}
|
|
|
|
proxy := &httputil.ReverseProxy{
|
|
Rewrite: func(r *httputil.ProxyRequest) {
|
|
r.SetURL(upstream)
|
|
|
|
// Именно Set, а не Add. Прокси, который **добавляет** заголовок к
|
|
// присланному, оставляет рядом со своим значением чужое — и сервис
|
|
// отвергает такой запрос целиком, потому что двух значений он не
|
|
// разбирает. Настоящий Caddy обязан делать то же самое, и это
|
|
// записано требованием к контуру в модели угроз.
|
|
r.Out.Header.Set(httpcontroller.LoginHeader, *login)
|
|
r.Out.Header.Set(httpcontroller.NameHeader, *name)
|
|
if *email != "" {
|
|
r.Out.Header.Set(httpcontroller.EmailHeader, *email)
|
|
} else {
|
|
r.Out.Header.Del(httpcontroller.EmailHeader)
|
|
}
|
|
},
|
|
}
|
|
|
|
log.Printf("подставной прокси: %s → %s, пришедший — %q", *listen, *target, *login)
|
|
log.Printf("приложение открывать по http://%s", *listen)
|
|
|
|
server := &http.Server{
|
|
Addr: *listen,
|
|
Handler: proxy,
|
|
// Таймаута чтения нет намеренно: сервис принимает записи на несколько
|
|
// часов, и прокси, обрывающий такую загрузку, ловил бы разработчика на
|
|
// поломке, которой в сервисе нет.
|
|
ReadTimeout: 0,
|
|
}
|
|
|
|
if err := server.ListenAndServe(); err != nil {
|
|
log.Fatalf("подставной прокси остановлен: %v", err)
|
|
}
|
|
}
|