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