@cryptotaxi247 / netdata-1 / commits / 8db74085f

add go.d/tor (#18361)

Ilya Mashchenko committed Aug 18, 2024 at 21:05 UTC 8db74085fa7fb8be81a6fa28d8221fe72139d1da
16 files changed +877 -1
src/collectors/python.d.plugin/python.d.conf
+1 -1
@@ -42,7 +42,6 @@ go_expvar: no
42 # smartd_log: yes
43 # spigotmc: yes
44 # traefik: yes
45 -# tor: yes
45 # varnish: yes
46 # w1sensor: yes
47 # zscores: no
@@ -80,5 +79,6 @@ riakkv: no # Removed (replaced with go.d/riak).
79 sensors: no # Removed (replaced with go.d/sensors).
80 squid: no # Removed (replaced with go.d/squid).
81 tomcat: no # Removed (replaced with go.d/tomcat)
82 +tor: no # Removed (replaced with go.d/tor).
83 puppet: no # Removed (replaced with go.d/puppet).
84 uwsgi: no # Removed (replaced with go.d/uwsgi).
src/go/plugin/go.d/README.md
+1
@@ -142,6 +142,7 @@ see the appropriate collector readme.
142 | [systemdunits](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/systemdunits) | Systemd unit state |
143 | [tengine](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/tengine) | Tengine |
144 | [tomcat](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/tomcat) | Tomcat |
145 +| [tor](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/tor) | Tor |
146 | [traefik](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/traefik) | Traefik |
147 | [upsd](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/upsd) | UPSd (Nut) |
148 | [unbound](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/unbound) | Unbound |
src/go/plugin/go.d/config/go.d.conf
+1
@@ -106,6 +106,7 @@ modules:
106 # systemdunits: yes
107 # tengine: yes
108 # tomcat: yes
109 +# tor: yes
110 # traefik: yes
111 # upsd: yes
112 # unbound: yes
src/go/plugin/go.d/config/go.d/sd/docker.conf
+7
@@ -76,6 +76,8 @@ classify:
76 expr: '{{ match "sp" .Image "*/squid */squid:*" }}'
77 - tags: "tengine"
78 expr: '{{ match "sp" .Image "*/tengine */tengine:*" }}'
79 + - tags: "tor"
80 + expr: '{{ and (eq .PrivatePort "9051") (match "sp" .Image "*/tor */tor:*") }}'
81 - tags: "tomcat"
82 expr: '{{ match "sp" .Image "tomcat tomcat:* */tomcat */tomcat:*" }}'
83 - tags: "vernemq"
@@ -243,6 +245,11 @@ compose:
245 module: tomcat
246 name: docker_{{.Name}}
247 url: http://{{.Address}}
248 + - selector: "tor"
249 + template: |
250 + module: tor
251 + name: docker_{{.Name}}
252 + address: {{.Address}}
253 - selector: "vernemq"
254 template: |
255 module: vernemq
src/go/plugin/go.d/config/go.d/sd/net_listeners.conf
+7
@@ -122,6 +122,8 @@ classify:
122 expr: '{{ and (eq .Port "9001") (eq .Comm "supervisord") }}'
123 - tags: "tomcat"
124 expr: '{{ and (eq .Port "8080") (glob .Cmdline "*tomcat*") }}'
125 + - tags: "tor"
126 + expr: '{{ and (eq .Port "9051") (eq .Comm "tor") }}'
127 - tags: "traefik"
128 expr: '{{ and (eq .Port "80" "8080") (eq .Comm "traefik") }}'
129 - tags: "unbound"
@@ -477,6 +479,11 @@ compose:
479 module: tomcat
480 name: local
481 url: http://{{.Address}}
482 + - selector: "tor"
483 + template: |
484 + module: tor
485 + name: local
486 + address: {{.Address}}
487 - selector: "unbound"
488 template: |
489 module: unbound
src/go/plugin/go.d/config/go.d/tor.conf new
+6
@@ -0,0 +1,6 @@
1 +## All available configuration options, their descriptions and default values:
2 +## https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/tor#readme
3 +
4 +#jobs:
5 +# - name: local
6 +# address: 127.0.0.1:9051
src/go/plugin/go.d/modules/init.go
+1
@@ -98,6 +98,7 @@ import (
98 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/systemdunits"
99 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/tengine"
100 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/tomcat"
101 + _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/tor"
102 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/traefik"
103 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/unbound"
104 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/upsd"
src/go/plugin/go.d/modules/tor/charts.go new
+43
@@ -0,0 +1,43 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package tor
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
7 +)
8 +
9 +const (
10 + prioTraffic = module.Priority + iota
11 + prioUptime
12 +)
13 +
14 +var charts = module.Charts{
15 + trafficChart.Copy(),
16 + uptimeChart.Copy(),
17 +}
18 +
19 +var trafficChart = module.Chart{
20 + ID: "traffic",
21 + Title: "Tor Traffic",
22 + Units: "KiB/s",
23 + Fam: "traffic",
24 + Ctx: "tor.traffic",
25 + Type: module.Area,
26 + Priority: prioTraffic,
27 + Dims: module.Dims{
28 + {ID: "traffic/read", Name: "read", Algo: module.Incremental, Div: 1024},
29 + {ID: "traffic/written", Name: "write", Algo: module.Incremental, Mul: -1, Div: 1024},
30 + },
31 +}
32 +
33 +var uptimeChart = module.Chart{
34 + ID: "uptime",
35 + Title: "Tor Uptime",
36 + Units: "seconds",
37 + Fam: "uptime",
38 + Ctx: "tor.uptime",
39 + Priority: prioUptime,
40 + Dims: module.Dims{
41 + {ID: "uptime"},
42 + },
43 +}
src/go/plugin/go.d/modules/tor/client.go new
+117
@@ -0,0 +1,117 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package tor
4 +
5 +import (
6 + "bytes"
7 + "errors"
8 + "fmt"
9 + "strings"
10 +
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/socket"
12 +)
13 +
14 +// https://spec.torproject.org/control-spec/index.html
15 +// https://github.com/torproject/stem/blob/master/stem/control.py
16 +
17 +const (
18 + cmdAuthenticate = "AUTHENTICATE"
19 + cmdQuit = "QUIT"
20 + cmdGetInfo = "GETINFO"
21 +)
22 +
23 +type controlConn interface {
24 + connect() error
25 + disconnect()
26 +
27 + getInfo(...string) ([]byte, error)
28 +}
29 +
30 +func newControlConn(conf Config) controlConn {
31 + return &torControlClient{
32 + password: conf.Password,
33 + conn: socket.New(socket.Config{
34 + Address: conf.Address,
35 + ConnectTimeout: conf.Timeout.Duration(),
36 + ReadTimeout: conf.Timeout.Duration(),
37 + WriteTimeout: conf.Timeout.Duration(),
38 + })}
39 +}
40 +
41 +type torControlClient struct {
42 + password string
43 + conn socket.Client
44 +}
45 +
46 +func (c *torControlClient) connect() error {
47 + if err := c.conn.Connect(); err != nil {
48 + return err
49 + }
50 +
51 + return c.authenticate()
52 +}
53 +
54 +func (c *torControlClient) authenticate() error {
55 + // https://spec.torproject.org/control-spec/commands.html#authenticate
56 +
57 + cmd := cmdAuthenticate
58 + if c.password != "" {
59 + cmd = fmt.Sprintf("%s \"%s\"", cmdAuthenticate, c.password)
60 + }
61 +
62 + var s string
63 + err := c.conn.Command(cmd+"\n", func(bs []byte) bool {
64 + s = string(bs)
65 + return false
66 + })
67 + if err != nil {
68 + return fmt.Errorf("authentication failed: %v", err)
69 + }
70 + if !strings.HasPrefix(s, "250") {
71 + return fmt.Errorf("authentication failed: %s", s)
72 + }
73 + return nil
74 +}
75 +
76 +func (c *torControlClient) disconnect() {
77 + // https://spec.torproject.org/control-spec/commands.html#quit
78 +
79 + _ = c.conn.Command(cmdQuit+"\n", func(bs []byte) bool { return false })
80 + _ = c.conn.Disconnect()
81 +}
82 +
83 +func (c *torControlClient) getInfo(keywords ...string) ([]byte, error) {
84 + // https://spec.torproject.org/control-spec/commands.html#getinfo
85 +
86 + if len(keywords) == 0 {
87 + return nil, errors.New("no keywords specified")
88 + }
89 + cmd := fmt.Sprintf("%s %s", cmdGetInfo, strings.Join(keywords, " "))
90 +
91 + var buf bytes.Buffer
92 + var err error
93 +
94 + clientErr := c.conn.Command(cmd+"\n", func(bs []byte) bool {
95 + s := string(bs)
96 +
97 + switch {
98 + case strings.HasPrefix(s, "250-"):
99 + buf.WriteString(strings.TrimPrefix(s, "250-"))
100 + buf.WriteByte('\n')
101 + return true
102 + case strings.HasPrefix(s, "250 "):
103 + return false
104 + default:
105 + err = errors.New(s)
106 + return false
107 + }
108 + })
109 + if clientErr != nil {
110 + return nil, fmt.Errorf("command '%s' failed: %v", cmd, clientErr)
111 + }
112 + if err != nil {
113 + return nil, fmt.Errorf("command '%s' failed: %v", cmd, err)
114 + }
115 +
116 + return buf.Bytes(), nil
117 +}
src/go/plugin/go.d/modules/tor/collect.go new
+65
@@ -0,0 +1,65 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package tor
4 +
5 +import (
6 + "bufio"
7 + "bytes"
8 + "fmt"
9 + "strconv"
10 + "strings"
11 +)
12 +
13 +func (t *Tor) collect() (map[string]int64, error) {
14 + if t.conn == nil {
15 + conn, err := t.establishConnection()
16 + if err != nil {
17 + return nil, err
18 + }
19 + t.conn = conn
20 + }
21 +
22 + mx := make(map[string]int64)
23 + if err := t.collectServerInfo(mx); err != nil {
24 + t.Cleanup()
25 + return nil, err
26 + }
27 +
28 + return mx, nil
29 +}
30 +
31 +func (t *Tor) collectServerInfo(mx map[string]int64) error {
32 + resp, err := t.conn.getInfo("traffic/read", "traffic/written", "uptime")
33 + if err != nil {
34 + return err
35 + }
36 +
37 + sc := bufio.NewScanner(bytes.NewReader(resp))
38 +
39 + for sc.Scan() {
40 + line := sc.Text()
41 +
42 + key, value, ok := strings.Cut(line, "=")
43 + if !ok {
44 + return fmt.Errorf("failed to parse metric: %s", line)
45 + }
46 +
47 + v, err := strconv.ParseInt(value, 10, 64)
48 + if err != nil {
49 + return fmt.Errorf("failed to parse metric %s value: %v", line, err)
50 + }
51 + mx[key] = v
52 + }
53 +
54 + return nil
55 +}
56 +
57 +func (t *Tor) establishConnection() (controlConn, error) {
58 + conn := t.newConn(t.Config)
59 +
60 + if err := conn.connect(); err != nil {
61 + return nil, err
62 + }
63 +
64 + return conn, nil
65 +}
src/go/plugin/go.d/modules/tor/config_schema.json new
+53
@@ -0,0 +1,53 @@
1 +{
2 + "jsonSchema": {
3 + "$schema": "http://json-schema.org/draft-07/schema#",
4 + "title": "Tor collector configuration.",
5 + "type": "object",
6 + "properties": {
7 + "update_every": {
8 + "title": "Update every",
9 + "description": "Data collection interval, measured in seconds.",
10 + "type": "integer",
11 + "minimum": 1,
12 + "default": 1
13 + },
14 + "address": {
15 + "title": "Address",
16 + "description": "The IP address and port where the Tor's Control Port listens for connections.",
17 + "type": "string",
18 + "default": "127.0.0.1:9051"
19 + },
20 + "timeout": {
21 + "title": "Timeout",
22 + "description": "Timeout for establishing a connection and communication (reading and writing) in seconds.",
23 + "type": "number",
24 + "minimum": 0.5,
25 + "default": 1
26 + },
27 + "password": {
28 + "title": "Password",
29 + "description": "The password for authentication.",
30 + "type": "string",
31 + "sensitive": true
32 + }
33 + },
34 + "required": [
35 + "address"
36 + ],
37 + "additionalProperties": false,
38 + "patternProperties": {
39 + "^name$": {}
40 + }
41 + },
42 + "uiSchema": {
43 + "uiOptions": {
44 + "fullPage": true
45 + },
46 + "timeout": {
47 + "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
48 + },
49 + "password": {
50 + "ui:widget": "password"
51 + }
52 + }
53 +}
src/go/plugin/go.d/modules/tor/metadata.yaml new
+135
@@ -0,0 +1,135 @@
1 +plugin_name: go.d.plugin
2 +modules:
3 + - meta:
4 + id: collector-go.d.plugin-tor
5 + plugin_name: go.d.plugin
6 + module_name: tor
7 + monitored_instance:
8 + name: Tor
9 + link: https://www.torproject.org/
10 + categories:
11 + - data-collection.vpns
12 + icon_filename: "tor.svg"
13 + related_resources:
14 + integrations:
15 + list: []
16 + info_provided_to_referring_integrations:
17 + description: ""
18 + keywords:
19 + - tor
20 + - traffic
21 + - vpn
22 + most_popular: false
23 + overview:
24 + data_collection:
25 + metrics_description: |
26 + Tracks Tor's download and upload traffic, as well as its uptime.
27 + method_description: |
28 + It reads the server's response to the [GETINFO](https://spec.torproject.org/control-spec/commands.html#getinfo) command.
29 + supported_platforms:
30 + include: []
31 + exclude: []
32 + multi_instance: true
33 + additional_permissions:
34 + description: ""
35 + default_behavior:
36 + auto_detection:
37 + description: |
38 + By default, it detects Tor instances running on localhost that are listening on port 9051.
39 + On startup, it tries to collect metrics from:
40 +
41 + - 127.0.0.1:9051
42 + limits:
43 + description: ""
44 + performance_impact:
45 + description: ""
46 + setup:
47 + prerequisites:
48 + list:
49 + - title: Enable Control Port
50 + description: |
51 + Enable `ControlPort` in `/etc/tor/torrc`.
52 + configuration:
53 + file:
54 + name: go.d/tor.conf
55 + options:
56 + description: |
57 + The following options can be defined globally: update_every, autodetection_retry.
58 + folding:
59 + title: Config options
60 + enabled: true
61 + list:
62 + - name: update_every
63 + description: Data collection frequency.
64 + default_value: 1
65 + required: false
66 + - name: autodetection_retry
67 + description: Recheck interval in seconds. Zero means no recheck will be scheduled.
68 + default_value: 0
69 + required: false
70 + - name: address
71 + description: The IP address and port where the Tor's Control Port listens for connections.
72 + default_value: 127.0.0.1:9051
73 + required: true
74 + - name: timeout
75 + description: Connection, read, and write timeout duration in seconds. The timeout includes name resolution.
76 + default_value: 1
77 + required: false
78 + - name: password
79 + description: Password for authentication.
80 + default_value: ""
81 + required: false
82 + examples:
83 + folding:
84 + title: Config
85 + enabled: true
86 + list:
87 + - name: Basic
88 + description: A basic example configuration.
89 + config: |
90 + jobs:
91 + - name: local
92 + address: 127.0.0.1:9051
93 + password: somePassword
94 + - name: Multi-instance
95 + description: |
96 + > **Note**: When you define multiple jobs, their names must be unique.
97 +
98 + Collecting metrics from local and remote instances.
99 + config: |
100 + jobs:
101 + - name: local
102 + address: 127.0.0.1:9051
103 + password: somePassword
104 +
105 + - name: remote
106 + address: 203.0.113.0:9051
107 + password: somePassword
108 + troubleshooting:
109 + problems:
110 + list: []
111 + alerts: []
112 + metrics:
113 + folding:
114 + title: Metrics
115 + enabled: false
116 + description: ""
117 + availability: []
118 + scopes:
119 + - name: global
120 + description: "These metrics refer to the entire monitored application."
121 + labels: []
122 + metrics:
123 + - name: tor.traffic
124 + description: Tor Traffic
125 + unit: "KiB/s"
126 + chart_type: area
127 + dimensions:
128 + - name: read
129 + - name: write
130 + - name: tor.uptime
131 + description: Tor Uptime
132 + unit: "seconds"
133 + chart_type: line
134 + dimensions:
135 + - name: uptime
src/go/plugin/go.d/modules/tor/testdata/config.json new
+6
@@ -0,0 +1,6 @@
1 +{
2 + "update_every": 123,
3 + "address": "ok",
4 + "timeout": 123.123,
5 + "password": "ok"
6 +}
src/go/plugin/go.d/modules/tor/testdata/config.yaml new
+4
@@ -0,0 +1,4 @@
1 +update_every: 123
2 +address: "ok"
3 +timeout: 123.123
4 +password: "ok"
src/go/plugin/go.d/modules/tor/tor.go new
+102
@@ -0,0 +1,102 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package tor
4 +
5 +import (
6 + _ "embed"
7 + "errors"
8 + "time"
9 +
10 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web"
12 +)
13 +
14 +//go:embed "config_schema.json"
15 +var configSchema string
16 +
17 +func init() {
18 + module.Register("tor", module.Creator{
19 + JobConfigSchema: configSchema,
20 + Create: func() module.Module { return New() },
21 + Config: func() any { return &Config{} },
22 + })
23 +}
24 +
25 +func New() *Tor {
26 + return &Tor{
27 + Config: Config{
28 + Address: "127.0.0.1:9051",
29 + Timeout: web.Duration(time.Second * 1),
30 + },
31 + newConn: newControlConn,
32 + charts: charts.Copy(),
33 + }
34 +}
35 +
36 +type Config struct {
37 + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
38 + Address string `yaml:"address" json:"address"`
39 + Timeout web.Duration `yaml:"timeout" json:"timeout"`
40 + Password string `yaml:"password" json:"password"`
41 +}
42 +
43 +type Tor struct {
44 + module.Base
45 + Config `yaml:",inline" json:""`
46 +
47 + charts *module.Charts
48 +
49 + newConn func(Config) controlConn
50 + conn controlConn
51 +}
52 +
53 +func (t *Tor) Configuration() any {
54 + return t.Config
55 +}
56 +
57 +func (t *Tor) Init() error {
58 + if t.Address == "" {
59 + t.Error("config: 'address' not set")
60 + return errors.New("address not set")
61 + }
62 +
63 + return nil
64 +}
65 +
66 +func (t *Tor) Check() error {
67 + mx, err := t.collect()
68 + if err != nil {
69 + t.Error(err)
70 + return err
71 + }
72 +
73 + if len(mx) == 0 {
74 + return errors.New("no metrics collected")
75 + }
76 +
77 + return nil
78 +}
79 +
80 +func (t *Tor) Charts() *module.Charts {
81 + return t.charts
82 +}
83 +
84 +func (t *Tor) Collect() map[string]int64 {
85 + mx, err := t.collect()
86 + if err != nil {
87 + t.Error(err)
88 + }
89 +
90 + if len(mx) == 0 {
91 + return nil
92 + }
93 +
94 + return mx
95 +}
96 +
97 +func (t *Tor) Cleanup() {
98 + if t.conn != nil {
99 + t.conn.disconnect()
100 + t.conn = nil
101 + }
102 +}
src/go/plugin/go.d/modules/tor/tor_test.go new
+328
@@ -0,0 +1,328 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package tor
4 +
5 +import (
6 + "bufio"
7 + "errors"
8 + "fmt"
9 + "io"
10 + "net"
11 + "os"
12 + "strings"
13 + "testing"
14 + "time"
15 +
16 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
17 +
18 + "github.com/stretchr/testify/assert"
19 + "github.com/stretchr/testify/require"
20 +)
21 +
22 +var (
23 + dataConfigJSON, _ = os.ReadFile("testdata/config.json")
24 + dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
25 +)
26 +
27 +func Test_testDataIsValid(t *testing.T) {
28 + for name, data := range map[string][]byte{
29 + "dataConfigJSON": dataConfigJSON,
30 + "dataConfigYAML": dataConfigYAML,
31 + } {
32 + require.NotNil(t, data, name)
33 + }
34 +}
35 +
36 +func TestTor_ConfigurationSerialize(t *testing.T) {
37 + module.TestConfigurationSerialize(t, &Tor{}, dataConfigJSON, dataConfigYAML)
38 +}
39 +
40 +func TestTor_Init(t *testing.T) {
41 + tests := map[string]struct {
42 + config Config
43 + wantFail bool
44 + }{
45 + "success with default config": {
46 + wantFail: false,
47 + config: New().Config,
48 + },
49 + "fails if address not set": {
50 + wantFail: true,
51 + config: func() Config {
52 + conf := New().Config
53 + conf.Address = ""
54 + return conf
55 + }(),
56 + },
57 + }
58 +
59 + for name, test := range tests {
60 + t.Run(name, func(t *testing.T) {
61 + tor := New()
62 + tor.Config = test.config
63 +
64 + if test.wantFail {
65 + assert.Error(t, tor.Init())
66 + } else {
67 + assert.NoError(t, tor.Init())
68 + }
69 + })
70 + }
71 +}
72 +
73 +func TestTor_Charts(t *testing.T) {
74 + assert.NotNil(t, New().Charts())
75 +}
76 +
77 +func TestTor_Check(t *testing.T) {
78 + tests := map[string]struct {
79 + prepare func() (*Tor, *mockTorDaemon)
80 + wantFail bool
81 + }{
82 + "success on valid response": {
83 + wantFail: false,
84 + prepare: prepareCaseOk,
85 + },
86 + "fails on connection refused": {
87 + wantFail: true,
88 + prepare: prepareCaseConnectionRefused,
89 + },
90 + }
91 + for name, test := range tests {
92 + t.Run(name, func(t *testing.T) {
93 + tor, daemon := test.prepare()
94 +
95 + defer func() {
96 + assert.NoError(t, daemon.Close(), "daemon.Close()")
97 + }()
98 + go func() {
99 + assert.NoError(t, daemon.Run(), "daemon.Run()")
100 + }()
101 +
102 + select {
103 + case <-daemon.started:
104 + case <-time.After(time.Second * 3):
105 + t.Errorf("mock tor daemon start timed out")
106 + }
107 +
108 + require.NoError(t, tor.Init())
109 +
110 + if test.wantFail {
111 + assert.Error(t, tor.Check())
112 + } else {
113 + assert.NoError(t, tor.Check())
114 + }
115 +
116 + tor.Cleanup()
117 +
118 + select {
119 + case <-daemon.stopped:
120 + case <-time.After(time.Second * 3):
121 + t.Errorf("mock tor daemon stop timed out")
122 + }
123 + })
124 + }
125 +}
126 +
127 +func TestTor_Collect(t *testing.T) {
128 + tests := map[string]struct {
129 + prepare func() (*Tor, *mockTorDaemon)
130 + wantMetrics map[string]int64
131 + wantCharts int
132 + }{
133 + "success on valid response": {
134 + prepare: prepareCaseOk,
135 + wantCharts: len(charts),
136 + wantMetrics: map[string]int64{
137 + "traffic/read": 100,
138 + "traffic/written": 100,
139 + "uptime": 100,
140 + },
141 + },
142 + "fails on connection refused": {
143 + prepare: prepareCaseConnectionRefused,
144 + wantCharts: len(charts),
145 + },
146 + }
147 +
148 + for name, test := range tests {
149 + t.Run(name, func(t *testing.T) {
150 + tor, daemon := test.prepare()
151 +
152 + defer func() {
153 + assert.NoError(t, daemon.Close(), "daemon.Close()")
154 + }()
155 + go func() {
156 + assert.NoError(t, daemon.Run(), "daemon.Run()")
157 + }()
158 +
159 + select {
160 + case <-daemon.started:
161 + case <-time.After(time.Second * 3):
162 + t.Errorf("mock tor daemon start timed out")
163 + }
164 +
165 + require.NoError(t, tor.Init())
166 +
167 + mx := tor.Collect()
168 +
169 + require.Equal(t, test.wantMetrics, mx)
170 +
171 + assert.Equal(t, test.wantCharts, len(*tor.Charts()), "want charts")
172 +
173 + if len(test.wantMetrics) > 0 {
174 + module.TestMetricsHasAllChartsDims(t, tor.Charts(), mx)
175 + }
176 +
177 + tor.Cleanup()
178 +
179 + select {
180 + case <-daemon.stopped:
181 + case <-time.After(time.Second * 3):
182 + t.Errorf("mock tordaemon stop timed out")
183 + }
184 + })
185 + }
186 +}
187 +
188 +func prepareCaseOk() (*Tor, *mockTorDaemon) {
189 + daemon := &mockTorDaemon{
190 + addr: "127.0.0.1:65001",
191 + started: make(chan struct{}),
192 + stopped: make(chan struct{}),
193 + }
194 +
195 + tor := New()
196 + tor.Address = daemon.addr
197 +
198 + return tor, daemon
199 +}
200 +
201 +func prepareCaseConnectionRefused() (*Tor, *mockTorDaemon) {
202 + ch := make(chan struct{})
203 + close(ch)
204 +
205 + daemon := &mockTorDaemon{
206 + addr: "127.0.0.1:65001",
207 + dontStart: true,
208 + started: ch,
209 + stopped: ch,
210 + }
211 +
212 + tor := New()
213 + tor.Address = daemon.addr
214 +
215 + return tor, daemon
216 +}
217 +
218 +type mockTorDaemon struct {
219 + addr string
220 + srv net.Listener
221 + started chan struct{}
222 + stopped chan struct{}
223 + dontStart bool
224 + authenticated bool
225 +}
226 +
227 +func (m *mockTorDaemon) Run() error {
228 + if m.dontStart {
229 + return nil
230 + }
231 +
232 + srv, err := net.Listen("tcp", m.addr)
233 + if err != nil {
234 + return err
235 + }
236 +
237 + m.srv = srv
238 +
239 + close(m.started)
240 + defer close(m.stopped)
241 +
242 + return m.handleConnections()
243 +}
244 +
245 +func (m *mockTorDaemon) Close() error {
246 + if m.srv != nil {
247 + err := m.srv.Close()
248 + m.srv = nil
249 + return err
250 + }
251 + return nil
252 +}
253 +
254 +func (m *mockTorDaemon) handleConnections() error {
255 + conn, err := m.srv.Accept()
256 + if err != nil || conn == nil {
257 + return errors.New("could not accept connection")
258 + }
259 + return m.handleConnection(conn)
260 +}
261 +
262 +func (m *mockTorDaemon) handleConnection(conn net.Conn) error {
263 + defer func() { _ = conn.Close() }()
264 +
265 + rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
266 + var line string
267 + var err error
268 +
269 + for {
270 + if line, err = rw.ReadString('\n'); err != nil {
271 + return fmt.Errorf("error reading from connection: %v", err)
272 + }
273 +
274 + line = strings.TrimSpace(line)
275 +
276 + cmd, param, _ := strings.Cut(line, " ")
277 +
278 + switch cmd {
279 + case cmdQuit:
280 + return m.handleQuit(conn)
281 + case cmdAuthenticate:
282 + err = m.handleAuthenticate(conn)
283 + case cmdGetInfo:
284 + err = m.handleGetInfo(conn, param)
285 + default:
286 + s := fmt.Sprintf("510 Unrecognized command \"%s\"\n", cmd)
287 + _, _ = rw.WriteString(s)
288 + return fmt.Errorf("unexpected command: %s", line)
289 + }
290 +
291 + _ = rw.Flush()
292 +
293 + if err != nil {
294 + return err
295 + }
296 + }
297 +}
298 +
299 +func (m *mockTorDaemon) handleQuit(conn io.Writer) error {
300 + _, err := conn.Write([]byte("250 closing connection\n"))
301 + return err
302 +}
303 +
304 +func (m *mockTorDaemon) handleAuthenticate(conn io.Writer) error {
305 + m.authenticated = true
306 + _, err := conn.Write([]byte("250 OK\n"))
307 + return err
308 +}
309 +
310 +func (m *mockTorDaemon) handleGetInfo(conn io.Writer, keywords string) error {
311 + if !m.authenticated {
312 + _, _ = conn.Write([]byte("514 Authentication required\n"))
313 + return errors.New("authentication required")
314 + }
315 +
316 + keywords = strings.Trim(keywords, "\"")
317 +
318 + for _, k := range strings.Fields(keywords) {
319 + s := fmt.Sprintf("250-%s=%d\n", k, 100)
320 +
321 + if _, err := conn.Write([]byte(s)); err != nil {
322 + return err
323 + }
324 + }
325 +
326 + _, err := conn.Write([]byte("250 OK\n"))
327 + return err
328 +}