package nntp import ( "bufio" "compress/flate" "context" "crypto/tls" "encoding/base64" "errors" "fmt" "io" "net" "strconv" "strings" "sync" "time" "unicode/utf8" "golang.org/x/net/proxy" ) const ( defaultTimeout = 30 * time.Second maxResponseLine = 1 << 20 maxMultilineBytes = 64 << 20 maxMultilineLines = 1_000_000 maxPostBytes = 10 << 20 maxPostLineBytes = 998 ) var tls12AESGCMSuites = []uint16{ tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, } type DialConfig struct { Host string Port string UseTLS bool StartTLS bool InsecureSkipVerify bool Username string Password string SASLMechanism string UseCompression bool ProxyType string ProxyAddress string Timeout time.Duration tlsConfig *tls.Config } type GroupInfo struct { Name string Low int64 High int64 EstimatedPost int64 Posting string } type GroupStatus struct { Name string Count int64 Low int64 High int64 } type ArticleHeader struct { Number int64 Subject string From string Date string MessageID string References string Bytes int64 Lines int64 } type HeaderValue struct { Number int64 Value string } type GroupDescription struct { Name string Description string } type ServerDate struct { Date string Time string } type ResponseError struct { Code int Message string } func (e *ResponseError) Error() string { return fmt.Sprintf("NNTP response %d: %s", e.Code, e.Message) } type Client struct { mu sync.Mutex conn net.Conn reader *bufio.Reader writer *bufio.Writer timeout time.Duration closed bool } func Dial(ctx context.Context, cfg DialConfig) (*Client, error) { if err := validateDialConfig(cfg); err != nil { return nil, err } if cfg.UseCompression && cfg.UseTLS { return nil, errors.New("COMPRESS DEFLATE cannot be used with TLS") } timeout := cfg.Timeout if timeout <= 0 { timeout = defaultTimeout } target := net.JoinHostPort(cfg.Host, cfg.Port) netDialer := &net.Dialer{Timeout: timeout, KeepAlive: 30 * time.Second} var raw net.Conn var err error if strings.EqualFold(cfg.ProxyType, "SOCKS5") { proxyDialer, proxyErr := proxy.SOCKS5("tcp", cfg.ProxyAddress, nil, netDialer) if proxyErr != nil { return nil, fmt.Errorf("configure SOCKS5 proxy: %w", proxyErr) } raw, err = proxyDialer.Dial("tcp", target) } else { raw, err = netDialer.DialContext(ctx, "tcp", target) } if err != nil { return nil, fmt.Errorf("connect to NNTP server: %w", err) } conn := raw if cfg.UseTLS && !cfg.StartTLS { tlsConfig := makeTLSConfig(cfg) tlsConn := tls.Client(raw, tlsConfig) if err := tlsConn.HandshakeContext(ctx); err != nil { raw.Close() return nil, fmt.Errorf("complete TLS handshake: %w", err) } conn = tlsConn } client := &Client{ conn: conn, reader: bufio.NewReaderSize(conn, 64*1024), writer: bufio.NewWriterSize(conn, 64*1024), timeout: timeout, } if err := client.setDeadline(); err != nil { client.Close() return nil, err } code, message, err := client.readResponse() if err != nil { client.Close() return nil, fmt.Errorf("read NNTP greeting: %w", err) } if code != 200 && code != 201 { client.Close() return nil, &ResponseError{Code: code, Message: message} } if cfg.StartTLS { code, message, err = client.command("STARTTLS") if err != nil { client.Close() return nil, err } if code != 382 { client.Close() return nil, &ResponseError{Code: code, Message: message} } tlsConn := tls.Client(client.conn, makeTLSConfig(cfg)) if err := tlsConn.HandshakeContext(ctx); err != nil { _ = client.conn.Close() return nil, fmt.Errorf("complete STARTTLS handshake: %w", err) } client.conn = tlsConn client.reader = bufio.NewReaderSize(tlsConn, 64*1024) client.writer = bufio.NewWriterSize(tlsConn, 64*1024) if err := client.setDeadline(); err != nil { client.Close() return nil, err } } if cfg.Username != "" { if err := client.authenticateWithConfig(cfg); err != nil { client.Close() return nil, err } } if cfg.UseCompression { if err := client.enableCompression(); err != nil { client.Close() return nil, err } } if code, message, err = client.command("MODE READER"); err != nil { client.Close() return nil, err } if code != 200 && code != 201 && code != 500 && code != 501 { client.Close() return nil, &ResponseError{Code: code, Message: message} } _ = client.conn.SetDeadline(time.Time{}) return client, nil } func (c *Client) Capabilities() ([]string, error) { c.mu.Lock() defer c.mu.Unlock() lines, err := c.capabilitiesUnlocked() if responseCode(err) == 500 || responseCode(err) == 501 { return nil, nil } return lines, err } func (c *Client) capabilitiesUnlocked() ([]string, error) { return c.multilineCommand("CAPABILITIES", 101) } func (c *Client) Help() ([]string, error) { c.mu.Lock() defer c.mu.Unlock() return c.multilineCommand("HELP", 100) } func (c *Client) Date() (ServerDate, error) { c.mu.Lock() defer c.mu.Unlock() code, message, err := c.commandUnlocked("DATE") if err != nil { return ServerDate{}, err } if code != 111 { return ServerDate{}, &ResponseError{Code: code, Message: message} } fields := strings.Fields(message) if len(fields) < 2 { return ServerDate{}, errors.New("malformed DATE response") } return ServerDate{Date: fields[0], Time: fields[1]}, nil } func (c *Client) ListOverviewFormat() ([]string, error) { c.mu.Lock() defer c.mu.Unlock() return c.multilineCommand("LIST OVERVIEW.FMT", 215) } func (c *Client) ListNewsGroups(pattern string) ([]GroupDescription, error) { if strings.ContainsAny(pattern, "\r\n") { return nil, errors.New("invalid LIST NEWSGROUPS pattern") } command := "LIST NEWSGROUPS" if strings.TrimSpace(pattern) != "" { command += " " + strings.TrimSpace(pattern) } c.mu.Lock() defer c.mu.Unlock() lines, err := c.multilineCommand(command, 215) if err != nil { return nil, err } groups := make([]GroupDescription, 0, len(lines)) for _, line := range lines { fields := strings.SplitN(line, " ", 2) if len(fields) == 0 || !validAtom(fields[0]) { continue } description := "" if len(fields) == 2 { description = strings.TrimSpace(fields[1]) } groups = append(groups, GroupDescription{Name: fields[0], Description: description}) } return groups, nil } func (c *Client) NewGroups(date, clock, timezone string) ([]string, error) { if !validCommandValue(date) || !validCommandValue(clock) || !validCommandValue(timezone) { return nil, errors.New("invalid NEWGROUPS arguments") } c.mu.Lock() defer c.mu.Unlock() return c.multilineCommand("NEWGROUPS "+date+" "+clock+" "+timezone, 231) } func (c *Client) NewNews(groups, date, clock, timezone string) ([]string, error) { if !validCommandValue(groups) || !validCommandValue(date) || !validCommandValue(clock) || !validCommandValue(timezone) { return nil, errors.New("invalid NEWNEWS arguments") } c.mu.Lock() defer c.mu.Unlock() return c.multilineCommand("NEWNEWS "+groups+" "+date+" "+clock+" "+timezone, 230) } func (c *Client) ListGroup(group string) ([]int64, error) { if !validAtom(group) { return nil, errors.New("invalid newsgroup name") } c.mu.Lock() defer c.mu.Unlock() lines, err := c.multilineCommand("LISTGROUP "+group, 211) if err != nil { return nil, err } articles := make([]int64, 0, len(lines)) for _, line := range lines { for _, token := range strings.Fields(line) { parts := strings.SplitN(token, "-", 2) first, parseErr := strconv.ParseInt(parts[0], 10, 64) if parseErr != nil || first < 1 { continue } last := first if len(parts) == 2 { last, parseErr = strconv.ParseInt(parts[1], 10, 64) if parseErr != nil || last < first || last-first > 100000 { continue } } for number := first; number <= last; number++ { articles = append(articles, number) } } } return articles, nil } func (c *Client) Head(number int64) (string, error) { return c.singleArticlePart("HEAD", number, 221) } func (c *Client) Body(number int64) (string, error) { return c.singleArticlePart("BODY", number, 222) } func (c *Client) Stat(number int64) (string, error) { if number < 1 { return "", errors.New("invalid article number") } c.mu.Lock() defer c.mu.Unlock() code, message, err := c.commandUnlocked("STAT " + strconv.FormatInt(number, 10)) if err != nil { return "", err } if code != 223 { return "", &ResponseError{Code: code, Message: message} } return message, nil } func (c *Client) Header(field string, first, last int64) ([]HeaderValue, error) { if !validAtom(field) || first < 1 || last < first || last-first > 10000 { return nil, errors.New("invalid HDR request") } c.mu.Lock() defer c.mu.Unlock() lines, err := c.multilineCommand("HDR "+field+" "+strconv.FormatInt(first, 10)+"-"+strconv.FormatInt(last, 10), 225) if err != nil { return nil, err } values := make([]HeaderValue, 0, len(lines)) for _, line := range lines { fields := strings.SplitN(line, "\t", 2) if len(fields) != 2 { continue } number, parseErr := strconv.ParseInt(fields[0], 10, 64) if parseErr != nil || number < 1 { continue } values = append(values, HeaderValue{Number: number, Value: fields[1]}) } return values, nil } func (c *Client) singleArticlePart(command string, number int64, expected int) (string, error) { if number < 1 { return "", errors.New("invalid article number") } c.mu.Lock() defer c.mu.Unlock() lines, err := c.multilineCommand(command+" "+strconv.FormatInt(number, 10), expected) if err != nil { return "", err } return strings.Join(lines, "\n"), nil } func (c *Client) ListActive() ([]GroupInfo, error) { c.mu.Lock() defer c.mu.Unlock() lines, err := c.multilineCommand("LIST ACTIVE", 215) if err != nil { return nil, err } groups := make([]GroupInfo, 0, len(lines)) for _, line := range lines { fields := strings.Fields(line) if len(fields) < 4 || !validAtom(fields[0]) { continue } high, highErr := strconv.ParseInt(fields[1], 10, 64) low, lowErr := strconv.ParseInt(fields[2], 10, 64) if highErr != nil || lowErr != nil || high < 0 || low < 0 { continue } groups = append(groups, GroupInfo{ Name: fields[0], Low: low, High: high, EstimatedPost: estimatePopulation(low, high), Posting: fields[3], }) } return groups, nil } func (c *Client) SelectGroup(group string) (GroupStatus, error) { if !validAtom(group) { return GroupStatus{}, errors.New("invalid newsgroup name") } c.mu.Lock() defer c.mu.Unlock() return c.selectGroupUnlocked(group) } func (c *Client) selectGroupUnlocked(group string) (GroupStatus, error) { code, message, err := c.commandUnlocked("GROUP " + group) if err != nil { return GroupStatus{}, err } if code != 211 { return GroupStatus{}, &ResponseError{Code: code, Message: message} } fields := strings.Fields(message) if len(fields) < 4 { return GroupStatus{}, errors.New("malformed GROUP response") } count, err := strconv.ParseInt(fields[0], 10, 64) if err != nil { return GroupStatus{}, errors.New("malformed GROUP article count") } low, err := strconv.ParseInt(fields[1], 10, 64) if err != nil { return GroupStatus{}, errors.New("malformed GROUP low article number") } high, err := strconv.ParseInt(fields[2], 10, 64) if err != nil { return GroupStatus{}, errors.New("malformed GROUP high article number") } return GroupStatus{Name: fields[3], Count: count, Low: low, High: high}, nil } func (c *Client) Overview(first, last int64) ([]ArticleHeader, error) { if first < 1 || last < first || last-first > 10_000 { return nil, errors.New("invalid overview range") } c.mu.Lock() defer c.mu.Unlock() return c.overviewUnlocked(first, last) } func (c *Client) overviewUnlocked(first, last int64) ([]ArticleHeader, error) { rangeArg := strconv.FormatInt(first, 10) + "-" + strconv.FormatInt(last, 10) lines, err := c.multilineCommand("OVER "+rangeArg, 224) if code := responseCode(err); code == 500 || code == 501 { lines, err = c.multilineCommand("XOVER "+rangeArg, 224) } if err != nil { return nil, err } headers := make([]ArticleHeader, 0, len(lines)) for _, line := range lines { fields := strings.Split(line, "\t") if len(fields) < 5 { continue } number, err := strconv.ParseInt(fields[0], 10, 64) if err != nil { continue } header := ArticleHeader{ Number: number, Subject: fields[1], From: fields[2], Date: fields[3], MessageID: fields[4], } if len(fields) > 5 { header.References = fields[5] } if len(fields) > 6 { header.Bytes, _ = strconv.ParseInt(fields[6], 10, 64) } if len(fields) > 7 { header.Lines, _ = strconv.ParseInt(fields[7], 10, 64) } headers = append(headers, header) } return headers, nil } func (c *Client) LatestOverview(group string, limit int64) (GroupStatus, []ArticleHeader, error) { if !validAtom(group) { return GroupStatus{}, nil, errors.New("invalid newsgroup name") } if limit < 1 || limit > 10_000 { return GroupStatus{}, nil, errors.New("overview limit must be between 1 and 10000") } c.mu.Lock() defer c.mu.Unlock() status, err := c.selectGroupUnlocked(group) if err != nil { return GroupStatus{}, nil, err } if status.Count == 0 || status.High < status.Low || status.High < 1 { return status, nil, nil } first := status.High - limit + 1 if first < status.Low { first = status.Low } headers, err := c.overviewUnlocked(first, status.High) return status, headers, err } func (c *Client) Article(number int64) (string, error) { if number < 1 { return "", errors.New("invalid article number") } c.mu.Lock() defer c.mu.Unlock() return c.articleUnlocked(number) } func (c *Client) ArticleInGroup(group string, number int64) (string, error) { if !validAtom(group) { return "", errors.New("invalid newsgroup name") } if number < 1 { return "", errors.New("invalid article number") } c.mu.Lock() defer c.mu.Unlock() if _, err := c.selectGroupUnlocked(group); err != nil { return "", err } return c.articleUnlocked(number) } func (c *Client) articleUnlocked(number int64) (string, error) { lines, err := c.multilineCommand("ARTICLE "+strconv.FormatInt(number, 10), 220) if err != nil { return "", err } return strings.Join(lines, "\n"), nil } func (c *Client) Post(article string) error { lines, err := normalizedArticleLines(article) if err != nil { return err } c.mu.Lock() defer c.mu.Unlock() code, message, err := c.commandUnlocked("POST") if err != nil { return err } if code != 340 { return &ResponseError{Code: code, Message: message} } if err := c.setDeadline(); err != nil { return err } for _, line := range lines { if strings.HasPrefix(line, ".") { line = "." + line } if _, err := c.writer.WriteString(line + "\r\n"); err != nil { return fmt.Errorf("write article: %w", err) } } if _, err := c.writer.WriteString(".\r\n"); err != nil { return fmt.Errorf("finish article: %w", err) } if err := c.writer.Flush(); err != nil { return fmt.Errorf("flush article: %w", err) } code, message, err = c.readResponse() if err != nil { return err } if code != 240 { return &ResponseError{Code: code, Message: message} } return nil } // ValidateArticle checks the NNTP/MIME-safe text constraints used by Post. // It accepts either LF or CRLF input and does not modify the article. func ValidateArticle(article string) error { _, err := normalizedArticleLines(article) return err } func normalizedArticleLines(article string) ([]string, error) { if len(article) == 0 || len(article) > maxPostBytes { return nil, fmt.Errorf("article must contain between 1 and %d bytes", maxPostBytes) } if !utf8.ValidString(article) { return nil, errors.New("article is not valid UTF-8") } if strings.ContainsRune(article, '\x00') { return nil, errors.New("article contains a NUL byte") } normalized := strings.ReplaceAll(article, "\r\n", "\n") normalized = strings.ReplaceAll(normalized, "\r", "\n") lines := strings.Split(normalized, "\n") for _, line := range lines { wireBytes := len(line) if strings.HasPrefix(line, ".") { wireBytes++ // NNTP dot-stuffing adds one byte on the wire. } if wireBytes > maxPostLineBytes { return nil, fmt.Errorf("article contains a line longer than %d octets", maxPostLineBytes) } } return lines, nil } func (c *Client) Close() error { c.mu.Lock() defer c.mu.Unlock() if c.closed { return nil } c.closed = true _ = c.setDeadline() _, _, _ = c.commandUnlocked("QUIT") return c.conn.Close() } func (c *Client) authenticate(username, password string) error { if !validCommandValue(username) || !validCommandValue(password) { return errors.New("credentials contain invalid control characters") } code, message, err := c.command("AUTHINFO USER " + username) if err != nil { return err } if code == 281 { return nil } if code != 381 { return &ResponseError{Code: code, Message: message} } if password == "" { return errors.New("server requires a password") } code, message, err = c.command("AUTHINFO PASS " + password) if err != nil { return err } if code != 281 { return &ResponseError{Code: code, Message: message} } return nil } func (c *Client) authenticateWithConfig(cfg DialConfig) error { if strings.TrimSpace(cfg.SASLMechanism) == "" { return c.authenticate(cfg.Username, cfg.Password) } mechanism := strings.ToUpper(strings.TrimSpace(cfg.SASLMechanism)) if mechanism != "PLAIN" { return fmt.Errorf("unsupported NNTP SASL mechanism %q", cfg.SASLMechanism) } capabilities, err := c.capabilitiesUnlocked() if err != nil { return fmt.Errorf("query NNTP capabilities for SASL: %w", err) } if !hasCapability(capabilities, "SASL", "PLAIN") { return errors.New("NNTP server does not advertise SASL PLAIN") } code, message, err := c.commandUnlocked("AUTHINFO SASL PLAIN") if err != nil { return err } if code != 383 { if code == 281 { return nil } return &ResponseError{Code: code, Message: message} } response := base64.StdEncoding.EncodeToString([]byte("\x00" + cfg.Username + "\x00" + cfg.Password)) if err := c.writeLineUnlocked(response); err != nil { return err } code, message, err = c.readResponse() if err != nil { return err } if code != 281 { return &ResponseError{Code: code, Message: message} } return nil } func (c *Client) enableCompression() error { capabilities, err := c.capabilitiesUnlocked() if err != nil { return fmt.Errorf("query NNTP capabilities for compression: %w", err) } if !hasCapability(capabilities, "COMPRESS", "DEFLATE") { return errors.New("NNTP server does not advertise COMPRESS DEFLATE") } code, message, err := c.commandUnlocked("COMPRESS DEFLATE") if err != nil { return err } if code != 206 { return &ResponseError{Code: code, Message: message} } compressed := newCompressedConn(c.conn) c.conn = compressed c.reader = bufio.NewReaderSize(compressed, 64*1024) c.writer = bufio.NewWriterSize(compressed, 64*1024) return nil } func hasCapability(lines []string, name string, value string) bool { for _, line := range lines { fields := strings.Fields(line) if len(fields) == 0 || !strings.EqualFold(fields[0], name) { continue } if value == "" { return true } for _, field := range fields[1:] { if strings.EqualFold(field, value) { return true } } } return false } func (c *Client) command(line string) (int, string, error) { c.mu.Lock() defer c.mu.Unlock() return c.commandUnlocked(line) } func (c *Client) commandUnlocked(line string) (int, string, error) { if c.closed && line != "QUIT" { return 0, "", errors.New("NNTP connection is closed") } if !validCommandValue(line) { return 0, "", errors.New("NNTP command contains invalid control characters") } if err := c.setDeadline(); err != nil { return 0, "", err } if err := c.writeLineUnlocked(line); err != nil { return 0, "", err } return c.readResponse() } func (c *Client) writeLineUnlocked(line string) error { if _, err := c.writer.WriteString(line + "\r\n"); err != nil { return fmt.Errorf("write NNTP command: %w", err) } if err := c.writer.Flush(); err != nil { return fmt.Errorf("flush NNTP command: %w", err) } return nil } type compressedConn struct { net.Conn reader io.ReadCloser writer *flate.Writer } func newCompressedConn(conn net.Conn) *compressedConn { writer, _ := flate.NewWriter(conn, flate.DefaultCompression) return &compressedConn{Conn: conn, reader: flate.NewReader(conn), writer: writer} } func (c *compressedConn) Read(p []byte) (int, error) { return c.reader.Read(p) } func (c *compressedConn) Write(p []byte) (int, error) { n, err := c.writer.Write(p) if flushErr := c.writer.Flush(); err == nil { err = flushErr } return n, err } func (c *compressedConn) Close() error { _ = c.writer.Close() _ = c.reader.Close() return c.Conn.Close() } func (c *Client) multilineCommand(command string, expectedCode int) ([]string, error) { code, message, err := c.commandUnlocked(command) if err != nil { return nil, err } if code != expectedCode { return nil, &ResponseError{Code: code, Message: message} } return c.readMultiline() } func (c *Client) readResponse() (int, string, error) { line, err := readLimitedLine(c.reader) if err != nil { return 0, "", err } if len(line) < 3 { return 0, "", errors.New("malformed NNTP response") } code, err := strconv.Atoi(line[:3]) if err != nil { return 0, "", errors.New("malformed NNTP response code") } message := "" if len(line) > 4 { message = line[4:] } return code, message, nil } func (c *Client) readMultiline() ([]string, error) { lines := make([]string, 0, 1024) total := 0 for len(lines) < maxMultilineLines { line, err := readLimitedLine(c.reader) if err != nil { return nil, err } if line == "." { return lines, nil } if strings.HasPrefix(line, "..") { line = line[1:] } total += len(line) if total > maxMultilineBytes { return nil, errors.New("NNTP multiline response exceeds size limit") } lines = append(lines, line) } return nil, errors.New("NNTP multiline response exceeds line limit") } func (c *Client) setDeadline() error { if err := c.conn.SetDeadline(time.Now().Add(c.timeout)); err != nil { return fmt.Errorf("set NNTP deadline: %w", err) } return nil } func readLimitedLine(reader *bufio.Reader) (string, error) { buffer := make([]byte, 0, 4096) for { fragment, err := reader.ReadSlice('\n') if len(buffer)+len(fragment) > maxResponseLine { return "", errors.New("NNTP response line exceeds size limit") } buffer = append(buffer, fragment...) if errors.Is(err, bufio.ErrBufferFull) { continue } if err != nil { if errors.Is(err, io.EOF) { return "", io.ErrUnexpectedEOF } return "", err } break } line := string(buffer) line = strings.TrimSuffix(line, "\n") line = strings.TrimSuffix(line, "\r") return line, nil } func validateDialConfig(cfg DialConfig) error { if cfg.Host == "" || !validCommandValue(cfg.Host) { return errors.New("invalid NNTP host") } port, err := strconv.Atoi(cfg.Port) if err != nil || port < 1 || port > 65535 { return errors.New("invalid NNTP port") } if cfg.ProxyType != "" && !strings.EqualFold(cfg.ProxyType, "DIRECT") && !strings.EqualFold(cfg.ProxyType, "SOCKS5") { return errors.New("unsupported proxy type") } if strings.EqualFold(cfg.ProxyType, "SOCKS5") { if _, _, err := net.SplitHostPort(cfg.ProxyAddress); err != nil { return fmt.Errorf("invalid SOCKS5 address: %w", err) } } if cfg.Username != "" && !cfg.UseTLS { return errors.New("NNTP authentication requires TLS") } if cfg.StartTLS && !cfg.UseTLS { return errors.New("STARTTLS requires TLS to be enabled") } if cfg.SASLMechanism != "" && cfg.Username == "" { return errors.New("SASL authentication requires a username") } return nil } func makeTLSConfig(cfg DialConfig) *tls.Config { tlsConfig := &tls.Config{ MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS13, CipherSuites: append([]uint16(nil), tls12AESGCMSuites...), ServerName: cfg.Host, InsecureSkipVerify: cfg.InsecureSkipVerify, } //nolint:gosec // explicit user opt-in for local or pinned deployments if cfg.tlsConfig != nil { tlsConfig = cfg.tlsConfig.Clone() if tlsConfig.MinVersion < tls.VersionTLS12 { tlsConfig.MinVersion = tls.VersionTLS12 } if tlsConfig.MaxVersion < tls.VersionTLS12 || tlsConfig.MaxVersion > tls.VersionTLS13 { tlsConfig.MaxVersion = tls.VersionTLS13 } if tlsConfig.CipherSuites == nil { tlsConfig.CipherSuites = append([]uint16(nil), tls12AESGCMSuites...) } if tlsConfig.ServerName == "" { tlsConfig.ServerName = cfg.Host } } return tlsConfig } func estimatePopulation(low, high int64) int64 { if low <= 0 || high < low { return 0 } return high - low + 1 } func validAtom(value string) bool { return value != "" && validCommandValue(value) && !strings.ContainsAny(value, " ") } func validCommandValue(value string) bool { return !strings.ContainsAny(value, "\x00\r\n") } func responseCode(err error) int { var responseErr *ResponseError if errors.As(err, &responseErr) { return responseErr.Code } return 0 }