firehol/netdata

View on GitHub
src/go/plugin/go.d/modules/tor/client.go

Summary

Maintainability
A
40 mins
Test Coverage
// SPDX-License-Identifier: GPL-3.0-or-later

package tor

import (
    "bytes"
    "errors"
    "fmt"
    "strings"

    "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/socket"
)

// https://spec.torproject.org/control-spec/index.html
// https://github.com/torproject/stem/blob/master/stem/control.py

const (
    cmdAuthenticate = "AUTHENTICATE"
    cmdQuit         = "QUIT"
    cmdGetInfo      = "GETINFO"
)

type controlConn interface {
    connect() error
    disconnect()

    getInfo(...string) ([]byte, error)
}

func newControlConn(conf Config) controlConn {
    return &torControlClient{
        password: conf.Password,
        conn: socket.New(socket.Config{
            Address:        conf.Address,
            ConnectTimeout: conf.Timeout.Duration(),
            ReadTimeout:    conf.Timeout.Duration(),
            WriteTimeout:   conf.Timeout.Duration(),
        })}
}

type torControlClient struct {
    password string
    conn     socket.Client
}

func (c *torControlClient) connect() error {
    if err := c.conn.Connect(); err != nil {
        return err
    }

    return c.authenticate()
}

func (c *torControlClient) authenticate() error {
    // https://spec.torproject.org/control-spec/commands.html#authenticate

    cmd := cmdAuthenticate
    if c.password != "" {
        cmd = fmt.Sprintf("%s \"%s\"", cmdAuthenticate, c.password)
    }

    var s string
    err := c.conn.Command(cmd+"\n", func(bs []byte) bool {
        s = string(bs)
        return false
    })
    if err != nil {
        return fmt.Errorf("authentication failed: %v", err)
    }
    if !strings.HasPrefix(s, "250") {
        return fmt.Errorf("authentication failed: %s", s)
    }
    return nil
}

func (c *torControlClient) disconnect() {
    // https://spec.torproject.org/control-spec/commands.html#quit

    _ = c.conn.Command(cmdQuit+"\n", func(bs []byte) bool { return false })
    _ = c.conn.Disconnect()
}

func (c *torControlClient) getInfo(keywords ...string) ([]byte, error) {
    // https://spec.torproject.org/control-spec/commands.html#getinfo

    if len(keywords) == 0 {
        return nil, errors.New("no keywords specified")
    }
    cmd := fmt.Sprintf("%s %s", cmdGetInfo, strings.Join(keywords, " "))

    var buf bytes.Buffer
    var err error

    clientErr := c.conn.Command(cmd+"\n", func(bs []byte) bool {
        s := string(bs)

        switch {
        case strings.HasPrefix(s, "250-"):
            buf.WriteString(strings.TrimPrefix(s, "250-"))
            buf.WriteByte('\n')
            return true
        case strings.HasPrefix(s, "250 "):
            return false
        default:
            err = errors.New(s)
            return false
        }
    })
    if clientErr != nil {
        return nil, fmt.Errorf("command '%s' failed: %v", cmd, clientErr)
    }
    if err != nil {
        return nil, fmt.Errorf("command '%s' failed: %v", cmd, err)
    }

    return buf.Bytes(), nil
}