master
go 175 lines 3.81 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package boinc
4
5 import (
6 "bytes"
7 "crypto/md5"
8 "encoding/xml"
9 "errors"
10 "fmt"
11 "log/slog"
12 "strings"
13
14 "github.com/netdata/netdata/go/plugins/logger"
15 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/socket"
16 )
17
18 // Based on: https://github.com/vorot93/boinc-client-rest-server/tree/master
19
20 type boincConn interface {
21 connect() error
22 disconnect()
23 authenticate() error
24 getResults() ([]boincReplyResult, error)
25 }
26
27 func newBoincConn(conf Config, log *logger.Logger) boincConn {
28 return &boincClient{
29 Logger: log,
30
31 password: conf.Password,
32 conn: socket.New(socket.Config{
33 Address: conf.Address,
34 Timeout: conf.Timeout.Duration(),
35 })}
36 }
37
38 type boincClient struct {
39 *logger.Logger
40 password string
41 conn socket.Client
42 }
43
44 func (c *boincClient) connect() error {
45 return c.conn.Connect()
46 }
47
48 func (c *boincClient) disconnect() {
49 _ = c.conn.Disconnect()
50 }
51
52 func (c *boincClient) authenticate() error {
53 // https://boinc.berkeley.edu/trac/wiki/GuiRpcProtocol#Authentication
54
55 req := &boincRequest{
56 Auth1: &struct{}{},
57 }
58
59 resp, err := c.send(req)
60 if err != nil {
61 return err
62 }
63 if resp.Nonce == nil {
64 return errors.New("auth1: empty nonce")
65 }
66
67 req = &boincRequest{
68 Auth2: &boincRequestAuthNonce{Hash: makeNonceMD5(*resp.Nonce, c.password)},
69 }
70
71 resp, err = c.send(req)
72 if err != nil {
73 return err
74 }
75 if resp.Unauthorized != nil || resp.Authorized == nil {
76 return errors.New("auth2: unauthorized")
77 }
78
79 return nil
80 }
81
82 func (c *boincClient) getResults() ([]boincReplyResult, error) {
83 req := &boincRequest{
84 GetResults: &boincRequestGetResults{},
85 }
86
87 resp, err := c.send(req)
88 if err != nil {
89 return nil, err
90 }
91
92 return resp.Results, nil
93 }
94
95 func (c *boincClient) send(req *boincRequest) (*boincReply, error) {
96 reqData, err := xml.Marshal(req)
97 if err != nil {
98 return nil, fmt.Errorf("failed to marshal request: %v", err)
99 }
100
101 reqData = append(reqData, 3)
102
103 if logger.Level.Enabled(slog.LevelDebug) {
104 c.Debugf("sending request: %s", string(reqData))
105 }
106
107 const (
108 respStart = "<boinc_gui_rpc_reply>"
109 respEnd = "</boinc_gui_rpc_reply>"
110 )
111
112 var b bytes.Buffer
113
114 if err := c.conn.Command(string(reqData), func(bs []byte) (bool, error) {
115 s := strings.TrimSpace(string(bs))
116 if s == "" {
117 return true, nil
118 }
119
120 if b.Len() == 0 && s != respStart {
121 return false, fmt.Errorf("unexpected response first line: %s", s)
122 }
123
124 b.WriteString(s)
125
126 return s != respEnd, nil
127 }); err != nil {
128 return nil, fmt.Errorf("failed to send command: %v", err)
129 }
130
131 if logger.Level.Enabled(slog.LevelDebug) {
132 c.Debugf("received response: %s", b.String())
133 }
134
135 respData := cleanReplyData(b.Bytes())
136
137 var resp boincReply
138
139 if err := xml.Unmarshal(respData, &resp); err != nil {
140 return nil, fmt.Errorf("failed to unmarshal reply: %v", err)
141 }
142
143 if resp.Error != nil {
144 return nil, fmt.Errorf("received error from server: %s", *resp.Error)
145 }
146 if resp.BadRequest != nil {
147 return nil, errors.New("received bad request response from server")
148 }
149 if resp.Unauthorized != nil {
150 return nil, errors.New("received unauthorized response from server")
151 }
152
153 return &resp, nil
154 }
155
156 func cleanReplyData(resp []byte) []byte {
157 tags := []string{"bad_request", "authorized", "unauthorized", "have_credentials", "cookie_required"}
158 s := expandEmptyTags(string(resp), tags)
159 return []byte(strings.ReplaceAll(s, `encoding="ISO-8859-1"`, `encoding="UTF-8"`))
160 }
161
162 func makeNonceMD5(nonce, pass string) string {
163 hex := fmt.Sprintf("%x", md5.Sum([]byte(nonce+pass)))
164 return hex
165 }
166
167 func expandEmptyTags(xmlString string, tags []string) string {
168 for _, tag := range tags {
169 emptyTag := fmt.Sprintf("<%s/>", tag)
170 expandedTag := fmt.Sprintf("<%s></%s>", tag, tag)
171 xmlString = strings.ReplaceAll(xmlString, emptyTag, expandedTag)
172 xmlString = strings.ReplaceAll(xmlString, fmt.Sprintf("<%s />", tag), expandedTag)
173 }
174 return xmlString
175 }