package main import ( "context" "flag" "fmt" "net/http" "os" "os/signal" "strconv" "syscall" "time" "golang.org/x/exp/slog" // pure go postgres driver. _ "github.com/lib/pq" // ent pure go sqlite3 driver instead of "github.com/mattn/go-sqlite3". _ "github.com/xiaoqidun/entps" "git.dotya.ml/mirre-mt/pcmt/app" "git.dotya.ml/mirre-mt/pcmt/app/settings" "git.dotya.ml/mirre-mt/pcmt/config" "git.dotya.ml/mirre-mt/pcmt/ent" moddb "git.dotya.ml/mirre-mt/pcmt/modules/db" "git.dotya.ml/mirre-mt/pcmt/slogging" ) const ( banner = ` ██████╗ ██████╗███╗ ███╗████████╗ ██╔══██╗██╔════╝████╗ ████║╚══██╔══╝ ██████╔╝██║ ██╔████╔██║ ██║ ██╔═══╝ ██║ ██║╚██╔╝██║ ██║ ██║ ╚██████╗██║ ╚═╝ ██║ ██║ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ` slug = `Password Compromise Monitoring Tool https://git.dotya.ml/mirre-mt/pcmt ____________________________________` licenseHeader = `pcmt - Password Compromise Monitoring Tool Copyright (C) git.dotya.ml/wanderer This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation version 3 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see .` ) var ( host = flag.String("host", "unset", "host address to listen on") port = flag.Int("port", -1, "TCP port to listen on") configFlag = flag.String("config", "config.dhall", "Default path of the config file") configIsPathFlag = flag.Bool("configIsPath", true, "Whether the provided config is path or raw config") devel = flag.Bool("devel", false, "Run the application in dev mode, connect to a local browser-sync instance for hot-reloading") license = flag.Bool("license", false, "Print licensing information and exit") version = "dev" // the global logger. slogger *slogging.Slogger // local logger instance. log slogging.Slogger ) func run() error { flag.Parse() if *license { fmt.Fprintln(os.Stderr, licenseHeader) return nil } printHeader() // TODO: allow different configuration formats (toml, ni) // TODO: rename main.go to pcmt.go // TODO: add .golangci-lint // TODO: add flake.nix // TODO: connect to postgres // TODO: design user schemas, models. // TODO: SBOM: https://actuated.dev/blog/sbom-in-github-actions // TODO: SBOM: https://www.docker.com/blog/generate-sboms-with-buildkit/ conf, err := config.LoadConfig(*configFlag, *configIsPathFlag) if err != nil { return fmt.Errorf("couldn't load config file '%s', full err: %w", *configFlag, err) } setting := settings.New() setting.Consolidate( conf, host, port, devel, version, ) slogger = slogging.Logger() // init is performed in the config package. log = *slogger // local copy. log.Logger = log.Logger.With( // local attrs. slog.Group("pcmt extra", slog.String("module", "run")), ) // expected connstring form for "github.com/xiaoqidun/entps": // "file:ent?mode=memory&cache=shared&_fk=1" // and for the postgres driver "github.com/lib/pq": // "host=127.0.0.1 sslmode=disable port=5432 user=postgres dbname=postgres password=postgres". connstr := os.Getenv("PCMT_CONNSTRING") dbtype := os.Getenv("PCMT_DBTYPE") setting.SetDbConnstring(connstr) // type can be one of "postgres" or "sqlite3". setting.SetDbType(dbtype) log.Infof("connecting to db at '%s'", connstr) db, err := ent.Open(setting.DbType(), setting.DbConnstring()) if err != nil { return fmt.Errorf("failed to open a connection to sqlite: %v", err) } defer db.Close() ctx := context.WithValue(context.Background(), moddb.CtxKey{}, slogger) log.Info("making sure that the db is set up") err = moddb.SetUp(ctx, db) if err != nil { return err } log.Info("attempting to automatically migrate db schema") // Run the auto migration tool. if err = db.Schema.Create(context.Background()); err != nil { return fmt.Errorf("failed creating schema resources: %v", err) } setting.SetDbIsSetUp(true) a := &app.App{} err = a.Init(setting, slogger, db) if err != nil { return err } a.PrintConfiguration() a.SetEmbeds(templates, assets) a.SetupRoutes() a.SetEchoSettings() // a.SetConfig(conf) // // TODO: add check for prometheus config setting. // if true { // // import "github.com/labstack/echo-contrib/prometheus" // p := prometheus.NewPrometheus("echo", nil) // p.Use(e) // } e := a.E() // channel used to check whether the app had troubles starting up. started := make(chan error, 1) defer close(started) go func(ok chan error) { p := setting.Port() h := setting.Host() address := h + ":" + strconv.Itoa(p) if err := e.Start(address); err != nil && err != http.ErrServerClosed { log.Error("troubles running the server, bailing...", "error", err) started <- err return } started <- nil }(started) quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt) signal.Notify(quit, syscall.SIGTERM) signal.Notify(quit, syscall.SIGHUP) // non-blocking channel receive. select { case err := <-started: if err != nil { return err } case <-quit: shutdownTimeout := 10 * time.Second ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer func() { log.Infof("Interrupt received, gracefully shutting down the server (timeout %s)", shutdownTimeout) cancel() signal.Stop(quit) close(quit) log.Info("Bye!") }() if err = e.Shutdown(ctx); err != nil { log.Error("There was an error shutting the server down") return err } } return nil } func printHeader() { fmt.Fprintf(os.Stderr, "\033[34m%s%s\033[0m\n\n\n", banner, slug, ) }