diff --git a/src/init.cpp b/src/init.cpp --- a/src/init.cpp +++ b/src/init.cpp @@ -1030,6 +1030,8 @@ 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", + ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST); SetupChainParamsBaseOptions(argsman); @@ -2292,8 +2294,32 @@ } uacomments.push_back(cmt); } + if (args.IsArgSet("-uaclient")) { + const std::string ua = args.GetArg("-uaclient", ""); + size_t colonpos = ua.find(':'); + if (std::string::npos == colonpos) { + return InitError(strprintf( + _("-uaclient (%s) must be of form ClientName:Version."), ua)); + } + const std::string name = ua.substr(0, colonpos); + const std::string version = + ua.substr(colonpos + 1, ua.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)); + } + } const std::string strSubVersion = - FormatSubVersion(CLIENT_NAME, CLIENT_VERSION, uacomments); + args.IsArgSet("-uaclient") + ? FormatSubVersionUserAgent(args.GetArg("-uaclient", ""), + uacomments) + : FormatSubVersion(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 @@ -3155,8 +3155,9 @@ } // Size compliance is checked at startup, it is safe to not check it again - std::string subversion = - FormatSubVersion(CLIENT_NAME, CLIENT_VERSION, uacomments); - - return subversion; + if (gArgs.IsArgSet("-uaclient")) { + return FormatSubVersionUserAgent(gArgs.GetArg("-uaclient", ""), + uacomments); + } + return FormatSubVersion(CLIENT_NAME, CLIENT_VERSION, uacomments); } 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()