2023-02-03 20:05:04 +01:00
|
|
|
// Copyright (C) 2023 Opsmate, Inc.
|
|
|
|
//
|
|
|
|
// This Source Code Form is subject to the terms of the Mozilla
|
|
|
|
// Public License, v. 2.0. If a copy of the MPL was not distributed
|
|
|
|
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
//
|
|
|
|
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
|
|
|
|
// See the Mozilla Public License for details.
|
|
|
|
|
|
|
|
package monitor
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
2023-02-20 16:02:48 +01:00
|
|
|
"errors"
|
2023-02-03 20:05:04 +01:00
|
|
|
"fmt"
|
2023-02-20 16:02:48 +01:00
|
|
|
"io/fs"
|
2023-02-03 20:05:04 +01:00
|
|
|
"os"
|
|
|
|
"os/exec"
|
2023-02-20 16:02:48 +01:00
|
|
|
"path/filepath"
|
2023-02-03 20:05:04 +01:00
|
|
|
"strings"
|
|
|
|
"sync"
|
2023-10-29 13:00:52 +01:00
|
|
|
"time"
|
2023-02-03 20:05:04 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
var stdoutMu sync.Mutex
|
|
|
|
|
|
|
|
type notification interface {
|
|
|
|
Environ() []string
|
2023-02-19 14:48:30 +01:00
|
|
|
Summary() string
|
2023-02-03 20:05:04 +01:00
|
|
|
Text() string
|
|
|
|
}
|
|
|
|
|
|
|
|
func notify(ctx context.Context, config *Config, notif notification) error {
|
|
|
|
if config.Stdout {
|
|
|
|
writeToStdout(notif)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(config.Email) > 0 {
|
2023-10-29 12:45:23 +01:00
|
|
|
if err := sendEmail(ctx, config.Email, notif); err != nil {
|
2023-02-03 20:05:04 +01:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if config.Script != "" {
|
|
|
|
if err := execScript(ctx, config.Script, notif); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-20 16:02:48 +01:00
|
|
|
if config.ScriptDir != "" {
|
|
|
|
if err := execScriptDir(ctx, config.ScriptDir, notif); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-03 20:05:04 +01:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func writeToStdout(notif notification) {
|
|
|
|
stdoutMu.Lock()
|
|
|
|
defer stdoutMu.Unlock()
|
|
|
|
os.Stdout.WriteString(notif.Text() + "\n")
|
|
|
|
}
|
|
|
|
|
2023-10-29 12:45:23 +01:00
|
|
|
func sendEmail(ctx context.Context, to []string, notif notification) error {
|
2023-02-03 20:05:04 +01:00
|
|
|
stdin := new(bytes.Buffer)
|
|
|
|
stderr := new(bytes.Buffer)
|
|
|
|
|
2023-10-29 12:45:23 +01:00
|
|
|
sendmailPath := "/usr/sbin/sendmail"
|
|
|
|
if envVar := os.Getenv("SENDMAIL_PATH"); envVar != "" {
|
|
|
|
sendmailPath = envVar
|
|
|
|
}
|
|
|
|
|
2023-02-03 20:05:04 +01:00
|
|
|
fmt.Fprintf(stdin, "To: %s\n", strings.Join(to, ", "))
|
2023-02-19 14:48:30 +01:00
|
|
|
fmt.Fprintf(stdin, "Subject: [certspotter] %s\n", notif.Summary())
|
2023-10-29 13:00:52 +01:00
|
|
|
fmt.Fprintf(stdin, "Date: %s\n", time.Now().Format(mailDateFormat))
|
|
|
|
fmt.Fprintf(stdin, "Message-ID: <%s>\n", generateMessageID())
|
2023-02-03 20:05:04 +01:00
|
|
|
fmt.Fprintf(stdin, "Mime-Version: 1.0\n")
|
|
|
|
fmt.Fprintf(stdin, "Content-Type: text/plain; charset=US-ASCII\n")
|
|
|
|
fmt.Fprintf(stdin, "X-Mailer: certspotter\n")
|
|
|
|
fmt.Fprintf(stdin, "\n")
|
|
|
|
fmt.Fprint(stdin, notif.Text())
|
|
|
|
|
|
|
|
args := []string{"-i", "--"}
|
|
|
|
args = append(args, to...)
|
|
|
|
|
2023-10-23 10:31:06 +02:00
|
|
|
sendmail := exec.CommandContext(ctx, sendmailPath, args...)
|
2023-02-03 20:05:04 +01:00
|
|
|
sendmail.Stdin = stdin
|
|
|
|
sendmail.Stderr = stderr
|
|
|
|
|
|
|
|
if err := sendmail.Run(); err == nil {
|
|
|
|
return nil
|
|
|
|
} else if ctx.Err() != nil {
|
|
|
|
return ctx.Err()
|
|
|
|
} else if exitErr, isExitError := err.(*exec.ExitError); isExitError && exitErr.Exited() {
|
|
|
|
return fmt.Errorf("error sending email to %v: sendmail failed with exit code %d and error %q", to, exitErr.ExitCode(), strings.TrimSpace(stderr.String()))
|
|
|
|
} else {
|
|
|
|
return fmt.Errorf("error sending email to %v: %w", to, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-03 20:35:26 +01:00
|
|
|
func execScript(ctx context.Context, scriptName string, notif notification) error {
|
2023-02-03 20:05:04 +01:00
|
|
|
stderr := new(bytes.Buffer)
|
|
|
|
|
2023-02-03 20:35:26 +01:00
|
|
|
cmd := exec.CommandContext(ctx, scriptName)
|
2023-02-03 20:05:04 +01:00
|
|
|
cmd.Env = os.Environ()
|
|
|
|
cmd.Env = append(cmd.Env, notif.Environ()...)
|
|
|
|
cmd.Stderr = stderr
|
|
|
|
|
|
|
|
if err := cmd.Run(); err == nil {
|
|
|
|
return nil
|
|
|
|
} else if ctx.Err() != nil {
|
|
|
|
return ctx.Err()
|
|
|
|
} else if exitErr, isExitError := err.(*exec.ExitError); isExitError && exitErr.Exited() {
|
2023-02-03 20:35:26 +01:00
|
|
|
return fmt.Errorf("script %q exited with code %d and error %q", scriptName, exitErr.ExitCode(), strings.TrimSpace(stderr.String()))
|
2023-02-03 20:05:04 +01:00
|
|
|
} else if isExitError {
|
2023-02-03 20:35:26 +01:00
|
|
|
return fmt.Errorf("script %q terminated by signal with error %q", scriptName, strings.TrimSpace(stderr.String()))
|
2023-02-03 20:05:04 +01:00
|
|
|
} else {
|
2023-02-03 20:38:02 +01:00
|
|
|
return fmt.Errorf("error executing script: %w", err)
|
2023-02-03 20:05:04 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-20 16:02:48 +01:00
|
|
|
func execScriptDir(ctx context.Context, dirPath string, notif notification) error {
|
|
|
|
dirents, err := os.ReadDir(dirPath)
|
|
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
|
|
return nil
|
|
|
|
} else if err != nil {
|
|
|
|
return fmt.Errorf("error executing scripts in directory %q: %w", dirPath, err)
|
|
|
|
}
|
|
|
|
for _, dirent := range dirents {
|
|
|
|
if strings.HasPrefix(dirent.Name(), ".") {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
scriptPath := filepath.Join(dirPath, dirent.Name())
|
|
|
|
info, err := os.Stat(scriptPath)
|
|
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
|
|
continue
|
|
|
|
} else if err != nil {
|
|
|
|
return fmt.Errorf("error executing %q in directory %q: %w", dirent.Name(), dirPath, err)
|
|
|
|
} else if info.Mode().IsRegular() && isExecutable(info.Mode()) {
|
|
|
|
if err := execScript(ctx, scriptPath, notif); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-02-03 20:05:04 +01:00
|
|
|
func isExecutable(mode os.FileMode) bool {
|
|
|
|
return mode&0111 != 0
|
|
|
|
}
|