From e3e3d226d7a7e07f4320f416d81c1bf517afaa68 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Fri, 13 Oct 2023 23:12:24 -0500 Subject: [PATCH] pool: Rework client user agent id logic. 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. --- pool/minerid.go | 109 ++++++++++++++++++++++++++++--------------- pool/minerid_test.go | 53 +++++++++++++-------- 2 files changed, 105 insertions(+), 57 deletions(-) diff --git a/pool/minerid.go b/pool/minerid.go index bcc9845c..20c19587 100644 --- a/pool/minerid.go +++ b/pool/minerid.go @@ -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 + } } } diff --git a/pool/minerid_test.go b/pool/minerid_test.go index a0127258..c01f0d9e 100644 --- a/pool/minerid_test.go +++ b/pool/minerid_test.go @@ -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) @@ -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}, @@ -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 { @@ -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 } }