PageRenderTime 26ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/hphp/runtime/server/fastcgi/fastcgi-session.cpp

https://gitlab.com/Blueprint-Marketing/hhvm
C++ | 717 lines | 495 code | 120 blank | 102 comment | 100 complexity | be74c794295ade6caa345411658fbfbf MD5 | raw file
  1. /*
  2. +----------------------------------------------------------------------+
  3. | HipHop for PHP |
  4. +----------------------------------------------------------------------+
  5. | Copyright (c) 2010-2015 Facebook, Inc. (http://www.facebook.com) |
  6. +----------------------------------------------------------------------+
  7. | This source file is subject to version 3.01 of the PHP license, |
  8. | that is bundled with this package in the file LICENSE, and is |
  9. | available through the world-wide-web at the following url: |
  10. | http://www.php.net/license/3_01.txt |
  11. | If you did not receive a copy of the PHP license and are unable to |
  12. | obtain it through the world-wide-web, please send a note to |
  13. | license@php.net so we can mail you a copy immediately. |
  14. +----------------------------------------------------------------------+
  15. */
  16. #include "hphp/runtime/server/fastcgi/fastcgi-session.h"
  17. #include "hphp/runtime/server/fastcgi/fastcgi-server.h"
  18. #include "hphp/util/logger.h"
  19. #include <folly/Memory.h>
  20. #include <folly/MoveWrapper.h>
  21. #include <folly/io/Cursor.h>
  22. #include <folly/io/IOBuf.h>
  23. #include <limits>
  24. namespace HPHP {
  25. using folly::IOBuf;
  26. using folly::IOBufQueue;
  27. using folly::io::Cursor;
  28. using folly::io::Appender;
  29. using folly::io::QueueAppender;
  30. ////////////////////////////////////////////////////////////////////////////////
  31. namespace fcgi {
  32. void KVParser::reset() {
  33. m_phase = Phase::READ_KEY_LENGTH;
  34. m_readBuf.clear();
  35. m_keyBuf.clear();
  36. m_valueBuf.clear();
  37. }
  38. std::tuple<
  39. std::unique_ptr<folly::IOBuf>,
  40. std::unique_ptr<folly::IOBuf>
  41. > KVParser::readNext() {
  42. assert(ready());
  43. m_phase = Phase::READ_KEY_LENGTH;
  44. auto key = m_keyBuf.move();
  45. auto val = m_valueBuf.move();
  46. process(); // check for more data
  47. return std::make_tuple(std::move(key), std::move(val));
  48. }
  49. bool KVParser::consume(std::unique_ptr<IOBuf> chain) {
  50. if (chain == nullptr || chain->computeChainDataLength() == 0) {
  51. if (m_readBuf.chainLength() != 0 || m_phase != Phase::READ_KEY_LENGTH) {
  52. // Malformed stream
  53. return false;
  54. }
  55. m_phase = Phase::COMPLETE;
  56. return true;
  57. }
  58. // The stream has been restarted
  59. if (m_phase == Phase::COMPLETE) m_phase = Phase::READ_KEY_LENGTH;
  60. m_readBuf.append(std::move(chain));
  61. // Can only process new data if we aren't currently waiting on a call to
  62. // readNext() to free up the key/value beffers
  63. if (m_phase != Phase::READY) {
  64. process();
  65. }
  66. return true;
  67. }
  68. void KVParser::process() {
  69. size_t available = m_readBuf.chainLength();
  70. size_t avail = available;
  71. Cursor cursor(m_readBuf.front());
  72. // if we can read a complete key/value then we are ready to be extracted
  73. if (parseKeyValue(cursor, avail)) {
  74. m_phase = Phase::READY;
  75. }
  76. m_readBuf.split(available - avail);
  77. }
  78. bool KVParser::parseKeyValue(Cursor& cursor, size_t& available) {
  79. if (m_phase == Phase::READ_KEY_LENGTH) {
  80. if (parseKeyValueLength(cursor, available, m_keyLength)) {
  81. m_keyLeft = m_keyLength;
  82. m_phase = Phase::READ_VALUE_LENGTH;
  83. } else {
  84. return false;
  85. }
  86. }
  87. if (m_phase == Phase::READ_VALUE_LENGTH) {
  88. if (parseKeyValueLength(cursor, available, m_valueLength)) {
  89. m_valueLeft = m_valueLength;
  90. m_phase = Phase::READ_KEY;
  91. } else {
  92. return false;
  93. }
  94. }
  95. if (m_phase == Phase::READ_KEY) {
  96. if (parseKeyValueContent(cursor, available, m_keyLeft, m_keyBuf)) {
  97. m_phase = Phase::READ_VALUE;
  98. } else {
  99. return false;
  100. }
  101. }
  102. if (m_phase == Phase::READ_VALUE) {
  103. if (parseKeyValueContent(cursor, available, m_valueLeft, m_valueBuf)) {
  104. m_phase = Phase::READ_KEY_LENGTH;
  105. return true;
  106. } else {
  107. return false;
  108. }
  109. }
  110. return false;
  111. }
  112. bool KVParser::parseKeyValueLength(Cursor& cursor,
  113. size_t& available,
  114. size_t& lengthReturn) {
  115. if (!available) {
  116. return false;
  117. }
  118. auto peeked = cursor.peek();
  119. if (*peeked.first & 0x80) { // highest bit is set
  120. if (available < sizeof(uint32_t)) {
  121. return false;
  122. }
  123. lengthReturn = cursor.readBE<uint32_t>();
  124. lengthReturn &= ~(0x80 << 24);
  125. available -= sizeof(uint32_t);
  126. } else {
  127. lengthReturn = cursor.readBE<uint8_t>();
  128. available--;
  129. }
  130. return true;
  131. }
  132. bool KVParser::parseKeyValueContent(Cursor& cursor,
  133. size_t& available,
  134. size_t& length,
  135. IOBufQueue& queue) {
  136. std::unique_ptr<IOBuf> buf;
  137. size_t len = cursor.cloneAtMost(buf, length);
  138. queue.append(std::move(buf));
  139. assert(length >= len);
  140. length -= len;
  141. assert(available >= len);
  142. available -= len;
  143. return (length == 0);
  144. }
  145. }
  146. ////////////////////////////////////////////////////////////////////////////////
  147. FastCGISession::FastCGISession(
  148. folly::EventBase* evBase,
  149. JobQueueDispatcher<FastCGIWorker>& dispatcher,
  150. folly::AsyncSocket::UniquePtr sock,
  151. const folly::SocketAddress& localAddr,
  152. const folly::SocketAddress& peerAddr)
  153. : m_eventBase(evBase)
  154. , m_dispatcher(dispatcher)
  155. , m_localAddr(localAddr)
  156. , m_peerAddr(peerAddr)
  157. , m_sock(std::move(sock))
  158. {
  159. ++m_eventCount; // pending readEOF
  160. m_sock->setReadCB(this);
  161. }
  162. void FastCGISession::timeoutExpired() noexcept {
  163. // Hard shutdown; socket timed out
  164. dropConnection();
  165. }
  166. void FastCGISession::describe(std::ostream& os) const {
  167. os << "[peerAddr: " << m_peerAddr
  168. << ", localAddr: " << m_localAddr
  169. << ", request_id: " << m_requestId
  170. << "]";
  171. }
  172. bool FastCGISession::isBusy() const {
  173. // We are busy whenever we are actively serving a request
  174. return m_requestId != 0;
  175. }
  176. void FastCGISession::notifyPendingShutdown() {
  177. closeWhenIdle();
  178. }
  179. void FastCGISession::closeWhenIdle() {
  180. if (!m_requestId) {
  181. m_sock->close(); // Flush any pending writes and close, calling close()
  182. // will immediately call readEOF and prevent any further
  183. // attempts to write data.
  184. // readEOF will call shutdown() which may free this out from under us, we
  185. // could add a DestructorGuard, but we'd only end up calling shutdown()
  186. // ourselves. Instead we return immediately.
  187. return;
  188. }
  189. m_keepConn = false; // will shutdown when request completes
  190. }
  191. void FastCGISession::dropConnection() {
  192. // Nothing else needs to be placed here. Calling closeWithReset() will cause
  193. // readEOF to be called immediately which will call shutdown().
  194. //
  195. // NB: If there are any pending writes they will all be failed. The last one
  196. // to fail will delete us.
  197. m_sock->closeWithReset();
  198. }
  199. void FastCGISession::dumpConnectionState(uint8_t loglevel) { /* nop */ }
  200. ////////////////////////////////////////////////////////////////////////////////
  201. // Borrowed from proxygen
  202. const uint32_t k_minReadSize = 1460;
  203. const uint32_t k_maxReadSize = 4000;
  204. void FastCGISession::getReadBuffer(void** bufReturn, size_t* lenReturn) {
  205. std::tie(*bufReturn, *lenReturn) = m_readBuf.preallocate(k_minReadSize,
  206. k_maxReadSize);
  207. }
  208. /*
  209. * readDataAvailable() - This is the primary entry point for FastCGI records.
  210. *
  211. * While processing FastCGI records it's possible that a socket error will
  212. * cause us to begin destructing, we construct a DestructorGuard and check
  213. * the value of m_shutdown periodically to guard against deadlock and use-
  214. * after-free bugs.
  215. */
  216. void FastCGISession::readDataAvailable(size_t len) noexcept {
  217. DestructorGuard dg(this);
  218. m_readBuf.postallocate(len);
  219. resetTimeout();
  220. // If we're shutting down don't process any further requests, we may be freed
  221. if (m_shutdown) {
  222. return;
  223. }
  224. auto origChain = m_readBuf.front();
  225. size_t avail = origChain ? origChain->computeChainDataLength() : 0;
  226. size_t total = avail;
  227. SCOPE_EXIT {
  228. m_readBuf.trimStart(total - avail);
  229. };
  230. if (!avail) return;
  231. auto chain = origChain->clone();
  232. while (!m_shutdown && avail >= sizeof(fcgi::record)) {
  233. chain->gather(sizeof(fcgi::record));
  234. auto const rec = reinterpret_cast<const fcgi::record*>(chain->data());
  235. if (avail < rec->size()) {
  236. return;
  237. }
  238. chain->gather(rec->size());
  239. avail -= rec->size();
  240. switch (rec->type) {
  241. case fcgi::BEGIN_REQUEST:
  242. onRecord(rec->getTyped<fcgi::begin_record>());
  243. break;
  244. case fcgi::ABORT_REQUEST:
  245. onRecord(rec->getTyped<fcgi::abort_record>());
  246. break;
  247. case fcgi::PARAMS:
  248. onStream(rec->getTyped<fcgi::params_record>(), chain.get());
  249. break;
  250. case fcgi::STDIN:
  251. onStream(rec->getTyped<fcgi::stdin_record>(), chain.get());
  252. break;
  253. case fcgi::GET_VALUES:
  254. onStream(rec->getTyped<fcgi::values_record>(), chain.get());
  255. break;
  256. case fcgi::GET_VALUES_RESULT:
  257. case fcgi::UNKNOWN_TYPE:
  258. case fcgi::END_REQUEST:
  259. case fcgi::STDOUT:
  260. case fcgi::STDERR:
  261. case fcgi::DATA:
  262. // Received malformed record data- bail out
  263. dropConnection();
  264. break;
  265. default:
  266. writeUnknownType(rec->type);
  267. }
  268. chain->trimStart(rec->size());
  269. }
  270. }
  271. void FastCGISession::readEOF() noexcept {
  272. ioStop();
  273. }
  274. ////////////////////////////////////////////////////////////////////////////////
  275. void FastCGISession::readErr(const folly::AsyncSocketException&) noexcept {
  276. ioStop();
  277. }
  278. void FastCGISession::writeErr(size_t,
  279. const folly::AsyncSocketException&) noexcept {
  280. ioStop();
  281. }
  282. void FastCGISession::writeSuccess() noexcept {
  283. if (--m_eventCount == 0 && m_shutdown) {
  284. // If we were terminating and this was the last pending event then trigger
  285. // the delete.
  286. destroy();
  287. }
  288. }
  289. ////////////////////////////////////////////////////////////////////////////////
  290. void FastCGISession::onStdOut(std::unique_ptr<IOBuf> chain) {
  291. // FastCGITransport doesn't run in the same event base. Calling into internal
  292. // functions here is unsafe from other threads so we enqueue the work for the
  293. // event base.
  294. folly::MoveWrapper<std::unique_ptr<IOBuf>> chain_wrapper(std::move(chain));
  295. m_eventBase->runInEventBaseThread([this, chain_wrapper]() mutable {
  296. writeStream(fcgi::STDOUT, std::move(*chain_wrapper));
  297. });
  298. }
  299. void FastCGISession::onComplete() {
  300. // FastCGITransport doesn't run in the same event base. Calling into internal
  301. // functions here is unsafe from other threads so we enqueue the work for the
  302. // event base.
  303. m_eventBase->runInEventBaseThread([&] {
  304. if (!m_aborting && !m_shutdown) {
  305. // If we're aborting we already wrote the end request. If we're shutting
  306. // down the socket is closed.
  307. writeEndRequest(m_requestId, 0, fcgi::REQUEST_COMPLETE);
  308. }
  309. // Reset state
  310. m_requestId = 0;
  311. m_paramsReader.reset();
  312. m_headersComplete = false;
  313. m_bodyComplete = false;
  314. m_transport.reset();
  315. --m_eventCount; // pending onComplete() received
  316. // Check if we were waiting to shutdown
  317. if (m_shutdown && !m_eventCount) {
  318. destroy();
  319. return; // not safe to continue after we delete ourselves
  320. }
  321. // Check if we were the last request on this channel
  322. if (!m_keepConn) {
  323. shutdown();
  324. return; // cannot continue execution after deleting self
  325. }
  326. // Clear the persistence flag
  327. m_keepConn = false;
  328. });
  329. }
  330. ////////////////////////////////////////////////////////////////////////////////
  331. void FastCGISession::ioStop() noexcept {
  332. if (m_transport) {
  333. // We set m_shutdown here because if the transport reenters and attempts
  334. // to write we will put the socket in a very bad state and fail all in
  335. // flight data.
  336. m_shutdown = true;
  337. // If the headers have not been fully received we never started the
  338. // transport and exiting without receiving an onComplete is safe.
  339. if (m_headersComplete) {
  340. m_bodyComplete = true;
  341. m_transport->onBodyComplete();
  342. }
  343. }
  344. // We may have read an EOF because someone deliberately called close() in
  345. // which case they may call shutdown() or we may already be inside shutdown()
  346. --m_eventCount;
  347. shutdown();
  348. }
  349. void FastCGISession::shutdown() noexcept {
  350. DestructorGuard dg(this); // close() may call destroy()
  351. m_shutdown = true;
  352. m_keepConn = false;
  353. // We may have gotten here via close(); if not perform the close ourselves.
  354. // close() may destroy() us, so we have the destructor guard
  355. if (m_sock->good()) {
  356. m_sock->close();
  357. }
  358. if (m_eventCount == 0) {
  359. destroy();
  360. }
  361. }
  362. void FastCGISession::enqueueWrite(std::unique_ptr<IOBuf> chain) {
  363. if (m_shutdown) {
  364. // If we're shutting down do not write any more data. Writing data to a
  365. // socket that has been closed will leave it in a very bad state.
  366. return;
  367. }
  368. ++m_eventCount;
  369. m_sock->writeChain(this, std::move(chain));
  370. }
  371. ////////////////////////////////////////////////////////////////////////////////
  372. void FastCGISession::onRecordImpl(const fcgi::begin_record* rec) {
  373. if (rec->requestId == 0) {
  374. // Garbage record
  375. dropConnection();
  376. return;
  377. }
  378. if (m_requestId) {
  379. if (m_requestId == rec->requestId) {
  380. // Malformed stream
  381. dropConnection();
  382. return;
  383. }
  384. // Already have an active connection
  385. writeEndRequest(rec->requestId, 0, fcgi::CANT_MULTIPLEX_CONN);
  386. return;
  387. }
  388. if (rec->role != fcgi::RESPONDER) {
  389. // Invalid role
  390. writeEndRequest(rec->requestId, 0, fcgi::UNKNOWN_ROLE);
  391. return;
  392. }
  393. // Until the job actually starts once we receive the headers we don't need
  394. // to register a pending onComplete()
  395. m_requestId = rec->requestId;
  396. m_transport = std::make_shared<FastCGITransport>(this);
  397. m_paramsReader.reset();
  398. // Determine if the server needs us to keep the channel open after the
  399. // request completes.
  400. m_keepConn = rec->flags & fcgi::KEEP_CONN;
  401. }
  402. void FastCGISession::onRecordImpl(const fcgi::abort_record* rec) {
  403. if (!m_requestId || rec->requestId != m_requestId) {
  404. // Garbage record
  405. dropConnection();
  406. return;
  407. }
  408. writeEndRequest(m_requestId, 1, fcgi::REQUEST_COMPLETE);
  409. m_aborting = true; // don't try to write REQUEST_COMPLETE again
  410. // There may still be a pending eventCount from an onComplete call from the
  411. // open tranport. We can't clear it here as there is no way to abort the
  412. // transport and we need to be around to receive any data it may try to send
  413. shutdown();
  414. }
  415. void FastCGISession::onStream(const fcgi::params_record* rec,
  416. const IOBuf* chain) {
  417. if (!m_requestId || rec->requestId != m_requestId || m_headersComplete) {
  418. // Garbage record
  419. dropConnection();
  420. return;
  421. }
  422. Cursor cur(chain);
  423. std::unique_ptr<IOBuf> segment;
  424. cur.skip(sizeof(fcgi::record));
  425. cur.cloneAtMost(segment, rec->contentLength);
  426. if (!m_paramsReader.consume(std::move(segment))) {
  427. // Malformed stream
  428. dropConnection();
  429. }
  430. while (m_paramsReader.ready()) {
  431. std::unique_ptr<IOBuf> key, val;
  432. std::tie(key, val) = m_paramsReader.readNext();
  433. m_transport->onHeader(std::move(key), std::move(val));
  434. }
  435. if (m_paramsReader.done()) {
  436. // If we've started shutting down then don't start the transport job.
  437. if (m_shutdown) {
  438. return;
  439. }
  440. m_headersComplete = true;
  441. m_transport->onHeadersComplete();
  442. // Now that the job is running we need to wait for a call to onComplete()
  443. ++m_eventCount;
  444. // This enqueue call would be safe from any thread because as the
  445. // JobQueueDispatcher is synchronized
  446. m_dispatcher.enqueue(std::make_shared<FastCGIJob>(m_transport));
  447. }
  448. }
  449. void FastCGISession::onStream(const fcgi::stdin_record* rec,
  450. const IOBuf* chain) {
  451. if (!m_requestId || rec->requestId != m_requestId || m_bodyComplete) {
  452. // Garbage record
  453. dropConnection();
  454. return;
  455. }
  456. if (!rec->contentLength) {
  457. m_bodyComplete = true;
  458. m_transport->onBodyComplete();
  459. return;
  460. }
  461. Cursor cur(chain);
  462. std::unique_ptr<IOBuf> segment;
  463. cur.skip(sizeof(fcgi::record));
  464. cur.cloneAtMost(segment, rec->contentLength);
  465. m_transport->onBody(std::move(segment));
  466. }
  467. void FastCGISession::onStream(const fcgi::values_record* rec,
  468. const IOBuf* chain) {
  469. if (m_requestId != 0) {
  470. // Garbage record
  471. dropConnection();
  472. return;
  473. }
  474. Cursor cur(chain);
  475. std::unique_ptr<IOBuf> segment;
  476. cur.skip(sizeof(fcgi::record));
  477. cur.cloneAtMost(segment, rec->contentLength);
  478. if (!m_capReader.consume(std::move(segment))) {
  479. // Malformed stream
  480. dropConnection();
  481. }
  482. while (m_capReader.ready()) {
  483. std::unique_ptr<IOBuf> key, val;
  484. std::tie(key, val) = m_capReader.readNext();
  485. size_t key_length = key ? key->computeChainDataLength() : 0;
  486. Cursor cursor(key.get());
  487. auto keyStr = cursor.readFixedString(key_length);
  488. writeCapability(keyStr);
  489. }
  490. }
  491. ////////////////////////////////////////////////////////////////////////////////
  492. const std::string k_getValueMaxConnKey = "FCGI_MAX_CONNS";
  493. const std::string k_getValueMaxRequestsKey = "FCGI_MAX_REQS";
  494. const std::string k_getValueMultiplexConnsKey = "FCGI_MPXS_CONNS";
  495. void FastCGISession::writeCapability(const std::string& key) {
  496. std::string value;
  497. if (key == k_getValueMaxConnKey) {
  498. value = std::to_string(m_dispatcher.getTargetNumWorkers());
  499. } else if (key == k_getValueMaxRequestsKey) {
  500. value = std::to_string(m_dispatcher.getTargetNumWorkers());
  501. } else if (key == k_getValueMultiplexConnsKey) {
  502. // multiplexed connections are not implemented
  503. value = "0";
  504. } else {
  505. // No-op we are supposed to ignore the keys that we
  506. // don't understand.
  507. return;
  508. }
  509. std::unique_ptr<IOBuf> chain;
  510. fcgi::values_result_record* rec;
  511. std::tie(chain, rec) = createRecord<fcgi::values_result_record>(
  512. fcgi::GET_VALUES_RESULT,
  513. 0, // management stream
  514. key.size() + value.size() + 2 * sizeof(uint32_t) // size hint
  515. );
  516. size_t len = 0;
  517. Appender cursor(chain.get(), 256);
  518. // Lengths can be sent as either bytes or double words.
  519. auto appendLength = [&] (const std::string& lenStr) {
  520. if (lenStr.size() > 255) {
  521. len += sizeof(uint32_t);
  522. cursor.writeBE<uint32_t>(lenStr.size() | (0x80 << 24));
  523. } else {
  524. len += sizeof(uint8_t);
  525. cursor.writeBE<uint8_t>(lenStr.size());
  526. }
  527. };
  528. auto appendData = [&] (const std::string& data) {
  529. len += data.size();
  530. cursor.push(reinterpret_cast<const uint8_t*>(data.c_str()), data.size());
  531. };
  532. appendLength(key);
  533. appendLength(value);
  534. appendData(key);
  535. appendData(value);
  536. rec->paddingLength = ((len + 7) & ~7) - len;
  537. rec->contentLength = len;
  538. cursor.ensure(rec->paddingLength);
  539. cursor.append(rec->paddingLength);
  540. enqueueWrite(std::move(chain));
  541. }
  542. void FastCGISession::writeEndRequest(uint16_t request_id,
  543. uint32_t app_status,
  544. fcgi::Status proto_status) {
  545. std::unique_ptr<IOBuf> chain;
  546. fcgi::end_record* rec;
  547. std::tie(chain, rec) = createRecord<fcgi::end_record>(
  548. fcgi::END_REQUEST,
  549. request_id
  550. );
  551. rec->appStatus = app_status;
  552. rec->protStatus = proto_status;
  553. enqueueWrite(std::move(chain));
  554. }
  555. void FastCGISession::writeUnknownType(fcgi::Type record_type) {
  556. std::unique_ptr<IOBuf> chain;
  557. fcgi::unknown_record* rec;
  558. std::tie(chain, rec) = createRecord<fcgi::unknown_record>(
  559. fcgi::UNKNOWN_TYPE,
  560. 0 // management record
  561. );
  562. rec->unknownType = record_type;
  563. enqueueWrite(std::move(chain));
  564. }
  565. void FastCGISession::writeStream(fcgi::Type type,
  566. std::unique_ptr<IOBuf> stream_chain) {
  567. assert(type == fcgi::STDOUT || type == fcgi::STDERR);
  568. if (stream_chain == nullptr) {
  569. return; // Nothing to do.
  570. }
  571. IOBufQueue queue(IOBufQueue::cacheChainLength());
  572. QueueAppender appender(&queue, 256);
  573. Cursor cursor(stream_chain.get());
  574. size_t maxChunk = std::numeric_limits<uint16_t>::max();
  575. size_t available = stream_chain->computeChainDataLength();
  576. while (available > 0) {
  577. size_t len = std::min(maxChunk, available);
  578. size_t pad = ((len + 7) & ~7) - len;
  579. appender.ensure(sizeof(fcgi::record));
  580. auto rec = reinterpret_cast<fcgi::record*>(appender.writableData());
  581. rec->version = fcgi::Version::Current;
  582. rec->type = type;
  583. rec->requestId = m_requestId;
  584. rec->contentLength = len;
  585. rec->paddingLength = pad;
  586. appender.append(sizeof(fcgi::record));
  587. std::unique_ptr<IOBuf> chunk;
  588. cursor.clone(chunk, len);
  589. available -= len;
  590. appender.insert(std::move(chunk));
  591. appender.ensure(pad);
  592. appender.append(pad);
  593. }
  594. enqueueWrite(queue.move());
  595. }
  596. ////////////////////////////////////////////////////////////////////////////////
  597. }