212 lines
4.9 KiB
Go
212 lines
4.9 KiB
Go
package notmain
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/letsencrypt/boulder/cmd"
|
|
"github.com/letsencrypt/boulder/db"
|
|
blog "github.com/letsencrypt/boulder/log"
|
|
"github.com/letsencrypt/boulder/policy"
|
|
"github.com/letsencrypt/boulder/sa"
|
|
)
|
|
|
|
type contactAuditor struct {
|
|
db *db.WrappedMap
|
|
resultsFile *os.File
|
|
writeToStdout bool
|
|
logger blog.Logger
|
|
}
|
|
|
|
type result struct {
|
|
id int64
|
|
contacts []string
|
|
createdAt string
|
|
}
|
|
|
|
func unmarshalContact(contact []byte) ([]string, error) {
|
|
var contacts []string
|
|
err := json.Unmarshal(contact, &contacts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return contacts, nil
|
|
}
|
|
|
|
func validateContacts(id int64, createdAt string, contacts []string) error {
|
|
// Setup a buffer to store any validation problems we encounter.
|
|
var probsBuff strings.Builder
|
|
|
|
// Helper to write validation problems to our buffer.
|
|
writeProb := func(contact string, prob string) {
|
|
// Add validation problem to buffer.
|
|
fmt.Fprintf(&probsBuff, "%d\t%s\tvalidation\t%q\t%q\t%q\n", id, createdAt, contact, prob, contacts)
|
|
}
|
|
|
|
for _, contact := range contacts {
|
|
if strings.HasPrefix(contact, "mailto:") {
|
|
err := policy.ValidEmail(strings.TrimPrefix(contact, "mailto:"))
|
|
if err != nil {
|
|
writeProb(contact, err.Error())
|
|
}
|
|
} else {
|
|
writeProb(contact, "missing 'mailto:' prefix")
|
|
}
|
|
}
|
|
|
|
if probsBuff.Len() != 0 {
|
|
return errors.New(probsBuff.String())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// beginAuditQuery executes the audit query and returns a cursor used to
|
|
// stream the results.
|
|
func (c contactAuditor) beginAuditQuery(ctx context.Context) (*sql.Rows, error) {
|
|
rows, err := c.db.QueryContext(ctx, `
|
|
SELECT DISTINCT id, contact, createdAt
|
|
FROM registrations
|
|
WHERE contact NOT IN ('[]', 'null');`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
func (c contactAuditor) writeResults(result string) {
|
|
if c.writeToStdout {
|
|
_, err := fmt.Print(result)
|
|
if err != nil {
|
|
c.logger.Errf("Error while writing result to stdout: %s", err)
|
|
}
|
|
}
|
|
|
|
if c.resultsFile != nil {
|
|
_, err := c.resultsFile.WriteString(result)
|
|
if err != nil {
|
|
c.logger.Errf("Error while writing result to file: %s", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// run retrieves a cursor from `beginAuditQuery` and then audits the
|
|
// `contact` column of all returned rows for abnormalities or policy
|
|
// violations.
|
|
func (c contactAuditor) run(ctx context.Context, resChan chan *result) error {
|
|
c.logger.Infof("Beginning database query")
|
|
rows, err := c.beginAuditQuery(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for rows.Next() {
|
|
var id int64
|
|
var contact []byte
|
|
var createdAt string
|
|
err := rows.Scan(&id, &contact, &createdAt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
contacts, err := unmarshalContact(contact)
|
|
if err != nil {
|
|
c.writeResults(fmt.Sprintf("%d\t%s\tunmarshal\t%q\t%q\n", id, createdAt, contact, err))
|
|
}
|
|
|
|
err = validateContacts(id, createdAt, contacts)
|
|
if err != nil {
|
|
c.writeResults(err.Error())
|
|
}
|
|
|
|
// Only used for testing.
|
|
if resChan != nil {
|
|
resChan <- &result{id, contacts, createdAt}
|
|
}
|
|
}
|
|
// Ensure the query wasn't interrupted before it could complete.
|
|
err = rows.Close()
|
|
if err != nil {
|
|
return err
|
|
} else {
|
|
c.logger.Info("Query completed successfully")
|
|
}
|
|
|
|
// Only used for testing.
|
|
if resChan != nil {
|
|
close(resChan)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type Config struct {
|
|
ContactAuditor struct {
|
|
DB cmd.DBConfig
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
configFile := flag.String("config", "", "File containing a JSON config.")
|
|
writeToStdout := flag.Bool("to-stdout", false, "Print the audit results to stdout.")
|
|
writeToFile := flag.Bool("to-file", false, "Write the audit results to a file.")
|
|
flag.Parse()
|
|
|
|
logger := cmd.NewLogger(cmd.SyslogConfig{StdoutLevel: 7})
|
|
|
|
if *configFile == "" {
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Load config from JSON.
|
|
configData, err := os.ReadFile(*configFile)
|
|
cmd.FailOnError(err, fmt.Sprintf("Error reading config file: %q", *configFile))
|
|
|
|
var cfg Config
|
|
err = json.Unmarshal(configData, &cfg)
|
|
cmd.FailOnError(err, "Couldn't unmarshal config")
|
|
|
|
db, err := sa.InitWrappedDb(cfg.ContactAuditor.DB, nil, logger)
|
|
cmd.FailOnError(err, "Couldn't setup database client")
|
|
|
|
var resultsFile *os.File
|
|
if *writeToFile {
|
|
resultsFile, err = os.Create(
|
|
fmt.Sprintf("contact-audit-%s.tsv", time.Now().Format("2006-01-02T15:04")),
|
|
)
|
|
cmd.FailOnError(err, "Failed to create results file")
|
|
}
|
|
|
|
// Setup and run contact-auditor.
|
|
auditor := contactAuditor{
|
|
db: db,
|
|
resultsFile: resultsFile,
|
|
writeToStdout: *writeToStdout,
|
|
logger: logger,
|
|
}
|
|
|
|
logger.Info("Running contact-auditor")
|
|
|
|
err = auditor.run(context.TODO(), nil)
|
|
cmd.FailOnError(err, "Audit was interrupted, results may be incomplete")
|
|
|
|
logger.Info("Audit finished successfully")
|
|
|
|
if *writeToFile {
|
|
logger.Infof("Audit results were written to: %s", resultsFile.Name())
|
|
resultsFile.Close()
|
|
}
|
|
|
|
}
|
|
|
|
func init() {
|
|
cmd.RegisterCommand("contact-auditor", main, &cmd.ConfigValidator{Config: &Config{}})
|
|
}
|