diff --git a/test/mail-test-srv/http.go b/test/mail-test-srv/http.go deleted file mode 100644 index 3b6fd916b..000000000 --- a/test/mail-test-srv/http.go +++ /dev/null @@ -1,111 +0,0 @@ -package main - -import ( - "fmt" - "io" - "log" - "net/http" - "strconv" - "strings" -) - -// filter filters mails based on the To: and From: fields. -// The zero value matches all mails. -type filter struct { - To string - From string -} - -func (f *filter) Match(m rcvdMail) bool { - if f.To != "" && f.To != m.To { - return false - } - if f.From != "" && f.From != m.From { - return false - } - return true -} - -/* -/count - number of mails -/count?to=foo@bar.com - number of mails for foo@bar.com -/count?from=service@test.org - number of mails sent by service@test.org -/clear - clear the mail list -/mail/0 - first mail -/mail/1 - second mail -/mail/0?to=foo@bar.com - first mail for foo@bar.com -/mail/1?to=foo@bar.com - second mail for foo@bar.com -/mail/1?to=foo@bar.com&from=service@test.org - second mail for foo@bar.com from service@test.org -*/ - -func (srv *mailSrv) setupHTTP(serveMux *http.ServeMux) { - serveMux.HandleFunc("/count", srv.httpCount) - serveMux.HandleFunc("/clear", srv.httpClear) - serveMux.Handle("/mail/", http.StripPrefix("/mail/", http.HandlerFunc(srv.httpGetMail))) -} - -func (srv *mailSrv) httpClear(w http.ResponseWriter, r *http.Request) { - if r.Method == "POST" { - srv.allMailMutex.Lock() - srv.allReceivedMail = nil - srv.allMailMutex.Unlock() - w.WriteHeader(200) - } else { - w.WriteHeader(405) - } -} - -func (srv *mailSrv) httpCount(w http.ResponseWriter, r *http.Request) { - count := 0 - srv.iterMail(extractFilter(r), func(m rcvdMail) bool { - count++ - return false - }) - fmt.Fprintf(w, "%d\n", count) -} - -func (srv *mailSrv) httpGetMail(w http.ResponseWriter, r *http.Request) { - mailNum, err := strconv.Atoi(strings.Trim(r.URL.Path, "/")) - if err != nil { - w.WriteHeader(400) - log.Println("mail-test-srv: bad request:", r.URL.Path, "-", err) - return - } - idx := 0 - found := srv.iterMail(extractFilter(r), func(m rcvdMail) bool { - if mailNum == idx { - printMail(w, m) - return true - } - idx++ - return false - }) - if !found { - w.WriteHeader(404) - } -} - -func extractFilter(r *http.Request) filter { - values := r.URL.Query() - return filter{To: values.Get("to"), From: values.Get("from")} -} - -func (srv *mailSrv) iterMail(f filter, cb func(rcvdMail) bool) bool { - srv.allMailMutex.Lock() - defer srv.allMailMutex.Unlock() - for _, v := range srv.allReceivedMail { - if !f.Match(v) { - continue - } - if cb(v) { - return true - } - } - return false -} - -func printMail(w io.Writer, mail rcvdMail) { - fmt.Fprintf(w, "FROM %s\n", mail.From) - fmt.Fprintf(w, "TO %s\n", mail.To) - fmt.Fprintf(w, "\n%s\n", mail.Mail) -} diff --git a/test/mail-test-srv/http_test.go b/test/mail-test-srv/http_test.go deleted file mode 100644 index 9bfb67742..000000000 --- a/test/mail-test-srv/http_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "io" - "net/http" - "net/http/httptest" - "strconv" - "strings" - "testing" -) - -func reqAndRecorder(t testing.TB, method, relativeUrl string, body io.Reader) (*httptest.ResponseRecorder, *http.Request) { - endURL := fmt.Sprintf("http://localhost:9381%s", relativeUrl) - r, err := http.NewRequest(method, endURL, body) - if err != nil { - t.Fatalf("could not construct request: %v", err) - } - return httptest.NewRecorder(), r -} - -func TestHTTPClear(t *testing.T) { - srv := mailSrv{} - w, r := reqAndRecorder(t, "POST", "/clear", nil) - srv.allReceivedMail = []rcvdMail{{}} - srv.httpClear(w, r) - if w.Code != 200 { - t.Errorf("expected 200, got %d", w.Code) - } - if len(srv.allReceivedMail) != 0 { - t.Error("/clear failed to clear mail buffer") - } - - w, r = reqAndRecorder(t, "GET", "/clear", nil) - srv.allReceivedMail = []rcvdMail{{}} - srv.httpClear(w, r) - if w.Code != 405 { - t.Errorf("expected 405, got %d", w.Code) - } - if len(srv.allReceivedMail) != 1 { - t.Error("GET /clear cleared the mail buffer") - } -} - -func TestHTTPCount(t *testing.T) { - srv := mailSrv{} - srv.allReceivedMail = []rcvdMail{ - {From: "a", To: "b"}, - {From: "a", To: "b"}, - {From: "a", To: "c"}, - {From: "c", To: "a"}, - {From: "c", To: "b"}, - } - - tests := []struct { - URL string - Count int - }{ - {URL: "/count", Count: 5}, - {URL: "/count?to=b", Count: 3}, - {URL: "/count?to=c", Count: 1}, - } - - var buf bytes.Buffer - for _, test := range tests { - w, r := reqAndRecorder(t, "GET", test.URL, nil) - buf.Reset() - w.Body = &buf - - srv.httpCount(w, r) - if w.Code != 200 { - t.Errorf("%s: expected 200, got %d", test.URL, w.Code) - } - n, err := strconv.Atoi(strings.TrimSpace(buf.String())) - if err != nil { - t.Errorf("%s: expected a number, got '%s'", test.URL, buf.String()) - } else if n != test.Count { - t.Errorf("%s: expected %d, got %d", test.URL, test.Count, n) - } - } -} diff --git a/test/mail-test-srv/main.go b/test/mail-test-srv/main.go deleted file mode 100644 index a7b5adf80..000000000 --- a/test/mail-test-srv/main.go +++ /dev/null @@ -1,248 +0,0 @@ -package main - -import ( - "bufio" - "bytes" - "context" - "crypto/tls" - "flag" - "fmt" - "log" - "net" - "net/http" - "net/mail" - "regexp" - "strings" - "sync" - - "github.com/letsencrypt/boulder/cmd" - blog "github.com/letsencrypt/boulder/log" -) - -type mailSrv struct { - closeFirst uint - allReceivedMail []rcvdMail - allMailMutex sync.Mutex - connNumber uint - connNumberMutex sync.RWMutex - logger blog.Logger -} - -type rcvdMail struct { - From string - To string - Mail string -} - -func expectLine(buf *bufio.Reader, expected string) error { - line, _, err := buf.ReadLine() - if err != nil { - return fmt.Errorf("readline: %v", err) - } - if string(line) != expected { - return fmt.Errorf("Expected %s, got %s", expected, line) - } - return nil -} - -var mailFromRegex = regexp.MustCompile(`^MAIL FROM:<(.*)>\s*BODY=8BITMIME\s*$`) -var rcptToRegex = regexp.MustCompile(`^RCPT TO:<(.*)>\s*$`) -var smtpErr501 = []byte("501 syntax error in parameters or arguments \r\n") -var smtpOk250 = []byte("250 OK \r\n") - -func (srv *mailSrv) handleConn(conn net.Conn) { - defer conn.Close() - srv.connNumberMutex.Lock() - srv.connNumber++ - srv.connNumberMutex.Unlock() - srv.logger.Infof("mail-test-srv: Got connection from %s", conn.RemoteAddr()) - - readBuf := bufio.NewReader(conn) - conn.Write([]byte("220 smtp.example.com ESMTP\r\n")) - err := expectLine(readBuf, "EHLO localhost") - if err != nil { - log.Printf("mail-test-srv: %s: %v\n", conn.RemoteAddr(), err) - return - } - conn.Write([]byte("250-PIPELINING\r\n")) - conn.Write([]byte("250-AUTH PLAIN LOGIN\r\n")) - conn.Write([]byte("250 8BITMIME\r\n")) - // This AUTH PLAIN is the output of: echo -en '\0cert-manager@example.com\0password' | base64 - // Must match the mail configs for integration tests. - err = expectLine(readBuf, "AUTH PLAIN AGNlcnQtbWFuYWdlckBleGFtcGxlLmNvbQBwYXNzd29yZA==") - if err != nil { - log.Printf("mail-test-srv: %s: %v\n", conn.RemoteAddr(), err) - return - } - conn.Write([]byte("235 2.7.0 Authentication successful\r\n")) - srv.logger.Infof("mail-test-srv: Successful auth from %s", conn.RemoteAddr()) - - // necessary commands: - // MAIL RCPT DATA QUIT - - var fromAddr string - var toAddr []string - - clearState := func() { - fromAddr = "" - toAddr = nil - } - - reader := bufio.NewScanner(readBuf) -scan: - for reader.Scan() { - line := reader.Text() - cmdSplit := strings.SplitN(line, " ", 2) - cmd := cmdSplit[0] - switch cmd { - case "QUIT": - conn.Write([]byte("221 Bye \r\n")) - break scan - case "RSET": - clearState() - conn.Write(smtpOk250) - case "NOOP": - conn.Write(smtpOk250) - case "MAIL": - srv.connNumberMutex.RLock() - if srv.connNumber <= srv.closeFirst { - // Half of the time, close cleanly to simulate the server side closing - // unexpectedly. - if srv.connNumber%2 == 0 { - log.Printf( - "mail-test-srv: connection # %d < -closeFirst parameter %d, disconnecting client. Bye!\n", - srv.connNumber, srv.closeFirst) - clearState() - conn.Close() - } else { - // The rest of the time, simulate a stale connection timeout by sending - // a SMTP 421 message. This replicates the timeout/close from issue - // 2249 - https://github.com/letsencrypt/boulder/issues/2249 - log.Printf( - "mail-test-srv: connection # %d < -closeFirst parameter %d, disconnecting with 421. Bye!\n", - srv.connNumber, srv.closeFirst) - clearState() - conn.Write([]byte("421 1.2.3 foo.bar.baz Error: timeout exceeded \r\n")) - conn.Close() - } - } - srv.connNumberMutex.RUnlock() - clearState() - matches := mailFromRegex.FindStringSubmatch(line) - if matches == nil { - log.Panicf("mail-test-srv: %s: MAIL FROM parse error\n", conn.RemoteAddr()) - } - addr, err := mail.ParseAddress(matches[1]) - if err != nil { - log.Panicf("mail-test-srv: %s: addr parse error: %v\n", conn.RemoteAddr(), err) - } - fromAddr = addr.Address - conn.Write(smtpOk250) - case "RCPT": - matches := rcptToRegex.FindStringSubmatch(line) - if matches == nil { - conn.Write(smtpErr501) - continue - } - addr, err := mail.ParseAddress(matches[1]) - if err != nil { - log.Panicf("mail-test-srv: %s: addr parse error: %v\n", conn.RemoteAddr(), err) - } - toAddr = append(toAddr, addr.Address) - conn.Write(smtpOk250) - case "DATA": - conn.Write([]byte("354 Start mail input \r\n")) - var msgBuf bytes.Buffer - - for reader.Scan() { - line := reader.Text() - msgBuf.WriteString(line) - msgBuf.WriteString("\r\n") - if strings.HasSuffix(msgBuf.String(), "\r\n.\r\n") { - break - } - } - if reader.Err() != nil { - log.Printf("mail-test-srv: read from %s: %v\n", conn.RemoteAddr(), reader.Err()) - return - } - - mailResult := rcvdMail{ - From: fromAddr, - Mail: msgBuf.String(), - } - srv.allMailMutex.Lock() - for _, rcpt := range toAddr { - mailResult.To = rcpt - srv.allReceivedMail = append(srv.allReceivedMail, mailResult) - log.Printf("mail-test-srv: Got mail: %s -> %s\n", fromAddr, rcpt) - } - srv.allMailMutex.Unlock() - conn.Write([]byte("250 Got mail \r\n")) - clearState() - } - } - if reader.Err() != nil { - log.Printf("mail-test-srv: read from %s: %s\n", conn.RemoteAddr(), reader.Err()) - } -} - -func (srv *mailSrv) serveSMTP(ctx context.Context, l net.Listener) error { - for { - conn, err := l.Accept() - if err != nil { - // If the accept call returned an error because the listener has been - // closed, then the context should have been canceled too. In that case, - // ignore the error. - select { - case <-ctx.Done(): - return nil - default: - return err - } - } - go srv.handleConn(conn) - } -} - -func main() { - var listenAPI = flag.String("http", "0.0.0.0:9381", "http port to listen on") - var listenSMTP = flag.String("smtp", "0.0.0.0:9380", "smtp port to listen on") - var certFilename = flag.String("cert", "", "certificate to serve") - var privKeyFilename = flag.String("key", "", "private key for certificate") - var closeFirst = flag.Uint("closeFirst", 0, "close first n connections after MAIL for reconnection tests") - - flag.Parse() - - cert, err := tls.LoadX509KeyPair(*certFilename, *privKeyFilename) - if err != nil { - log.Fatal(err) - } - l, err := tls.Listen("tcp", *listenSMTP, &tls.Config{ - Certificates: []tls.Certificate{cert}, - }) - if err != nil { - log.Fatalf("Couldn't bind %q for SMTP: %s", *listenSMTP, err) - } - defer l.Close() - - srv := mailSrv{ - closeFirst: *closeFirst, - logger: cmd.NewLogger(cmd.SyslogConfig{StdoutLevel: 7}), - } - - srv.setupHTTP(http.DefaultServeMux) - go func() { - err := http.ListenAndServe(*listenAPI, http.DefaultServeMux) //nolint: gosec // No request timeout is fine for test-only code. - if err != nil { - log.Fatalln("Couldn't start HTTP server", err) - } - }() - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - go cmd.FailOnError(srv.serveSMTP(ctx, l), "Failed to accept connection") - - cmd.WaitForSignal() -} diff --git a/test/startservers.py b/test/startservers.py index 4f4b508ba..4e2ce1b24 100644 --- a/test/startservers.py +++ b/test/startservers.py @@ -50,10 +50,6 @@ SERVICES = ( 8109, 9491, 'publisher.boulder', ('./bin/boulder', 'boulder-publisher', '--config', os.path.join(config_dir, 'publisher.json'), '--addr', ':9491', '--debug-addr', ':8109'), None), - Service('mail-test-srv', - 9380, None, None, - ('./bin/mail-test-srv', '--closeFirst', '5', '--cert', 'test/certs/ipki/localhost/cert.pem', '--key', 'test/certs/ipki/localhost/key.pem'), - None), Service('ocsp-responder', 8005, None, None, ('./bin/boulder', 'ocsp-responder', '--config', os.path.join(config_dir, 'ocsp-responder.json'), '--addr', ':4002', '--debug-addr', ':8005'), @@ -116,7 +112,7 @@ SERVICES = ( Service('bad-key-revoker', 8020, None, None, ('./bin/boulder', 'bad-key-revoker', '--config', os.path.join(config_dir, 'bad-key-revoker.json'), '--debug-addr', ':8020'), - ('boulder-ra-1', 'boulder-ra-2', 'mail-test-srv')), + ('boulder-ra-1', 'boulder-ra-2')), # Note: the nonce-service instances bind to specific ports, not "all interfaces", # because they use their explicitly bound port in calculating the nonce # prefix, which is used by WFEs when deciding where to redeem nonces.