parent
79a7cb3c1b
commit
99be9909a6
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
@ -50,10 +50,6 @@ SERVICES = (
|
||||||
8109, 9491, 'publisher.boulder',
|
8109, 9491, 'publisher.boulder',
|
||||||
('./bin/boulder', 'boulder-publisher', '--config', os.path.join(config_dir, 'publisher.json'), '--addr', ':9491', '--debug-addr', ':8109'),
|
('./bin/boulder', 'boulder-publisher', '--config', os.path.join(config_dir, 'publisher.json'), '--addr', ':9491', '--debug-addr', ':8109'),
|
||||||
None),
|
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',
|
Service('ocsp-responder',
|
||||||
8005, None, None,
|
8005, None, None,
|
||||||
('./bin/boulder', 'ocsp-responder', '--config', os.path.join(config_dir, 'ocsp-responder.json'), '--addr', ':4002', '--debug-addr', ':8005'),
|
('./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',
|
Service('bad-key-revoker',
|
||||||
8020, None, None,
|
8020, None, None,
|
||||||
('./bin/boulder', 'bad-key-revoker', '--config', os.path.join(config_dir, 'bad-key-revoker.json'), '--debug-addr', ':8020'),
|
('./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",
|
# Note: the nonce-service instances bind to specific ports, not "all interfaces",
|
||||||
# because they use their explicitly bound port in calculating the nonce
|
# because they use their explicitly bound port in calculating the nonce
|
||||||
# prefix, which is used by WFEs when deciding where to redeem nonces.
|
# prefix, which is used by WFEs when deciding where to redeem nonces.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue