2023-05-20 20:15:57 +02:00
|
|
|
// Copyright 2023 wanderer <a_mirre at utb dot cz>
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2023-03-19 22:03:12 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2023-03-22 22:56:25 +01:00
|
|
|
"context"
|
2023-03-19 22:03:12 +01:00
|
|
|
"flag"
|
2023-04-13 00:07:08 +02:00
|
|
|
"fmt"
|
2023-03-22 22:13:23 +01:00
|
|
|
"net/http"
|
2023-03-22 22:56:25 +01:00
|
|
|
"os"
|
|
|
|
"os/signal"
|
2023-05-02 00:04:04 +02:00
|
|
|
"strconv"
|
2023-08-22 20:56:32 +02:00
|
|
|
"sync"
|
2023-04-13 00:07:08 +02:00
|
|
|
"syscall"
|
2023-03-22 22:56:25 +01:00
|
|
|
"time"
|
2023-03-19 22:03:12 +01:00
|
|
|
|
2023-05-09 17:35:00 +02:00
|
|
|
"golang.org/x/exp/slog"
|
2023-05-05 23:10:56 +02:00
|
|
|
// pure go postgres driver.
|
|
|
|
_ "github.com/lib/pq"
|
2023-04-13 00:07:08 +02:00
|
|
|
// ent pure go sqlite3 driver instead of "github.com/mattn/go-sqlite3".
|
|
|
|
_ "github.com/xiaoqidun/entps"
|
|
|
|
|
2023-03-22 22:13:23 +01:00
|
|
|
"git.dotya.ml/mirre-mt/pcmt/app"
|
2023-05-03 02:18:29 +02:00
|
|
|
"git.dotya.ml/mirre-mt/pcmt/app/settings"
|
2023-03-19 22:03:12 +01:00
|
|
|
"git.dotya.ml/mirre-mt/pcmt/config"
|
2023-04-13 00:07:08 +02:00
|
|
|
"git.dotya.ml/mirre-mt/pcmt/ent"
|
2023-05-05 22:52:59 +02:00
|
|
|
moddb "git.dotya.ml/mirre-mt/pcmt/modules/db"
|
2023-08-22 20:56:32 +02:00
|
|
|
"git.dotya.ml/mirre-mt/pcmt/modules/hibp"
|
2023-08-10 19:06:13 +02:00
|
|
|
"git.dotya.ml/mirre-mt/pcmt/modules/localbreach"
|
2023-04-19 05:30:52 +02:00
|
|
|
"git.dotya.ml/mirre-mt/pcmt/slogging"
|
2023-04-13 00:07:08 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2023-04-19 21:22:00 +02:00
|
|
|
banner = `
|
|
|
|
██████╗ ██████╗███╗ ███╗████████╗
|
|
|
|
██╔══██╗██╔════╝████╗ ████║╚══██╔══╝
|
|
|
|
██████╔╝██║ ██╔████╔██║ ██║
|
|
|
|
██╔═══╝ ██║ ██║╚██╔╝██║ ██║
|
|
|
|
██║ ╚██████╗██║ ╚═╝ ██║ ██║
|
|
|
|
╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝
|
|
|
|
`
|
|
|
|
slug = `Password Compromise Monitoring Tool
|
2023-07-29 23:37:30 +02:00
|
|
|
version: '%s'
|
2023-04-19 21:22:00 +02:00
|
|
|
https://git.dotya.ml/mirre-mt/pcmt
|
2023-04-19 23:36:12 +02:00
|
|
|
____________________________________`
|
2023-04-13 00:07:08 +02:00
|
|
|
|
|
|
|
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 <https://www.gnu.org/licenses/>.`
|
2023-03-19 22:03:12 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2023-08-13 17:00:46 +02:00
|
|
|
version = "dev"
|
2023-05-09 17:35:00 +02:00
|
|
|
// the global logger.
|
2023-05-11 17:06:20 +02:00
|
|
|
slogger *slogging.Slogger
|
2023-05-09 17:35:00 +02:00
|
|
|
// local logger instance.
|
2023-05-11 17:06:20 +02:00
|
|
|
log slogging.Slogger
|
2023-03-19 22:03:12 +01:00
|
|
|
)
|
|
|
|
|
2023-07-19 18:25:46 +02:00
|
|
|
func run() error { //nolint:gocognit
|
2023-04-13 00:07:08 +02:00
|
|
|
flag.Parse()
|
2023-03-22 22:13:23 +01:00
|
|
|
|
2023-08-12 14:57:56 +02:00
|
|
|
if *licenseFlag {
|
2023-04-19 23:36:12 +02:00
|
|
|
fmt.Fprintln(os.Stderr, licenseHeader)
|
2023-05-03 04:26:42 +02:00
|
|
|
return nil
|
2023-04-19 23:36:12 +02:00
|
|
|
}
|
|
|
|
|
2023-07-29 23:37:30 +02:00
|
|
|
if *versionFlag {
|
|
|
|
fmt.Fprintf(os.Stderr, "pcmt version %s\n", version)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-08-13 18:06:24 +02:00
|
|
|
// skip printing program header and some other stuff.
|
|
|
|
shortCircuit := false
|
2023-08-13 16:57:19 +02:00
|
|
|
|
2023-07-19 18:25:46 +02:00
|
|
|
var doingImport bool
|
|
|
|
|
|
|
|
if importFlag != nil && *importFlag != "" {
|
|
|
|
doingImport = true
|
2023-08-13 18:06:24 +02:00
|
|
|
shortCircuit = true
|
|
|
|
|
|
|
|
config.BeQuiet()
|
2023-08-13 16:57:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if *printMigrationFlag {
|
2023-08-13 18:06:24 +02:00
|
|
|
shortCircuit = true
|
|
|
|
|
|
|
|
config.BeQuiet()
|
2023-07-19 18:25:46 +02:00
|
|
|
}
|
|
|
|
|
2023-08-13 18:06:24 +02:00
|
|
|
if !shortCircuit {
|
2023-07-19 18:25:46 +02:00
|
|
|
printHeader()
|
|
|
|
}
|
2023-04-19 23:36:12 +02:00
|
|
|
|
2023-08-02 15:26:51 +02:00
|
|
|
slogger = slogging.Logger()
|
|
|
|
if slogger == nil {
|
|
|
|
slogger = slogging.Init(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
log = *slogger // local copy.
|
|
|
|
log.Logger = log.Logger.With(
|
|
|
|
// local attrs.
|
|
|
|
slog.Group("pcmt extra", slog.String("module", "run")),
|
|
|
|
)
|
|
|
|
|
2023-05-17 13:21:14 +02:00
|
|
|
// TODO: allow different configuration formats (toml, ini)
|
2023-04-13 00:07:08 +02:00
|
|
|
// TODO: rename main.go to pcmt.go
|
|
|
|
// TODO: add flake.nix
|
|
|
|
// TODO: SBOM: https://actuated.dev/blog/sbom-in-github-actions
|
|
|
|
// TODO: SBOM: https://www.docker.com/blog/generate-sboms-with-buildkit/
|
2023-05-17 13:21:14 +02:00
|
|
|
// TODO: integrate with Graylog (https://github.com/samber/slog-graylog).
|
2023-05-17 20:55:09 +02:00
|
|
|
// TODO: add mailer (https://github.com/wneessen/go-mail).
|
|
|
|
// TODO: deploy containers with podman using Ansible (https://www.redhat.com/sysadmin/automate-podman-ansible)
|
|
|
|
// TODO: add health checks for pod's containers (db, app)
|
2023-04-13 00:07:08 +02:00
|
|
|
|
2023-08-13 16:48:39 +02:00
|
|
|
// load the config if possible, an error could mean multiple things:
|
|
|
|
// * just couldn't read the config
|
|
|
|
// * parsed the config but:
|
|
|
|
// * the schema provided is too new for the app - keep the older schema or update the app
|
|
|
|
// * the schema provided is too old for the app - upgrade the config schema or downgrade the app
|
2023-05-13 19:44:32 +02:00
|
|
|
conf, err := config.Load(*configFlag, *configIsPathFlag)
|
2023-03-19 22:03:12 +01:00
|
|
|
if err != nil {
|
2023-05-21 18:59:12 +02:00
|
|
|
return fmt.Errorf("couldn't load the configuration (isPath: '%t') '%s', full err: %w",
|
|
|
|
*configIsPathFlag, *configFlag, err,
|
|
|
|
)
|
2023-03-22 22:13:23 +01:00
|
|
|
}
|
|
|
|
|
2023-05-05 23:10:56 +02:00
|
|
|
setting := settings.New()
|
|
|
|
|
2023-08-13 18:06:24 +02:00
|
|
|
if !shortCircuit {
|
|
|
|
setting.Consolidate(
|
|
|
|
conf, hostFlag, portFlag, develFlag, version,
|
|
|
|
)
|
|
|
|
}
|
2023-05-05 23:10:56 +02:00
|
|
|
|
|
|
|
// 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")
|
|
|
|
|
2023-05-17 13:35:13 +02:00
|
|
|
// check and bail early.
|
|
|
|
switch {
|
|
|
|
case connstr == "" || dbtype == "":
|
|
|
|
log.Errorf("PCMT_CONNSTRING or PCMT_DBTYPE or *both* were UNSET, bailing...")
|
|
|
|
return errDBNotConfigured
|
|
|
|
|
|
|
|
case dbtype != "postgres" && dbtype != "sqlite3":
|
|
|
|
log.Errorf("unsupported DB type specified, bailing...")
|
|
|
|
return errUnsupportedDBType
|
|
|
|
|
2023-05-17 20:40:24 +02:00
|
|
|
default:
|
|
|
|
setting.SetDbConnstring(connstr)
|
|
|
|
// type can be one of "postgres" or "sqlite3".
|
|
|
|
setting.SetDbType(dbtype)
|
|
|
|
}
|
2023-04-19 21:41:51 +02:00
|
|
|
|
2023-04-19 05:30:52 +02:00
|
|
|
log.Infof("connecting to db at '%s'", connstr)
|
2023-04-19 21:41:51 +02:00
|
|
|
|
2023-05-05 23:10:56 +02:00
|
|
|
db, err := ent.Open(setting.DbType(), setting.DbConnstring())
|
2023-04-13 00:07:08 +02:00
|
|
|
if err != nil {
|
2023-07-19 18:25:46 +02:00
|
|
|
if doingImport {
|
2023-08-10 19:06:13 +02:00
|
|
|
log.Error("Import was not possible", "error", err)
|
|
|
|
fmt.Fprintf(os.Stdout, "\033[31m✗\033[0m Import was not possible due to an error: %q\n", err)
|
|
|
|
|
|
|
|
return errImportFailed
|
2023-07-19 18:25:46 +02:00
|
|
|
}
|
|
|
|
|
2023-08-10 19:06:13 +02:00
|
|
|
log.Errorf("Error while connecting to DB: %q", err)
|
|
|
|
|
|
|
|
return errDBConnFailed
|
2023-04-13 00:07:08 +02:00
|
|
|
}
|
|
|
|
defer db.Close()
|
2023-03-22 22:13:23 +01:00
|
|
|
|
2023-05-09 17:35:00 +02:00
|
|
|
ctx := context.WithValue(context.Background(), moddb.CtxKey{}, slogger)
|
2023-05-05 22:52:59 +02:00
|
|
|
|
2023-08-13 16:57:19 +02:00
|
|
|
if *printMigrationFlag {
|
2023-08-24 18:43:24 +02:00
|
|
|
log.Debug("printing the upcoming migration to stdout")
|
2023-08-13 16:57:19 +02:00
|
|
|
return moddb.PrintMigration(ctx, db)
|
|
|
|
}
|
|
|
|
|
2023-05-21 18:50:41 +02:00
|
|
|
log.Info("ensuring the db is set up and attempting to automatically migrate db schema")
|
2023-05-05 22:52:59 +02:00
|
|
|
|
2023-05-21 18:50:41 +02:00
|
|
|
// make sure the database is set up and optionally creates an administrator
|
|
|
|
// user (only when setting up the db).
|
|
|
|
if err = moddb.SetUp(ctx, db, setting.InitCreateAdmin(), setting.InitAdminPassword()); err != nil {
|
2023-05-05 22:52:59 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-05-05 22:58:13 +02:00
|
|
|
setting.SetDbIsSetUp(true)
|
|
|
|
|
2023-08-10 19:06:13 +02:00
|
|
|
// don't load the app, return early.
|
|
|
|
if doingImport {
|
|
|
|
lb, err := localbreach.Load(*importFlag)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-08-20 05:20:33 +02:00
|
|
|
if _, err = localbreach.ImportLocalBreach(
|
|
|
|
context.WithValue(
|
|
|
|
context.Background(), localbreach.CtxKey{}, slogger),
|
|
|
|
db, lb,
|
|
|
|
); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-08-10 19:06:13 +02:00
|
|
|
log.Infof("Import succeeded: %+v", lb)
|
2023-08-20 05:20:33 +02:00
|
|
|
fmt.Fprint(os.Stdout, "\033[32m✓\033[0m Import succeeded \n")
|
2023-08-10 19:06:13 +02:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-04-13 00:07:08 +02:00
|
|
|
a := &app.App{}
|
2023-03-22 22:13:23 +01:00
|
|
|
|
2023-05-17 20:40:24 +02:00
|
|
|
if err = a.Init(setting, slogger, db); err != nil {
|
2023-03-22 22:13:23 +01:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-04-13 00:07:08 +02:00
|
|
|
a.PrintConfiguration()
|
2023-05-11 04:40:10 +02:00
|
|
|
a.SetEmbeds(templates, assets)
|
2023-05-31 22:42:50 +02:00
|
|
|
|
|
|
|
if err = a.SetupRoutes(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-08-11 23:18:54 +02:00
|
|
|
a.SetServerSettings()
|
2023-04-13 00:07:08 +02:00
|
|
|
|
2023-05-21 16:00:22 +02:00
|
|
|
if err = setting.EraseENVs(); err != nil {
|
|
|
|
log.Error("failed to erase PCMT ENVs")
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debug("erased PCMT ENVs")
|
|
|
|
|
2023-04-13 00:07:08 +02:00
|
|
|
e := a.E()
|
2023-05-02 00:04:04 +02:00
|
|
|
// channel used to check whether the app had troubles starting up.
|
|
|
|
started := make(chan error, 1)
|
|
|
|
|
2023-08-22 20:56:32 +02:00
|
|
|
go func() {
|
|
|
|
defer close(started)
|
2023-05-03 05:58:09 +02:00
|
|
|
|
2023-05-03 02:18:29 +02:00
|
|
|
p := setting.Port()
|
|
|
|
h := setting.Host()
|
2023-05-02 00:04:04 +02:00
|
|
|
|
|
|
|
address := h + ":" + strconv.Itoa(p)
|
|
|
|
|
2023-05-03 05:58:09 +02:00
|
|
|
if err := e.Start(address); err != nil && err != http.ErrServerClosed {
|
|
|
|
log.Error("troubles running the server, bailing...", "error", err)
|
2023-05-02 00:04:04 +02:00
|
|
|
|
2023-08-22 20:56:32 +02:00
|
|
|
started <- err
|
2023-05-02 00:04:04 +02:00
|
|
|
|
2023-05-03 05:58:09 +02:00
|
|
|
return
|
2023-03-22 22:56:25 +01:00
|
|
|
}
|
2023-05-03 04:58:47 +02:00
|
|
|
|
2023-08-22 20:56:32 +02:00
|
|
|
started <- nil
|
|
|
|
}()
|
2023-05-02 00:04:04 +02:00
|
|
|
|
2023-05-03 05:58:09 +02:00
|
|
|
quit := make(chan os.Signal, 1)
|
2023-08-22 20:56:32 +02:00
|
|
|
schedQuit := make(chan os.Signal, 1)
|
|
|
|
errCh := make(chan error, 1)
|
|
|
|
wg := &sync.WaitGroup{}
|
2023-05-03 05:58:09 +02:00
|
|
|
|
2023-08-22 20:56:32 +02:00
|
|
|
handleSigs(schedQuit, quit)
|
|
|
|
|
2023-08-24 18:43:24 +02:00
|
|
|
wg.Add(1)
|
|
|
|
go func() { // nolint:wsl
|
|
|
|
defer wg.Done() // for this goroutine
|
|
|
|
|
|
|
|
// this is for the scheduler goroutine.
|
2023-08-22 20:56:32 +02:00
|
|
|
wg.Add(1) // nolint:staticcheck
|
|
|
|
go hibp.RunReqScheduler(schedQuit, errCh, wg) // nolint:wsl
|
2023-08-24 18:43:24 +02:00
|
|
|
|
|
|
|
// TODO: pass a chan to the scheduler for reporting readiness.
|
|
|
|
log.Info("Giving HIBP requests scheduler time to start")
|
|
|
|
time.Sleep(time.Second)
|
|
|
|
|
|
|
|
if err = hibp.CheckSaveAllBreaches(
|
|
|
|
context.WithValue(context.Background(), hibp.CtxKey{}, slogger),
|
|
|
|
db); err != nil {
|
|
|
|
log.Error("failed to refresh HIBP db cache")
|
|
|
|
}
|
2023-08-22 20:56:32 +02:00
|
|
|
}()
|
2023-05-03 05:58:09 +02:00
|
|
|
|
2023-05-03 03:03:06 +02:00
|
|
|
// non-blocking channel receive.
|
|
|
|
select {
|
2023-08-22 20:56:32 +02:00
|
|
|
// monitor the server start-up chan.
|
2023-05-03 03:03:06 +02:00
|
|
|
case err := <-started:
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-08-22 20:56:32 +02:00
|
|
|
case err := <-errCh:
|
|
|
|
defer func() {
|
|
|
|
signal.Stop(quit)
|
|
|
|
|
|
|
|
close(quit)
|
|
|
|
close(errCh)
|
|
|
|
}()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return errHIBPSchedulerFailed
|
|
|
|
}
|
|
|
|
|
2023-05-03 05:58:09 +02:00
|
|
|
case <-quit:
|
2023-08-22 20:56:32 +02:00
|
|
|
// after the timeout the server forcefully quits.
|
2023-05-03 05:58:09 +02:00
|
|
|
shutdownTimeout := 10 * time.Second
|
2023-03-22 22:56:25 +01:00
|
|
|
|
2023-08-22 20:56:32 +02:00
|
|
|
log.Infof("Interrupt received, gracefully shutting down the server (timeout %s)", shutdownTimeout)
|
|
|
|
|
2023-05-03 05:58:09 +02:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
|
|
|
defer func() {
|
|
|
|
cancel()
|
2023-03-22 22:56:25 +01:00
|
|
|
|
2023-05-03 05:58:09 +02:00
|
|
|
signal.Stop(quit)
|
2023-04-19 21:41:51 +02:00
|
|
|
|
2023-05-03 05:58:09 +02:00
|
|
|
close(quit)
|
2023-08-22 20:56:32 +02:00
|
|
|
close(errCh)
|
2023-05-02 00:04:04 +02:00
|
|
|
|
2023-05-03 05:58:09 +02:00
|
|
|
log.Info("Bye!")
|
|
|
|
}()
|
2023-05-02 00:04:04 +02:00
|
|
|
|
2023-05-03 05:58:09 +02:00
|
|
|
if err = e.Shutdown(ctx); err != nil {
|
|
|
|
log.Error("There was an error shutting the server down")
|
|
|
|
return err
|
|
|
|
}
|
2023-08-22 20:56:32 +02:00
|
|
|
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
log.Error("Failed to stop the server in time, yolo")
|
|
|
|
return ctx.Err()
|
|
|
|
|
|
|
|
default:
|
|
|
|
wg.Wait()
|
|
|
|
}
|
2023-03-19 22:03:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2023-04-13 00:07:08 +02:00
|
|
|
|
2023-08-22 20:56:32 +02:00
|
|
|
// handleSigs configures given chans to be notified on os signals.
|
|
|
|
func handleSigs(chans ...chan os.Signal) {
|
|
|
|
sigs := []os.Signal{os.Interrupt, syscall.SIGHUP, syscall.SIGTERM}
|
|
|
|
|
|
|
|
for _, ch := range chans {
|
|
|
|
signal.Notify(ch, sigs...)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-13 00:07:08 +02:00
|
|
|
func printHeader() {
|
2023-07-29 23:37:30 +02:00
|
|
|
slug := fmt.Sprintf(slug, version)
|
|
|
|
|
2023-04-19 21:22:00 +02:00
|
|
|
fmt.Fprintf(os.Stderr,
|
2023-04-19 23:36:12 +02:00
|
|
|
"\033[34m%s%s\033[0m\n\n\n",
|
2023-04-19 21:22:00 +02:00
|
|
|
banner,
|
|
|
|
slug,
|
|
|
|
)
|
2023-04-13 00:07:08 +02:00
|
|
|
}
|