maddy

git clone git://git.lin.moe/fmaddy/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package tests
 20
 21import (
 22	"bufio"
 23	"crypto/tls"
 24	"encoding/base64"
 25	"fmt"
 26	"io"
 27	"net"
 28	"path"
 29	"strconv"
 30	"strings"
 31	"time"
 32)
 33
 34// Conn is a helper that simplifies testing of text protocol interactions.
 35type Conn struct {
 36	T *T
 37
 38	WriteTimeout time.Duration
 39	ReadTimeout  time.Duration
 40
 41	allowIOErr bool
 42
 43	Conn    net.Conn
 44	Scanner *bufio.Scanner
 45}
 46
 47// AllowIOErr toggles whether I/O errors should be returned to the caller of
 48// Conn method or should immedately fail the test.
 49//
 50// By default (ok = false), the latter happens.
 51func (c *Conn) AllowIOErr(ok bool) {
 52	c.allowIOErr = ok
 53}
 54
 55// Write writes the string to the connection socket.
 56func (c *Conn) Write(s string) {
 57	c.T.Helper()
 58
 59	// Make sure the test will not accidentally hang waiting for I/O forever if
 60	// the server breaks.
 61	if err := c.Conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout)); err != nil {
 62		c.fatal("Cannot set write deadline: %v", err)
 63	}
 64	defer func() {
 65		if err := c.Conn.SetWriteDeadline(time.Time{}); err != nil {
 66			c.log('-', "Failed to reset connection deadline: %v", err)
 67		}
 68	}()
 69
 70	c.log('>', "%s", s)
 71	if _, err := io.WriteString(c.Conn, s); err != nil {
 72		c.fatal("Unexpected I/O error: %v", err)
 73	}
 74}
 75
 76func (c *Conn) Writeln(s string) {
 77	c.T.Helper()
 78
 79	c.Write(s + "\r\n")
 80}
 81
 82func (c *Conn) Readln() (string, error) {
 83	c.T.Helper()
 84
 85	// Make sure the test will not accidentally hang waiting for I/O forever if
 86	// the server breaks.
 87	if err := c.Conn.SetReadDeadline(time.Now().Add(c.ReadTimeout)); err != nil {
 88		c.fatal("Cannot set write deadline: %v", err)
 89	}
 90	defer func() {
 91		if err := c.Conn.SetReadDeadline(time.Time{}); err != nil {
 92			c.log('-', "Failed to reset connection deadline: %v", err)
 93		}
 94	}()
 95
 96	if !c.Scanner.Scan() {
 97		if err := c.Scanner.Err(); err != nil {
 98			if c.allowIOErr {
 99				return "", err
100			}
101			c.fatal("Unexpected I/O error: %v", err)
102		}
103		if c.allowIOErr {
104			return "", io.EOF
105		}
106		c.fatal("Unexpected EOF")
107	}
108
109	c.log('<', "%v", c.Scanner.Text())
110
111	return c.Scanner.Text(), nil
112}
113
114func (c *Conn) Expect(line string) {
115	c.T.Helper()
116
117	actual, err := c.Readln()
118	if err != nil {
119		c.T.Fatal("Unexpected I/O error:", err)
120	}
121
122	if line != actual {
123		c.T.Fatalf("Response line not matching the expected one, want %q", line)
124	}
125}
126
127// ExpectPattern reads a line from the connection socket and checks whether is
128// matches the supplied shell pattern (as defined by path.Match). The original
129// line is returned.
130func (c *Conn) ExpectPattern(pat string) string {
131	c.T.Helper()
132
133	line, err := c.Readln()
134	if err != nil {
135		c.T.Fatal("Unexpected I/O error:", err)
136	}
137
138	match, err := path.Match(pat, line)
139	if err != nil {
140		c.T.Fatal("Malformed pattern:", err)
141	}
142	if !match {
143		c.T.Fatalf("Response line not matching the expected pattern, want %q", pat)
144	}
145
146	return line
147}
148
149func (c *Conn) fatal(f string, args ...interface{}) {
150	c.T.Helper()
151	c.log('-', f, args...)
152	c.T.FailNow()
153}
154
155func (c *Conn) log(direction rune, f string, args ...interface{}) {
156	c.T.Helper()
157
158	local, remote := c.Conn.LocalAddr().(*net.TCPAddr), c.Conn.RemoteAddr().(*net.TCPAddr)
159	msg := strings.Builder{}
160	if local.IP.IsLoopback() {
161		msg.WriteString(strconv.Itoa(local.Port))
162	} else {
163		msg.WriteString(local.String())
164	}
165
166	msg.WriteRune(' ')
167	msg.WriteRune(direction)
168	msg.WriteRune(' ')
169
170	if remote.IP.IsLoopback() {
171		textPort := c.T.portsRev[uint16(remote.Port)]
172		if textPort != "" {
173			msg.WriteString(textPort)
174		} else {
175			msg.WriteString(strconv.Itoa(remote.Port))
176		}
177	} else {
178		msg.WriteString(local.String())
179	}
180
181	if _, ok := c.Conn.(*tls.Conn); ok {
182		msg.WriteString(" [tls]")
183	}
184	msg.WriteString(": ")
185	fmt.Fprintf(&msg, f, args...)
186	c.T.Log(strings.TrimRight(msg.String(), "\r\n "))
187}
188
189func (c *Conn) TLS() {
190	c.T.Helper()
191
192	tlsC := tls.Client(c.Conn, &tls.Config{
193		ServerName:         "maddy.test",
194		InsecureSkipVerify: true,
195	})
196	if err := tlsC.Handshake(); err != nil {
197		c.fatal("TLS handshake fail: %v", err)
198	}
199
200	c.Conn = tlsC
201	c.Scanner = bufio.NewScanner(c.Conn)
202}
203
204func (c *Conn) SMTPPlainAuth(username, password string, expectOk bool) {
205	c.T.Helper()
206
207	resp := append([]byte{0x00}, username...)
208	resp = append(resp, 0x00)
209	resp = append(resp, password...)
210	c.Writeln("AUTH PLAIN " + base64.StdEncoding.EncodeToString(resp))
211	if expectOk {
212		c.ExpectPattern("235 *")
213	} else {
214		c.ExpectPattern("*")
215	}
216}
217
218func (c *Conn) SMTPNegotation(ourName string, requireExts, blacklistExts []string) {
219	c.T.Helper()
220
221	needCapsMap := make(map[string]bool)
222	blacklistCapsMap := make(map[string]bool)
223	for _, ext := range requireExts {
224		needCapsMap[ext] = false
225	}
226	for _, ext := range blacklistExts {
227		blacklistCapsMap[ext] = false
228	}
229
230	c.Writeln("EHLO " + ourName)
231
232	// Consume the first line from socket, it is either initial greeting (sent
233	// before we sent EHLO) or the EHLO reply in case of re-negotiation after
234	// STARTTLS.
235	l, err := c.Readln()
236	if err != nil {
237		c.T.Fatal("I/O error during SMTP negotiation:", err)
238	}
239	if strings.HasPrefix(l, "220") {
240		// That was initial greeting, consume one more line.
241		c.ExpectPattern("250-*")
242	}
243
244	var caps []string
245capsloop:
246	for {
247		line, err := c.Readln()
248		if err != nil {
249			c.T.Fatal("I/O error during SMTP negotiation:", err)
250		}
251
252		switch {
253		case strings.HasPrefix(line, "250-"):
254			caps = append(caps, strings.TrimPrefix(line, "250-"))
255		case strings.HasPrefix(line, "250 "):
256			caps = append(caps, strings.TrimPrefix(line, "250 "))
257			break capsloop
258		default:
259			c.T.Fatal("Unexpected reply during SMTP negotiation:", line)
260		}
261	}
262
263	for _, ext := range caps {
264		needCapsMap[ext] = true
265		if _, ok := blacklistCapsMap[ext]; ok {
266			blacklistCapsMap[ext] = true
267		}
268	}
269	for ext, status := range needCapsMap {
270		if !status {
271			c.T.Fatalf("Capability %v is missing but required", ext)
272		}
273	}
274	for ext, status := range blacklistCapsMap {
275		if status {
276			c.T.Fatalf("Capability %v is present but not allowed", ext)
277		}
278	}
279}
280
281func (c *Conn) Close() error {
282	return c.Conn.Close()
283}
284
285func (c *Conn) Rebind(subtest *T) *Conn {
286	cpy := *c
287	cpy.T = subtest
288	return &cpy
289}