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',
|
||||
('./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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue