package web
import (
"context"
"fmt"
"net"
"net/http"
"net/http/pprof"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/trim21/errgo"
"github.com/bangumi/server/config"
"github.com/bangumi/server/config/env"
"github.com/bangumi/server/internal/metrics"
"github.com/bangumi/server/internal/pkg/gtime"
"github.com/bangumi/server/internal/pkg/logger"
"github.com/bangumi/server/openapi"
"github.com/bangumi/server/web/mw/recovery"
"github.com/bangumi/server/web/req/cf"
)
const headerProcessTime = "x-process-time-ms"
const headerServerVersion = "x-server-version"
func New() *echo.Echo {
app := echo.New()
app.HTTPErrorHandler = getDefaultErrorHandler()
app.JSONSerializer = jsonSerializer{}
app.IPExtractor = func(request *http.Request) string {
ip := request.Header.Get(cf.HeaderRequestIP)
if ip == "" {
ra, _, _ := net.SplitHostPort(request.RemoteAddr)
return ra
}
return ip
}
if env.Development {
app.Static("/openapi/", "./openapi/")
app.GET("/debug", func(c *echo.Context) error {
return c.JSON(http.StatusOK, map[string]any{
"ip": c.RealIP(),
})
})
} else {
app.StaticFS("/openapi/", openapi.Static)
}
app.Use(recovery.New())
app.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
metrics.RequestCount.Inc()
start := time.Now()
c.Response().Header().Set(headerServerVersion, config.Version)
err := next(c)
sub := time.Since(start)
metrics.RequestHistogram.Observe(sub.Seconds())
c.Response().Header().Set(headerProcessTime, strconv.FormatInt(sub.Milliseconds(), 10))
return err
}
})
app.GET("/metrics", echo.WrapHandler(promhttp.Handler()))
app.GET("/debug/pprof/cmdline", echo.WrapHandler(http.HandlerFunc(pprof.Cmdline)))
app.GET("/debug/pprof/profile", echo.WrapHandler(http.HandlerFunc(pprof.Profile)))
app.GET("/debug/pprof/symbol", echo.WrapHandler(http.HandlerFunc(pprof.Symbol)))
app.GET("/debug/pprof/trace", echo.WrapHandler(http.HandlerFunc(pprof.Trace)))
app.GET("/debug/pprof/*", echo.WrapHandler(http.HandlerFunc(pprof.Index)))
if env.Development {
app.Use(genFakeRequestID)
}
app.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization},
ExposeHeaders: []string{headerProcessTime, headerServerVersion, cf.HeaderRequestID},
MaxAge: gtime.OneWeekSec,
}))
app.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
ctx, cancel := context.WithTimeout(context.WithoutCancel(c.Request().Context()), time.Minute)
defer cancel()
reqID := c.Request().Header.Get(cf.HeaderRequestID)
if reqID == "" {
reqID = uuid.Must(uuid.NewV7()).String()
}
c.SetRequest(c.Request().WithContext(context.WithValue(ctx, logger.RequestKey, &logger.RequestTrace{
IP: c.RealIP(),
ReqID: reqID,
Path: c.Request().RequestURI,
})))
return next(c)
}
})
app.GET("/openapi", func(c *echo.Context) error {
return c.Redirect(http.StatusFound, "/openapi/")
})
return app
}
func NewTestingApp() *echo.Echo {
app := echo.New()
app.HTTPErrorHandler = getDefaultErrorHandler()
app.Use(genFakeRequestID)
return app
}
func Start(c config.AppConfig, app *echo.Echo) error {
addr := c.ListenAddr()
logger.Infoln("http server listening at", addr)
if env.Development {
fmt.Printf("\nvisit http://%s/\n\n", strings.ReplaceAll(addr, "0.0.0.0", "127.0.0.1"))
}
return errgo.Wrap(app.Start(c.ListenAddr()), "echo.Start")
}