// Command devtools — оснастка разработчика: то, что нужно для локального // прогона и никогда не едет в боевой образ. // // Пакет один на все такие инструменты, а не по пакету на инструмент. Причина // счётная: каждый отдельный пакет стоит четырёх мест — строка сборки образа, // «Деплой» в устройстве, «Команды» в памятке, README, — и забытая строка сборки // тихо кладёт инструмент разработчика в боевой образ. Один пакет платит эти // четыре места **однажды**, сколько бы подкоманд в нём ни завелось. // // Подкоманд две: `proxy` — подставной обратный прокси, `resume` — возврат // остановленной записи в работу. Вторая встала на место панели владельца: // панели у сервиса больше нет, а экраны правки записи приносят отдельные задачи. // // Имена заголовков берутся **константами транспорта**, а не литералами: они // нормативны, и второй список разошёлся бы с первым молча — локальный вход // перестал бы узнавать кого бы то ни было, а искали бы поломку в сервисе. // // Вывод идёт 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:]) case "resume": runResume(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 подставной обратный прокси: ставит заголовок и шлёт запрос сервису resume вернуть остановленную запись в работу `) } // 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) } }