go-nntp-plusplus/client/client.go

363 lines
8.6 KiB
Go

// Package nntpclient provides an NNTP Client.
package nntpclient
import (
"crypto/tls"
"errors"
"io"
"net"
"net/textproto"
"strconv"
"strings"
"git.maride.cc/maride/go-nntp-plusplus"
)
// Client is an NNTP client.
type Client struct {
conn *textproto.Conn
netconn net.Conn
tls bool
Banner string
capabilities []string
}
// New connects a client to an NNTP server.
func New(network, addr string) (*Client, error) {
netconn, err := net.Dial(network, addr)
if err != nil {
return nil, err
}
return connect(netconn)
}
// NewConn wraps an existing connection, for example one opened with tls.Dial
func NewConn(netconn net.Conn) (*Client, error) {
client, err := connect(netconn)
if err != nil {
return nil, err
}
if _, ok := netconn.(*tls.Conn); ok {
client.tls = true
}
return client, nil
}
// NewTLS connects to an NNTP server over a dedicated TLS port like 563
func NewTLS(network, addr string, config *tls.Config) (*Client, error) {
netconn, err := tls.Dial(network, addr, config)
if err != nil {
return nil, err
}
client, err := connect(netconn)
if err != nil {
return nil, err
}
client.tls = true
return client, nil
}
func connect(netconn net.Conn) (*Client, error) {
conn := textproto.NewConn(netconn)
_, msg, err := conn.ReadCodeLine(200)
if err != nil {
return nil, err
}
return &Client{
conn: conn,
netconn: netconn,
Banner: msg,
}, nil
}
// Close this client.
func (c *Client) Close() error {
return c.conn.Close()
}
// Authenticate against an NNTP server using authinfo user/pass
func (c *Client) Authenticate(user, pass string) (msg string, err error) {
err = c.conn.PrintfLine("authinfo user %s", user)
if err != nil {
return
}
_, _, err = c.conn.ReadCodeLine(381)
if err != nil {
return
}
err = c.conn.PrintfLine("authinfo pass %s", pass)
if err != nil {
return
}
_, msg, err = c.conn.ReadCodeLine(250)
return
}
func parsePosting(p string) nntp.PostingStatus {
switch p {
case "y":
return nntp.PostingPermitted
case "m":
return nntp.PostingModerated
}
return nntp.PostingNotPermitted
}
// List groups
func (c *Client) List(sub string) (rv []nntp.Group, err error) {
_, _, err = c.Command("LIST "+sub, 215)
if err != nil {
return
}
var groupLines []string
groupLines, err = c.conn.ReadDotLines()
if err != nil {
return
}
rv = make([]nntp.Group, 0, len(groupLines))
for _, l := range groupLines {
parts := strings.Split(l, " ")
high, errh := strconv.ParseInt(parts[1], 10, 64)
low, errl := strconv.ParseInt(parts[2], 10, 64)
if errh == nil && errl == nil {
rv = append(rv, nntp.Group{
Name: parts[0],
High: high,
Low: low,
Posting: parsePosting(parts[3]),
})
}
}
return
}
// Group selects a group.
func (c *Client) Group(name string) (rv nntp.Group, err error) {
var msg string
_, msg, err = c.Command("GROUP "+name, 211)
if err != nil {
return
}
// count first last name
parts := strings.Split(msg, " ")
if len(parts) != 4 {
err = errors.New("Don't know how to parse result: " + msg)
}
rv.Count, err = strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return
}
rv.Low, err = strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return
}
rv.High, err = strconv.ParseInt(parts[2], 10, 64)
if err != nil {
return
}
rv.Name = parts[3]
return
}
// Article grabs an article
func (c *Client) Article(specifier string) (int64, string, io.Reader, error) {
err := c.conn.PrintfLine("ARTICLE %s", specifier)
if err != nil {
return 0, "", nil, err
}
return c.articleish(220)
}
// Head gets the headers for an article
func (c *Client) Head(specifier string) (int64, string, io.Reader, error) {
err := c.conn.PrintfLine("HEAD %s", specifier)
if err != nil {
return 0, "", nil, err
}
return c.articleish(221)
}
// Body gets the body of an article
func (c *Client) Body(specifier string) (int64, string, io.Reader, error) {
err := c.conn.PrintfLine("BODY %s", specifier)
if err != nil {
return 0, "", nil, err
}
return c.articleish(222)
}
func (c *Client) articleish(expected int) (int64, string, io.Reader, error) {
_, msg, err := c.conn.ReadCodeLine(expected)
if err != nil {
return 0, "", nil, err
}
parts := strings.SplitN(msg, " ", 2)
n, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return 0, "", nil, err
}
return n, parts[1], c.conn.DotReader(), nil
}
// Post a new article
//
// The reader should contain the entire article, headers and body in
// RFC822ish format.
func (c *Client) Post(r io.Reader) error {
err := c.conn.PrintfLine("POST")
if err != nil {
return err
}
_, _, err = c.conn.ReadCodeLine(340)
if err != nil {
return err
}
w := c.conn.DotWriter()
_, err = io.Copy(w, r)
if err != nil {
// This seems really bad
return err
}
w.Close()
_, _, err = c.conn.ReadCodeLine(240)
return err
}
// Command sends a low-level command and get a response.
//
// This will return an error if the code doesn't match the expectCode
// prefix. For example, if you specify "200", the response code MUST
// be 200 or you'll get an error. If you specify "2", any code from
// 200 (inclusive) to 300 (exclusive) will be success. An expectCode
// of -1 disables this behavior.
func (c *Client) Command(cmd string, expectCode int) (int, string, error) {
err := c.conn.PrintfLine(cmd)
if err != nil {
return 0, "", err
}
return c.conn.ReadCodeLine(expectCode)
}
// asLines issues a command and returns the response's data block as lines.
func (c *Client) asLines(cmd string, expectCode int) ([]string, error) {
_, _, err := c.Command(cmd, expectCode)
if err != nil {
return nil, err
}
return c.conn.ReadDotLines()
}
// Capabilities retrieves a list of supported capabilities.
//
// See https://datatracker.ietf.org/doc/html/rfc3977#section-5.2.2
func (c *Client) Capabilities() ([]string, error) {
caps, err := c.asLines("CAPABILITIES", 101)
if err != nil {
return nil, err
}
for i, line := range caps {
caps[i] = strings.ToUpper(line)
}
c.capabilities = caps
return caps, nil
}
// GetCapability returns a complete capability line.
//
// "Each capability line consists of one or more tokens, which MUST be
// separated by one or more space or TAB characters."
//
// From https://datatracker.ietf.org/doc/html/rfc3977#section-3.3.1
func (c *Client) GetCapability(capability string) string {
capability = strings.ToUpper(capability)
for _, capa := range c.capabilities {
i := strings.IndexAny(capa, "\t ")
if i != -1 && capa[:i] == capability {
return capa
}
if capa == capability {
return capa
}
}
return ""
}
// HasCapabilityArgument indicates whether a capability arg is supported.
//
// Here, "argument" means any token after the label in a capabilities response
// line. Some, like "ACTIVE" in "LIST ACTIVE", are not command arguments but
// rather "keyword" components of compound commands called "variants."
//
// See https://datatracker.ietf.org/doc/html/rfc3977#section-9.5
func (c *Client) HasCapabilityArgument(
capability, argument string,
) (bool, error) {
if c.capabilities == nil {
return false, errors.New("Capabilities unpopulated")
}
capLine := c.GetCapability(capability)
if capLine == "" {
return false, errors.New("No such capability")
}
argument = strings.ToUpper(argument)
for _, capArg := range strings.Fields(capLine)[1:] {
if capArg == argument {
return true, nil
}
}
return false, nil
}
// ListOverviewFmt performs a LIST OVERVIEW.FMT query.
//
// According to the spec, the presence of an "OVER" line in the capabilities
// response means this LIST variant is supported, so there's no reason to
// check for it among the keywords in the "LIST" line, strictly speaking.
//
// See https://datatracker.ietf.org/doc/html/rfc3977#section-3.3.2
func (c *Client) ListOverviewFmt() ([]string, error) {
fields, err := c.asLines("LIST OVERVIEW.FMT", 215)
if err != nil {
return nil, err
}
return fields, nil
}
// Over returns a list of raw overview lines with tab-separated fields.
func (c *Client) Over(specifier string) ([]string, error) {
lines, err := c.asLines("OVER "+specifier, 224)
if err != nil {
return nil, err
}
return lines, nil
}
func (c *Client) HasTLS() bool {
return c.tls
}
// StartTLS sends the STARTTLS command and refreshes capabilities.
//
// See https://datatracker.ietf.org/doc/html/rfc4642 and net/smtp.go, from
// which this was adapted, and maybe NNTP.startls in Python's nntplib also.
func (c *Client) StartTLS(config *tls.Config) error {
if c.tls {
return errors.New("TLS already active")
}
_, _, err := c.Command("STARTTLS", 382)
if err != nil {
return err
}
c.netconn = tls.Client(c.netconn, config)
c.conn = textproto.NewConn(c.netconn)
c.tls = true
_, err = c.Capabilities()
if err != nil {
return err
}
return nil
}