src/go/plugin/go.d/modules/tor/client.go
// 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
}