1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package tests2021import (22 "bufio"23 "crypto/tls"24 "encoding/base64"25 "fmt"26 "io"27 "net"28 "path"29 "strconv"30 "strings"31 "time"32)3334// Conn is a helper that simplifies testing of text protocol interactions.35type Conn struct {36 T *T3738 WriteTimeout time.Duration39 ReadTimeout time.Duration4041 allowIOErr bool4243 Conn net.Conn44 Scanner *bufio.Scanner45}4647// AllowIOErr toggles whether I/O errors should be returned to the caller of48// 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 = ok53}5455// Write writes the string to the connection socket.56func (c *Conn) Write(s string) {57 c.T.Helper()5859 // Make sure the test will not accidentally hang waiting for I/O forever if60 // 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 }()6970 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}7576func (c *Conn) Writeln(s string) {77 c.T.Helper()7879 c.Write(s + "\r\n")80}8182func (c *Conn) Readln() (string, error) {83 c.T.Helper()8485 // Make sure the test will not accidentally hang waiting for I/O forever if86 // 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 }()9596 if !c.Scanner.Scan() {97 if err := c.Scanner.Err(); err != nil {98 if c.allowIOErr {99 return "", err100 }101 c.fatal("Unexpected I/O error: %v", err)102 }103 if c.allowIOErr {104 return "", io.EOF105 }106 c.fatal("Unexpected EOF")107 }108109 c.log('<', "%v", c.Scanner.Text())110111 return c.Scanner.Text(), nil112}113114func (c *Conn) Expect(line string) {115 c.T.Helper()116117 actual, err := c.Readln()118 if err != nil {119 c.T.Fatal("Unexpected I/O error:", err)120 }121122 if line != actual {123 c.T.Fatalf("Response line not matching the expected one, want %q", line)124 }125}126127// ExpectPattern reads a line from the connection socket and checks whether is128// matches the supplied shell pattern (as defined by path.Match). The original129// line is returned.130func (c *Conn) ExpectPattern(pat string) string {131 c.T.Helper()132133 line, err := c.Readln()134 if err != nil {135 c.T.Fatal("Unexpected I/O error:", err)136 }137138 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 }145146 return line147}148149func (c *Conn) fatal(f string, args ...interface{}) {150 c.T.Helper()151 c.log('-', f, args...)152 c.T.FailNow()153}154155func (c *Conn) log(direction rune, f string, args ...interface{}) {156 c.T.Helper()157158 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 }165166 msg.WriteRune(' ')167 msg.WriteRune(direction)168 msg.WriteRune(' ')169170 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 }180181 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}188189func (c *Conn) TLS() {190 c.T.Helper()191192 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 }199200 c.Conn = tlsC201 c.Scanner = bufio.NewScanner(c.Conn)202}203204func (c *Conn) SMTPPlainAuth(username, password string, expectOk bool) {205 c.T.Helper()206207 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}217218func (c *Conn) SMTPNegotation(ourName string, requireExts, blacklistExts []string) {219 c.T.Helper()220221 needCapsMap := make(map[string]bool)222 blacklistCapsMap := make(map[string]bool)223 for _, ext := range requireExts {224 needCapsMap[ext] = false225 }226 for _, ext := range blacklistExts {227 blacklistCapsMap[ext] = false228 }229230 c.Writeln("EHLO " + ourName)231232 // Consume the first line from socket, it is either initial greeting (sent233 // before we sent EHLO) or the EHLO reply in case of re-negotiation after234 // 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 }243244 var caps []string245capsloop:246 for {247 line, err := c.Readln()248 if err != nil {249 c.T.Fatal("I/O error during SMTP negotiation:", err)250 }251252 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 capsloop258 default:259 c.T.Fatal("Unexpected reply during SMTP negotiation:", line)260 }261 }262263 for _, ext := range caps {264 needCapsMap[ext] = true265 if _, ok := blacklistCapsMap[ext]; ok {266 blacklistCapsMap[ext] = true267 }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}280281func (c *Conn) Close() error {282 return c.Conn.Close()283}284285func (c *Conn) Rebind(subtest *T) *Conn {286 cpy := *c287 cpy.T = subtest288 return &cpy289}