From 1472308d679fe7ed165c203070335fd58024d4da Mon Sep 17 00:00:00 2001 From: Gavin Andresen Date: Tue, 19 Mar 2013 11:35:04 -0400 Subject: Some unit tests for CAlert --- src/test/alert_tests.cpp | 132 +++++++++++++++++++++++++++++++++++++++++++++++ src/test/data/alertTests | Bin 0 -> 1123 bytes 2 files changed, 132 insertions(+) create mode 100644 src/test/alert_tests.cpp create mode 100644 src/test/data/alertTests diff --git a/src/test/alert_tests.cpp b/src/test/alert_tests.cpp new file mode 100644 index 0000000000..c8d409db9e --- /dev/null +++ b/src/test/alert_tests.cpp @@ -0,0 +1,132 @@ +// +// Unit tests for alert system +// + +#include +#include + +#include "alert.h" +#include "serialize.h" +#include "util.h" + +BOOST_AUTO_TEST_SUITE(Alert_tests) + +#if 0 +// +// alertTests contains 7 alerts, generated with this code: +// (SignAndSave code not shown, alert signing key is secret) +// +{ + CAlert alert; + alert.nRelayUntil = 60; + alert.nExpiration = 24 * 60 * 60; + alert.nID = 1; + alert.nCancel = 0; // cancels previous messages up to this ID number + alert.nMinVer = 0; // These versions are protocol versions + alert.nMaxVer = 70001; + alert.nPriority = 1; + alert.strComment = "Alert comment"; + alert.strStatusBar = "Alert 1"; + + SignAndSave(alert, "test/alertTests"); + + alert.setSubVer.insert(std::string("/Satoshi:0.1.0/")); + alert.strStatusBar = "Alert 1 for Satoshi 0.1.0"; + SignAndSave(alert, "test/alertTests"); + + alert.setSubVer.insert(std::string("/Satoshi:0.2.0/")); + alert.strStatusBar = "Alert 1 for Satoshi 0.1.0, 0.2.0"; + SignAndSave(alert, "test/alertTests"); + + alert.setSubVer.clear(); + alert.nID = 2; + alert.nCancel = 1; + alert.strStatusBar = "Alert 2, cancels 1"; + SignAndSave(alert, "test/alertTests"); + + alert.nExpiration += 60; + SignAndSave(alert, "test/alertTests"); + + alert.nMinVer = 11; + alert.nMaxVer = 22; + SignAndSave(alert, "test/alertTests"); + + alert.strStatusBar = "Alert 2 for Satoshi 0.1.0"; + alert.setSubVer.insert(std::string("/Satoshi:0.1.0/")); + SignAndSave(alert, "test/alertTests"); +} +#endif + + +std::vector +read_alerts(const std::string& filename) +{ + std::vector result; + + namespace fs = boost::filesystem; + fs::path testFile = fs::current_path() / "test" / "data" / filename; +#ifdef TEST_DATA_DIR + if (!fs::exists(testFile)) + { + testFile = fs::path(BOOST_PP_STRINGIZE(TEST_DATA_DIR)) / filename; + } +#endif + FILE* fp = fopen(testFile.string().c_str(), "rb"); + if (!fp) return result; + + + CAutoFile filein = CAutoFile(fp, SER_DISK, CLIENT_VERSION); + if (!filein) return result; + + try { + while (!feof(filein)) + { + CAlert alert; + filein >> alert; + result.push_back(alert); + } + } + catch (std::exception) { } + + return result; +} + +BOOST_AUTO_TEST_CASE(AlertApplies) +{ + SetMockTime(11); + + std::vector alerts = read_alerts("alertTests"); + + BOOST_FOREACH(const CAlert& alert, alerts) + { + BOOST_CHECK(alert.CheckSignature()); + } + // Matches: + BOOST_CHECK(alerts[0].AppliesTo(1, "")); + BOOST_CHECK(alerts[0].AppliesTo(70001, "")); + BOOST_CHECK(alerts[0].AppliesTo(1, "/Satoshi:11.11.11/")); + + BOOST_CHECK(alerts[1].AppliesTo(1, "/Satoshi:0.1.0/")); + BOOST_CHECK(alerts[1].AppliesTo(70001, "/Satoshi:0.1.0/")); + + BOOST_CHECK(alerts[2].AppliesTo(1, "/Satoshi:0.1.0/")); + BOOST_CHECK(alerts[2].AppliesTo(1, "/Satoshi:0.2.0/")); + + // Don't match: + BOOST_CHECK(!alerts[0].AppliesTo(-1, "")); + BOOST_CHECK(!alerts[0].AppliesTo(70002, "")); + + BOOST_CHECK(!alerts[1].AppliesTo(1, "")); + BOOST_CHECK(!alerts[1].AppliesTo(1, "Satoshi:0.1.0")); + BOOST_CHECK(!alerts[1].AppliesTo(1, "/Satoshi:0.1.0")); + BOOST_CHECK(!alerts[1].AppliesTo(1, "Satoshi:0.1.0/")); + BOOST_CHECK(!alerts[1].AppliesTo(-1, "/Satoshi:0.1.0/")); + BOOST_CHECK(!alerts[1].AppliesTo(70002, "/Satoshi:0.1.0/")); + BOOST_CHECK(!alerts[1].AppliesTo(1, "/Satoshi:0.2.0/")); + + BOOST_CHECK(!alerts[2].AppliesTo(1, "/Satoshi:0.3.0/")); + + SetMockTime(0); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/data/alertTests b/src/test/data/alertTests new file mode 100644 index 0000000000..126954e024 Binary files /dev/null and b/src/test/data/alertTests differ -- cgit v1.2.3 From e5f163a041d5a45ea72448e11cfc30abb16f10b6 Mon Sep 17 00:00:00 2001 From: Gavin Andresen Date: Tue, 19 Mar 2013 14:08:21 -0400 Subject: -alertnotify= Runs a shell command when an AppliesToMe() alert is received. %s in the string is replaced with the alert.strStatusBar message. --- src/alert.cpp | 33 +++++++++++++- src/alert.h | 2 +- src/init.cpp | 1 + src/test/alert_tests.cpp | 111 ++++++++++++++++++++++++++++++++++------------- src/test/data/alertTests | Bin 1123 -> 1283 bytes 5 files changed, 115 insertions(+), 32 deletions(-) diff --git a/src/alert.cpp b/src/alert.cpp index 48920629e2..4b029840dd 100644 --- a/src/alert.cpp +++ b/src/alert.cpp @@ -2,6 +2,9 @@ // Alert system // +#include +#include +#include #include #include @@ -165,7 +168,7 @@ CAlert CAlert::getAlertByHash(const uint256 &hash) return retval; } -bool CAlert::ProcessAlert() +bool CAlert::ProcessAlert(bool fThread) { if (!CheckSignature()) return false; @@ -229,9 +232,35 @@ bool CAlert::ProcessAlert() // Add to mapAlerts mapAlerts.insert(make_pair(GetHash(), *this)); - // Notify UI if it applies to me + // Notify UI and -alertnotify if it applies to me if(AppliesToMe()) + { uiInterface.NotifyAlertChanged(GetHash(), CT_NEW); + std::string strCmd = GetArg("-alertnotify", ""); + if (!strCmd.empty()) + { + // Alert text should be plain ascii coming from a trusted source, but to + // be safe we first strip anything not in safeChars, then add single quotes around + // the whole string before passing it to the shell: + std::string singleQuote("'"); + // safeChars chosen to allow simple messages/URLs/email addresses, but avoid anything + // even possibly remotely dangerous like & or > + std::string safeChars("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890 .,;_/:?@"); + std::string safeStatus; + for (std::string::size_type i = 0; i < strStatusBar.size(); i++) + { + if (safeChars.find(strStatusBar[i]) != std::string::npos) + safeStatus.push_back(strStatusBar[i]); + } + safeStatus = singleQuote+safeStatus+singleQuote; + boost::replace_all(strCmd, "%s", safeStatus); + + if (fThread) + boost::thread t(runCommand, strCmd); // thread runs free + else + runCommand(strCmd); + } + } } printf("accepted alert %d, AppliesToMe()=%d\n", nID, AppliesToMe()); diff --git a/src/alert.h b/src/alert.h index 7949c76972..25e140f573 100644 --- a/src/alert.h +++ b/src/alert.h @@ -91,7 +91,7 @@ public: bool AppliesToMe() const; bool RelayTo(CNode* pnode) const; bool CheckSignature() const; - bool ProcessAlert(); + bool ProcessAlert(bool fThread = true); /* * Get copy of (active) alert object by hash. Returns a null alert if it is not found. diff --git a/src/init.cpp b/src/init.cpp index 5b8436651a..b61d1b9358 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -301,6 +301,7 @@ std::string HelpMessage() " -rpcconnect= " + _("Send commands to node running on (default: 127.0.0.1)") + "\n" + " -blocknotify= " + _("Execute command when the best block changes (%s in cmd is replaced by block hash)") + "\n" + " -walletnotify= " + _("Execute command when a wallet transaction changes (%s in cmd is replaced by TxID)") + "\n" + + " -alertnotify= " + _("Execute command when a relevant alert is received (%s in cmd is replaced by message)") + "\n" + " -upgradewallet " + _("Upgrade wallet to latest format") + "\n" + " -keypool= " + _("Set key pool size to (default: 100)") + "\n" + " -rescan " + _("Rescan the block chain for missing wallet transactions") + "\n" + diff --git a/src/test/alert_tests.cpp b/src/test/alert_tests.cpp index c8d409db9e..f7a11376d3 100644 --- a/src/test/alert_tests.cpp +++ b/src/test/alert_tests.cpp @@ -4,13 +4,12 @@ #include #include +#include #include "alert.h" #include "serialize.h" #include "util.h" -BOOST_AUTO_TEST_SUITE(Alert_tests) - #if 0 // // alertTests contains 7 alerts, generated with this code: @@ -39,64 +38,89 @@ BOOST_AUTO_TEST_SUITE(Alert_tests) SignAndSave(alert, "test/alertTests"); alert.setSubVer.clear(); - alert.nID = 2; + ++alert.nID; alert.nCancel = 1; + alert.nPriority = 100; alert.strStatusBar = "Alert 2, cancels 1"; SignAndSave(alert, "test/alertTests"); alert.nExpiration += 60; + ++alert.nID; SignAndSave(alert, "test/alertTests"); + ++alert.nID; alert.nMinVer = 11; alert.nMaxVer = 22; SignAndSave(alert, "test/alertTests"); + ++alert.nID; alert.strStatusBar = "Alert 2 for Satoshi 0.1.0"; alert.setSubVer.insert(std::string("/Satoshi:0.1.0/")); SignAndSave(alert, "test/alertTests"); + + ++alert.nID; + alert.nMinVer = 0; + alert.nMaxVer = 999999; + alert.strStatusBar = "Evil Alert'; /bin/ls; echo '"; + alert.setSubVer.clear(); + SignAndSave(alert, "test/alertTests"); } #endif - -std::vector -read_alerts(const std::string& filename) +struct ReadAlerts { - std::vector result; - - namespace fs = boost::filesystem; - fs::path testFile = fs::current_path() / "test" / "data" / filename; -#ifdef TEST_DATA_DIR - if (!fs::exists(testFile)) + ReadAlerts() { - testFile = fs::path(BOOST_PP_STRINGIZE(TEST_DATA_DIR)) / filename; - } + std::string filename("alertTests"); + namespace fs = boost::filesystem; + fs::path testFile = fs::current_path() / "test" / "data" / filename; +#ifdef TEST_DATA_DIR + if (!fs::exists(testFile)) + { + testFile = fs::path(BOOST_PP_STRINGIZE(TEST_DATA_DIR)) / filename; + } #endif - FILE* fp = fopen(testFile.string().c_str(), "rb"); - if (!fp) return result; + FILE* fp = fopen(testFile.string().c_str(), "rb"); + if (!fp) return; - CAutoFile filein = CAutoFile(fp, SER_DISK, CLIENT_VERSION); - if (!filein) return result; + CAutoFile filein = CAutoFile(fp, SER_DISK, CLIENT_VERSION); + if (!filein) return; - try { - while (!feof(filein)) - { - CAlert alert; - filein >> alert; - result.push_back(alert); + try { + while (!feof(filein)) + { + CAlert alert; + filein >> alert; + alerts.push_back(alert); + } } + catch (std::exception) { } } - catch (std::exception) { } + ~ReadAlerts() { } + + static std::vector read_lines(boost::filesystem::path filepath) + { + std::vector result; + + std::ifstream f(filepath.string().c_str()); + std::string line; + while (std::getline(f,line)) + result.push_back(line); + + return result; + } + + std::vector alerts; +}; + +BOOST_FIXTURE_TEST_SUITE(Alert_tests, ReadAlerts) - return result; -} BOOST_AUTO_TEST_CASE(AlertApplies) { SetMockTime(11); - std::vector alerts = read_alerts("alertTests"); - BOOST_FOREACH(const CAlert& alert, alerts) { BOOST_CHECK(alert.CheckSignature()); @@ -129,4 +153,33 @@ BOOST_AUTO_TEST_CASE(AlertApplies) SetMockTime(0); } + +// This uses sh 'echo' to test the -alertnotify function, writing to a +// /tmp file. So skip it on Windows: +#ifndef WIN32 +BOOST_AUTO_TEST_CASE(AlertNotify) +{ + SetMockTime(11); + + boost::filesystem::path temp = GetTempPath() / "alertnotify.txt"; + boost::filesystem::remove(temp); + + mapArgs["-alertnotify"] = std::string("echo %s >> ") + temp.string(); + + BOOST_FOREACH(CAlert alert, alerts) + alert.ProcessAlert(false); + + std::vector r = read_lines(temp); + BOOST_CHECK_EQUAL(r.size(), 4u); + BOOST_CHECK_EQUAL(r[0], "Alert 1"); + BOOST_CHECK_EQUAL(r[1], "Alert 2, cancels 1"); + BOOST_CHECK_EQUAL(r[2], "Alert 2, cancels 1"); + BOOST_CHECK_EQUAL(r[3], "Evil Alert; /bin/ls; echo "); // single-quotes should be removed + + boost::filesystem::remove(temp); + + SetMockTime(0); +} +#endif + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/data/alertTests b/src/test/data/alertTests index 126954e024..7fc4528961 100644 Binary files a/src/test/data/alertTests and b/src/test/data/alertTests differ -- cgit v1.2.3 From 3d9d2d423bbdf97ef729b8c4a2dddc30a01e1415 Mon Sep 17 00:00:00 2001 From: Gavin Andresen Date: Thu, 21 Mar 2013 10:08:21 -0400 Subject: Recommend alertnotify --- src/bitcoinrpc.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bitcoinrpc.cpp b/src/bitcoinrpc.cpp index 4a6cc42efc..b6d8de4a18 100644 --- a/src/bitcoinrpc.cpp +++ b/src/bitcoinrpc.cpp @@ -769,7 +769,9 @@ void ThreadRPCServer2(void* parg) "rpcpassword=%s\n" "(you do not need to remember this password)\n" "The username and password MUST NOT be the same.\n" - "If the file does not exist, create it with owner-readable-only file permissions.\n"), + "If the file does not exist, create it with owner-readable-only file permissions.\n" + "It is also recommended to set alertnotify so you are notified of problems;\n" + "for example: alertnotify=echo %%s | mail -s \"Bitcoin Alert\" admin@foo.com\n"), strWhatAmI.c_str(), GetConfigFile().string().c_str(), EncodeBase58(&rand_pwd[0],&rand_pwd[0]+32).c_str()), -- cgit v1.2.3