Skip to content

Commit 28c6e8d

Browse files
committed
Merge #10408: Net: Improvements to Tor control port parser
49a199b torcontrol: Handle escapes in Tor QuotedStrings (Jack Grigg) 0182a11 torcontrol: Log invalid parameters in Tor reply strings where meaningful (Jack Grigg) 0b6f40d torcontrol: Check for reading errors in ReadBinaryFile (Jack Grigg) d63677b torcontrol: Fix ParseTorReplyMapping (Jack Grigg) 29f3c20 torcontrol: Add unit tests for Tor reply parsers (Jack Grigg) d8e03c0 torcontrol: Improve comments (Jack Grigg) Tree-SHA512: aa3ce8072d20299b38c4ba9471af7fab1f5df096c237bf40a96ee9274a357f7366f95ced0cc80f8da1f22f6455a1a8e68bad9a5ff71817eef3397b6aefcbc7ae
2 parents 962cd3f + 49a199b commit 28c6e8d

File tree

3 files changed

+275
-7
lines changed

3 files changed

+275
-7
lines changed

src/Makefile.test.include

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ BITCOIN_TESTS =\
7878
test/testutil.cpp \
7979
test/testutil.h \
8080
test/timedata_tests.cpp \
81+
test/torcontrol_tests.cpp \
8182
test/transaction_tests.cpp \
8283
test/txvalidationcache_tests.cpp \
8384
test/versionbits_tests.cpp \

