...

Source file src/github.com/letsencrypt/boulder/test/mail-test-srv/main.go

Documentation: github.com/letsencrypt/boulder/test/mail-test-srv

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"crypto/tls"
     8  	"flag"
     9  	"fmt"
    10  	"log"
    11  	"net"
    12  	"net/http"
    13  	"net/mail"
    14  	"regexp"
    15  	"strings"
    16  	"sync"
    17  
    18  	"github.com/letsencrypt/boulder/cmd"
    19  	blog "github.com/letsencrypt/boulder/log"
    20  )
    21  
    22  type mailSrv struct {
    23  	closeFirst      uint
    24  	allReceivedMail []rcvdMail
    25  	allMailMutex    sync.Mutex
    26  	connNumber      uint
    27  	connNumberMutex sync.RWMutex
    28  	logger          blog.Logger
    29  }
    30  
    31  type rcvdMail struct {
    32  	From string
    33  	To   string
    34  	Mail string
    35  }
    36  
    37  func expectLine(buf *bufio.Reader, expected string) error {
    38  	line, _, err := buf.ReadLine()
    39  	if err != nil {
    40  		return fmt.Errorf("readline: %v", err)
    41  	}
    42  	if string(line) != expected {
    43  		return fmt.Errorf("Expected %s, got %s", expected, line)
    44  	}
    45  	return nil
    46  }
    47  
    48  var mailFromRegex = regexp.MustCompile(`^MAIL FROM:<(.*)>\s*BODY=8BITMIME\s*$`)
    49  var rcptToRegex = regexp.MustCompile(`^RCPT TO:<(.*)>\s*$`)
    50  var smtpErr501 = []byte("501 syntax error in parameters or arguments \r\n")
    51  var smtpOk250 = []byte("250 OK \r\n")
    52  
    53  func (srv *mailSrv) handleConn(conn net.Conn) {
    54  	defer conn.Close()
    55  	srv.connNumberMutex.Lock()
    56  	srv.connNumber++
    57  	srv.connNumberMutex.Unlock()
    58  	srv.logger.Infof("mail-test-srv: Got connection from %s", conn.RemoteAddr())
    59  
    60  	readBuf := bufio.NewReader(conn)
    61  	conn.Write([]byte("220 smtp.example.com ESMTP\r\n"))
    62  	err := expectLine(readBuf, "EHLO localhost")
    63  	if err != nil {
    64  		log.Printf("mail-test-srv: %s: %v\n", conn.RemoteAddr(), err)
    65  		return
    66  	}
    67  	conn.Write([]byte("250-PIPELINING\r\n"))
    68  	conn.Write([]byte("250-AUTH PLAIN LOGIN\r\n"))
    69  	conn.Write([]byte("250 8BITMIME\r\n"))
    70  	// This AUTH PLAIN is the output of: echo -en '\0cert-manager@example.com\0password' | base64
    71  	// Must match the mail configs for integration tests.
    72  	err = expectLine(readBuf, "AUTH PLAIN AGNlcnQtbWFuYWdlckBleGFtcGxlLmNvbQBwYXNzd29yZA==")
    73  	if err != nil {
    74  		log.Printf("mail-test-srv: %s: %v\n", conn.RemoteAddr(), err)
    75  		return
    76  	}
    77  	conn.Write([]byte("235 2.7.0 Authentication successful\r\n"))
    78  	srv.logger.Infof("mail-test-srv: Successful auth from %s", conn.RemoteAddr())
    79  
    80  	// necessary commands:
    81  	// MAIL RCPT DATA QUIT
    82  
    83  	var fromAddr string
    84  	var toAddr []string
    85  
    86  	clearState := func() {
    87  		fromAddr = ""
    88  		toAddr = nil
    89  	}
    90  
    91  	reader := bufio.NewScanner(readBuf)
    92  scan:
    93  	for reader.Scan() {
    94  		line := reader.Text()
    95  		cmdSplit := strings.SplitN(line, " ", 2)
    96  		cmd := cmdSplit[0]
    97  		switch cmd {
    98  		case "QUIT":
    99  			conn.Write([]byte("221 Bye \r\n"))
   100  			break scan
   101  		case "RSET":
   102  			clearState()
   103  			conn.Write(smtpOk250)
   104  		case "NOOP":
   105  			conn.Write(smtpOk250)
   106  		case "MAIL":
   107  			srv.connNumberMutex.RLock()
   108  			if srv.connNumber <= srv.closeFirst {
   109  				// Half of the time, close cleanly to simulate the server side closing
   110  				// unexpectedly.
   111  				if srv.connNumber%2 == 0 {
   112  					log.Printf(
   113  						"mail-test-srv: connection # %d < -closeFirst parameter %d, disconnecting client. Bye!\n",
   114  						srv.connNumber, srv.closeFirst)
   115  					clearState()
   116  					conn.Close()
   117  				} else {
   118  					// The rest of the time, simulate a stale connection timeout by sending
   119  					// a SMTP 421 message. This replicates the timeout/close from issue
   120  					// 2249 - https://github.com/letsencrypt/boulder/issues/2249
   121  					log.Printf(
   122  						"mail-test-srv: connection # %d < -closeFirst parameter %d, disconnecting with 421. Bye!\n",
   123  						srv.connNumber, srv.closeFirst)
   124  					clearState()
   125  					conn.Write([]byte("421 1.2.3 foo.bar.baz Error: timeout exceeded \r\n"))
   126  					conn.Close()
   127  				}
   128  			}
   129  			srv.connNumberMutex.RUnlock()
   130  			clearState()
   131  			matches := mailFromRegex.FindStringSubmatch(line)
   132  			if matches == nil {
   133  				log.Panicf("mail-test-srv: %s: MAIL FROM parse error\n", conn.RemoteAddr())
   134  			}
   135  			addr, err := mail.ParseAddress(matches[1])
   136  			if err != nil {
   137  				log.Panicf("mail-test-srv: %s: addr parse error: %v\n", conn.RemoteAddr(), err)
   138  			}
   139  			fromAddr = addr.Address
   140  			conn.Write(smtpOk250)
   141  		case "RCPT":
   142  			matches := rcptToRegex.FindStringSubmatch(line)
   143  			if matches == nil {
   144  				conn.Write(smtpErr501)
   145  				continue
   146  			}
   147  			addr, err := mail.ParseAddress(matches[1])
   148  			if err != nil {
   149  				log.Panicf("mail-test-srv: %s: addr parse error: %v\n", conn.RemoteAddr(), err)
   150  			}
   151  			toAddr = append(toAddr, addr.Address)
   152  			conn.Write(smtpOk250)
   153  		case "DATA":
   154  			conn.Write([]byte("354 Start mail input \r\n"))
   155  			var msgBuf bytes.Buffer
   156  
   157  			for reader.Scan() {
   158  				line := reader.Text()
   159  				msgBuf.WriteString(line)
   160  				msgBuf.WriteString("\r\n")
   161  				if strings.HasSuffix(msgBuf.String(), "\r\n.\r\n") {
   162  					break
   163  				}
   164  			}
   165  			if reader.Err() != nil {
   166  				log.Printf("mail-test-srv: read from %s: %v\n", conn.RemoteAddr(), reader.Err())
   167  				return
   168  			}
   169  
   170  			mailResult := rcvdMail{
   171  				From: fromAddr,
   172  				Mail: msgBuf.String(),
   173  			}
   174  			srv.allMailMutex.Lock()
   175  			for _, rcpt := range toAddr {
   176  				mailResult.To = rcpt
   177  				srv.allReceivedMail = append(srv.allReceivedMail, mailResult)
   178  				log.Printf("mail-test-srv: Got mail: %s -> %s\n", fromAddr, rcpt)
   179  			}
   180  			srv.allMailMutex.Unlock()
   181  			conn.Write([]byte("250 Got mail \r\n"))
   182  			clearState()
   183  		}
   184  	}
   185  	if reader.Err() != nil {
   186  		log.Printf("mail-test-srv: read from %s: %s\n", conn.RemoteAddr(), reader.Err())
   187  	}
   188  }
   189  
   190  func (srv *mailSrv) serveSMTP(ctx context.Context, l net.Listener) error {
   191  	for {
   192  		conn, err := l.Accept()
   193  		if err != nil {
   194  			// If the accept call returned an error because the listener has been
   195  			// closed, then the context should have been canceled too. In that case,
   196  			// ignore the error.
   197  			select {
   198  			case <-ctx.Done():
   199  				return nil
   200  			default:
   201  				return err
   202  			}
   203  		}
   204  		go srv.handleConn(conn)
   205  	}
   206  }
   207  
   208  func main() {
   209  	var listenAPI = flag.String("http", "0.0.0.0:9381", "http port to listen on")
   210  	var listenSMTP = flag.String("smtp", "0.0.0.0:9380", "smtp port to listen on")
   211  	var certFilename = flag.String("cert", "", "certificate to serve")
   212  	var privKeyFilename = flag.String("key", "", "private key for certificate")
   213  	var closeFirst = flag.Uint("closeFirst", 0, "close first n connections after MAIL for reconnection tests")
   214  
   215  	flag.Parse()
   216  
   217  	cert, err := tls.LoadX509KeyPair(*certFilename, *privKeyFilename)
   218  	if err != nil {
   219  		log.Fatal(err)
   220  	}
   221  	l, err := tls.Listen("tcp", *listenSMTP, &tls.Config{
   222  		Certificates: []tls.Certificate{cert},
   223  	})
   224  	if err != nil {
   225  		log.Fatalf("Couldn't bind %q for SMTP: %s", *listenSMTP, err)
   226  	}
   227  	defer l.Close()
   228  
   229  	srv := mailSrv{
   230  		closeFirst: *closeFirst,
   231  		logger:     cmd.NewLogger(cmd.SyslogConfig{StdoutLevel: 7}),
   232  	}
   233  
   234  	srv.setupHTTP(http.DefaultServeMux)
   235  	go func() {
   236  		// The gosec linter complains that timeouts cannot be set here. That's fine,
   237  		// because this is test-only code.
   238  		////nolint:gosec
   239  		err := http.ListenAndServe(*listenAPI, http.DefaultServeMux)
   240  		if err != nil {
   241  			log.Fatalln("Couldn't start HTTP server", err)
   242  		}
   243  	}()
   244  
   245  	ctx, cancel := context.WithCancel(context.Background())
   246  	defer cancel()
   247  
   248  	go cmd.FailOnError(srv.serveSMTP(ctx, l), "Failed to accept connection")
   249  
   250  	cmd.WaitForSignal()
   251  }
   252  

View as plain text