diff --git a/src/clientversion.h b/src/clientversion.h --- a/src/clientversion.h +++ b/src/clientversion.h @@ -48,11 +48,12 @@ extern const std::string CLIENT_NAME; extern const std::string CLIENT_BUILD; +std::string FormatVersion(int nVersion); 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, + const std::string &version); #endif // WINDRES_PREPROC diff --git a/src/clientversion.cpp b/src/clientversion.cpp --- a/src/clientversion.cpp +++ b/src/clientversion.cpp @@ -44,7 +44,7 @@ const std::string CLIENT_BUILD(BUILD_DESC BUILD_SUFFIX); -static std::string FormatVersion(int nVersion) { +std::string FormatVersion(int nVersion) { if (nVersion % 100 == 0) { return strprintf("%d.%d.%d", nVersion / 1000000, (nVersion / 10000) % 100, (nVersion / 100) % 100); @@ -82,12 +82,11 @@ } /** - * 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, + const std::string &version) { std::ostringstream ss; - ss << name << ":" << FormatVersion(nClientVersion); - return FormatSubVersionUserAgent(ss.str(), comments); + ss << name << ":" << version; + return ss.str(); } diff --git a/src/init.cpp b/src/init.cpp --- a/src/init.cpp +++ b/src/init.cpp @@ -1030,6 +1030,11 @@ argsman.AddArg("-uacomment=", "Append comment to the user agent string", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST); + argsman.AddArg("-uaclientname=", "Set user agent client name", + ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST); + argsman.AddArg("-uaclientversion=", + "Set user agent client version", ArgsManager::ALLOW_ANY, + OptionsCategory::DEBUG_TEST); SetupChainParamsBaseOptions(argsman); @@ -2299,8 +2304,22 @@ } uacomments.push_back(cmt); } + const std::string client_name = args.GetArg("-uaclientname", CLIENT_NAME); + const std::string client_version = + args.GetArg("-uaclientversion", FormatVersion(CLIENT_VERSION)); + if (client_name != SanitizeString(client_name, SAFE_CHARS_UA_COMMENT)) { + return InitError(strprintf( + _("-uaclientname (%s) contains invalid characters."), client_name)); + } + if (client_version != + SanitizeString(client_version, SAFE_CHARS_UA_COMMENT)) { + return InitError( + strprintf(_("-uaclientversion (%s) contains invalid characters."), + client_version)); + } + const std::string 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 @@ -3212,9 +3212,11 @@ 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); + const std::string client_name = gArgs.GetArg("-uaclientname", CLIENT_NAME); + const std::string client_version = + gArgs.GetArg("-uaclientversion", FormatVersion(CLIENT_VERSION)); + const std::string 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 @@ -1839,23 +1839,16 @@ "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_FormatVersion) { + BOOST_CHECK_EQUAL(FormatVersion(98700), std::string("0.9.87")); + BOOST_CHECK_EQUAL(FormatVersion(98701), std::string("0.9.87.1")); + BOOST_CHECK_EQUAL(FormatVersion(9098700), std::string("9.9.87")); + BOOST_CHECK_EQUAL(FormatVersion(9098701), std::string("9.9.87.1")); +} + +BOOST_AUTO_TEST_CASE(test_FormatUserAgent) { + BOOST_CHECK_EQUAL(FormatUserAgent("Test", "0.9.99"), + 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,94 @@ +#!/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 -uaclientname and -uaclientversion") + default_useragent = self.nodes[0].getnetworkinfo()["subversion"] + expected = "/Bitcoin ABC:" + assert_equal(default_useragent[:len(expected)], expected) + default_version = default_useragent[default_useragent.index(':') + 1:] + default_version = default_version[:default_version.index('/')] + + self.restart_node(0, ["-uaclientname=Foo Client"]) + foo_ua = self.nodes[0].getnetworkinfo()["subversion"] + expected = f"/Foo Client:{default_version}" + assert_equal(foo_ua[:len(expected)], expected) + + self.restart_node(0, ["-uaclientversion=123.45"]) + foo_ua = self.nodes[0].getnetworkinfo()["subversion"] + expected = "/Bitcoin ABC:123.45" + assert_equal(foo_ua[:len(expected)], expected) + + self.log.info( + "non-numeric version allowed (although not recommended in BIP14)") + self.restart_node(0, ["-uaclientversion=Version Two"]) + foo_ua = self.nodes[0].getnetworkinfo()["subversion"] + expected = "/Bitcoin ABC:Version Two" + assert_equal(foo_ua[:len(expected)], expected) + + self.log.info("test -uaclient doesn't break -uacomment") + self.restart_node(0, ["-uaclientname=Bar Client", + "-uaclientversion=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 -uaclientname max length") + self.stop_node(0) + 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( + ["-uaclientname=" + "a" * 256], expected, match=ErrorMatch.FULL_REGEX) + + self.log.info("test -uaclientversion 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( + ["-uaclientversion=" + "a" * 256], expected, match=ErrorMatch.FULL_REGEX) + + self.log.info("test -uaclientname and -uaclientversion 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( + ["-uaclientname=" + "a" * 128, "-uaclientversion=" + "a" * 128], expected, match=ErrorMatch.FULL_REGEX) + + self.log.info( + "test -uaclientname and -uaclientversion invalid characters") + for invalid_char in ['/', ':', '(', ')', '*', '!', '₿', '🏃']: + # for client name + expected = r"Error: -uaclientname \(" + \ + re.escape(invalid_char) + r"\) contains invalid characters\." + self.nodes[0].assert_start_raises_init_error( + ["-uaclientname=" + invalid_char], + expected, match=ErrorMatch.FULL_REGEX) + # for client version + expected = r"Error: -uaclientversion \(" + \ + re.escape(invalid_char) + r"\) contains invalid characters\." + self.nodes[0].assert_start_raises_init_error( + ["-uaclientversion=" + invalid_char], + expected, match=ErrorMatch.FULL_REGEX) + # for both + expected = r"Error: -uaclientname \(" + \ + re.escape(invalid_char) + r"\) contains invalid characters\." + self.nodes[0].assert_start_raises_init_error( + ["-uaclientname=" + invalid_char, + "-uaclientversion=" + invalid_char], + expected, match=ErrorMatch.FULL_REGEX) + + +if __name__ == '__main__': + UseragentTest().main()