diff --git a/src/clientversion.h b/src/clientversion.h --- a/src/clientversion.h +++ b/src/clientversion.h @@ -51,8 +51,7 @@ std::string FormatFullVersion(); std::string FormatSubVersionUserAgent(const std::string &userAgent, const std::vector &comments); -std::string FormatSubVersion(const std::string &name, int nClientVersion, - const std::vector &comments); +std::string FormatUserAgent(const std::string &name, int nClientVersion); #endif // WINDRES_PREPROC diff --git a/src/clientversion.cpp b/src/clientversion.cpp --- a/src/clientversion.cpp +++ b/src/clientversion.cpp @@ -82,12 +82,10 @@ } /** - * Format the subversion field according to BIP 14 spec - * (https://github.com/bitcoin/bips/blob/master/bip-0014.mediawiki) + * Format the BIP-14 user agent in the form of ClientName:Version */ -std::string FormatSubVersion(const std::string &name, int nClientVersion, - const std::vector &comments) { +std::string FormatUserAgent(const std::string &name, int nClientVersion) { std::ostringstream ss; ss << name << ":" << FormatVersion(nClientVersion); - return FormatSubVersionUserAgent(ss.str(), comments); + return ss.str(); } diff --git a/src/init.cpp b/src/init.cpp --- a/src/init.cpp +++ b/src/init.cpp @@ -1030,6 +1030,10 @@ argsman.AddArg("-uacomment=", "Append comment to the user agent string", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST); + argsman.AddArg("-uaclient=", + "Set user agent client name and version in the form of " + "ClientName:Version", + ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST); SetupChainParamsBaseOptions(argsman); @@ -2292,8 +2296,33 @@ } uacomments.push_back(cmt); } + std::string uaclient; + if (args.IsArgSet("-uaclient")) { + uaclient = args.GetArg("-uaclient", ""); + size_t colonpos = uaclient.find(':'); + if (std::string::npos == colonpos) { + return InitError(strprintf( + _("-uaclient (%s) must be of form ClientName:Version."), + uaclient)); + } + const std::string name = uaclient.substr(0, colonpos); + const std::string version = + uaclient.substr(colonpos + 1, uaclient.size() - colonpos - 1); + if (name != SanitizeString(name, SAFE_CHARS_UA_COMMENT)) { + return InitError(strprintf( + _("-uaclient client name (%s) contains invalid characters."), + name)); + } + if (version != SanitizeString(version, SAFE_CHARS_UA_COMMENT)) { + return InitError(strprintf(_("-uaclient client version (%s) " + "contains invalid characters."), + version)); + } + } else { + uaclient = FormatUserAgent(CLIENT_NAME, CLIENT_VERSION); + } const std::string strSubVersion = - FormatSubVersion(CLIENT_NAME, CLIENT_VERSION, uacomments); + FormatSubVersionUserAgent(uaclient, uacomments); if (strSubVersion.size() > MAX_SUBVERSION_LENGTH) { return InitError(strprintf( _("Total length of network version string (%i) exceeds maximum " diff --git a/src/net.cpp b/src/net.cpp --- a/src/net.cpp +++ b/src/net.cpp @@ -3154,9 +3154,13 @@ uacomments.push_back(cmt); } - // Size compliance is checked at startup, it is safe to not check it again - std::string subversion = - FormatSubVersion(CLIENT_NAME, CLIENT_VERSION, uacomments); + std::string uaclient; + if (gArgs.IsArgSet("-uaclient")) { + uaclient = gArgs.GetArg("-uaclient", ""); + } else { + uaclient = FormatUserAgent(CLIENT_NAME, CLIENT_VERSION); + } - return subversion; + // Size compliance is checked at startup, it is safe to not check it again + return FormatSubVersionUserAgent(uaclient, uacomments); } diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp --- a/src/test/util_tests.cpp +++ b/src/test/util_tests.cpp @@ -1833,23 +1833,9 @@ "Testing that normal newlines do not get indented.\nLike here."); } -BOOST_AUTO_TEST_CASE(test_FormatSubVersion) { - std::vector comments; - comments.push_back(std::string("comment1")); - std::vector comments2; - comments2.push_back(std::string("comment1")); - // Semicolon is discouraged but not forbidden by BIP-0014 - comments2.push_back(SanitizeString( - std::string("Comment2; .,_?@-; !\"#$%&'()*+/<=>[]\\^`{|}~"), - SAFE_CHARS_UA_COMMENT)); - BOOST_CHECK_EQUAL( - FormatSubVersion("Test", 99900, std::vector()), - std::string("/Test:0.9.99/")); - BOOST_CHECK_EQUAL(FormatSubVersion("Test", 99900, comments), - std::string("/Test:0.9.99(comment1)/")); - BOOST_CHECK_EQUAL( - FormatSubVersion("Test", 99900, comments2), - std::string("/Test:0.9.99(comment1; Comment2; .,_?@-; )/")); +BOOST_AUTO_TEST_CASE(test_FormatUserAgent) { + BOOST_CHECK_EQUAL(FormatUserAgent("Test", 99900), + std::string("Test:0.9.99")); } BOOST_AUTO_TEST_CASE(test_FormatSubVersionUserAgent) { diff --git a/test/functional/feature_uaclient.py b/test/functional/feature_uaclient.py new file mode 100755 --- /dev/null +++ b/test/functional/feature_uaclient.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test the -uaclient option.""" + +import re + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.test_node import ErrorMatch +from test_framework.util import assert_equal + + +class UseragentTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + + def run_test(self): + self.log.info("test -uaclient") + default_useragent = self.nodes[0].getnetworkinfo()["subversion"] + expected = "/Bitcoin ABC:" + assert_equal(default_useragent[:len(expected)], expected) + + self.restart_node(0, ["-uaclient=Foo Client:2000"]) + foo_ua = self.nodes[0].getnetworkinfo()["subversion"] + expected = "/Foo Client:2000" + assert_equal(foo_ua[:len(expected)], expected) + + self.log.info( + "non-numeric version allowed (although not recommended in BIP14)") + self.restart_node(0, ["-uaclient=Foo Client:Version Two"]) + foo_ua = self.nodes[0].getnetworkinfo()["subversion"] + expected = "/Foo Client:Version Two" + assert_equal(foo_ua[:len(expected)], expected) + + self.log.info("test -uaclient doesn't break -uacomment") + self.restart_node(0, ["-uaclient=Bar Client:3000", + "-uacomment=spam bacon and eggs"]) + bar_ua = self.nodes[0].getnetworkinfo()["subversion"] + expected = "/Bar Client:3000" + assert_equal(bar_ua[:len(expected)], expected) + assert "spam bacon and eggs" in bar_ua + + self.log.info("test -uaclient doesn't have form Name:Version") + self.stop_node(0) + expected = r"Error: -uaclient \(spam\) must be of form ClientName:Version\." + self.nodes[0].assert_start_raises_init_error( + ["-uaclient=spam"], expected, match=ErrorMatch.FULL_REGEX) + + self.log.info("test -uaclient multiple colons") + self.stop_node(0) + expected = r"Error: -uaclient client version \(2:3\) contains invalid characters\." + self.nodes[0].assert_start_raises_init_error( + ["-uaclient=spam:2:3"], expected, match=ErrorMatch.FULL_REGEX) + + self.log.info("test -uaclient max length") + expected = r"Error: Total length of network version string \([0-9]+\) exceeds maximum length \([0-9]+\)\. Reduce the number or size of uacomments\." + self.nodes[0].assert_start_raises_init_error( + ["-uaclient=" + "a" * 256 + ":3000"], expected, match=ErrorMatch.FULL_REGEX) + + self.log.info("test -uaclient invalid characters") + for invalid_char in ['/', '(', ')', '*', '!', '₿', '🏃']: + # for client name + expected = r"Error: -uaclient client name \(" + \ + re.escape(invalid_char) + r"\) contains invalid characters\." + self.nodes[0].assert_start_raises_init_error( + ["-uaclient=" + invalid_char + ":Version"], expected, match=ErrorMatch.FULL_REGEX) + # for client version + expected = r"Error: -uaclient client version \(" + \ + re.escape(invalid_char) + r"\) contains invalid characters\." + self.nodes[0].assert_start_raises_init_error( + ["-uaclient=Name:" + invalid_char], expected, match=ErrorMatch.FULL_REGEX) + + +if __name__ == '__main__': + UseragentTest().main()