diff --git a/src/clientversion.h b/src/clientversion.h --- a/src/clientversion.h +++ b/src/clientversion.h @@ -48,11 +48,10 @@ 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, + const std::vector &comments); #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); @@ -60,15 +60,14 @@ } /** - * Format the subversion field according to BIP 14 spec using given userAgent. + * Format the subversion field according to BIP 14 spec. * (https://github.com/bitcoin/bips/blob/master/bip-0014.mediawiki) */ -std::string -FormatSubVersionUserAgent(const std::string &userAgent, - const std::vector &comments) { +std::string FormatUserAgent(const std::string &name, const std::string &version, + const std::vector &comments) { std::ostringstream ss; ss << "/"; - ss << userAgent; + ss << name << ":" << version; if (!comments.empty()) { std::vector::const_iterator it(comments.begin()); ss << "(" << *it; @@ -80,14 +79,3 @@ ss << "/"; return ss.str(); } - -/** - * Format the subversion field according to BIP 14 spec - * (https://github.com/bitcoin/bips/blob/master/bip-0014.mediawiki) - */ -std::string FormatSubVersion(const std::string &name, int nClientVersion, - const std::vector &comments) { - std::ostringstream ss; - ss << name << ":" << FormatVersion(nClientVersion); - return FormatSubVersionUserAgent(ss.str(), comments); -} 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,21 @@ } 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 strSubVersion = - FormatSubVersion(CLIENT_NAME, CLIENT_VERSION, uacomments); + FormatUserAgent(client_name, client_version, 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,10 @@ 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)); - return subversion; + // Size compliance is checked at startup, it is safe to not check it again + return FormatUserAgent(client_name, client_version, 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,26 +1839,14 @@ "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_FormatSubVersionUserAgent) { +BOOST_AUTO_TEST_CASE(test_FormatUserAgent) { std::vector comments; comments.push_back(std::string("comment1")); std::vector comments2; @@ -1868,12 +1856,12 @@ std::string("Comment2; .,_?@-; !\"#$%&'()*+/<=>[]\\^`{|}~"), SAFE_CHARS_UA_COMMENT)); BOOST_CHECK_EQUAL( - FormatSubVersionUserAgent("Test:0.9.99", std::vector()), + FormatUserAgent("Test", "0.9.99", std::vector()), std::string("/Test:0.9.99/")); - BOOST_CHECK_EQUAL(FormatSubVersionUserAgent("Test:0.9.99", comments), + BOOST_CHECK_EQUAL(FormatUserAgent("Test", "0.9.99", comments), std::string("/Test:0.9.99(comment1)/")); BOOST_CHECK_EQUAL( - FormatSubVersionUserAgent("Test:0.9.99", comments2), + FormatUserAgent("Test", "0.9.99", comments2), std::string("/Test:0.9.99(comment1; Comment2; .,_?@-; )/")); } 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 -uaclientname and -uaclientversion 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()