/workdir/bitcoin/src/i2p.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | // Copyright (c) 2020-2022 The Bitcoin Core 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 <chainparams.h> |
6 | | #include <common/args.h> |
7 | | #include <compat/compat.h> |
8 | | #include <compat/endian.h> |
9 | | #include <crypto/sha256.h> |
10 | | #include <i2p.h> |
11 | | #include <logging.h> |
12 | | #include <netaddress.h> |
13 | | #include <netbase.h> |
14 | | #include <random.h> |
15 | | #include <script/parsing.h> |
16 | | #include <sync.h> |
17 | | #include <tinyformat.h> |
18 | | #include <util/fs.h> |
19 | | #include <util/readwritefile.h> |
20 | | #include <util/sock.h> |
21 | | #include <util/strencodings.h> |
22 | | #include <util/threadinterrupt.h> |
23 | | |
24 | | #include <chrono> |
25 | | #include <memory> |
26 | | #include <stdexcept> |
27 | | #include <string> |
28 | | |
29 | | using util::Split; |
30 | | |
31 | | namespace i2p { |
32 | | |
33 | | /** |
34 | | * Swap Standard Base64 <-> I2P Base64. |
35 | | * Standard Base64 uses `+` and `/` as last two characters of its alphabet. |
36 | | * I2P Base64 uses `-` and `~` respectively. |
37 | | * So it is easy to detect in which one is the input and convert to the other. |
38 | | * @param[in] from Input to convert. |
39 | | * @return converted `from` |
40 | | */ |
41 | | static std::string SwapBase64(const std::string& from) |
42 | 0 | { |
43 | 0 | std::string to; |
44 | 0 | to.resize(from.size()); |
45 | 0 | for (size_t i = 0; i < from.size(); ++i) { Branch (45:24): [True: 0, False: 0]
|
46 | 0 | switch (from[i]) { |
47 | 0 | case '-': Branch (47:9): [True: 0, False: 0]
|
48 | 0 | to[i] = '+'; |
49 | 0 | break; |
50 | 0 | case '~': Branch (50:9): [True: 0, False: 0]
|
51 | 0 | to[i] = '/'; |
52 | 0 | break; |
53 | 0 | case '+': Branch (53:9): [True: 0, False: 0]
|
54 | 0 | to[i] = '-'; |
55 | 0 | break; |
56 | 0 | case '/': Branch (56:9): [True: 0, False: 0]
|
57 | 0 | to[i] = '~'; |
58 | 0 | break; |
59 | 0 | default: Branch (59:9): [True: 0, False: 0]
|
60 | 0 | to[i] = from[i]; |
61 | 0 | break; |
62 | 0 | } |
63 | 0 | } |
64 | 0 | return to; |
65 | 0 | } |
66 | | |
67 | | /** |
68 | | * Decode an I2P-style Base64 string. |
69 | | * @param[in] i2p_b64 I2P-style Base64 string. |
70 | | * @return decoded `i2p_b64` |
71 | | * @throw std::runtime_error if decoding fails |
72 | | */ |
73 | | static Binary DecodeI2PBase64(const std::string& i2p_b64) |
74 | 0 | { |
75 | 0 | const std::string& std_b64 = SwapBase64(i2p_b64); |
76 | 0 | auto decoded = DecodeBase64(std_b64); |
77 | 0 | if (!decoded) { Branch (77:9): [True: 0, False: 0]
|
78 | 0 | throw std::runtime_error(strprintf("Cannot decode Base64: \"%s\"", i2p_b64)); |
79 | 0 | } |
80 | 0 | return std::move(*decoded); |
81 | 0 | } |
82 | | |
83 | | /** |
84 | | * Derive the .b32.i2p address of an I2P destination (binary). |
85 | | * @param[in] dest I2P destination. |
86 | | * @return the address that corresponds to `dest` |
87 | | * @throw std::runtime_error if conversion fails |
88 | | */ |
89 | | static CNetAddr DestBinToAddr(const Binary& dest) |
90 | 0 | { |
91 | 0 | CSHA256 hasher; |
92 | 0 | hasher.Write(dest.data(), dest.size()); |
93 | 0 | unsigned char hash[CSHA256::OUTPUT_SIZE]; |
94 | 0 | hasher.Finalize(hash); |
95 | |
|
96 | 0 | CNetAddr addr; |
97 | 0 | const std::string addr_str = EncodeBase32(hash, false) + ".b32.i2p"; |
98 | 0 | if (!addr.SetSpecial(addr_str)) { Branch (98:9): [True: 0, False: 0]
|
99 | 0 | throw std::runtime_error(strprintf("Cannot parse I2P address: \"%s\"", addr_str)); |
100 | 0 | } |
101 | | |
102 | 0 | return addr; |
103 | 0 | } |
104 | | |
105 | | /** |
106 | | * Derive the .b32.i2p address of an I2P destination (I2P-style Base64). |
107 | | * @param[in] dest I2P destination. |
108 | | * @return the address that corresponds to `dest` |
109 | | * @throw std::runtime_error if conversion fails |
110 | | */ |
111 | | static CNetAddr DestB64ToAddr(const std::string& dest) |
112 | 0 | { |
113 | 0 | const Binary& decoded = DecodeI2PBase64(dest); |
114 | 0 | return DestBinToAddr(decoded); |
115 | 0 | } |
116 | | |
117 | | namespace sam { |
118 | | |
119 | | Session::Session(const fs::path& private_key_file, |
120 | | const Proxy& control_host, |
121 | | CThreadInterrupt* interrupt) |
122 | 0 | : m_private_key_file{private_key_file}, |
123 | 0 | m_control_host{control_host}, |
124 | 0 | m_interrupt{interrupt}, |
125 | 0 | m_transient{false} |
126 | 0 | { |
127 | 0 | } |
128 | | |
129 | | Session::Session(const Proxy& control_host, CThreadInterrupt* interrupt) |
130 | 0 | : m_control_host{control_host}, |
131 | 0 | m_interrupt{interrupt}, |
132 | 0 | m_transient{true} |
133 | 0 | { |
134 | 0 | } |
135 | | |
136 | | Session::~Session() |
137 | 0 | { |
138 | 0 | LOCK(m_mutex); |
139 | 0 | Disconnect(); |
140 | 0 | } |
141 | | |
142 | | bool Session::Listen(Connection& conn) |
143 | 0 | { |
144 | 0 | try { |
145 | 0 | LOCK(m_mutex); |
146 | 0 | CreateIfNotCreatedAlready(); |
147 | 0 | conn.me = m_my_addr; |
148 | 0 | conn.sock = StreamAccept(); |
149 | 0 | return true; |
150 | 0 | } catch (const std::runtime_error& e) { |
151 | 0 | LogPrintLevel(BCLog::I2P, BCLog::Level::Error, "Couldn't listen: %s\n", e.what()); |
152 | 0 | CheckControlSock(); |
153 | 0 | } |
154 | 0 | return false; |
155 | 0 | } |
156 | | |
157 | | bool Session::Accept(Connection& conn) |
158 | 0 | { |
159 | 0 | AssertLockNotHeld(m_mutex); |
160 | |
|
161 | 0 | std::string errmsg; |
162 | 0 | bool disconnect{false}; |
163 | |
|
164 | 0 | while (!*m_interrupt) { Branch (164:12): [True: 0, False: 0]
|
165 | 0 | Sock::Event occurred; |
166 | 0 | if (!conn.sock->Wait(MAX_WAIT_FOR_IO, Sock::RECV, &occurred)) { Branch (166:13): [True: 0, False: 0]
|
167 | 0 | errmsg = "wait on socket failed"; |
168 | 0 | break; |
169 | 0 | } |
170 | | |
171 | 0 | if (occurred == 0) { Branch (171:13): [True: 0, False: 0]
|
172 | | // Timeout, no incoming connections or errors within MAX_WAIT_FOR_IO. |
173 | 0 | continue; |
174 | 0 | } |
175 | | |
176 | 0 | std::string peer_dest; |
177 | 0 | try { |
178 | 0 | peer_dest = conn.sock->RecvUntilTerminator('\n', MAX_WAIT_FOR_IO, *m_interrupt, MAX_MSG_SIZE); |
179 | 0 | } catch (const std::runtime_error& e) { |
180 | 0 | errmsg = e.what(); |
181 | 0 | break; |
182 | 0 | } |
183 | | |
184 | 0 | CNetAddr peer_addr; |
185 | 0 | try { |
186 | 0 | peer_addr = DestB64ToAddr(peer_dest); |
187 | 0 | } catch (const std::runtime_error& e) { |
188 | | // The I2P router is expected to send the Base64 of the connecting peer, |
189 | | // but it may happen that something like this is sent instead: |
190 | | // STREAM STATUS RESULT=I2P_ERROR MESSAGE="Session was closed" |
191 | | // In that case consider the session damaged and close it right away, |
192 | | // even if the control socket is alive. |
193 | 0 | if (peer_dest.find("RESULT=I2P_ERROR") != std::string::npos) { Branch (193:17): [True: 0, False: 0]
|
194 | 0 | errmsg = strprintf("unexpected reply that hints the session is unusable: %s", peer_dest); |
195 | 0 | disconnect = true; |
196 | 0 | } else { |
197 | 0 | errmsg = e.what(); |
198 | 0 | } |
199 | 0 | break; |
200 | 0 | } |
201 | | |
202 | 0 | conn.peer = CService(peer_addr, I2P_SAM31_PORT); |
203 | |
|
204 | 0 | return true; |
205 | 0 | } |
206 | | |
207 | 0 | if (*m_interrupt) { Branch (207:9): [True: 0, False: 0]
|
208 | 0 | LogPrintLevel(BCLog::I2P, BCLog::Level::Debug, "Accept was interrupted\n"); |
209 | 0 | } else { |
210 | 0 | LogPrintLevel(BCLog::I2P, BCLog::Level::Debug, "Error accepting%s: %s\n", disconnect ? " (will close the session)" : "", errmsg); |
211 | 0 | } |
212 | 0 | if (disconnect) { Branch (212:9): [True: 0, False: 0]
|
213 | 0 | LOCK(m_mutex); |
214 | 0 | Disconnect(); |
215 | 0 | } else { |
216 | 0 | CheckControlSock(); |
217 | 0 | } |
218 | 0 | return false; |
219 | 0 | } |
220 | | |
221 | | bool Session::Connect(const CService& to, Connection& conn, bool& proxy_error) |
222 | 0 | { |
223 | | // Refuse connecting to arbitrary ports. We don't specify any destination port to the SAM proxy |
224 | | // when connecting (SAM 3.1 does not use ports) and it forces/defaults it to I2P_SAM31_PORT. |
225 | 0 | if (to.GetPort() != I2P_SAM31_PORT) { Branch (225:9): [True: 0, False: 0]
|
226 | 0 | LogPrintLevel(BCLog::I2P, BCLog::Level::Debug, "Error connecting to %s, connection refused due to arbitrary port %s\n", to.ToStringAddrPort(), to.GetPort()); |
227 | 0 | proxy_error = false; |
228 | 0 | return false; |
229 | 0 | } |
230 | | |
231 | 0 | proxy_error = true; |
232 | |
|
233 | 0 | std::string session_id; |
234 | 0 | std::unique_ptr<Sock> sock; |
235 | 0 | conn.peer = to; |
236 | |
|
237 | 0 | try { |
238 | 0 | { |
239 | 0 | LOCK(m_mutex); |
240 | 0 | CreateIfNotCreatedAlready(); |
241 | 0 | session_id = m_session_id; |
242 | 0 | conn.me = m_my_addr; |
243 | 0 | sock = Hello(); |
244 | 0 | } |
245 | |
|
246 | 0 | const Reply& lookup_reply = |
247 | 0 | SendRequestAndGetReply(*sock, strprintf("NAMING LOOKUP NAME=%s", to.ToStringAddr())); |
248 | |
|
249 | 0 | const std::string& dest = lookup_reply.Get("VALUE"); |
250 | |
|
251 | 0 | const Reply& connect_reply = SendRequestAndGetReply( |
252 | 0 | *sock, strprintf("STREAM CONNECT ID=%s DESTINATION=%s SILENT=false", session_id, dest), |
253 | 0 | false); |
254 | |
|
255 | 0 | const std::string& result = connect_reply.Get("RESULT"); |
256 | |
|
257 | 0 | if (result == "OK") { Branch (257:13): [True: 0, False: 0]
|
258 | 0 | conn.sock = std::move(sock); |
259 | 0 | return true; |
260 | 0 | } |
261 | | |
262 | 0 | if (result == "INVALID_ID") { Branch (262:13): [True: 0, False: 0]
|
263 | 0 | LOCK(m_mutex); |
264 | 0 | Disconnect(); |
265 | 0 | throw std::runtime_error("Invalid session id"); |
266 | 0 | } |
267 | | |
268 | 0 | if (result == "CANT_REACH_PEER" || result == "TIMEOUT") { Branch (268:13): [True: 0, False: 0]
Branch (268:44): [True: 0, False: 0]
|
269 | 0 | proxy_error = false; |
270 | 0 | } |
271 | |
|
272 | 0 | throw std::runtime_error(strprintf("\"%s\"", connect_reply.full)); |
273 | 0 | } catch (const std::runtime_error& e) { |
274 | 0 | LogPrintLevel(BCLog::I2P, BCLog::Level::Debug, "Error connecting to %s: %s\n", to.ToStringAddrPort(), e.what()); |
275 | 0 | CheckControlSock(); |
276 | 0 | return false; |
277 | 0 | } |
278 | 0 | } |
279 | | |
280 | | // Private methods |
281 | | |
282 | | std::string Session::Reply::Get(const std::string& key) const |
283 | 0 | { |
284 | 0 | const auto& pos = keys.find(key); |
285 | 0 | if (pos == keys.end() || !pos->second.has_value()) { Branch (285:9): [True: 0, False: 0]
Branch (285:9): [True: 0, False: 0]
Branch (285:30): [True: 0, False: 0]
|
286 | 0 | throw std::runtime_error( |
287 | 0 | strprintf("Missing %s= in the reply to \"%s\": \"%s\"", key, request, full)); |
288 | 0 | } |
289 | 0 | return pos->second.value(); |
290 | 0 | } |
291 | | |
292 | | Session::Reply Session::SendRequestAndGetReply(const Sock& sock, |
293 | | const std::string& request, |
294 | | bool check_result_ok) const |
295 | 0 | { |
296 | 0 | sock.SendComplete(request + "\n", MAX_WAIT_FOR_IO, *m_interrupt); |
297 | |
|
298 | 0 | Reply reply; |
299 | | |
300 | | // Don't log the full "SESSION CREATE ..." because it contains our private key. |
301 | 0 | reply.request = request.substr(0, 14) == "SESSION CREATE" ? "SESSION CREATE ..." : request; Branch (301:21): [True: 0, False: 0]
|
302 | | |
303 | | // It could take a few minutes for the I2P router to reply as it is querying the I2P network |
304 | | // (when doing name lookup, for example). Notice: `RecvUntilTerminator()` is checking |
305 | | // `m_interrupt` more often, so we would not be stuck here for long if `m_interrupt` is |
306 | | // signaled. |
307 | 0 | static constexpr auto recv_timeout = 3min; |
308 | |
|
309 | 0 | reply.full = sock.RecvUntilTerminator('\n', recv_timeout, *m_interrupt, MAX_MSG_SIZE); |
310 | |
|
311 | 0 | for (const auto& kv : Split(reply.full, ' ')) { Branch (311:25): [True: 0, False: 0]
|
312 | 0 | const auto& pos = std::find(kv.begin(), kv.end(), '='); |
313 | 0 | if (pos != kv.end()) { Branch (313:13): [True: 0, False: 0]
|
314 | 0 | reply.keys.emplace(std::string{kv.begin(), pos}, std::string{pos + 1, kv.end()}); |
315 | 0 | } else { |
316 | 0 | reply.keys.emplace(std::string{kv.begin(), kv.end()}, std::nullopt); |
317 | 0 | } |
318 | 0 | } |
319 | |
|
320 | 0 | if (check_result_ok && reply.Get("RESULT") != "OK") { Branch (320:9): [True: 0, False: 0]
Branch (320:9): [True: 0, False: 0]
Branch (320:28): [True: 0, False: 0]
|
321 | 0 | throw std::runtime_error( |
322 | 0 | strprintf("Unexpected reply to \"%s\": \"%s\"", request, reply.full)); |
323 | 0 | } |
324 | | |
325 | 0 | return reply; |
326 | 0 | } |
327 | | |
328 | | std::unique_ptr<Sock> Session::Hello() const |
329 | 0 | { |
330 | 0 | auto sock = m_control_host.Connect(); |
331 | |
|
332 | 0 | if (!sock) { Branch (332:9): [True: 0, False: 0]
|
333 | 0 | throw std::runtime_error(strprintf("Cannot connect to %s", m_control_host.ToString())); |
334 | 0 | } |
335 | | |
336 | 0 | SendRequestAndGetReply(*sock, "HELLO VERSION MIN=3.1 MAX=3.1"); |
337 | |
|
338 | 0 | return sock; |
339 | 0 | } |
340 | | |
341 | | void Session::CheckControlSock() |
342 | 0 | { |
343 | 0 | LOCK(m_mutex); |
344 | |
|
345 | 0 | std::string errmsg; |
346 | 0 | if (m_control_sock && !m_control_sock->IsConnected(errmsg)) { Branch (346:9): [True: 0, False: 0]
Branch (346:27): [True: 0, False: 0]
|
347 | 0 | LogPrintLevel(BCLog::I2P, BCLog::Level::Debug, "Control socket error: %s\n", errmsg); |
348 | 0 | Disconnect(); |
349 | 0 | } |
350 | 0 | } |
351 | | |
352 | | void Session::DestGenerate(const Sock& sock) |
353 | 0 | { |
354 | | // https://geti2p.net/spec/common-structures#key-certificates |
355 | | // "7" or "EdDSA_SHA512_Ed25519" - "Recent Router Identities and Destinations". |
356 | | // Use "7" because i2pd <2.24.0 does not recognize the textual form. |
357 | | // If SIGNATURE_TYPE is not specified, then the default one is DSA_SHA1. |
358 | 0 | const Reply& reply = SendRequestAndGetReply(sock, "DEST GENERATE SIGNATURE_TYPE=7", false); |
359 | |
|
360 | 0 | m_private_key = DecodeI2PBase64(reply.Get("PRIV")); |
361 | 0 | } |
362 | | |
363 | | void Session::GenerateAndSavePrivateKey(const Sock& sock) |
364 | 0 | { |
365 | 0 | DestGenerate(sock); |
366 | | |
367 | | // umask is set to 0077 in common/system.cpp, which is ok. |
368 | 0 | if (!WriteBinaryFile(m_private_key_file, Branch (368:9): [True: 0, False: 0]
|
369 | 0 | std::string(m_private_key.begin(), m_private_key.end()))) { |
370 | 0 | throw std::runtime_error( |
371 | 0 | strprintf("Cannot save I2P private key to %s", fs::quoted(fs::PathToString(m_private_key_file)))); |
372 | 0 | } |
373 | 0 | } |
374 | | |
375 | | Binary Session::MyDestination() const |
376 | 0 | { |
377 | | // From https://geti2p.net/spec/common-structures#destination: |
378 | | // "They are 387 bytes plus the certificate length specified at bytes 385-386, which may be |
379 | | // non-zero" |
380 | 0 | static constexpr size_t DEST_LEN_BASE = 387; |
381 | 0 | static constexpr size_t CERT_LEN_POS = 385; |
382 | |
|
383 | 0 | uint16_t cert_len; |
384 | |
|
385 | 0 | if (m_private_key.size() < CERT_LEN_POS + sizeof(cert_len)) { Branch (385:9): [True: 0, False: 0]
|
386 | 0 | throw std::runtime_error(strprintf("The private key is too short (%d < %d)", |
387 | 0 | m_private_key.size(), |
388 | 0 | CERT_LEN_POS + sizeof(cert_len))); |
389 | 0 | } |
390 | | |
391 | 0 | memcpy(&cert_len, &m_private_key.at(CERT_LEN_POS), sizeof(cert_len)); |
392 | 0 | cert_len = be16toh_internal(cert_len); |
393 | |
|
394 | 0 | const size_t dest_len = DEST_LEN_BASE + cert_len; |
395 | |
|
396 | 0 | if (dest_len > m_private_key.size()) { Branch (396:9): [True: 0, False: 0]
|
397 | 0 | throw std::runtime_error(strprintf("Certificate length (%d) designates that the private key should " |
398 | 0 | "be %d bytes, but it is only %d bytes", |
399 | 0 | cert_len, |
400 | 0 | dest_len, |
401 | 0 | m_private_key.size())); |
402 | 0 | } |
403 | | |
404 | 0 | return Binary{m_private_key.begin(), m_private_key.begin() + dest_len}; |
405 | 0 | } |
406 | | |
407 | | void Session::CreateIfNotCreatedAlready() |
408 | 0 | { |
409 | 0 | std::string errmsg; |
410 | 0 | if (m_control_sock && m_control_sock->IsConnected(errmsg)) { Branch (410:9): [True: 0, False: 0]
Branch (410:27): [True: 0, False: 0]
|
411 | 0 | return; |
412 | 0 | } |
413 | | |
414 | 0 | const auto session_type = m_transient ? "transient" : "persistent"; Branch (414:31): [True: 0, False: 0]
|
415 | 0 | const auto session_id = GetRandHash().GetHex().substr(0, 10); // full is overkill, too verbose in the logs |
416 | |
|
417 | 0 | LogPrintLevel(BCLog::I2P, BCLog::Level::Debug, "Creating %s SAM session %s with %s\n", session_type, session_id, m_control_host.ToString()); |
418 | |
|
419 | 0 | auto sock = Hello(); |
420 | |
|
421 | 0 | if (m_transient) { Branch (421:9): [True: 0, False: 0]
|
422 | | // The destination (private key) is generated upon session creation and returned |
423 | | // in the reply in DESTINATION=. |
424 | 0 | const Reply& reply = SendRequestAndGetReply( |
425 | 0 | *sock, |
426 | 0 | strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=TRANSIENT SIGNATURE_TYPE=7 " |
427 | 0 | "i2cp.leaseSetEncType=4,0 inbound.quantity=1 outbound.quantity=1", |
428 | 0 | session_id)); |
429 | |
|
430 | 0 | m_private_key = DecodeI2PBase64(reply.Get("DESTINATION")); |
431 | 0 | } else { |
432 | | // Read our persistent destination (private key) from disk or generate |
433 | | // one and save it to disk. Then use it when creating the session. |
434 | 0 | const auto& [read_ok, data] = ReadBinaryFile(m_private_key_file); |
435 | 0 | if (read_ok) { Branch (435:13): [True: 0, False: 0]
|
436 | 0 | m_private_key.assign(data.begin(), data.end()); |
437 | 0 | } else { |
438 | 0 | GenerateAndSavePrivateKey(*sock); |
439 | 0 | } |
440 | |
|
441 | 0 | const std::string& private_key_b64 = SwapBase64(EncodeBase64(m_private_key)); |
442 | |
|
443 | 0 | SendRequestAndGetReply(*sock, |
444 | 0 | strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=%s " |
445 | 0 | "i2cp.leaseSetEncType=4,0 inbound.quantity=3 outbound.quantity=3", |
446 | 0 | session_id, |
447 | 0 | private_key_b64)); |
448 | 0 | } |
449 | |
|
450 | 0 | m_my_addr = CService(DestBinToAddr(MyDestination()), I2P_SAM31_PORT); |
451 | 0 | m_session_id = session_id; |
452 | 0 | m_control_sock = std::move(sock); |
453 | |
|
454 | 0 | LogPrintLevel(BCLog::I2P, BCLog::Level::Info, "%s SAM session %s created, my address=%s\n", |
455 | 0 | Capitalize(session_type), |
456 | 0 | m_session_id, |
457 | 0 | m_my_addr.ToStringAddrPort()); |
458 | 0 | } |
459 | | |
460 | | std::unique_ptr<Sock> Session::StreamAccept() |
461 | 0 | { |
462 | 0 | auto sock = Hello(); |
463 | |
|
464 | 0 | const Reply& reply = SendRequestAndGetReply( |
465 | 0 | *sock, strprintf("STREAM ACCEPT ID=%s SILENT=false", m_session_id), false); |
466 | |
|
467 | 0 | const std::string& result = reply.Get("RESULT"); |
468 | |
|
469 | 0 | if (result == "OK") { Branch (469:9): [True: 0, False: 0]
|
470 | 0 | return sock; |
471 | 0 | } |
472 | | |
473 | 0 | if (result == "INVALID_ID") { Branch (473:9): [True: 0, False: 0]
|
474 | | // If our session id is invalid, then force session re-creation on next usage. |
475 | 0 | Disconnect(); |
476 | 0 | } |
477 | |
|
478 | 0 | throw std::runtime_error(strprintf("\"%s\"", reply.full)); |
479 | 0 | } |
480 | | |
481 | | void Session::Disconnect() |
482 | 0 | { |
483 | 0 | if (m_control_sock) { Branch (483:9): [True: 0, False: 0]
|
484 | 0 | if (m_session_id.empty()) { Branch (484:13): [True: 0, False: 0]
|
485 | 0 | LogPrintLevel(BCLog::I2P, BCLog::Level::Info, "Destroying incomplete SAM session\n"); |
486 | 0 | } else { |
487 | 0 | LogPrintLevel(BCLog::I2P, BCLog::Level::Info, "Destroying SAM session %s\n", m_session_id); |
488 | 0 | } |
489 | 0 | m_control_sock.reset(); |
490 | 0 | } |
491 | 0 | m_session_id.clear(); |
492 | 0 | } |
493 | | } // namespace sam |
494 | | } // namespace i2p |