2020-03-04 07:54:04 -06:00
|
|
|
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
|
|
|
|
/*
|
|
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
*/
|
|
|
|
|
2020-05-09 13:41:40 -05:00
|
|
|
/*
|
|
|
|
* The ProxyProtocol creates a web-socket like connection over HTTP
|
|
|
|
* requests. URLs are formed like this:
|
|
|
|
* 0 1 2 3 4 5
|
2021-11-15 09:26:57 -06:00
|
|
|
* /cool/<encoded-document-url>/ws/<session-id>/<command>/<serial>
|
2020-05-09 13:41:40 -05:00
|
|
|
* <session-id> can be 'unknown'
|
|
|
|
* <command> can be 'open', 'write', 'wait', or 'close'
|
|
|
|
*/
|
|
|
|
|
2020-03-04 07:54:04 -06:00
|
|
|
#include <config.h>
|
|
|
|
|
|
|
|
#include "DocumentBroker.hpp"
|
|
|
|
#include "ClientSession.hpp"
|
|
|
|
#include "ProxyProtocol.hpp"
|
|
|
|
#include "Exceptions.hpp"
|
2021-11-18 06:08:14 -06:00
|
|
|
#include "COOLWSD.hpp"
|
2020-03-04 07:54:04 -06:00
|
|
|
#include <Socket.hpp>
|
|
|
|
|
|
|
|
#include <atomic>
|
|
|
|
#include <cassert>
|
|
|
|
|
|
|
|
void DocumentBroker::handleProxyRequest(
|
|
|
|
const std::string& id,
|
|
|
|
const Poco::URI& uriPublic,
|
|
|
|
const bool isReadOnly,
|
2020-05-12 15:10:56 -05:00
|
|
|
const RequestDetails &requestDetails,
|
2020-05-09 13:41:40 -05:00
|
|
|
const std::shared_ptr<StreamSocket> &socket)
|
2020-03-04 07:54:04 -06:00
|
|
|
{
|
2020-05-09 13:41:40 -05:00
|
|
|
std::shared_ptr<ClientSession> clientSession;
|
2020-05-25 09:52:37 -05:00
|
|
|
if (requestDetails.equals(RequestDetails::Field::Command, "open"))
|
2020-03-04 07:54:04 -06:00
|
|
|
{
|
2020-05-07 15:11:38 -05:00
|
|
|
bool isLocal = socket->isLocal();
|
|
|
|
LOG_TRC("proxy: validate that socket is from localhost: " << isLocal);
|
|
|
|
if (!isLocal)
|
|
|
|
throw BadRequestException("invalid host - only connect from localhost");
|
|
|
|
|
2020-03-21 10:07:10 -05:00
|
|
|
LOG_TRC("proxy: Create session for " << _docKey);
|
2020-03-04 07:54:04 -06:00
|
|
|
clientSession = createNewClientSession(
|
|
|
|
std::make_shared<ProxyProtocolHandler>(),
|
2020-05-12 15:10:56 -05:00
|
|
|
id, uriPublic, isReadOnly, requestDetails);
|
2020-03-04 07:54:04 -06:00
|
|
|
addSession(clientSession);
|
2021-11-18 06:08:14 -06:00
|
|
|
COOLWSD::checkDiskSpaceAndWarnClients(true);
|
|
|
|
COOLWSD::checkSessionLimitsAndWarnClients();
|
2020-03-19 10:54:28 -05:00
|
|
|
|
2020-05-12 17:52:25 -05:00
|
|
|
const std::string &sessionId = clientSession->getOrCreateProxyAccess();
|
|
|
|
LOG_TRC("proxy: Returning sessionId " << sessionId);
|
2020-03-19 10:54:28 -05:00
|
|
|
|
|
|
|
std::ostringstream oss;
|
|
|
|
oss << "HTTP/1.1 200 OK\r\n"
|
|
|
|
"Last-Modified: " << Util::getHttpTimeNow() << "\r\n"
|
|
|
|
"User-Agent: " WOPI_AGENT_STRING "\r\n"
|
2020-05-12 17:52:25 -05:00
|
|
|
"Content-Length: " << sessionId.size() << "\r\n"
|
2020-03-19 10:54:28 -05:00
|
|
|
"Content-Type: application/json\r\n"
|
|
|
|
"X-Content-Type-Options: nosniff\r\n"
|
2020-05-12 17:52:25 -05:00
|
|
|
"\r\n" << sessionId;
|
2020-03-19 10:54:28 -05:00
|
|
|
|
|
|
|
socket->send(oss.str());
|
|
|
|
socket->shutdown();
|
|
|
|
return;
|
2020-03-04 07:54:04 -06:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2020-05-25 09:52:37 -05:00
|
|
|
const std::string sessionId = requestDetails.getField(RequestDetails::Field::SessionId);
|
2020-03-21 10:07:10 -05:00
|
|
|
LOG_TRC("proxy: find session for " << _docKey << " with id " << sessionId);
|
2020-03-04 07:54:04 -06:00
|
|
|
for (const auto &it : _sessions)
|
|
|
|
{
|
2020-05-12 17:52:25 -05:00
|
|
|
if (it.second->getOrCreateProxyAccess() == sessionId)
|
2020-03-04 07:54:04 -06:00
|
|
|
{
|
|
|
|
clientSession = it.second;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!clientSession)
|
|
|
|
{
|
|
|
|
LOG_ERR("Invalid session id used " << sessionId);
|
|
|
|
throw BadRequestException("invalid session id");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
auto protocol = clientSession->getProtocol();
|
|
|
|
auto streamSocket = std::static_pointer_cast<StreamSocket>(socket);
|
|
|
|
streamSocket->setHandler(protocol);
|
|
|
|
|
|
|
|
// this DocumentBroker's poll handles reading & writing
|
|
|
|
addSocketToPoll(socket);
|
|
|
|
|
2020-05-09 13:41:40 -05:00
|
|
|
auto proxy = std::static_pointer_cast<ProxyProtocolHandler>(protocol);
|
2020-05-25 09:52:37 -05:00
|
|
|
if (requestDetails.equals(RequestDetails::Field::Command, "close"))
|
2020-05-09 13:41:40 -05:00
|
|
|
{
|
|
|
|
LOG_TRC("Close session");
|
|
|
|
proxy->notifyDisconnected();
|
|
|
|
return;
|
|
|
|
}
|
2020-03-04 07:54:04 -06:00
|
|
|
|
2020-05-25 09:52:37 -05:00
|
|
|
const bool isWaiting = requestDetails.equals(RequestDetails::Field::Command, "wait");
|
2020-03-20 15:15:08 -05:00
|
|
|
proxy->handleRequest(isWaiting, socket);
|
2020-03-04 07:54:04 -06:00
|
|
|
}
|
|
|
|
|
2020-03-19 10:54:28 -05:00
|
|
|
bool ProxyProtocolHandler::parseEmitIncoming(
|
|
|
|
const std::shared_ptr<StreamSocket> &socket)
|
|
|
|
{
|
2021-10-25 11:05:54 -05:00
|
|
|
Buffer& in = socket->getInBuffer();
|
2020-03-19 10:54:28 -05:00
|
|
|
|
2020-05-09 13:41:40 -05:00
|
|
|
#if 0 // protocol debugging.
|
2020-03-19 10:54:28 -05:00
|
|
|
std::stringstream oss;
|
|
|
|
socket->dumpState(oss);
|
|
|
|
LOG_TRC("Parse message:\n" << oss.str());
|
2020-05-09 13:41:40 -05:00
|
|
|
#endif
|
2020-03-19 10:54:28 -05:00
|
|
|
|
|
|
|
while (in.size() > 0)
|
|
|
|
{
|
2020-04-18 12:40:59 -05:00
|
|
|
// Type
|
|
|
|
if ((in[0] != 'T' && in[0] != 'B') || in.size() < 2)
|
2020-03-19 10:54:28 -05:00
|
|
|
{
|
|
|
|
LOG_ERR("Invalid message type " << in[0]);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
auto it = in.begin() + 1;
|
2020-04-18 12:40:59 -05:00
|
|
|
|
|
|
|
// Serial
|
2020-03-19 10:54:28 -05:00
|
|
|
for (; it != in.end() && *it != '\n'; ++it);
|
|
|
|
*it = '\0';
|
2020-04-18 12:40:59 -05:00
|
|
|
uint64_t serial = strtoll( &in[1], nullptr, 16 );
|
|
|
|
in.erase(in.begin(), it + 1);
|
|
|
|
if (in.size() < 2)
|
|
|
|
{
|
|
|
|
LOG_ERR("Invalid message framing size " << in.size());
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Length
|
|
|
|
it = in.begin();
|
|
|
|
for (; it != in.end() && *it != '\n'; ++it);
|
|
|
|
*it = '\0';
|
|
|
|
uint64_t len = strtoll( &in[0], nullptr, 16 );
|
2020-03-19 10:54:28 -05:00
|
|
|
in.erase(in.begin(), it + 1);
|
|
|
|
if (len > in.size())
|
|
|
|
{
|
|
|
|
LOG_ERR("Invalid message length " << len << " vs " << in.size());
|
|
|
|
return false;
|
|
|
|
}
|
2020-04-18 12:40:59 -05:00
|
|
|
|
2020-03-19 10:54:28 -05:00
|
|
|
// far from efficient:
|
|
|
|
std::vector<char> data;
|
|
|
|
data.insert(data.begin(), in.begin(), in.begin() + len + 1);
|
2021-10-25 11:05:54 -05:00
|
|
|
in.eraseFirst(len);
|
2020-03-19 10:54:28 -05:00
|
|
|
|
|
|
|
if (in.size() < 1 || in[0] != '\n')
|
|
|
|
{
|
|
|
|
LOG_ERR("Missing final newline");
|
|
|
|
return false;
|
|
|
|
}
|
2021-10-25 11:05:54 -05:00
|
|
|
in.eraseFirst(1);
|
2020-03-19 10:54:28 -05:00
|
|
|
|
2020-04-18 12:40:59 -05:00
|
|
|
if (serial != _inSerial + 1)
|
|
|
|
LOG_ERR("Serial mismatch " << serial << " vs. " << (_inSerial + 1));
|
|
|
|
_inSerial = serial;
|
2020-03-19 10:54:28 -05:00
|
|
|
_msgHandler->handleMessage(data);
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-03-20 15:15:08 -05:00
|
|
|
void ProxyProtocolHandler::handleRequest(bool isWaiting, const std::shared_ptr<Socket> &socket)
|
2020-03-04 07:54:04 -06:00
|
|
|
{
|
2020-03-19 10:54:28 -05:00
|
|
|
auto streamSocket = std::static_pointer_cast<StreamSocket>(socket);
|
|
|
|
|
2020-03-21 10:07:10 -05:00
|
|
|
LOG_INF("proxy: handle request type: " << (isWaiting ? "wait" : "respond") <<
|
|
|
|
" on socket #" << socket->getFD());
|
2020-03-19 10:54:28 -05:00
|
|
|
|
2020-03-20 15:15:08 -05:00
|
|
|
if (!isWaiting)
|
2020-03-19 10:54:28 -05:00
|
|
|
{
|
|
|
|
if (!_msgHandler)
|
2020-03-21 10:07:10 -05:00
|
|
|
LOG_WRN("proxy: unusual - incoming message with no-one to handle it");
|
2020-03-19 10:54:28 -05:00
|
|
|
else if (!parseEmitIncoming(streamSocket))
|
|
|
|
{
|
|
|
|
std::stringstream oss;
|
|
|
|
streamSocket->dumpState(oss);
|
2020-03-21 10:07:10 -05:00
|
|
|
LOG_ERR("proxy: bad socket structure " << oss.str());
|
2020-03-19 10:54:28 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-07 10:05:14 -05:00
|
|
|
bool sentMsg = flushQueueTo(streamSocket);
|
|
|
|
if (!sentMsg && isWaiting)
|
2020-03-19 10:54:28 -05:00
|
|
|
{
|
2020-03-21 10:07:10 -05:00
|
|
|
LOG_TRC("proxy: queue a waiting out socket #" << streamSocket->getFD());
|
2020-03-20 15:15:08 -05:00
|
|
|
// longer running 'write socket' (marked 'read' by the client)
|
|
|
|
_outSockets.push_back(streamSocket);
|
|
|
|
if (_outSockets.size() > 16)
|
|
|
|
{
|
2020-03-21 10:07:10 -05:00
|
|
|
LOG_ERR("proxy: Unexpected - client opening many concurrent waiting connections " << _outSockets.size());
|
2020-03-20 15:15:08 -05:00
|
|
|
// cleanup older waiting sockets.
|
|
|
|
auto sockWeak = _outSockets.front();
|
|
|
|
_outSockets.erase(_outSockets.begin());
|
|
|
|
auto sock = sockWeak.lock();
|
|
|
|
if (sock)
|
|
|
|
sock->shutdown();
|
|
|
|
}
|
2020-03-19 10:54:28 -05:00
|
|
|
}
|
|
|
|
else
|
2020-03-20 15:15:08 -05:00
|
|
|
{
|
2020-05-07 10:05:14 -05:00
|
|
|
if (!sentMsg)
|
|
|
|
{
|
|
|
|
// FIXME: we should really wait around a bit.
|
|
|
|
LOG_TRC("Nothing to send - closing immediately");
|
|
|
|
std::ostringstream oss;
|
|
|
|
oss << "HTTP/1.1 200 OK\r\n"
|
|
|
|
"Last-Modified: " << Util::getHttpTimeNow() << "\r\n"
|
|
|
|
"User-Agent: " WOPI_AGENT_STRING "\r\n"
|
|
|
|
"Content-Length: " << 0 << "\r\n"
|
|
|
|
"\r\n";
|
|
|
|
streamSocket->send(oss.str());
|
|
|
|
}
|
|
|
|
else
|
|
|
|
LOG_TRC("Returned a reply immediately");
|
|
|
|
|
2020-03-19 10:54:28 -05:00
|
|
|
socket->shutdown();
|
2020-03-20 15:15:08 -05:00
|
|
|
}
|
2020-03-19 10:54:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
void ProxyProtocolHandler::handleIncomingMessage(SocketDisposition &disposition)
|
|
|
|
{
|
|
|
|
std::stringstream oss;
|
|
|
|
disposition.getSocket()->dumpState(oss);
|
|
|
|
LOG_ERR("If you got here, it means we failed to parse this properly in handleRequest: " << oss.str());
|
|
|
|
}
|
|
|
|
|
2020-05-09 13:41:40 -05:00
|
|
|
void ProxyProtocolHandler::notifyDisconnected()
|
|
|
|
{
|
|
|
|
if (_msgHandler)
|
|
|
|
_msgHandler->onDisconnect();
|
|
|
|
}
|
|
|
|
|
2020-03-19 10:54:28 -05:00
|
|
|
int ProxyProtocolHandler::sendMessage(const char *msg, const size_t len, bool text, bool flush)
|
|
|
|
{
|
2020-04-18 12:40:59 -05:00
|
|
|
_writeQueue.push_back(std::make_shared<Message>(msg, len, text, _outSerial++));
|
2020-03-21 15:03:37 -05:00
|
|
|
if (flush)
|
2020-03-19 10:54:28 -05:00
|
|
|
{
|
2020-03-21 15:03:37 -05:00
|
|
|
auto sock = popOutSocket();
|
|
|
|
if (sock)
|
|
|
|
{
|
|
|
|
flushQueueTo(sock);
|
|
|
|
sock->shutdown();
|
|
|
|
}
|
2020-03-19 10:54:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return len;
|
|
|
|
}
|
|
|
|
|
|
|
|
int ProxyProtocolHandler::sendTextMessage(const char *msg, const size_t len, bool flush) const
|
|
|
|
{
|
|
|
|
LOG_TRC("ProxyHack - send text msg " + std::string(msg, len));
|
|
|
|
return const_cast<ProxyProtocolHandler *>(this)->sendMessage(msg, len, true, flush);
|
|
|
|
}
|
|
|
|
|
|
|
|
int ProxyProtocolHandler::sendBinaryMessage(const char *data, const size_t len, bool flush) const
|
|
|
|
{
|
|
|
|
LOG_TRC("ProxyHack - send binary msg len " << len);
|
|
|
|
return const_cast<ProxyProtocolHandler *>(this)->sendMessage(data, len, false, flush);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ProxyProtocolHandler::shutdown(bool goingAway, const std::string &statusMessage)
|
|
|
|
{
|
|
|
|
LOG_TRC("ProxyHack - shutdown " << goingAway << ": " << statusMessage);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ProxyProtocolHandler::getIOStats(uint64_t &sent, uint64_t &recv)
|
|
|
|
{
|
|
|
|
sent = recv = 0;
|
|
|
|
}
|
|
|
|
|
2020-06-03 11:14:03 -05:00
|
|
|
void ProxyProtocolHandler::dumpProxyState(std::ostream& os)
|
2020-03-19 10:54:28 -05:00
|
|
|
{
|
2020-03-20 15:15:08 -05:00
|
|
|
os << "proxy protocol sockets: " << _outSockets.size() << " writeQueue: " << _writeQueue.size() << ":\n";
|
2020-05-24 08:10:18 -05:00
|
|
|
os << '\t';
|
2020-04-14 11:01:41 -05:00
|
|
|
for (auto &it : _outSockets)
|
|
|
|
{
|
|
|
|
auto sock = it.lock();
|
2020-05-24 08:10:18 -05:00
|
|
|
os << '#' << (sock ? sock->getFD() : -2) << ' ';
|
2020-04-14 11:01:41 -05:00
|
|
|
}
|
2020-05-24 08:10:18 -05:00
|
|
|
os << '\n';
|
2020-06-02 02:44:08 -05:00
|
|
|
for (const auto& it : _writeQueue)
|
2021-03-11 12:12:20 -06:00
|
|
|
Util::dumpHex(os, *it, "\twrite queue entry:", "\t\t");
|
2020-03-21 09:27:15 -05:00
|
|
|
if (_msgHandler)
|
|
|
|
_msgHandler->dumpState(os);
|
2020-03-19 10:54:28 -05:00
|
|
|
}
|
|
|
|
|
2020-03-20 15:45:38 -05:00
|
|
|
int ProxyProtocolHandler::getPollEvents(std::chrono::steady_clock::time_point /* now */,
|
|
|
|
int64_t &/* timeoutMaxMs */)
|
|
|
|
{
|
|
|
|
int events = POLLIN;
|
|
|
|
if (_msgHandler && _msgHandler->hasQueuedMessages())
|
|
|
|
events |= POLLOUT;
|
|
|
|
return events;
|
|
|
|
}
|
|
|
|
|
2020-03-21 10:07:10 -05:00
|
|
|
/// slurp from the core to us, @returns true if there are messages to send
|
2021-03-07 11:57:13 -06:00
|
|
|
bool ProxyProtocolHandler::slurpHasMessages(std::size_t capacity)
|
2020-03-19 10:54:28 -05:00
|
|
|
{
|
2021-03-07 11:57:13 -06:00
|
|
|
if (_msgHandler)
|
|
|
|
_msgHandler->writeQueuedMessages(capacity);
|
2020-03-21 10:07:10 -05:00
|
|
|
|
|
|
|
return _writeQueue.size() > 0;
|
|
|
|
}
|
|
|
|
|
2021-03-07 11:57:13 -06:00
|
|
|
void ProxyProtocolHandler::performWrites(std::size_t capacity)
|
2020-03-21 10:07:10 -05:00
|
|
|
{
|
2021-03-07 11:57:13 -06:00
|
|
|
if (!slurpHasMessages(capacity))
|
2020-03-19 10:54:28 -05:00
|
|
|
return;
|
|
|
|
|
2020-03-21 10:07:10 -05:00
|
|
|
auto sock = popOutSocket();
|
2020-03-19 10:54:28 -05:00
|
|
|
if (sock)
|
|
|
|
{
|
2020-03-21 10:07:10 -05:00
|
|
|
LOG_TRC("proxy: performWrites");
|
2020-03-19 10:54:28 -05:00
|
|
|
flushQueueTo(sock);
|
|
|
|
sock->shutdown();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ProxyProtocolHandler::flushQueueTo(const std::shared_ptr<StreamSocket> &socket)
|
|
|
|
{
|
2021-03-07 11:57:13 -06:00
|
|
|
if (!slurpHasMessages(socket->getSendBufferCapacity()))
|
2020-03-21 10:07:10 -05:00
|
|
|
return false;
|
2020-03-19 10:54:28 -05:00
|
|
|
|
|
|
|
size_t totalSize = 0;
|
2020-06-02 02:44:08 -05:00
|
|
|
for (const auto& it : _writeQueue)
|
2020-03-19 10:54:28 -05:00
|
|
|
totalSize += it->size();
|
|
|
|
|
|
|
|
if (!totalSize)
|
|
|
|
return false;
|
|
|
|
|
2020-03-21 10:07:10 -05:00
|
|
|
LOG_TRC("proxy: flushQueue of size " << totalSize << " to socket #" << socket->getFD() << " & close");
|
|
|
|
|
2020-03-19 10:54:28 -05:00
|
|
|
std::ostringstream oss;
|
|
|
|
oss << "HTTP/1.1 200 OK\r\n"
|
|
|
|
"Last-Modified: " << Util::getHttpTimeNow() << "\r\n"
|
|
|
|
"User-Agent: " WOPI_AGENT_STRING "\r\n"
|
|
|
|
"Content-Length: " << totalSize << "\r\n"
|
|
|
|
"Content-Type: application/json\r\n"
|
|
|
|
"X-Content-Type-Options: nosniff\r\n"
|
|
|
|
"\r\n";
|
|
|
|
socket->send(oss.str());
|
|
|
|
|
2020-06-02 02:44:08 -05:00
|
|
|
for (const auto& it : _writeQueue)
|
2020-03-19 10:54:28 -05:00
|
|
|
socket->send(it->data(), it->size(), false);
|
|
|
|
_writeQueue.clear();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// LRU-ness ...
|
2020-03-21 10:07:10 -05:00
|
|
|
std::shared_ptr<StreamSocket> ProxyProtocolHandler::popOutSocket()
|
2020-03-19 10:54:28 -05:00
|
|
|
{
|
|
|
|
std::weak_ptr<StreamSocket> sock;
|
2020-03-20 15:15:08 -05:00
|
|
|
while (!_outSockets.empty())
|
2020-03-19 10:54:28 -05:00
|
|
|
{
|
2020-03-20 15:15:08 -05:00
|
|
|
sock = _outSockets.front();
|
|
|
|
_outSockets.erase(_outSockets.begin());
|
2020-03-19 10:54:28 -05:00
|
|
|
auto realSock = sock.lock();
|
|
|
|
if (realSock)
|
2020-03-21 10:07:10 -05:00
|
|
|
{
|
|
|
|
LOG_TRC("proxy: popped an out socket #" << realSock->getFD() << " leaving: " << _outSockets.size());
|
2020-03-19 10:54:28 -05:00
|
|
|
return realSock;
|
2020-03-21 10:07:10 -05:00
|
|
|
}
|
2020-03-19 10:54:28 -05:00
|
|
|
}
|
2020-03-21 10:07:10 -05:00
|
|
|
LOG_TRC("proxy: no out sockets to pop.");
|
2020-03-19 10:54:28 -05:00
|
|
|
return std::shared_ptr<StreamSocket>();
|
2020-03-04 07:54:04 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|