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 } }