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
45 var probsBuff strings.Builder
46
47
48 writeProb := func(contact string, prob string) {
49
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
71
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
100
101
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
129 if resChan != nil {
130 resChan <- &result{id, contacts, createdAt}
131 }
132 }
133
134 err = rows.Close()
135 if err != nil {
136 return err
137 } else {
138 c.logger.Info("Query completed successfully")
139 }
140
141
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
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
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