176 lines
5.7 KiB
Go
176 lines
5.7 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"git.ilapage.cn/OPC/chorus/admin/app/admin/router"
|
|
"git.ilapage.cn/OPC/chorus/admin/app/chorus"
|
|
"git.ilapage.cn/OPC/chorus/admin/common/database"
|
|
common "git.ilapage.cn/OPC/chorus/admin/common/middleware"
|
|
ext "git.ilapage.cn/OPC/chorus/admin/config"
|
|
sharedconfig "git.ilapage.cn/OPC/chorus/internal/config"
|
|
safehttp "git.ilapage.cn/OPC/chorus/internal/platform/http"
|
|
platformstorage "git.ilapage.cn/OPC/chorus/internal/platform/storage"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/go-admin-team/go-admin-core/config/source/file"
|
|
"github.com/go-admin-team/go-admin-core/sdk"
|
|
coreapi "github.com/go-admin-team/go-admin-core/sdk/api"
|
|
coreconfig "github.com/go-admin-team/go-admin-core/sdk/config"
|
|
"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
|
"github.com/spf13/cobra"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var configPath string
|
|
|
|
var rootCmd = &cobra.Command{
|
|
Use: "chorus-admin",
|
|
SilenceUsage: true,
|
|
}
|
|
|
|
var serverCmd = &cobra.Command{
|
|
Use: "server",
|
|
Short: "Start the Chorus go-admin backend",
|
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
|
return setup(configPath)
|
|
},
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return serve()
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
serverCmd.Flags().StringVarP(&configPath, "config", "c", "", "go-admin settings file")
|
|
rootCmd.AddCommand(serverCmd)
|
|
}
|
|
|
|
func Execute() error {
|
|
return rootCmd.Execute()
|
|
}
|
|
|
|
func setup(path string) error {
|
|
if strings.TrimSpace(path) == "" {
|
|
return errors.New("--config is required; use config/settings.example.yml as a starting point")
|
|
}
|
|
coreconfig.ExtendConfig = &ext.ExtConfig
|
|
coreconfig.Setup(file.NewSource(file.WithPath(path)), database.Setup)
|
|
serviceConfig, err := serviceConfigFromEnvironment(os.LookupEnv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mediaStorage, err := platformstorage.NewLocalReader(ext.ExtConfig.Chorus.StorageRoot)
|
|
if err != nil {
|
|
return fmt.Errorf("configure read-only Chorus storage: %w", err)
|
|
}
|
|
serviceConfig.config.Storage = mediaStorage
|
|
chorus.Configure(func(db *gorm.DB) (*chorus.Service, error) {
|
|
return chorus.NewService(db, serviceConfig.config)
|
|
})
|
|
return nil
|
|
}
|
|
|
|
type configuredService struct {
|
|
config chorus.Config
|
|
}
|
|
|
|
func serviceConfigFromEnvironment(lookup func(string) (string, bool)) (configuredService, error) {
|
|
read := func(name string) string {
|
|
value, _ := lookup(name)
|
|
return strings.TrimSpace(value)
|
|
}
|
|
configured := configuredService{}
|
|
allowRaw := read("CHORUS_ADMIN_ALLOW_CONNECTIVITY_PROBES")
|
|
if allowRaw == "" || allowRaw == "0" {
|
|
return configured, nil
|
|
}
|
|
if allowRaw != "1" {
|
|
return configuredService{}, errors.New("CHORUS_ADMIN_ALLOW_CONNECTIVITY_PROBES must be 1 when set")
|
|
}
|
|
cooldown, err := positiveDuration(read("CHORUS_ADMIN_CONNECTIVITY_COOLDOWN_SECONDS"), time.Second)
|
|
if err != nil {
|
|
return configuredService{}, errors.New("CHORUS_ADMIN_CONNECTIVITY_COOLDOWN_SECONDS must be a positive integer")
|
|
}
|
|
timeout, err := positiveDuration(read("CHORUS_PROVIDER_HTTP_TIMEOUT_SECONDS"), time.Second)
|
|
if err != nil {
|
|
return configuredService{}, errors.New("CHORUS_PROVIDER_HTTP_TIMEOUT_SECONDS must be a positive integer")
|
|
}
|
|
maxResponse, err := positiveInt64(read("CHORUS_PROVIDER_MAX_RESPONSE_BYTES"))
|
|
if err != nil {
|
|
return configuredService{}, errors.New("CHORUS_PROVIDER_MAX_RESPONSE_BYTES must be a positive integer")
|
|
}
|
|
allowedPorts, err := sharedconfig.ParseProviderAllowedPorts(read("CHORUS_PROVIDER_ALLOWED_PORTS"))
|
|
if err != nil {
|
|
return configuredService{}, errors.New("CHORUS_PROVIDER_ALLOWED_PORTS must be a comma-separated list of ports")
|
|
}
|
|
client, err := safehttp.New(safehttp.Config{Timeout: timeout, MaxRedirects: 3, AllowedPorts: allowedPorts})
|
|
if err != nil {
|
|
return configuredService{}, errors.New("configure SSRF-safe connectivity client")
|
|
}
|
|
probe, err := chorus.NewLiveProbe(safehttp.NewProviderClient(client))
|
|
if err != nil {
|
|
return configuredService{}, errors.New("configure connectivity probe")
|
|
}
|
|
configured.config = chorus.Config{AllowConnectivityChecks: true, ConnectivityCooldown: cooldown, MaxResponseBytes: maxResponse, Probe: probe}
|
|
return configured, nil
|
|
}
|
|
|
|
func positiveDuration(raw string, unit time.Duration) (time.Duration, error) {
|
|
value, err := strconv.ParseInt(raw, 10, 64)
|
|
if err != nil || value <= 0 {
|
|
return 0, errors.New("invalid positive duration")
|
|
}
|
|
return time.Duration(value) * unit, nil
|
|
}
|
|
|
|
func positiveInt64(raw string) (int64, error) {
|
|
value, err := strconv.ParseInt(raw, 10, 64)
|
|
if err != nil || value <= 0 {
|
|
return 0, errors.New("invalid positive integer")
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func serve() error {
|
|
if coreconfig.ApplicationConfig.Mode == pkg.ModeProd.String() {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
}
|
|
engine := gin.New()
|
|
sdk.Runtime.SetEngine(engine)
|
|
engine.Use(common.Sentinel()).Use(common.RequestId(pkg.TrafficKey)).Use(coreapi.SetRequestLogger)
|
|
common.InitMiddleware(engine)
|
|
router.InitRouter()
|
|
|
|
server := &http.Server{
|
|
Addr: fmt.Sprintf("%s:%d", coreconfig.ApplicationConfig.Host, coreconfig.ApplicationConfig.Port),
|
|
Handler: engine,
|
|
ReadHeaderTimeout: 5 * time.Second,
|
|
ReadTimeout: time.Duration(coreconfig.ApplicationConfig.ReadTimeout) * time.Second,
|
|
WriteTimeout: time.Duration(coreconfig.ApplicationConfig.WriterTimeout) * time.Second,
|
|
IdleTimeout: 60 * time.Second,
|
|
}
|
|
shutdown, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
result := make(chan error, 1)
|
|
go func() { result <- server.ListenAndServe() }()
|
|
select {
|
|
case <-shutdown.Done():
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
return server.Shutdown(ctx)
|
|
case err := <-result:
|
|
if errors.Is(err, http.ErrServerClosed) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
}
|