Skip to content

Commit

Permalink
pool: Rework client user agent id logic.
Browse files Browse the repository at this point in the history
This reworks the client user agent identification logic to improve its
efficiency and flexibility as well as to easily support old minor
versions.

It does this by changing the matching logic to first parse the user
agent into its individual components and then attempting to match
against that parsed information using matching functions as opposed to
encoding the more specific matching logic directly into a regular
expression.

The user agent parsing first attempts to split it into a client name and
version part and when that is successful further attempts to parse the
version part into the individual semantic version components using a
regular expression with capture groups.

The matching functions are closures that accept the parsed user agent
details and may impose arbitrary criteria.

For convenience a default matching function is added that requires the
user agent to have a provided client name and major version as well as a
minor version that is less than or equal to specified value.

The user agent matching tests are updated accordingly.

Finally, `decred-gominer` is updated to support up to version 2.1.x so
the pool will work with both version 2.0.0 as well as the master branch
that will be moving to version 2.1.0-pre for ongoing development.
  • Loading branch information
davecgh authored and jholdstock committed Oct 18, 2023
1 parent 740fe73 commit f1be8b1
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 57 deletions.
109 changes: 71 additions & 38 deletions pool/minerid.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,86 @@ package pool

import (
"fmt"
"regexp"
"strings"

errs "github.com/decred/dcrpool/errors"
"github.com/decred/dcrpool/internal/semver"
)

// newUserAgentRE returns a compiled regular expression that matches a user
// agent with the provided client name, major version, and minor version as well
// as any patch, pre-release, and build metadata suffix that are valid per the
// semantic versioning 2.0.0 spec.
//
// For reference, user agents are expected to be of the form "name/version"
// where the name is a string and the version follows the semantic versioning
// 2.0.0 spec.
func newUserAgentRE(clientName string, clientMajor, clientMinor uint32) *regexp.Regexp {
// semverBuildAndMetadataSuffixRE is a regular expression to match the
// optional pre-release and build metadata portions of a semantic version
// 2.0 string.
const semverBuildAndMetadataSuffixRE = `(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-]` +
`[0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?` +
`(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?`

return regexp.MustCompile(fmt.Sprintf(`^%s\/%d\.%d\.(0|[1-9]\d*)%s$`,
clientName, clientMajor, clientMinor, semverBuildAndMetadataSuffixRE))
}

