| 76 | | std::string tmp = info.getAnnounceUrl(); |
|---|
| 77 | | parse_info<> nfo = parse( |
|---|
| 78 | | tmp.data(), tmp.data() + tmp.size(), |
|---|
| 79 | | (str_p("http://") | str_p("udp://")) |
|---|
| 80 | | >> *( ( ':' >> uint_p[assign_a(m_trackerPort)] ) |
|---|
| 81 | | | ( '/' >> *graph_p[push_back_a(m_trackerUrl)] >> end_p) |
|---|
| 82 | | | graph_p[push_back_a(m_trackerHost)] |
|---|
| 83 | | ) |
|---|
| 84 | | ); |
|---|
| 85 | | m_trackerIp.push_back(IPV4Address(m_trackerHost, m_trackerPort)); |
|---|
| 86 | | m_curIp = m_trackerIp.begin(); |
|---|
| 87 | | if ( |
|---|
| 88 | | m_trackerIp[0].getAddr() < std::numeric_limits<uint32_t>::max() |
|---|
| 89 | | && m_trackerIp[0].getPort() > 0 |
|---|
| 90 | | ) { |
|---|
| 91 | | connectToTracker(*m_curIp); |
|---|
| 92 | | } else if (nfo.full) { |
|---|
| 93 | | m_trackerIp.clear(); |
|---|
| 94 | | m_curIp = m_trackerIp.end(); |
|---|
| 95 | | lookupTrackerHost(); |
|---|
| 96 | | } else { |
|---|
| 97 | | logDebug(boost::format("Parse failed at %s") % nfo.stop); |
|---|
| | 57 | TorrentInfo::AIter i = info.announceBegin(); |
|---|
| | 58 | while (i != info.announceEnd()) { |
|---|
| | 59 | std::string tmp = *i; |
|---|
| | 60 | std::string host, url; |
|---|
| | 61 | uint16_t port = 80; |
|---|
| | 62 | parse_info<> nfo = parse( |
|---|
| | 63 | tmp.data(), tmp.data() + tmp.size(), |
|---|
| | 64 | (str_p("http://") | str_p("udp://")) |
|---|
| | 65 | >> *( ( ':' >> uint_p[assign_a(port)] ) |
|---|
| | 66 | | ( '/' >> *graph_p[push_back_a(url)] >> end_p) |
|---|
| | 67 | | graph_p[push_back_a(host)] |
|---|
| | 68 | ) |
|---|
| | 69 | ); |
|---|
| | 70 | try { |
|---|
| | 71 | Tracker *tr = new Tracker(host, url, port, info); |
|---|
| | 72 | tr->foundPeer.connect( |
|---|
| | 73 | boost::bind(&Torrent::createClient, this, _1) |
|---|
| | 74 | ); |
|---|
| | 75 | tr->getUploaded.connect( |
|---|
| | 76 | boost::bind(&Torrent::getUploaded, this) |
|---|
| | 77 | ); |
|---|
| | 78 | tr->getDownloaded.connect( |
|---|
| | 79 | boost::bind(&Torrent::getDownloaded, this) |
|---|
| | 80 | ); |
|---|
| | 81 | tr->getPartData.connect( |
|---|
| | 82 | boost::bind(&Torrent::getPartData, this) |
|---|
| | 83 | ); |
|---|
| | 84 | m_trackers.insert(tr); |
|---|
| | 85 | } catch (std::exception &e) { |
|---|
| | 86 | logDebug(boost::format("Tracker: %s") % e.what()); |
|---|
| | 87 | } |
|---|
| | 88 | ++i; |
|---|
| 142 | | } |
|---|
| 143 | | |
|---|
| 144 | | void Torrent::lookupTrackerHost() { |
|---|
| 145 | | DNS::lookup(m_trackerHost, this, &Torrent::onResolverEvent); |
|---|
| 146 | | logMsg("Looking up host " + m_trackerHost + "... "); |
|---|
| 147 | | } |
|---|
| 148 | | |
|---|
| 149 | | void Torrent::onResolverEvent(HostInfo info) { |
|---|
| 150 | | if (info.error()) { |
|---|
| 151 | | logError( |
|---|
| 152 | | boost::format("[%s] Host lookup failed: %s") |
|---|
| 153 | | % info.getName() % info.errorMsg() |
|---|
| 154 | | ); |
|---|
| 155 | | Utils::timedCallback(this, &Torrent::lookupTrackerHost, 60000); |
|---|
| 156 | | } else { |
|---|
| 157 | | m_trackerIp = info.getAddresses(); |
|---|
| 158 | | for (uint32_t i = 0; i < m_trackerIp.size(); ++i) { |
|---|
| 159 | | m_trackerIp[i].setPort(m_trackerPort); |
|---|
| 160 | | } |
|---|
| 161 | | m_curIp = m_trackerIp.begin(); |
|---|
| 162 | | connectToTracker(*m_curIp); |
|---|
| 163 | | } |
|---|
| 164 | | } |
|---|
| 165 | | |
|---|
| 166 | | void Torrent::connectToTracker(IPV4Address addr) try { |
|---|
| 167 | | logMsg( |
|---|
| 168 | | boost::format("Connecting to tracker %s[%s]:%d...") |
|---|
| 169 | | % m_trackerHost % addr.getAddrStr() |
|---|
| 170 | | % m_trackerPort |
|---|
| 171 | | ); |
|---|
| 172 | | m_socket.reset(new TcpSocket(this, &Torrent::onSocketEvent)); |
|---|
| 173 | | m_socket->connect(addr); |
|---|
| 174 | | } catch (std::exception &e) { |
|---|
| 175 | | logError( |
|---|
| 176 | | boost::format("[%s] Error connecting to tracker[%s]: %s") |
|---|
| 177 | | % getName() % m_trackerHost % e.what() |
|---|
| 178 | | ); |
|---|
| 179 | | onSocketEvent(m_socket.get(), SOCK_ERR); |
|---|
| 180 | | } |
|---|
| 181 | | |
|---|
| 182 | | void Torrent::onSocketEvent(TcpSocket *sock, SocketEvent evt) { |
|---|
| 183 | | CHECK_RET(sock == m_socket.get()); |
|---|
| 184 | | |
|---|
| 185 | | if (evt == SOCK_CONNECTED) { |
|---|
| 186 | | sendGetRequest(); |
|---|
| 187 | | } else if (evt == SOCK_READ) { |
|---|
| 188 | | *m_socket >> m_inBuffer; |
|---|
| 189 | | parseBuffer(); |
|---|
| 190 | | } else if ( |
|---|
| 191 | | evt == SOCK_CONNFAILED || evt == SOCK_TIMEOUT || |
|---|
| 192 | | evt == SOCK_ERR || evt == SOCK_LOST |
|---|
| 193 | | ) { |
|---|
| 194 | | logError( |
|---|
| 195 | | boost::format( |
|---|
| 196 | | "[%s] Connection to tracker failed/lost[%s]." |
|---|
| 197 | | ) % getName() % m_trackerHost |
|---|
| 198 | | ); |
|---|
| 199 | | if (m_inBuffer.size()) { |
|---|
| 200 | | parseBuffer(); |
|---|
| 201 | | } |
|---|
| 202 | | if (m_inBuffer.size()) { |
|---|
| 203 | | logDebug( |
|---|
| 204 | | "Buffered data but did not parse: " |
|---|
| 205 | | + Utils::hexDump(m_inBuffer) |
|---|
| 206 | | ); |
|---|
| 207 | | } |
|---|
| 208 | | m_socket.reset(); |
|---|
| 209 | | m_inBuffer.clear(); |
|---|
| 210 | | if (m_trackerIp.size() > 1) { |
|---|
| 211 | | if (m_curIp != m_trackerIp.end()) { |
|---|
| 212 | | ++m_curIp; |
|---|
| 213 | | } |
|---|
| 214 | | if (m_curIp == m_trackerIp.end()) { |
|---|
| 215 | | m_curIp = m_trackerIp.begin(); |
|---|
| 216 | | } |
|---|
| 217 | | connectToTracker(*m_curIp); |
|---|
| 218 | | } else if (m_interval) { |
|---|
| 219 | | Utils::timedCallback( |
|---|
| 220 | | boost::bind( |
|---|
| 221 | | &Torrent::connectToTracker, this, |
|---|
| 222 | | *m_curIp |
|---|
| 223 | | ), m_interval * 1000 |
|---|
| 224 | | ); |
|---|
| 225 | | logMsg( |
|---|
| 226 | | boost::format("[%s] Re-trying in %dm %ds.") |
|---|
| 227 | | % m_trackerHost % (m_interval / 60) |
|---|
| 228 | | % (m_interval % 60) |
|---|
| 229 | | ); |
|---|
| 230 | | } else if (!m_interval) { |
|---|
| 231 | | Utils::timedCallback( |
|---|
| 232 | | boost::bind( |
|---|
| 233 | | &Torrent::connectToTracker, this, |
|---|
| 234 | | *m_curIp |
|---|
| 235 | | ), 60000 |
|---|
| 236 | | ); |
|---|
| 237 | | } |
|---|
| 238 | | } else if (evt == SOCK_BLOCKED) { |
|---|
| 239 | | logError( |
|---|
| 240 | | boost::format( |
|---|
| 241 | | "[%s] Connection attempt to the Tracker[%s] " |
|---|
| 242 | | "was blocked by IpFilter!" |
|---|
| 243 | | ) % getName() % m_trackerHost |
|---|
| 244 | | ); |
|---|
| 245 | | } |
|---|
| 246 | | } |
|---|
| 247 | | |
|---|
| 248 | | void Torrent::sendGetRequest() { |
|---|
| 249 | | boost::format fmt( |
|---|
| 250 | | "GET /%s?%s HTTP/1.0\r\n" |
|---|
| 251 | | // "User-Agent: Hydranode 0.1.2;Linux\r\n" // optional |
|---|
| 252 | | "Connection: close\r\n" |
|---|
| 253 | | // "Accept-Encoding: gzip\r\n" // we don't have gzip decompressor |
|---|
| 254 | | "Host: %s:%d\r\n" |
|---|
| 255 | | // these are just to be nice (Azureus sends them) |
|---|
| 256 | | // "Content-Type: application/x-www-form-urlencoded" |
|---|
| 257 | | // "Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2" |
|---|
| 258 | | ); |
|---|
| 259 | | fmt % m_trackerUrl; |
|---|
| 260 | | std::ostringstream args; |
|---|
| 261 | | args << "info_hash=" << Utils::urlEncode( |
|---|
| 262 | | m_info.getInfoHash().toString(), true |
|---|
| 263 | | ) << "&"; |
|---|
| 264 | | args << "peer_id=" << BitTorrent::instance().getId() << "&"; |
|---|
| 265 | | args << "key=" << BitTorrent::instance().getKey() << "&"; |
|---|
| 266 | | args << "port=" << BitTorrent::instance().getPort() << "&"; |
|---|
| 267 | | args << "uploaded=" << m_uploaded << "&"; |
|---|
| 268 | | args << "downloaded=" << m_downloaded << "&"; |
|---|
| 269 | | if (m_partData) { |
|---|
| 270 | | args << "left="; |
|---|
| 271 | | args << m_partData->getSize() - m_partData->getCompleted(); |
|---|
| 272 | | args << "&"; |
|---|
| 273 | | } else { |
|---|
| 274 | | args << "left=0&"; |
|---|
| 275 | | } |
|---|
| 276 | | if (!m_interval) { // if no interval, means we just started up |
|---|
| 277 | | args << "event=" << "started" << "&"; |
|---|
| 278 | | } |
|---|
| 279 | | args << "num_want=" << 80 << "&"; |
|---|
| 280 | | args << "compact=" << 1; |
|---|
| 281 | | fmt % args.str() % m_trackerHost % m_trackerPort; |
|---|
| 282 | | |
|---|
| 283 | | std::string final = fmt.str(); |
|---|
| 284 | | logTrace(TORRENT, "Sending HTTP GET request:\n" + final); |
|---|
| 285 | | *m_socket << final << Socket::Endl << Socket::Endl; |
|---|
| 286 | | float ratio = 0.0; |
|---|
| 287 | | if (m_downloaded) { |
|---|
| 288 | | ratio = m_uploaded / m_downloaded; |
|---|
| 289 | | } |
|---|
| 290 | | logMsg( |
|---|
| 291 | | boost::format( |
|---|
| 292 | | "Torrent Statistics: Uploaded: %s " |
|---|
| 293 | | "Downloaded: %s Ratio: %5.3f" |
|---|
| 294 | | ) % Utils::bytesToString(m_uploaded) |
|---|
| 295 | | % Utils::bytesToString(m_downloaded) |
|---|
| 296 | | % ratio |
|---|
| 297 | | ); |
|---|
| 298 | | } |
|---|
| 299 | | |
|---|
| 300 | | void Torrent::parseBuffer() { |
|---|
| 301 | | using namespace boost::spirit; |
|---|
| 302 | | uint32_t rc = 0; |
|---|
| 303 | | std::string rcText; |
|---|
| 304 | | uint32_t clen = 0; |
|---|
| 305 | | std::string ctype; |
|---|
| 306 | | std::string cenc, tenc; |
|---|
| 307 | | |
|---|
| 308 | | std::string sepc("\r\n"); |
|---|
| 309 | | uint32_t headerEnd = m_inBuffer.find(sepc + sepc); |
|---|
| 310 | | if (headerEnd == std::string::npos) { |
|---|
| 311 | | headerEnd = m_inBuffer.find("\n\n"); |
|---|
| 312 | | if (headerEnd == std::string::npos) { |
|---|
| 313 | | return; |
|---|
| 314 | | } else { |
|---|
| 315 | | sepc = "\n"; |
|---|
| 316 | | } |
|---|
| 317 | | } |
|---|
| 318 | | std::string headerData = m_inBuffer.substr(0, headerEnd); |
|---|
| 319 | | std::string contentData = m_inBuffer.substr(headerEnd + sepc.size()*2); |
|---|
| 320 | | boost::char_separator<char> sep(sepc.c_str()); |
|---|
| 321 | | boost::tokenizer<boost::char_separator<char> > tok(headerData, sep); |
|---|
| 322 | | boost::tokenizer<boost::char_separator<char> >::iterator it( |
|---|
| 323 | | tok.begin() |
|---|
| 324 | | ); |
|---|
| 325 | | while (it != tok.end()) { |
|---|
| 326 | | std::string tmp(*it); |
|---|
| 327 | | parse_info<> info = parse( |
|---|
| 328 | | tmp.c_str(), |
|---|
| 329 | | ( |
|---|
| 330 | | as_lower_d["content-length:"] |
|---|
| 331 | | >> uint_p[assign_a(clen)] |
|---|
| 332 | | ) | ( |
|---|
| 333 | | as_lower_d["content-type:"] |
|---|
| 334 | | >> *anychar_p[push_back_a(ctype)] |
|---|
| 335 | | ) | ( |
|---|
| 336 | | as_lower_d["content-encoding:"] |
|---|
| 337 | | >> *anychar_p[push_back_a(cenc)] |
|---|
| 338 | | ) | ( |
|---|
| 339 | | as_lower_d["transfer-encoding:"] |
|---|
| 340 | | >> *anychar_p[push_back_a(tenc)] |
|---|
| 341 | | ) | ( |
|---|
| 342 | | as_lower_d["http/"] >> uint_p >> '.' >> uint_p |
|---|
| 343 | | >> uint_p[assign_a(rc)] |
|---|
| 344 | | >> *anychar_p[push_back_a(rcText)] |
|---|
| 345 | | ) | *anychar_p |
|---|
| 346 | | , space_p |
|---|
| 347 | | ); |
|---|
| 348 | | ++it; |
|---|
| 349 | | } |
|---|
| 350 | | if (clen && contentData.size() != clen) { |
|---|
| 351 | | logTrace(TORRENT, "Dont have full content yet."); |
|---|
| 352 | | return; |
|---|
| 353 | | } |
|---|
| 354 | | if (tenc == "chunked") { |
|---|
| 355 | | // HTTP/1.1 protocol - doesn't work yet properly |
|---|
| 356 | | logTrace(TORRENT, "====" + contentData + "===="); |
|---|
| 357 | | int chunkLen; |
|---|
| 358 | | std::string content; |
|---|
| 359 | | parse_info<> info = parse( |
|---|
| 360 | | contentData.c_str(), |
|---|
| 361 | | hex_p[assign_a(chunkLen)] >> eol_p |
|---|
| 362 | | >> repeat_p(boost::ref(chunkLen)) |
|---|
| 363 | | [anychar_p[push_back_a(content)]] |
|---|
| 364 | | >> eol_p >> "0" >> eol_p >> eol_p |
|---|
| 365 | | ); |
|---|
| 366 | | if (info.full) { |
|---|
| 367 | | parseContent(content); |
|---|
| 368 | | } else { |
|---|
| 369 | | logDebug( |
|---|
| 370 | | boost::format("Parse failed at: %s") % info.stop |
|---|
| 371 | | ); |
|---|
| 372 | | logDebug(boost::format("chunklen = %d") % chunkLen); |
|---|
| 373 | | logDebug(boost::format("content = %s") % content); |
|---|
| 374 | | return; |
|---|
| 375 | | } |
|---|
| 376 | | } |
|---|
| 377 | | logMsg(boost::format("HTTP reply from tracker: %d %s") % rc % rcText); |
|---|
| 378 | | #ifndef NDEBUG |
|---|
| 379 | | std::string tmpContent(contentData); |
|---|
| 380 | | // get rid of non-printable characters |
|---|
| 381 | | for (uint32_t i = 0; i < tmpContent.size(); ++i) { |
|---|
| 382 | | if (tmpContent[i] < 32 || tmpContent[i] > 126) { |
|---|
| 383 | | tmpContent.replace(i, 1, "."); |
|---|
| 384 | | } |
|---|
| 385 | | } |
|---|
| 386 | | logTrace(TORRENT, "Content data: " + tmpContent); |
|---|
| 387 | | #endif |
|---|
| 388 | | parseContent(contentData); |
|---|
| 389 | | } |
|---|
| 390 | | |
|---|
| 391 | | void Torrent::parseContent(const std::string &data) { |
|---|
| 392 | | INIT_PARSER(); |
|---|
| 393 | | |
|---|
| 394 | | std::string errorMsg, warningMsg, cpList; |
|---|
| 395 | | |
|---|
| 396 | | std::vector<Peer> peerList; |
|---|
| 397 | | Peer tmpPeer; |
|---|
| 398 | | rule<> peer_list = 'l' >> *( |
|---|
| 399 | | 'd' >> *( ("7:peer id" >> BSTR(tmpPeer.m_id)) |
|---|
| 400 | | | ("2:ip" >> BSTR(tmpPeer.m_ip)) |
|---|
| 401 | | | ("4:port" >> BINT(tmpPeer.m_port)) |
|---|
| 402 | | | unknown |
|---|
| 403 | | ) |
|---|
| 404 | | >> ch_p('e')[push_back_a(peerList, tmpPeer)][clear_a(tmpPeer)] |
|---|
| 405 | | ) >> 'e'; |
|---|
| 406 | | |
|---|
| 407 | | parse_info<> info = parse( |
|---|
| 408 | | data.data(), data.data() + data.size(), |
|---|
| 409 | | 'd' >> *( ("14:failure reason" >> BSTR(errorMsg)) |
|---|
| 410 | | | ("15:warning message" >> BSTR(warningMsg)) |
|---|
| 411 | | | ("8:interval" >> BINT(m_interval)) |
|---|
| 412 | | | ("12:min interval" >> BINT(m_minInterval)) |
|---|
| 413 | | | ("10:tracker id" >> BSTR(m_trackerId)) |
|---|
| 414 | | | ("8:complete" >> BINT(m_completeSrc)) |
|---|
| 415 | | | ("10:incomplete" >> BINT(m_partialSrc)) |
|---|
| 416 | | | ("5:peers" >> (peer_list | BSTR(cpList))) |
|---|
| 417 | | | unknown |
|---|
| 418 | | ) |
|---|
| 419 | | >> 'e' |
|---|
| 420 | | ); |
|---|
| 421 | | if (!info.full) { |
|---|
| 422 | | logTrace(TORRENT, |
|---|
| 423 | | boost::format("Tracker response:\n%s") |
|---|
| 424 | | % Utils::hexDump(data) |
|---|
| 425 | | ); |
|---|
| 426 | | logTrace(TORRENT, |
|---|
| 427 | | boost::format("Parsing failed at: %s") |
|---|
| 428 | | % Utils::hexDump(info.stop) |
|---|
| 429 | | ); |
|---|
| 430 | | return; |
|---|
| 431 | | } |
|---|
| 432 | | |
|---|
| 433 | | if (errorMsg.size()) { |
|---|
| 434 | | logError(getName() + ": " + errorMsg); |
|---|
| 435 | | } |
|---|
| 436 | | if (warningMsg.size()) { |
|---|
| 437 | | logWarning(getName() + ": " + warningMsg); |
|---|
| 438 | | } |
|---|
| 439 | | if (cpList.size() && !peerList.size()) { |
|---|
| 440 | | CHECK(cpList.size() % 6 == 0); |
|---|
| 441 | | std::istringstream i(cpList); |
|---|
| 442 | | tmpPeer.clear(); |
|---|
| 443 | | for (uint32_t j = 0; j < cpList.size() / 6; ++j) { |
|---|
| 444 | | tmpPeer.m_cip = Utils::getVal<uint32_t>(i); |
|---|
| 445 | | tmpPeer.m_port = Utils::getVal<uint16_t>(i); |
|---|
| 446 | | tmpPeer.m_port = SWAP16_ON_LE(tmpPeer.m_port); |
|---|
| 447 | | peerList.push_back(tmpPeer); |
|---|
| 448 | | } |
|---|
| 449 | | } |
|---|
| 450 | | if (m_minInterval) { |
|---|
| 451 | | logMsg( |
|---|
| 452 | | boost::format("Minimum tracker reask interval: %dm%ds") |
|---|
| 453 | | % (m_minInterval / 60) % (m_minInterval % 60) |
|---|
| 454 | | ); |
|---|
| 455 | | } |
|---|
| 456 | | if (m_interval) { |
|---|
| 457 | | logMsg( |
|---|
| 458 | | boost::format("Tracker reask interval: %dm%ds") |
|---|
| 459 | | % (m_interval / 60) % (m_interval % 60) |
|---|
| 460 | | ); |
|---|
| 461 | | } |
|---|
| 462 | | if (m_completeSrc) { |
|---|
| 463 | | logMsg( |
|---|
| 464 | | boost::format("Complete sources: %d") |
|---|
| 465 | | % m_completeSrc |
|---|
| 466 | | ); |
|---|
| 467 | | } |
|---|
| 468 | | if (m_partialSrc) { |
|---|
| 469 | | logMsg( |
|---|
| 470 | | boost::format("Partial sources: %d") |
|---|
| 471 | | % m_partialSrc |
|---|
| 472 | | ); |
|---|
| 473 | | } |
|---|
| 474 | | if (peerList.size()) { |
|---|
| 475 | | logMsg( |
|---|
| 476 | | boost::format("Received %d peers from tracker.") |
|---|
| 477 | | % peerList.size() |
|---|
| 478 | | ); |
|---|
| 479 | | for (uint32_t i = 0; i < peerList.size(); ++i) { |
|---|
| 480 | | IPV4Address addr; |
|---|
| 481 | | if (peerList[i].m_cip) { |
|---|
| 482 | | addr.setAddr(peerList[i].m_cip); |
|---|
| 483 | | } else { |
|---|
| 484 | | addr.setAddr(peerList[i].m_ip); |
|---|
| 485 | | } |
|---|
| 486 | | addr.setPort(peerList[i].m_port); |
|---|
| 487 | | try { |
|---|
| 488 | | createClient(addr); |
|---|
| 489 | | } catch (std::exception &e) { |
|---|
| 490 | | logDebug( |
|---|
| 491 | | boost::format( |
|---|
| 492 | | "Error while creating BT " |
|---|
| 493 | | "peer connection: %s" |
|---|
| 494 | | ) % e.what() |
|---|
| 495 | | ); |
|---|
| 496 | | } |
|---|
| 497 | | } |
|---|
| 498 | | } |
|---|
| 499 | | if (!m_interval) { |
|---|
| 500 | | m_interval = 60; |
|---|
| 501 | | } |
|---|
| 502 | | |
|---|
| 503 | | Utils::timedCallback( |
|---|
| 504 | | boost::bind(&Torrent::connectToTracker, this, *m_curIp), |
|---|
| 505 | | m_interval * 1000 |
|---|
| 506 | | ); |
|---|
| 507 | | m_socket->disconnect(); |
|---|
| 508 | | m_socket.reset(); |
|---|
| 509 | | m_inBuffer.clear(); |
|---|
| | 133 | for_each(m_trackers.begin(),m_trackers.end(),bind(delete_ptr(), __1)); |
|---|