maddy

Fork https://github.com/foxcpp/maddy

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

  1//go:build integration
  2// +build integration
  3
  4/*
  5Maddy Mail Server - Composable all-in-one email server.
  6Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  7
  8This program is free software: you can redistribute it and/or modify
  9it under the terms of the GNU General Public License as published by
 10the Free Software Foundation, either version 3 of the License, or
 11(at your option) any later version.
 12
 13This program is distributed in the hope that it will be useful,
 14but WITHOUT ANY WARRANTY; without even the implied warranty of
 15MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 16GNU General Public License for more details.
 17
 18You should have received a copy of the GNU General Public License
 19along with this program.  If not, see <https://www.gnu.org/licenses/>.
 20*/
 21
 22package tests_test
 23
 24import (
 25	"bufio"
 26	"errors"
 27	"flag"
 28	"io/ioutil"
 29	"os"
 30	"os/exec"
 31	"path/filepath"
 32	"strings"
 33	"syscall"
 34	"testing"
 35	"time"
 36
 37	"github.com/foxcpp/maddy/tests"
 38)
 39
 40var ChasquidExecutable string
 41
 42func init() {
 43	flag.StringVar(&ChasquidExecutable, "integration.chasquid", "chasquid", "path to chasquid executable for interop tests")
 44}
 45
 46const chasquidConf = `smtp_address: "127.0.0.2:44444"
 47submission_address: "127.0.0.1:44443"
 48
 49data_dir: "$ROOT"
 50mail_log_path: "/dev/null"
 51
 52dovecot_auth: true
 53dovecot_userdb_path: "$AUTH_CLIENT" # needs any Unix socket, not actually used
 54dovecot_client_path: "$AUTH_CLIENT"
 55`
 56
 57// RSA 1024, valid for *.example.invalid, 127.0.0.1, 127.0.0.2,, 127.0.0.3
 58// until Nov 18 17:13:45 2029 GMT.
 59const testServerCert = `-----BEGIN CERTIFICATE-----
 60MIICDzCCAXigAwIBAgIRAJ1x+qCW7L+Hs6sRU8BHmWkwDQYJKoZIhvcNAQELBQAw
 61EjEQMA4GA1UEChMHQWNtZSBDbzAeFw0xOTExMTgxNzEzNDVaFw0yOTExMTUxNzEz
 62NDVaMBIxEDAOBgNVBAoTB0FjbWUgQ28wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ
 63AoGBAPINKMyuu3AvzndLDS2/BroA+DRUcAhWPBxMxG1b1BkkHisAZWteKajKmwdO
 64O13N8HHBRPPOD56AAPLZGNxYLHn6nel7AiH8k40/xC5tDOthqA82+00fwJHDFCnW
 65oDLOLcO17HulPvfCSWfefc+uee4kajPa+47hutqZH2bGMTXhAgMBAAGjZTBjMA4G
 66A1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAA
 67MC4GA1UdEQQnMCWCESouZXhhbXBsZS5pbnZhbGlkhwR/AAABhwR/AAAChwR/AAAD
 68MA0GCSqGSIb3DQEBCwUAA4GBAGRn3C2NbwR4cyQmTRm5jcaqi1kAYyEu6U8Q9PJW
 69Q15BXMKUTx2lw//QScK9MH2JpKxDuzWDSvaxZMnTxgri2uiplqpe8ydsWj6Wl0q9
 702XMGJ9LIxTZk5+cyZP2uOolvmSP/q8VFTyk9Udl6KUZPQyoiiDq4rBFUIxUyb+bX
 71pHkR
 72-----END CERTIFICATE-----`
 73
 74const testServerKey = `-----BEGIN PRIVATE KEY-----
 75MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAPINKMyuu3AvzndL
 76DS2/BroA+DRUcAhWPBxMxG1b1BkkHisAZWteKajKmwdOO13N8HHBRPPOD56AAPLZ
 77GNxYLHn6nel7AiH8k40/xC5tDOthqA82+00fwJHDFCnWoDLOLcO17HulPvfCSWfe
 78fc+uee4kajPa+47hutqZH2bGMTXhAgMBAAECgYEAgPjSDH3uEdDnSlkLJJzskJ+D
 79oR58s3R/gvTElSCg2uSLzo3ffF4oBHAwOqxMpabdvz8j5mSdne7Gkp9qx72TtEG2
 80wt6uX1tZhm2UTAkInH8IQDthj98P8vAWQsS6HHEIMErsrW2CyUrAt/+o1BRg/hWW
 81zixA3CLTthhZTJkaUCECQQD5EM16UcTAKfhr3IZppgq+ZsAOMkeCl3XVV9gHo32i
 82DL6UFAb27BAYyjfcZB1fPou4RszX0Ryu9yU0P5qm6N47AkEA+MpdAPkaPziY0ok4
 83e9Tcee6P0mIR+/AHk9GliVX2P74DDoOHyMXOSRBwdb+z2tYjrdjkNEL1Txe+sHny
 84k/EukwJBAOBqlmqPwNNRPeiaRHZvSSD0XjqsbSirJl48D4gadPoNt66fOQNGAt8D
 85Xj/z6U9HgQdiq/IOFmVEhT5FzSh1jL8CQQD3Myth8iGQO84tM0c6U3CWfuHMqsEv
 860XnV+HNAmHdLMqOa4joi1dh4ZKs5dDdi828UJ/PnsbhI1FEWzLSpJvWdAkAkVWqf
 87AC/TvWvEZLA6Z5CllyNzZJ7XvtIaNOosxHDolyZ1HMWMlfEb2K2ZXWLy5foKPeoY
 88Xi3olS9rB0J+Rvjz
 89-----END PRIVATE KEY-----`
 90
 91func runChasquid(t *testing.T, authClientPath string) (string, *exec.Cmd) {
 92	tempDir := t.TempDir()
 93	t.Log("Using", tempDir)
 94
 95	chasquidConf := strings.NewReplacer(
 96		"$ROOT", tempDir,
 97		"$AUTH_CLIENT", authClientPath).Replace(chasquidConf)
 98	err := ioutil.WriteFile(filepath.Join(tempDir, "chasquid.conf"), []byte(chasquidConf), os.ModePerm)
 99	if err != nil {
100		t.Fatal(err)
101	}
102	if err := os.MkdirAll(filepath.Join(tempDir, "certs", "example.org"), os.ModePerm); err != nil {
103		t.Fatal(err)
104	}
105	err = ioutil.WriteFile(filepath.Join(tempDir, "certs", "example.org", "fullchain.pem"), []byte(testServerCert), os.ModePerm)
106	if err != nil {
107		t.Fatal(err)
108	}
109	err = ioutil.WriteFile(filepath.Join(tempDir, "certs", "example.org", "privkey.pem"), []byte(testServerKey), os.ModePerm)
110	if err != nil {
111		t.Fatal(err)
112	}
113	if err := os.MkdirAll(filepath.Join(tempDir, "domains", "example.org"), os.ModePerm); err != nil {
114		t.Fatal(err)
115	}
116
117	err = ioutil.WriteFile(filepath.Join(tempDir, "chasquid.conf"), []byte(chasquidConf), os.ModePerm)
118	if err != nil {
119		t.Fatal(err)
120	}
121
122	cmd := exec.Command(ChasquidExecutable, "-v=2", "-config_dir", tempDir)
123	t.Log("Launching", cmd.String())
124	stderr, err := cmd.StderrPipe()
125	if err != nil {
126		t.Fatal(err)
127	}
128
129	if err := cmd.Start(); err != nil {
130		t.Fatal(err)
131	}
132
133	ready := make(chan struct{}, 1)
134
135	go func() {
136		scnr := bufio.NewScanner(stderr)
137		for scnr.Scan() {
138			line := scnr.Text()
139
140			// One of messages printed near completing initialization.
141			if strings.Contains(line, "Loading certificates") {
142				time.Sleep(1 * time.Second)
143				ready <- struct{}{}
144			}
145
146			t.Log("chasquid:", line)
147		}
148		if err := scnr.Err(); err != nil {
149			t.Log("stderr I/O error:", err)
150		}
151	}()
152
153	<-ready
154
155	return tempDir, cmd
156}
157
158func cleanChasquid(t *testing.T, tempDir string, cmd *exec.Cmd) {
159	cmd.Process.Signal(syscall.SIGTERM)
160	os.RemoveAll(tempDir)
161}
162
163func TestSASLServerWithChasquid(tt *testing.T) {
164	tt.Parallel()
165
166	_, err := exec.LookPath(ChasquidExecutable)
167	if err != nil {
168		if errors.Is(err, exec.ErrNotFound) {
169			tt.Skip("No chasquid executable found, skipping interop. tests")
170		}
171		tt.Fatal(err)
172	}
173
174	t := tests.NewT(tt)
175	t.DNS(nil)
176	t.Port("smtp")
177	t.Config(`
178		dovecot_sasld unix://{env:TEST_STATE_DIR}/auth.sock {
179			auth pass_table static {
180				# tester@example.org:123456
181				entry tester@example.org "bcrypt:$2a$04$0SaXE/WOMBOfk5jyaKjo.OHkioRljdhMznLnYCg1nrksu9iLd51Ri"
182			}
183		}`)
184	t.Run(1)
185	defer t.Close()
186
187	chasquidDir, cmd := runChasquid(tt, filepath.Join(t.StateDir(), "auth.sock"))
188	defer cleanChasquid(tt, chasquidDir, cmd)
189
190	c := t.ConnUnnamed(44443)
191	defer c.Close()
192	c.SMTPNegotation("localhost", nil, nil)
193	c.Writeln("STARTTLS")
194	c.ExpectPattern("220 *")
195	c.TLS()
196	c.Writeln("AUTH PLAIN AHRlc3RAZXhhbXBsZS5vcmcAMTIzNDU2") // 0x00 test@example.org 0x00 123456 (invalid user)
197	c.ExpectPattern("535 *")
198	c.Writeln("AUTH PLAIN AHRlc3RlckBleGFtcGxlLm9yZwAxMjM0NQ==") // 0x00 tester 0x00 12345 (invalid password)
199	c.ExpectPattern("535 *")
200	c.Writeln("AUTH PLAIN AHRlc3RlckBleGFtcGxlLm9yZwAxMjM0NTY=") // 0x00 tester 0x00 123456
201	c.ExpectPattern("235 *")
202}