Bound line reads with readLineLimited to prevent a peer from exhausting memory by withholding line terminators, wrap previously bare error returns for consistent context, surface XML decoder Skip errors, and replace the goto in the XMPP feature scan with a labeled break. New starttls_test.go exercises SMTP/IMAP/POP3/XMPP/LDAP success and not-advertised paths through net.Pipe-mocked servers.
86 lines
2.1 KiB
Go
86 lines
2.1 KiB
Go
package checker
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
)
|
|
|
|
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 := rw.WriteString("EHLO checker.happydomain.org\r\n"); 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
|
|
}
|