firehol/netdata

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

Summary

Maintainability
A
1 hr
Test Coverage
// SPDX-License-Identifier: GPL-3.0-or-later

package boinc

import (
    "bytes"
    "crypto/md5"
    "encoding/xml"
    "errors"
    "fmt"
    "log/slog"
    "strings"

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

// Based on: https://github.com/vorot93/boinc-client-rest-server/tree/master

type boincConn interface {
    connect() error
    disconnect()
    authenticate() error
    getResults() ([]boincReplyResult, error)
}

func newBoincConn(conf Config, log *logger.Logger) boincConn {
    return &boincClient{
        Logger: log,

        password: conf.Password,
        conn: socket.New(socket.Config{
            Address:        conf.Address,
            ConnectTimeout: conf.Timeout.Duration(),
            ReadTimeout:    conf.Timeout.Duration(),
            WriteTimeout:   conf.Timeout.Duration(),
        })}
}

type boincClient struct {
    *logger.Logger
    password string
    conn     socket.Client
}

func (c *boincClient) connect() error {
    return c.conn.Connect()
}

func (c *boincClient) disconnect() {
    _ = c.conn.Disconnect()
}

func (c *boincClient) authenticate() error {
    // https://boinc.berkeley.edu/trac/wiki/GuiRpcProtocol#Authentication

    req := &boincRequest{
        Auth1: &struct{}{},
    }

    resp, err := c.send(req)
    if err != nil {
        return err
    }
    if resp.Nonce == nil {
        return errors.New("auth1: empty nonce")
    }

    req = &boincRequest{
        Auth2: &boincRequestAuthNonce{Hash: makeNonceMD5(*resp.Nonce, c.password)},
    }

    resp, err = c.send(req)
    if err != nil {
        return err
    }
    if resp.Unauthorized != nil || resp.Authorized == nil {
        return errors.New("auth2: unauthorized")
    }

    return nil
}

func (c *boincClient) getResults() ([]boincReplyResult, error) {
    req := &boincRequest{
        GetResults: &boincRequestGetResults{},
    }

    resp, err := c.send(req)
    if err != nil {
        return nil, err
    }

    return resp.Results, nil
}

func (c *boincClient) send(req *boincRequest) (*boincReply, error) {
    reqData, err := xml.Marshal(req)
    if err != nil {
        return nil, fmt.Errorf("failed to marshal request: %v", err)
    }

    reqData = append(reqData, 3)

    if logger.Level.Enabled(slog.LevelDebug) {
        c.Debugf("sending request: %s", string(reqData))
    }

    const (
        respStart = "<boinc_gui_rpc_reply>"
        respEnd   = "</boinc_gui_rpc_reply>"
    )

    var b bytes.Buffer

    clientErr := c.conn.Command(string(reqData), func(bs []byte) bool {
        s := strings.TrimSpace(string(bs))
        if s == "" {
            return true
        }

        if b.Len() == 0 && s != respStart {
            err = fmt.Errorf("unexpected response first line: %s", s)
            return false
        }

        b.WriteString(s)

        return s != respEnd
    })
    if clientErr != nil {
        return nil, fmt.Errorf("failed to send command: %v", clientErr)
    }
    if err != nil {
        return nil, fmt.Errorf("failed to send command: %v", err)
    }

    if logger.Level.Enabled(slog.LevelDebug) {
        c.Debugf("received response: %s", string(b.Bytes()))
    }

    respData := cleanReplyData(b.Bytes())

    var resp boincReply

    if err := xml.Unmarshal(respData, &resp); err != nil {
        return nil, fmt.Errorf("failed to unmarshal reply: %v", err)
    }

    if resp.Error != nil {
        return nil, fmt.Errorf("received error from server: %s", *resp.Error)
    }
    if resp.BadRequest != nil {
        return nil, errors.New("received bad request response from server")
    }
    if resp.Unauthorized != nil {
        return nil, errors.New("received unauthorized response from server")
    }

    return &resp, nil
}

func cleanReplyData(resp []byte) []byte {
    tags := []string{"bad_request", "authorized", "unauthorized", "have_credentials", "cookie_required"}
    s := expandEmptyTags(string(resp), tags)
    return []byte(strings.ReplaceAll(s, `encoding="ISO-8859-1"`, `encoding="UTF-8"`))
}

func makeNonceMD5(nonce, pass string) string {
    hex := fmt.Sprintf("%x", md5.Sum([]byte(nonce+pass)))
    return hex
}

func expandEmptyTags(xmlString string, tags []string) string {
    for _, tag := range tags {
        emptyTag := fmt.Sprintf("<%s/>", tag)
        expandedTag := fmt.Sprintf("<%s></%s>", tag, tag)
        xmlString = strings.ReplaceAll(xmlString, emptyTag, expandedTag)
        xmlString = strings.ReplaceAll(xmlString, fmt.Sprintf("<%s />", tag), expandedTag)
    }
    return xmlString
}