var (
// These regular expressions are used to identify the expected mining
// clients by the user agents in their mining.subscribe requests.
cpuRE = newUserAgentRE("cpuminer", 1, 0)
gominerRE = newUserAgentRE("decred-gominer", 2, 0)
nhRE = newUserAgentRE("NiceHash", 1, 0)

// miningClients maps regular expressions to the supported mining client IDs
// for all user agents that match the regular expression.
miningClients = map[*regexp.Regexp][]string{
cpuRE: {CPU},
gominerRE: {Gominer},
nhRE: {NiceHashValidator},
// supportedClientUserAgents maps user agents that match a pattern to the
// supported mining client IDs.
supportedClientUserAgents = []userAgentToClientsFilter{
{matchesUserAgentMaxMinor("cpuminer", 1, 0), []string{CPU}},
{matchesUserAgentMaxMinor("decred-gominer", 2, 1), []string{Gominer}},
{matchesUserAgentMaxMinor("NiceHash", 1, 0), []string{NiceHashValidator}},
}
)

// identifyMiningClients returns the possible mining client IDs for a given user agent
// or an error when the user agent is not supported.
// parsedUserAgent houses the individual components of a parsed user agent
// string.
type parsedUserAgent struct {
semver.ParsedSemVer
clientName string
}

// parseUserAgent attempts to parse a user agent into its constituent parts and
// returns whether or not it was successful.
func parseUserAgent(userAgent string) (*parsedUserAgent, bool) {
// Attempt to split the user agent into the client name and client version
// parts.
parts := strings.SplitN(userAgent, "/", 2)
if len(parts) != 2 {
return nil, false
}
clientName := parts[0]
clientVer := parts[1]

// Attempt to parse the client version into the constituent semantic version
// 2.0.0 parts.
parsedSemVer, err := semver.Parse(clientVer)
if err != nil {
return nil, false
}

return &parsedUserAgent{
ParsedSemVer: *parsedSemVer,
clientName: clientName,
}, true
}

// userAgentMatchFn defines a match function that takes a parsed user agent and
// returns whether or not it matches some criteria.
type userAgentMatchFn func(*parsedUserAgent) bool

// userAgentToClientsFilter houses a function to use for matching a user agent
// along with the clients all user agents that match are mapped to.
type userAgentToClientsFilter struct {
matchFn userAgentMatchFn
clients []string
}

// matchesUserAgentMaxMinor returns a user agent matching function that returns
// true in the case the user agent matches the provided client name and major
// version and its minor version is less than or equal to the provided minor
// version.
func matchesUserAgentMaxMinor(clientName string, requiredMajor, maxMinor uint32) userAgentMatchFn {
return func(parsedUA *parsedUserAgent) bool {
return parsedUA.clientName == clientName &&
parsedUA.Major == requiredMajor &&
parsedUA.Minor <= maxMinor
}
}

// identifyMiningClients returns the possible mining client IDs for a given user
// agent or an error when the user agent is not supported.
func identifyMiningClients(userAgent string) ([]string, error) {
for re, clients := range miningClients {
if re.MatchString(userAgent) {
return clients, nil
parsedUA, ok := parseUserAgent(userAgent)
if ok {
for _, filter := range supportedClientUserAgents {
if filter.matchFn(parsedUA) {
return filter.clients, nil
}
}
}

Expand Down
53 changes: 34 additions & 19 deletions pool/minerid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@ import (
"testing"
)

// TestNewUserAgentRE ensures the mining client user agent regular-expression
// matching logic works as intended.
func TestNewUserAgentRE(t *testing.T) {
// perRETest describes a test to run against the same regular expression.
type perRETest struct {
// TestUserAgentMatching ensures the mining client user agent matching logic
// works as intended.
func TestUserAgentMatching(t *testing.T) {
// perClientTest describes a test to run against the same client.
type perClientTest struct {
clientUA string // user agent string to test
wantMatch bool // expected match result
}

// makePerRETests returns a series of tests for a variety of client UAs that
// are generated based on the provided parameters to help ensure the exact
// semantics that each test intends to test are actually what is being
// makePerClientTests returns a series of tests for a variety of client UAs
// that are generated based on the provided parameters to help ensure the
// exact semantics that each test intends to test are actually what is being
// tested.
makePerRETests := func(client string, major, minor uint32) []perRETest {
makePerClientTests := func(client string, major, minor uint32) []perClientTest {
p := fmt.Sprintf
pcmm := func(format string, a ...interface{}) string {
params := make([]interface{}, 0, len(a)+3)
Expand All @@ -32,7 +32,15 @@ func TestNewUserAgentRE(t *testing.T) {
params = append(params, a...)
return p(format, params...)
}
return []perRETest{

// Old minor revisions are allowed.
var tests []perClientTest
if minor > 0 {
test := perClientTest{p("%s/%d.%d.0", client, major, minor-1), true}
tests = append(tests, test)
}

return append(tests, []perClientTest{
// All patch versions including multi digit are allowed.
{pcmm("%s/%d.%d.0"), true},
{pcmm("%s/%d.%d.1"), true},
Expand Down Expand Up @@ -116,7 +124,7 @@ func TestNewUserAgentRE(t *testing.T) {
{p("%s/+justmeta", client), false},
{pcmm("%s/%d.%d.7+meta+meta"), false},
{pcmm("%s/%d.%d.7-whatever+meta+meta"), false},
}
}...)
}

tests := []struct {
Expand Down Expand Up @@ -147,17 +155,24 @@ func TestNewUserAgentRE(t *testing.T) {
}}

for _, test := range tests {
// Create the compiled regular expression as well as client UAs and
// expected results.
re := newUserAgentRE(test.clientName, test.major, test.minor)
perRETests := makePerRETests(test.clientName, test.major, test.minor)
// Create a match function for the provided data as well as client UAs
// and expected results.
matchFn := matchesUserAgentMaxMinor(test.clientName, test.major,
test.minor)
subTests := makePerClientTests(test.clientName, test.major, test.minor)

// Ensure all of the client UAs produce the expected match results.
for _, subTest := range perRETests {
gotMatch := re.MatchString(subTest.clientUA)
for _, subTest := range subTests {
// Attempt to parse and match against the user agent.
var gotMatch bool
if parsedUA, ok := parseUserAgent(subTest.clientUA); ok {
gotMatch = matchFn(parsedUA)
}

if gotMatch != subTest.wantMatch {
t.Errorf("%s: (ua: %q): unexpected match result -- got %v, want %v",
test.name, subTest.clientUA, gotMatch, subTest.wantMatch)
t.Errorf("%s: (ua: %q): unexpected match result -- got %v, "+
"want %v", test.name, subTest.clientUA, gotMatch,
subTest.wantMatch)
continue
}
}
Expand Down

0 comments on commit f1be8b1

Please sign in to comment.