src/test/torcontrol_tests.cpp

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// Copyright (c) 2017 The Zcash developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
//
5+
#include "test/test_bitcoin.h"
6+
#include "torcontrol.cpp"
7+
8+
#include <boost/test/unit_test.hpp>
9+
10+
11+
BOOST_FIXTURE_TEST_SUITE(torcontrol_tests, BasicTestingSetup)
12+
13+
void CheckSplitTorReplyLine(std::string input, std::string command, std::string args)
14+
{
15+
BOOST_TEST_MESSAGE(std::string("CheckSplitTorReplyLine(") + input + ")");
16+
auto ret = SplitTorReplyLine(input);
17+
BOOST_CHECK_EQUAL(ret.first, command);
18+
BOOST_CHECK_EQUAL(ret.second, args);
19+
}
20+
21+
BOOST_AUTO_TEST_CASE(util_SplitTorReplyLine)
22+
{
23+
// Data we should receive during normal usage
24+
CheckSplitTorReplyLine(
25+
"PROTOCOLINFO PIVERSION",
26+
"PROTOCOLINFO", "PIVERSION");
27+
CheckSplitTorReplyLine(
28+
"AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"/home/x/.tor/control_auth_cookie\"",
29+
"AUTH", "METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"/home/x/.tor/control_auth_cookie\"");
30+
CheckSplitTorReplyLine(
31+
"AUTH METHODS=NULL",
32+
"AUTH", "METHODS=NULL");
33+
CheckSplitTorReplyLine(
34+
"AUTH METHODS=HASHEDPASSWORD",
35+
"AUTH", "METHODS=HASHEDPASSWORD");
36+
CheckSplitTorReplyLine(
37+
"VERSION Tor=\"0.2.9.8 (git-a0df013ea241b026)\"",
38+
"VERSION", "Tor=\"0.2.9.8 (git-a0df013ea241b026)\"");
39+
CheckSplitTorReplyLine(
40+
"AUTHCHALLENGE SERVERHASH=aaaa SERVERNONCE=bbbb",
41+
"AUTHCHALLENGE", "SERVERHASH=aaaa SERVERNONCE=bbbb");
42+
43+
// Other valid inputs
44+
CheckSplitTorReplyLine("COMMAND", "COMMAND", "");
45+
CheckSplitTorReplyLine("COMMAND SOME ARGS", "COMMAND", "SOME ARGS");
46+
47+
// These inputs are valid because PROTOCOLINFO accepts an OtherLine that is
48+
// just an OptArguments, which enables multiple spaces to be present
49+
// between the command and arguments.
50+
CheckSplitTorReplyLine("COMMAND ARGS", "COMMAND", " ARGS");
51+
CheckSplitTorReplyLine("COMMAND EVEN+more ARGS", "COMMAND", " EVEN+more ARGS");
52+
}
53+
54+
void CheckParseTorReplyMapping(std::string input, std::map<std::string,std::string> expected)
55+
{
56+
BOOST_TEST_MESSAGE(std::string("CheckParseTorReplyMapping(") + input + ")");
57+
auto ret = ParseTorReplyMapping(input);
58+
BOOST_CHECK_EQUAL(ret.size(), expected.size());
59+
auto r_it = ret.begin();
60+
auto e_it = expected.begin();
61+
while (r_it != ret.end() && e_it != expected.end()) {
62+
BOOST_CHECK_EQUAL(r_it->first, e_it->first);
63+
BOOST_CHECK_EQUAL(r_it->second, e_it->second);
64+
r_it++;
65+
e_it++;
66+
}
67+
}
68+
69+
BOOST_AUTO_TEST_CASE(util_ParseTorReplyMapping)
70+
{
71+
// Data we should receive during normal usage
72+
CheckParseTorReplyMapping(
73+
"METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"/home/x/.tor/control_auth_cookie\"", {
74+
{"METHODS", "COOKIE,SAFECOOKIE"},
75+
{"COOKIEFILE", "/home/x/.tor/control_auth_cookie"},
76+
});
77+
CheckParseTorReplyMapping(
78+
"METHODS=NULL", {
79+
{"METHODS", "NULL"},
80+
});
81+
CheckParseTorReplyMapping(
82+
"METHODS=HASHEDPASSWORD", {
83+
{"METHODS", "HASHEDPASSWORD"},
84+
});
85+
CheckParseTorReplyMapping(
86+
"Tor=\"0.2.9.8 (git-a0df013ea241b026)\"", {
87+
{"Tor", "0.2.9.8 (git-a0df013ea241b026)"},
88+
});
89+
CheckParseTorReplyMapping(
90+
"SERVERHASH=aaaa SERVERNONCE=bbbb", {
91+
{"SERVERHASH", "aaaa"},
92+
{"SERVERNONCE", "bbbb"},
93+
});
94+
CheckParseTorReplyMapping(
95+
"ServiceID=exampleonion1234", {
96+
{"ServiceID", "exampleonion1234"},
97+
});
98+
CheckParseTorReplyMapping(
99+
"PrivateKey=RSA1024:BLOB", {
100+
{"PrivateKey", "RSA1024:BLOB"},
101+
});
102+
CheckParseTorReplyMapping(
103+
"ClientAuth=bob:BLOB", {
104+
{"ClientAuth", "bob:BLOB"},
105+
});
106+
107+
// Other valid inputs
108+
CheckParseTorReplyMapping(
109+
"Foo=Bar=Baz Spam=Eggs", {
110+
{"Foo", "Bar=Baz"},
111+
{"Spam", "Eggs"},
112+
});
113+
CheckParseTorReplyMapping(
114+
"Foo=\"Bar=Baz\"", {
115+
{"Foo", "Bar=Baz"},
116+
});
117+
CheckParseTorReplyMapping(
118+
"Foo=\"Bar Baz\"", {
119+
{"Foo", "Bar Baz"},
120+
});
121+
122+
// Escapes
123+
CheckParseTorReplyMapping(
124+
"Foo=\"Bar\\ Baz\"", {
125+
{"Foo", "Bar Baz"},
126+
});
127+
CheckParseTorReplyMapping(
128+
"Foo=\"Bar\\Baz\"", {
129+
{"Foo", "BarBaz"},
130+
});
131+
CheckParseTorReplyMapping(
132+
"Foo=\"Bar\\@Baz\"", {
133+
{"Foo", "Bar@Baz"},
134+
});
135+
CheckParseTorReplyMapping(
136+
"Foo=\"Bar\\\"Baz\" Spam=\"\\\"Eggs\\\"\"", {
137+
{"Foo", "Bar\"Baz"},
138+
{"Spam", "\"Eggs\""},
139+
});
140+
CheckParseTorReplyMapping(
141+
"Foo=\"Bar\\\\Baz\"", {
142+
{"Foo", "Bar\\Baz"},
143+
});
144+
145+
// C escapes
146+
CheckParseTorReplyMapping(
147+
"Foo=\"Bar\\nBaz\\t\" Spam=\"\\rEggs\" Octals=\"\\1a\\11\\17\\18\\81\\377\\378\\400\\2222\" Final=Check", {
148+
{"Foo", "Bar\nBaz\t"},
149+
{"Spam", "\rEggs"},
150+
{"Octals", "\1a\11\17\1" "881\377\37" "8\40" "0\222" "2"},
151+
{"Final", "Check"},
152+
});
153+
CheckParseTorReplyMapping(
154+
"Valid=Mapping Escaped=\"Escape\\\\\"", {
155+
{"Valid", "Mapping"},
156+
{"Escaped", "Escape\\"},
157+
});
158+
CheckParseTorReplyMapping(
159+
"Valid=Mapping Bare=\"Escape\\\"", {});
160+
CheckParseTorReplyMapping(
161+
"OneOctal=\"OneEnd\\1\" TwoOctal=\"TwoEnd\\11\"", {
162+
{"OneOctal", "OneEnd\1"},
163+
{"TwoOctal", "TwoEnd\11"},
164+
});
165+
166+
// Special handling for null case
167+
// (needed because string comparison reads the null as end-of-string)
168+
BOOST_TEST_MESSAGE(std::string("CheckParseTorReplyMapping(Null=\"\\0\")"));
169+
auto ret = ParseTorReplyMapping("Null=\"\\0\"");
170+
BOOST_CHECK_EQUAL(ret.size(), 1);
171+
auto r_it = ret.begin();
172+
BOOST_CHECK_EQUAL(r_it->first, "Null");
173+
BOOST_CHECK_EQUAL(r_it->second.size(), 1);
174+
BOOST_CHECK_EQUAL(r_it->second[0], '\0');
175+
176+
// A more complex valid grammar. PROTOCOLINFO accepts a VersionLine that
177+
// takes a key=value pair followed by an OptArguments, making this valid.
178+
// Because an OptArguments contains no semantic data, there is no point in
179+
// parsing it.
180+
CheckParseTorReplyMapping(
181+
"SOME=args,here MORE optional=arguments here", {
182+
{"SOME", "args,here"},
183+
});
184+
185+
// Inputs that are effectively invalid under the target grammar.
186+
// PROTOCOLINFO accepts an OtherLine that is just an OptArguments, which
187+
// would make these inputs valid. However,
188+
// - This parser is never used in that situation, because the
189+
// SplitTorReplyLine parser enables OtherLine to be skipped.
190+
// - Even if these were valid, an OptArguments contains no semantic data,
191+
// so there is no point in parsing it.
192+
CheckParseTorReplyMapping("ARGS", {});
193+
CheckParseTorReplyMapping("MORE ARGS", {});
194+
CheckParseTorReplyMapping("MORE ARGS", {});
195+
CheckParseTorReplyMapping("EVEN more=ARGS", {});
196+
CheckParseTorReplyMapping("EVEN+more ARGS", {});
197+
}
198+
199+
BOOST_AUTO_TEST_SUITE_END()

