package server import ( "fmt" "io/ioutil" "net/http" "os" "os/signal" "strconv" "strings" "syscall" "time" "videoweb/app/appg" "videoweb/app/router" // "videoweb/app/service/configser" // "videoweb/app/service/translationser" "videoweb/pub/common/log" "videoweb/pub/common/srvversion" // "videoweb/pub/storage/proxy" // "videoweb/web/service" "gopkg.in/yaml.v2" ) func init() { time.Local, _ = time.LoadLocation("Asia/Shanghai") } func Run(args []string) { showVersion() if err := initConfig(); err != nil { log.Error("init config failed", log.E(err)) os.Exit(1) } initHttpServer() initDHServer() //tgbot.SendToProgram("启动", "app启动完毕") shutdownHandler() } func initConfig() error { buf, err := ioutil.ReadFile("conf/app.yaml") if err != nil { return err } if err = yaml.Unmarshal(buf, &appg.Conf); err != nil { return err } return nil } func showVersion() { log.Info(fmt.Sprintf("sevice version:%s", srvversion.SERVICE_VERSION)) log.Info(fmt.Sprintf("git version:%s", srvversion.GIT_VERSION)) log.Info(fmt.Sprintf("build date:%s", srvversion.BUILD_DATE)) log.Info(fmt.Sprintf("build ip:%s", srvversion.BUILD_IP)) } // headWriter 拦截 WriteHeader 和 Write,丢弃 body 并记录长度, // 在 flush 时补设 Content-Length 后再发送真正的状态码。 type headWriter struct { http.ResponseWriter contentLength int statusCode int wroteHeader bool } func (w *headWriter) WriteHeader(code int) { if !w.wroteHeader { w.statusCode = code w.wroteHeader = true } } func (w *headWriter) Write(b []byte) (int, error) { if !w.wroteHeader { w.statusCode = http.StatusOK w.wroteHeader = true } w.contentLength += len(b) return len(b), nil } // Flush 实现 http.Flusher,HEAD 响应无 body,做 no-op func (w *headWriter) Flush() {} // Unwrap 让 http.NewResponseController 能发现底层 writer 的其他接口 func (w *headWriter) Unwrap() http.ResponseWriter { return w.ResponseWriter } // flush 将缓存的 Content-Length 和状态码写入底层 ResponseWriter func (w *headWriter) flush() { if !w.wroteHeader { w.statusCode = http.StatusOK w.wroteHeader = true } if w.contentLength > 0 && w.Header().Get("Content-Length") == "" { w.Header().Set("Content-Length", strconv.Itoa(w.contentLength)) } w.ResponseWriter.WriteHeader(w.statusCode) } // headToGet 将 HEAD 改写为 GET 让 Gin 路由匹配,同时用 headWriter 丢弃 body。 // 通过 X-Origin-Head 内部 header 让 templatecache / filecache 等中间件识别出原始 HEAD 请求, // 从而避免一次性 URL 污染缓存造成 DoS 放大(进入前先 Del 防客户端伪造)。 func headToGet(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { r.Header.Del("X-Origin-Head") if r.Method == http.MethodHead { r.Method = http.MethodGet r.Header.Set("X-Origin-Head", "1") hw := &headWriter{ResponseWriter: w} h.ServeHTTP(hw, r) hw.flush() return } h.ServeHTTP(w, r) }) } func initHttpServer() { addr := fmt.Sprintf(":%s", appg.Conf.ListenAddr) srv := &http.Server{ Addr: addr, Handler: headToGet(router.Init()), } go func() { // service connections log.Info("服务已启动", log.Any("地址", addr)) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal("listen:", log.E(err)) } }() } func initDHServer() { if strings.TrimSpace(appg.Conf.DHListenAddr) == "" { return } addr := fmt.Sprintf(":%s", appg.Conf.DHListenAddr) srv := &http.Server{ Addr: addr, Handler: headToGet(router.DHInit()), } go func() { log.Info("导航服务已启动", log.Any("地址", addr)) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal("listen:", log.E(err)) } }() } func shutdownHandler() { quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Kill) <-quit log.Warn("Shutdown... server") }