package actions import ( "context" "io" "net/http" "os" "os/signal" "path/filepath" "strings" "time" "git.t-juice.club/torjus/gpaste" "git.t-juice.club/torjus/gpaste/api" "git.t-juice.club/torjus/gpaste/files" "git.t-juice.club/torjus/gpaste/users" "github.com/google/uuid" "github.com/urfave/cli/v2" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func ActionServe(c *cli.Context) error { configPath := "gpaste-server.toml" if c.IsSet("config") { configPath = c.String("config") } var ( cfg *gpaste.ServerConfig r io.ReadCloser ) r, err := os.Open(configPath) if err != nil { cfg = &gpaste.ServerConfig{ LogLevel: "INFO", URL: "localhost:8080", ListenAddr: ":8080", SigningSecret: "TODO: CHANGE THIS LOL", Store: &gpaste.ServerStoreConfig{ Type: "memory", }, } } else { defer r.Close() cfg, err = gpaste.ServerConfigFromReader(r) if err != nil { if err != nil { return cli.Exit(err, 1) } } } // Setup loggers rootLogger := getRootLogger(cfg.LogLevel) serverLogger := rootLogger.Named("SERV") accessLogger := rootLogger.Named("ACCS") // Setup contexts for clean shutdown rootCtx, rootCancel := signal.NotifyContext(context.Background(), os.Interrupt) defer rootCancel() httpCtx, httpCancel := context.WithCancel(rootCtx) defer httpCancel() httpShutdownCtx, httpShutdownCancel := context.WithCancel(context.Background()) defer httpShutdownCancel() // Setup stores // Files fileStore, fileClose, err := getFileStore(cfg) if err != nil { return err } defer fileClose() // nolint: errcheck // Users userStore, userClose, err := getUserStore(cfg) if err != nil { return err } defer userClose() // nolint: errcheck if userList, err := userStore.List(); err != nil { serverLogger.Panicw("Error checking userstore for users.", "error", err) } else if len(userList) < 1 { admin := users.User{ Username: "admin", Role: users.RoleAdmin, } password := uuid.NewString() if err := admin.SetPassword(password); err != nil { serverLogger.DPanic("Error setting admin-user password.", "error", err) } serverLogger.Warnw("Created admin-user.", "username", admin.Username, "password", password) } // Auth auth := gpaste.NewAuthService(userStore, []byte(cfg.SigningSecret)) go func() { srv := api.NewHTTPServer(cfg) srv.Users = userStore srv.Files = fileStore srv.Addr = cfg.ListenAddr srv.Logger = serverLogger srv.AccessLogger = accessLogger srv.Auth = auth // Wait for cancel go func() { <-httpCtx.Done() timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) // nolint: gomnd defer cancel() _ = srv.Shutdown(timeoutCtx) }() serverLogger.Infow("Starting HTTP server.", "addr", cfg.ListenAddr) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { serverLogger.Errorw("Error during shutdown.", "error", err) } serverLogger.Infow("HTTP server shutdown complete.", "addr", cfg.ListenAddr) httpShutdownCancel() }() <-httpShutdownCtx.Done() return nil } func getRootLogger(level string) *zap.SugaredLogger { logEncoderConfig := zap.NewProductionEncoderConfig() logEncoderConfig.EncodeCaller = zapcore.ShortCallerEncoder logEncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder logEncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder logEncoderConfig.EncodeDuration = zapcore.StringDurationEncoder rootLoggerConfig := &zap.Config{ Level: zap.NewAtomicLevelAt(zap.DebugLevel), OutputPaths: []string{"stdout"}, ErrorOutputPaths: []string{"stdout"}, Encoding: "console", EncoderConfig: logEncoderConfig, DisableCaller: true, } switch strings.ToUpper(level) { case "DEBUG": rootLoggerConfig.DisableCaller = false rootLoggerConfig.Level = zap.NewAtomicLevelAt(zap.DebugLevel) case "INFO": rootLoggerConfig.Level = zap.NewAtomicLevelAt(zap.InfoLevel) case "WARN", "WARNING": rootLoggerConfig.Level = zap.NewAtomicLevelAt(zap.WarnLevel) case "ERR", "ERROR": rootLoggerConfig.Level = zap.NewAtomicLevelAt(zap.ErrorLevel) } rootLogger, err := rootLoggerConfig.Build() if err != nil { panic(err) } return rootLogger.Sugar() } // nolint: ireturn func getUserStore(cfg *gpaste.ServerConfig) (users.UserStore, func() error, error) { closer := func() error { return nil } switch cfg.Store.Type { case "memory": return users.NewMemoryUserStore(), closer, nil case "fs": path := filepath.Join(cfg.Store.FS.Dir, "gpaste-users.db") bs, err := users.NewBoltUserStore(path) if err != nil { return nil, closer, cli.Exit("error setting up user store", 1) } return bs, bs.Close, nil default: return nil, closer, cli.Exit("no userstore configured", 1) } } // nolint: ireturn func getFileStore(cfg *gpaste.ServerConfig) (files.FileStore, func() error, error) { closer := func() error { return nil } switch cfg.Store.Type { case "memory": return files.NewMemoryFileStore(), closer, nil case "fs": var err error s, err := files.NewFSFileStore(cfg.Store.FS.Dir) if err != nil { return nil, closer, cli.Exit("error setting up filestore", 1) } return s, closer, nil default: return nil, closer, cli.Exit("No store configured", 1) } }