src/torcontrol.cpp

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright (c) 2015-2016 The Bitcoin Core developers
2+
// Copyright (c) 2017 The Zcash developers
23
// Distributed under the MIT software license, see the accompanying
34
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
45

@@ -249,6 +250,8 @@ bool TorControlConnection::Command(const std::string &cmd, const ReplyHandlerCB&
249250

250251
/* Split reply line in the form 'AUTH METHODS=...' into a type
251252
* 'AUTH' and arguments 'METHODS=...'.
253+
* Grammar is implicitly defined in https://spec.torproject.org/control-spec by
254+
* the server reply formats for PROTOCOLINFO (S3.21) and AUTHCHALLENGE (S3.24).
252255
*/
253256
static std::pair<std::string,std::string> SplitTorReplyLine(const std::string &s)
254257
{
@@ -264,35 +267,85 @@ static std::pair<std::string,std::string> SplitTorReplyLine(const std::string &s
264267
}
265268

266269
/** Parse reply arguments in the form 'METHODS=COOKIE,SAFECOOKIE COOKIEFILE=".../control_auth_cookie"'.
270+
* Returns a map of keys to values, or an empty map if there was an error.
271+
* Grammar is implicitly defined in https://spec.torproject.org/control-spec by
272+
* the server reply formats for PROTOCOLINFO (S3.21), AUTHCHALLENGE (S3.24),
273+
* and ADD_ONION (S3.27). See also sections 2.1 and 2.3.
267274
*/
268275
static std::map<std::string,std::string> ParseTorReplyMapping(const std::string &s)
269276
{
270277
std::map<std::string,std::string> mapping;
271278
size_t ptr=0;
272279
while (ptr < s.size()) {
273280
std::string key, value;
274-
while (ptr < s.size() && s[ptr] != '=') {
281+
while (ptr < s.size() && s[ptr] != '=' && s[ptr] != ' ') {
275282
key.push_back(s[ptr]);
276283
++ptr;
277284
}
278285
if (ptr == s.size()) // unexpected end of line
279286
return std::map<std::string,std::string>();
287+
if (s[ptr] == ' ') // The remaining string is an OptArguments
288+
break;
280289
++ptr; // skip '='
281290
if (ptr < s.size() && s[ptr] == '"') { // Quoted string
282-
++ptr; // skip '='
291+
++ptr; // skip opening '"'
283292
bool escape_next = false;
284-
while (ptr < s.size() && (!escape_next && s[ptr] != '"')) {
285-
escape_next = (s[ptr] == '\\');
293+
while (ptr < s.size() && (escape_next || s[ptr] != '"')) {
294+
// Repeated backslashes must be interpreted as pairs
295+
escape_next = (s[ptr] == '\\' && !escape_next);
286296
value.push_back(s[ptr]);
287297
++ptr;
288298
}
289299
if (ptr == s.size()) // unexpected end of line
290300
return std::map<std::string,std::string>();
291301
++ptr; // skip closing '"'
292-
/* TODO: unescape value - according to the spec this depends on the
293-
* context, some strings use C-LogPrintf style escape codes, some
294-
* don't. So may be better handled at the call site.
302+
/**
303+
* Unescape value. Per https://spec.torproject.org/control-spec section 2.1.1:
304+
*
305+
* For future-proofing, controller implementors MAY use the following
306+
* rules to be compatible with buggy Tor implementations and with
307+
* future ones that implement the spec as intended:
308+
*
309+
* Read \n \t \r and \0 ... \377 as C escapes.
310+
* Treat a backslash followed by any other character as that character.
295311
*/
312+
std::string escaped_value;
313+
for (size_t i = 0; i < value.size(); ++i) {
314+
if (value[i] == '\\') {
315+
// This will always be valid, because if the QuotedString
316+
// ended in an odd number of backslashes, then the parser
317+
// would already have returned above, due to a missing
318+
// terminating double-quote.
319+
++i;
320+
if (value[i] == 'n') {
321+
escaped_value.push_back('\n');
322+
} else if (value[i] == 't') {
323+
escaped_value.push_back('\t');
324+
} else if (value[i] == 'r') {
325+
escaped_value.push_back('\r');
326+
} else if ('0' <= value[i] && value[i] <= '7') {
327+
size_t j;
328+
// Octal escape sequences have a limit of three octal digits,
329+
// but terminate at the first character that is not a valid
330+
// octal digit if encountered sooner.
331+
for (j = 1; j < 3 && (i+j) < value.size() && '0' <= value[i+j] && value[i+j] <= '7'; ++j) {}
332+
// Tor restricts first digit to 0-3 for three-digit octals.
333+
// A leading digit of 4-7 would therefore be interpreted as
334+
// a two-digit octal.
335+
if (j == 3 && value[i] > '3') {
336+
j--;
337+
}
338+
escaped_value.push_back(strtol(value.substr(i, j).c_str(), NULL, 8));
339+
// Account for automatic incrementing at loop end
340+
i += j - 1;
341+
} else {
342+
escaped_value.push_back(value[i]);
343+
}
344+
} else {
345+
escaped_value.push_back(value[i]);
346+
}
347+
}
348+
value = escaped_value;
296349
} else { // Unquoted value. Note that values can contain '=' at will, just no spaces
297350
while (ptr < s.size() && s[ptr] != ' ') {
298351
value.push_back(s[ptr]);
@@ -322,6 +375,10 @@ static std::pair<bool,std::string> ReadBinaryFile(const fs::path &filename, size
322375
char buffer[128];
323376
size_t n;
324377
while ((n=fread(buffer, 1, sizeof(buffer), f)) > 0) {
378+
// Check for reading errors so we don't return any data if we couldn't
379+
// read the entire file (or up to maxsize)
380+
if (ferror(f))
381+
return std::make_pair(false,"");
325382
retval.append(buffer, buffer+n);
326383
if (retval.size() > maxsize)
327384
break;
@@ -438,6 +495,13 @@ void TorController::add_onion_cb(TorControlConnection& _conn, const TorControlRe
438495
if ((i = m.find("PrivateKey")) != m.end())
439496
private_key = i->second;
440497
}
498+
if (service_id.empty()) {
499+
LogPrintf("tor: Error parsing ADD_ONION parameters:\n");
500+
for (const std::string &s : reply.lines) {
501+
LogPrintf(" %s\n", SanitizeString(s));
502+
}
503+
return;
504+
}
441505
service = LookupNumeric(std::string(service_id+".onion").c_str(), GetListenPort());
442506
LogPrintf("tor: Got service ID %s, advertising service %s\n", service_id, service.ToString());
443507
if (WriteBinaryFile(GetPrivateKeyFile(), private_key)) {
@@ -515,6 +579,10 @@ void TorController::authchallenge_cb(TorControlConnection& _conn, const TorContr
515579
std::pair<std::string,std::string> l = SplitTorReplyLine(reply.lines[0]);
516580
if (l.first == "AUTHCHALLENGE") {
517581
std::map<std::string,std::string> m = ParseTorReplyMapping(l.second);
582+
if (m.empty()) {
583+
LogPrintf("tor: Error parsing AUTHCHALLENGE parameters: %s\n", SanitizeString(l.second));
584+
return;
585+
}
518586
std::vector<uint8_t> serverHash = ParseHex(m["SERVERHASH"]);
519587
std::vector<uint8_t> serverNonce = ParseHex(m["SERVERNONCE"]);
520588
LogPrint(BCLog::TOR, "tor: AUTHCHALLENGE ServerHash %s ServerNonce %s\n", HexStr(serverHash), HexStr(serverNonce));

0 commit comments

Comments
 (0)