checker-tls/checker/starttls_smtp.go

91 lines
2.3 KiB
Go

package checker
import (
"bufio"
"fmt"
"net"
"strings"
)
// EHLOHostname is the hostname sent in the SMTP EHLO command during STARTTLS
// negotiation. Override it at startup (e.g. via -ldflags or programmatically)
// to match the identity of the host running the checker.
var EHLOHostname = "checker.localhost"
func init() {
registerStartTLS("smtp", starttlsSMTP)
registerStartTLS("submission", starttlsSMTP)
}
// starttlsSMTP implements ESMTP EHLO + STARTTLS (RFC 3207).
func starttlsSMTP(conn net.Conn, sni string) error {
rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
if err := readSMTPGreeting(rw.Reader); err != nil {
return fmt.Errorf("read greeting: %w", err)
}
if _, err := fmt.Fprintf(rw, "EHLO %s\r\n", EHLOHostname); err != nil {
return fmt.Errorf("write ehlo: %w", err)
}
if err := rw.Flush(); err != nil {
return fmt.Errorf("flush ehlo: %w", err)
}
lines, err := readSMTPResponse(rw.Reader)
if err != nil {
return fmt.Errorf("read ehlo: %w", err)
}
if !hasSTARTTLSExt(lines) {
return fmt.Errorf("%w: EHLO did not advertise STARTTLS", errStartTLSNotOffered)
}
if _, err := rw.WriteString("STARTTLS\r\n"); err != nil {
return fmt.Errorf("write starttls: %w", err)
}
if err := rw.Flush(); err != nil {
return fmt.Errorf("flush starttls: %w", err)
}
resp, err := readSMTPResponse(rw.Reader)
if err != nil {
return fmt.Errorf("read starttls: %w", err)
}
if len(resp) == 0 || !strings.HasPrefix(resp[0], "220") {
return fmt.Errorf("server refused STARTTLS: %s", strings.Join(resp, " / "))
}
return nil
}
func readSMTPGreeting(r *bufio.Reader) error {
_, err := readSMTPResponse(r)
return err
}
// readSMTPResponse reads one multi-line SMTP response (lines with "NNN-" are
// continuation, "NNN " terminates).
func readSMTPResponse(r *bufio.Reader) ([]string, error) {
var out []string
for {
line, err := readLineLimited(r)
if err != nil {
return out, err
}
line = strings.TrimRight(line, "\r\n")
out = append(out, line)
if len(line) < 4 || line[3] == ' ' {
return out, nil
}
}
}
func hasSTARTTLSExt(lines []string) bool {
for _, l := range lines {
if len(l) < 4 {
continue
}
rest := strings.ToUpper(strings.TrimSpace(l[4:]))
if rest == "STARTTLS" || strings.HasPrefix(rest, "STARTTLS ") {
return true
}
}
return false
}