...

Source file src/github.com/letsencrypt/boulder/cmd/contact-auditor/main.go

Documentation: github.com/letsencrypt/boulder/cmd/contact-auditor

     1  package notmain
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"encoding/json"
     7  	"errors"
     8  	"flag"
     9  	"fmt"
    10  	"os"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/letsencrypt/boulder/cmd"
    15  	"github.com/letsencrypt/boulder/db"
    16  	blog "github.com/letsencrypt/boulder/log"
    17  	"github.com/letsencrypt/boulder/policy"
    18  	"github.com/letsencrypt/boulder/sa"
    19  )
    20  
    21  type contactAuditor struct {
    22  	db            *db.WrappedMap
    23  	resultsFile   *os.File
    24  	writeToStdout bool
    25  	logger        blog.Logger
    26  }
    27  
    28  type result struct {
    29  	id        int64
    30  	contacts  []string
    31  	createdAt string
    32  }
    33  
    34  func unmarshalContact(contact []byte) ([]string, error) {
    35  	var contacts []string
    36  	err := json.Unmarshal(contact, &contacts)
    37  	if err != nil {
    38  		return nil, err
    39  	}
    40  	return contacts, nil
    41  }
    42  
    43  func validateContacts(id int64, createdAt string, contacts []string) error {
    44  	// Setup a buffer to store any validation problems we encounter.
    45  	var probsBuff strings.Builder
    46  
    47  	// Helper to write validation problems to our buffer.
    48  	writeProb := func(contact string, prob string) {
    49  		// Add validation problem to buffer.
    50  		fmt.Fprintf(&probsBuff, "%d\t%s\tvalidation\t%q\t%q\t%q\n", id, createdAt, contact, prob, contacts)
    51  	}
    52  
    53  	for _, contact := range contacts {
    54  		if strings.HasPrefix(contact, "mailto:") {
    55  			err := policy.ValidEmail(strings.TrimPrefix(contact, "mailto:"))
    56  			if err != nil {
    57  				writeProb(contact, err.Error())
    58  			}
    59  		} else {
    60  			writeProb(contact, "missing 'mailto:' prefix")
    61  		}
    62  	}
    63  
    64  	if probsBuff.Len() != 0 {
    65  		return errors.New(probsBuff.String())
    66  	}
    67  	return nil
    68  }
    69  
    70  // beginAuditQuery executes the audit query and returns a cursor used to
    71  // stream the results.
    72  func (c contactAuditor) beginAuditQuery(ctx context.Context) (*sql.Rows, error) {
    73  	rows, err := c.db.QueryContext(ctx, `
    74  		SELECT DISTINCT id, contact, createdAt
    75  		FROM registrations
    76  		WHERE contact NOT IN ('[]', 'null');`)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	return rows, nil
    81  }
    82  
    83  func (c contactAuditor) writeResults(result string) {
    84  	if c.writeToStdout {
    85  		_, err := fmt.Print(result)
    86  		if err != nil {
    87  			c.logger.Errf("Error while writing result to stdout: %s", err)
    88  		}
    89  	}
    90  
    91  	if c.resultsFile != nil {
    92  		_, err := c.resultsFile.WriteString(result)
    93  		if err != nil {
    94  			c.logger.Errf("Error while writing result to file: %s", err)
    95  		}
    96  	}
    97  }
    98  
    99  // run retrieves a cursor from `beginAuditQuery` and then audits the
   100  // `contact` column of all returned rows for abnormalities or policy
   101  // violations.
   102  func (c contactAuditor) run(ctx context.Context, resChan chan *result) error {
   103  	c.logger.Infof("Beginning database query")
   104  	rows, err := c.beginAuditQuery(ctx)
   105  	if err != nil {
   106  		return err
   107  	}
   108  
   109  	for rows.Next() {
   110  		var id int64
   111  		var contact []byte
   112  		var createdAt string
   113  		err := rows.Scan(&id, &contact, &createdAt)
   114  		if err != nil {
   115  			return err
   116  		}
   117  
   118  		contacts, err := unmarshalContact(contact)
   119  		if err != nil {
   120  			c.writeResults(fmt.Sprintf("%d\t%s\tunmarshal\t%q\t%q\n", id, createdAt, contact, err))
   121  		}
   122  
   123  		err = validateContacts(id, createdAt, contacts)
   124  		if err != nil {
   125  			c.writeResults(err.Error())
   126  		}
   127  
   128  		// Only used for testing.
   129  		if resChan != nil {
   130  			resChan <- &result{id, contacts, createdAt}
   131  		}
   132  	}
   133  	// Ensure the query wasn't interrupted before it could complete.
   134  	err = rows.Close()
   135  	if err != nil {
   136  		return err
   137  	} else {
   138  		c.logger.Info("Query completed successfully")
   139  	}
   140  
   141  	// Only used for testing.
   142  	if resChan != nil {
   143  		close(resChan)
   144  	}
   145  
   146  	return nil
   147  }
   148  
   149  type Config struct {
   150  	ContactAuditor struct {
   151  		DB cmd.DBConfig
   152  	}
   153  }
   154  
   155  func main() {
   156  	configFile := flag.String("config", "", "File containing a JSON config.")
   157  	writeToStdout := flag.Bool("to-stdout", false, "Print the audit results to stdout.")
   158  	writeToFile := flag.Bool("to-file", false, "Write the audit results to a file.")
   159  	flag.Parse()
   160  
   161  	logger := cmd.NewLogger(cmd.SyslogConfig{StdoutLevel: 7})
   162  	logger.Info(cmd.VersionString())
   163  
   164  	if *configFile == "" {
   165  		flag.Usage()
   166  		os.Exit(1)
   167  	}
   168  
   169  	// Load config from JSON.
   170  	configData, err := os.ReadFile(*configFile)
   171  	cmd.FailOnError(err, fmt.Sprintf("Error reading config file: %q", *configFile))
   172  
   173  	var cfg Config
   174  	err = json.Unmarshal(configData, &cfg)
   175  	cmd.FailOnError(err, "Couldn't unmarshal config")
   176  
   177  	db, err := sa.InitWrappedDb(cfg.ContactAuditor.DB, nil, logger)
   178  	cmd.FailOnError(err, "Couldn't setup database client")
   179  
   180  	var resultsFile *os.File
   181  	if *writeToFile {
   182  		resultsFile, err = os.Create(
   183  			fmt.Sprintf("contact-audit-%s.tsv", time.Now().Format("2006-01-02T15:04")),
   184  		)
   185  		cmd.FailOnError(err, "Failed to create results file")
   186  	}
   187  
   188  	// Setup and run contact-auditor.
   189  	auditor := contactAuditor{
   190  		db:            db,
   191  		resultsFile:   resultsFile,
   192  		writeToStdout: *writeToStdout,
   193  		logger:        logger,
   194  	}
   195  
   196  	logger.Info("Running contact-auditor")
   197  
   198  	err = auditor.run(context.TODO(), nil)
   199  	cmd.FailOnError(err, "Audit was interrupted, results may be incomplete")
   200  
   201  	logger.Info("Audit finished successfully")
   202  
   203  	if *writeToFile {
   204  		logger.Infof("Audit results were written to: %s", resultsFile.Name())
   205  		resultsFile.Close()
   206  	}
   207  
   208  }
   209  
   210  func init() {
   211  	cmd.RegisterCommand("contact-auditor", main, &cmd.ConfigValidator{Config: &Config{}})
   212  }
   213  

View as plain text