diff --git a/src/core/core.pri b/src/core/core.pri index 432aa5a1..360a8423 100644 --- a/src/core/core.pri +++ b/src/core/core.pri @@ -87,6 +87,7 @@ HEADERS += \ $$PWD/zrpcmanager.h \ $$PWD/zrpcrequest.h \ $$PWD/statusreasons.h \ + $$PWD/jsonpointer.h \ $$PWD/inspectdata.h \ $$PWD/cors.h \ $$PWD/simplehttpserver.h \ @@ -119,6 +120,7 @@ SOURCES += \ $$PWD/zrpcmanager.cpp \ $$PWD/zrpcrequest.cpp \ $$PWD/statusreasons.cpp \ + $$PWD/jsonpointer.cpp \ $$PWD/cors.cpp \ $$PWD/simplehttpserver.cpp \ $$PWD/stats.cpp \ diff --git a/src/handler/jsonpointer.cpp b/src/core/jsonpointer.cpp similarity index 100% rename from src/handler/jsonpointer.cpp rename to src/core/jsonpointer.cpp diff --git a/src/handler/jsonpointer.h b/src/core/jsonpointer.h similarity index 100% rename from src/handler/jsonpointer.h rename to src/core/jsonpointer.h diff --git a/src/core/packet/wscontrolpacket.cpp b/src/core/packet/wscontrolpacket.cpp index 22e61d6b..3ee0e5c0 100644 --- a/src/core/packet/wscontrolpacket.cpp +++ b/src/core/packet/wscontrolpacket.cpp @@ -196,6 +196,9 @@ Variant WsControlPacket::toVariant() const { case Item::Refresh: typeStr = "refresh"; break; + case Item::AutoRespond: + typeStr = "auto-respond"; + break; case Item::Close: typeStr = "close"; break; @@ -261,6 +264,15 @@ Variant WsControlPacket::toVariant() const { if (!item.keepAliveMode.isEmpty()) vitem["keep-alive-mode"] = item.keepAliveMode; + if (!item.matchContentType.isEmpty()) + vitem["match-content-type"] = item.matchContentType; + + if (!item.matchContent.isNull()) + vitem["match-content"] = item.matchContent; + + if (!item.matchContentPtr.isEmpty()) + vitem["match-content-ptr"] = item.matchContentPtr; + vitems += vitem; } @@ -321,6 +333,8 @@ bool WsControlPacket::fromVariant(const Variant &in) { item.type = Item::Subscribe; else if (typeStr == "refresh") item.type = Item::Refresh; + else if (typeStr == "auto-respond") + item.type = Item::AutoRespond; else if (typeStr == "close") item.type = Item::Close; else if (typeStr == "detach") @@ -463,6 +477,31 @@ bool WsControlPacket::fromVariant(const Variant &in) { item.keepAliveMode = keepAliveMode; } + if (vitem.contains("match-content-type")) { + if (typeId(vitem["match-content-type"]) != VariantType::ByteArray) + return false; + + QByteArray matchContentType = vitem["match-content-type"].toByteArray(); + if (!matchContentType.isEmpty()) + item.matchContentType = matchContentType; + } + + if (vitem.contains("match-content")) { + if (typeId(vitem["match-content"]) != VariantType::ByteArray) + return false; + + item.matchContent = vitem["match-content"].toByteArray(); + } + + if (vitem.contains("match-content-ptr")) { + if (typeId(vitem["match-content-ptr"]) != VariantType::ByteArray) + return false; + + QByteArray matchContentPtr = vitem["match-content-ptr"].toByteArray(); + if (!matchContentPtr.isEmpty()) + item.matchContentPtr = matchContentPtr; + } + items += item; } diff --git a/src/core/packet/wscontrolpacket.h b/src/core/packet/wscontrolpacket.h index 46d470dd..6f7ddea9 100644 --- a/src/core/packet/wscontrolpacket.h +++ b/src/core/packet/wscontrolpacket.h @@ -44,6 +44,7 @@ class WsControlPacket { Send, KeepAliveSetup, Refresh, + AutoRespond, Close, Detach, Ack @@ -68,6 +69,9 @@ class WsControlPacket { int ttl; int timeout; QByteArray keepAliveMode; + QByteArray matchContentType; + QByteArray matchContent; + QByteArray matchContentPtr; Item() : type((Type)-1), diff --git a/src/core/wscontrol.h b/src/core/wscontrol.h index feb6f67a..a90a8a3d 100644 --- a/src/core/wscontrol.h +++ b/src/core/wscontrol.h @@ -23,10 +23,38 @@ #ifndef WSCONTROL_H #define WSCONTROL_H +#include "websocket.h" + namespace WsControl { enum KeepAliveMode { NoKeepAlive, Idle, Interval }; -} +class AutoRespondConfig { +public: + WebSocket::Frame::Type matchType; + QByteArray matchContent; + QByteArray matchContentPtr; + WebSocket::Frame::Type type; + QByteArray content; + + AutoRespondConfig() : matchType((WebSocket::Frame::Type)-1), type((WebSocket::Frame::Type)-1) {} + + /// Returns true if the config has no matching criteria. + bool isEmpty() const { return (((int)matchType) < 0 && matchContent.isNull()); } + + /// Returns true if the config has matching critera and response data. + bool isEnabled() const { return (!isEmpty() && (((int)type) >= 0 || !content.isNull())); } + + /// Returns true if this config has the same matching criteria as `other`. + bool matches(const AutoRespondConfig &other) { + return (matchType == other.matchType && + ((matchContent.isNull() && other.matchContent.isNull()) || + (!matchContent.isNull() && !other.matchContent.isNull() && + matchContent == other.matchContent)) && + matchContentPtr == other.matchContentPtr); + } +}; + +} // namespace WsControl #endif diff --git a/src/handler/handler.pri b/src/handler/handler.pri index 4ecbfba4..1a82020c 100644 --- a/src/handler/handler.pri +++ b/src/handler/handler.pri @@ -1,7 +1,6 @@ HEADERS += \ $$PWD/deferred.h \ $$PWD/variantutil.h \ - $$PWD/jsonpointer.h \ $$PWD/jsonpatch.h \ $$PWD/detectrule.h \ $$PWD/lastids.h \ @@ -32,7 +31,6 @@ HEADERS += \ SOURCES += \ $$PWD/deferred.cpp \ $$PWD/variantutil.cpp \ - $$PWD/jsonpointer.cpp \ $$PWD/jsonpatch.cpp \ $$PWD/sessionrequest.cpp \ $$PWD/requeststate.cpp \ diff --git a/src/handler/handlerengine.cpp b/src/handler/handlerengine.cpp index 8b8242e0..ce02d27c 100644 --- a/src/handler/handlerengine.cpp +++ b/src/handler/handlerengine.cpp @@ -110,6 +110,21 @@ static QList parseItems(const VariantList &vitems, bool *ok = 0, return out; } +static QByteArray messageTypeToContentType(WsControlMessage::MessageType t) { + switch (t) { + case WsControlMessage::Text: + return "text"; + case WsControlMessage::Binary: + return "binary"; + case WsControlMessage::Ping: + return "ping"; + case WsControlMessage::Pong: + return "pong"; + default: + return QByteArray(); + } +} + class InspectWorker : public Deferred { public: std::unique_ptr req; @@ -2486,23 +2501,9 @@ class HandlerEngine::Private { i.type = WsControlPacket::Item::KeepAliveSetup; if (!cm.content.isNull()) { - QByteArray contentType; - switch (cm.messageType) { - case WsControlMessage::Text: - contentType = "text"; - break; - case WsControlMessage::Binary: - contentType = "binary"; - break; - case WsControlMessage::Ping: - contentType = "ping"; - break; - case WsControlMessage::Pong: - contentType = "pong"; - break; - default: + QByteArray contentType = messageTypeToContentType(cm.messageType); + if (contentType.isNull()) continue; // Unrecognized type, ignore - } s->keepAliveType = contentType; s->keepAliveMessage = cm.content; @@ -2520,29 +2521,42 @@ class HandlerEngine::Private { outItems += i; } else if (cm.type == WsControlMessage::SendDelayed) { - QByteArray contentType; - switch (cm.messageType) { - case WsControlMessage::Text: - contentType = "text"; - break; - case WsControlMessage::Binary: - contentType = "binary"; - break; - case WsControlMessage::Ping: - contentType = "ping"; - break; - case WsControlMessage::Pong: - contentType = "pong"; - break; - default: + QByteArray contentType = messageTypeToContentType(cm.messageType); + if (contentType.isNull()) continue; // Unrecognized type, ignore - } int timeout = (cm.timeout > 0 ? cm.timeout : DEFAULT_WS_SENDDELAYED_TIMEOUT); s->sendDelayed(contentType, cm.content, timeout); } else if (cm.type == WsControlMessage::FlushDelayed) { s->flushDelayed(); + } else if (cm.type == WsControlMessage::AutoRespond) { + WsControlPacket::Item i; + i.cid = item.cid; + i.type = WsControlPacket::Item::AutoRespond; + + QByteArray matchContentType; + QByteArray contentType; + + if (cm.matchMessageType >= 0) { + matchContentType = messageTypeToContentType(cm.matchMessageType); + if (matchContentType.isNull()) + continue; // Unrecognized type, ignore + } + + if (cm.messageType >= 0) { + contentType = messageTypeToContentType(cm.messageType); + if (contentType.isNull()) + continue; // Unrecognized type, ignore + } + + i.matchContentType = matchContentType; + i.matchContentPtr = cm.matchContentPtr.toUtf8(); + i.matchContent = cm.matchContent; + i.contentType = contentType; + i.message = cm.content; + + outItems += i; } } else if (item.type == WsControlPacket::Item::NeedKeepAlive) { if (!s->keepAliveMessage.isNull()) { diff --git a/src/handler/variantutil.cpp b/src/handler/variantutil.cpp index 412b051a..e7a4a14f 100644 --- a/src/handler/variantutil.cpp +++ b/src/handler/variantutil.cpp @@ -237,6 +237,61 @@ QString getString(const Variant &in, const QString &parentName, const QString &c return str; } +QByteArray getBytes(const Variant &in, const QString &parentName, const QString &childName, + const QString &childBinName, bool required, bool *ok, QString *errorMessage, + bool *isBin) { + QByteArray out; + + QString pn = !parentName.isEmpty() ? parentName : QString("object"); + + if (keyedObjectContains(in, childBinName)) { + if (isBin) + *isBin = true; + + Variant vcontentBin = keyedObjectGetValue(in, childBinName); + + if (typeId(in) == VariantType::Map) // JSON input + { + if (typeId(vcontentBin) != VariantType::String) { + setError(ok, errorMessage, + QString("%1 contains '%2' with wrong type").arg(pn, childBinName)); + return QByteArray(); + } + + out = QByteArray::fromBase64(vcontentBin.toString().toUtf8()); + } else { + if (typeId(vcontentBin) != VariantType::ByteArray) { + setError(ok, errorMessage, + QString("%1 contains '%2' with wrong type").arg(pn, childBinName)); + return QByteArray(); + } + + out = vcontentBin.toByteArray(); + } + } else if (keyedObjectContains(in, childName)) { + if (isBin) + *isBin = false; + + Variant vcontent = keyedObjectGetValue(in, childName); + if (typeId(vcontent) == VariantType::ByteArray) + out = vcontent.toByteArray(); + else if (typeId(vcontent) == VariantType::String) + out = vcontent.toString().toUtf8(); + else { + setError(ok, errorMessage, + QString("%1 contains '%2' with wrong type").arg(pn, childName)); + return QByteArray(); + } + } else if (required) { + setError(ok, errorMessage, + QString("%1 does not contain '%2' nor '%3'").arg(pn, childName, childBinName)); + return QByteArray(); + } + + setSuccess(ok, errorMessage); + return out; +} + bool convertToJsonStyleInPlace(Variant *in) { // Hash -> Map // ByteArray (UTF-8) -> String diff --git a/src/handler/variantutil.h b/src/handler/variantutil.h index acb991aa..287c5b96 100644 --- a/src/handler/variantutil.h +++ b/src/handler/variantutil.h @@ -46,6 +46,9 @@ VariantList getList(const Variant &in, const QString &parentName, const QString QString getString(const Variant &in, bool *ok = 0); QString getString(const Variant &in, const QString &parentName, const QString &childName, bool required, bool *ok = 0, QString *errorMessage = 0); +QByteArray getBytes(const Variant &in, const QString &parentName, const QString &childName, + const QString &childBinName, bool required, bool *ok = 0, + QString *errorMessage = 0, bool *isBin = 0); // Return true if item modified bool convertToJsonStyleInPlace(Variant *in); diff --git a/src/handler/wscontrolmessage.cpp b/src/handler/wscontrolmessage.cpp index a1dc81db..d74a709b 100644 --- a/src/handler/wscontrolmessage.cpp +++ b/src/handler/wscontrolmessage.cpp @@ -29,6 +29,19 @@ using namespace VariantUtil; +static WsControlMessage::MessageType messageTypeFromString(const QString &s) { + if (s == "text") + return WsControlMessage::Text; + else if (s == "binary") + return WsControlMessage::Binary; + else if (s == "ping") + return WsControlMessage::Ping; + else if (s == "pong") + return WsControlMessage::Pong; + else + return (WsControlMessage::MessageType)-1; +} + WsControlMessage WsControlMessage::fromVariant(const Variant &in, bool *ok, QString *errorMessage) { QString pn = "grip control packet"; @@ -65,6 +78,8 @@ WsControlMessage WsControlMessage::fromVariant(const Variant &in, bool *ok, QStr out.type = SendDelayed; else if (type == "flush-delayed") out.type = FlushDelayed; + else if (type == "auto-respond") + out.type = AutoRespond; else { setError(ok, errorMessage, QString("'type' contains unknown value: %1").arg(type)); return WsControlMessage(); @@ -136,15 +151,9 @@ WsControlMessage WsControlMessage::fromVariant(const Variant &in, bool *ok, QStr } if (!typeStr.isNull()) { - if (typeStr == "text") - out.messageType = Text; - else if (typeStr == "binary") - out.messageType = Binary; - else if (typeStr == "ping") - out.messageType = Ping; - else if (typeStr == "pong") - out.messageType = Pong; - else { + out.messageType = messageTypeFromString(typeStr); + + if ((int)out.messageType < 0) { setError(ok, errorMessage, QString("%1 contains 'message-type' with unknown value").arg(pn)); return WsControlMessage(); @@ -154,44 +163,20 @@ WsControlMessage WsControlMessage::fromVariant(const Variant &in, bool *ok, QStr out.messageType = Text; } - if (keyedObjectContains(in, "content-bin")) { - Variant vcontentBin = keyedObjectGetValue(in, "content-bin"); - - if (typeId(in) == VariantType::Map) // JSON input - { - if (typeId(vcontentBin) != VariantType::String) { - setError(ok, errorMessage, - QString("%1 contains 'content-bin' with wrong type").arg(pn)); - return WsControlMessage(); - } - - out.content = QByteArray::fromBase64(vcontentBin.toString().toUtf8()); - } else { - if (typeId(vcontentBin) != VariantType::ByteArray) { - setError(ok, errorMessage, - QString("%1 contains 'content-bin' with wrong type").arg(pn)); - return WsControlMessage(); - } - - out.content = vcontentBin.toByteArray(); - } + bool isBin = false; + out.content = getBytes(in, pn, "content", "content-bin", false, &ok_, errorMessage, &isBin); + if (!ok_) { + if (ok) + *ok = false; + return WsControlMessage(); + } - if (((int)out.messageType) == -1) + if (((int)out.messageType) == -1) { + if (isBin) { out.messageType = Binary; - } else if (keyedObjectContains(in, "content")) { - Variant vcontent = keyedObjectGetValue(in, "content"); - if (typeId(vcontent) == VariantType::ByteArray) - out.content = vcontent.toByteArray(); - else if (typeId(vcontent) == VariantType::String) - out.content = vcontent.toString().toUtf8(); - else { - setError(ok, errorMessage, - QString("%1 contains 'content' with wrong type").arg(pn)); - return WsControlMessage(); - } - - if (((int)out.messageType) == -1) + } else { out.messageType = Text; + } } if (!out.content.isNull()) { @@ -224,6 +209,88 @@ WsControlMessage WsControlMessage::fromVariant(const Variant &in, bool *ok, QStr if (!mode.isNull()) out.keepAliveMode = mode.toUtf8(); } + } else if (out.type == AutoRespond) { + QString matchTypeStr = getString(in, pn, "match-message-type", false, &ok_, errorMessage); + if (!ok_) { + if (ok) + *ok = false; + return WsControlMessage(); + } + + if (!matchTypeStr.isNull()) { + out.matchMessageType = messageTypeFromString(matchTypeStr); + + if ((int)out.matchMessageType < 0) { + setError(ok, errorMessage, + QString("%1 contains 'match-message-type' with unknown value").arg(pn)); + return WsControlMessage(); + } + } + + bool isBin = false; + out.matchContent = getBytes(in, pn, "match-content", "match-content-bin", false, &ok_, + errorMessage, &isBin); + if (!ok_) { + if (ok) + *ok = false; + return WsControlMessage(); + } + + // If match-content specified, default match-message-type to the input type + if (!out.matchContent.isNull() && ((int)out.matchMessageType) < 0) { + if (isBin) { + out.matchMessageType = Binary; + } else { + out.matchMessageType = Text; + } + } + + out.matchContentPtr = getString(in, pn, "match-content-ptr", false, &ok_, errorMessage); + if (!ok_) { + if (ok) + *ok = false; + return WsControlMessage(); + } + + // match-message-type or match-content must be specified to accept response config + if (((int)out.matchMessageType) >= 0 || !out.matchContent.isNull()) { + QString typeStr = getString(in, pn, "message-type", false, &ok_, errorMessage); + if (!ok_) { + if (ok) + *ok = false; + return WsControlMessage(); + } + + if (!typeStr.isNull()) { + out.messageType = messageTypeFromString(typeStr); + + if ((int)out.messageType < 0) { + setError(ok, errorMessage, + QString("%1 contains 'message-type' with unknown value").arg(pn)); + return WsControlMessage(); + } + } else { + // Default + out.messageType = Text; + } + + isBin = false; + out.content = + getBytes(in, pn, "content", "content-bin", false, &ok_, errorMessage, &isBin); + if (!ok_) { + if (ok) + *ok = false; + return WsControlMessage(); + } + + if (((int)out.messageType) < 0) { + if (isBin) { + out.messageType = Binary; + } else { + out.messageType = Text; + } + } + } } return out; diff --git a/src/handler/wscontrolmessage.h b/src/handler/wscontrolmessage.h index 76857d69..66ae7eb4 100644 --- a/src/handler/wscontrolmessage.h +++ b/src/handler/wscontrolmessage.h @@ -37,7 +37,8 @@ class WsControlMessage { SetMeta, KeepAlive, SendDelayed, - FlushDelayed + FlushDelayed, + AutoRespond, }; enum MessageType { Text, Binary, Ping, Pong }; @@ -52,8 +53,15 @@ class WsControlMessage { QByteArray content; int timeout; QByteArray keepAliveMode; + MessageType matchMessageType; + QByteArray matchContent; + QString matchContentPtr; - WsControlMessage() : type((Type)-1), messageType((MessageType)-1), timeout(-1) {} + WsControlMessage() + : type((Type)-1), + messageType((MessageType)-1), + timeout(-1), + matchMessageType((MessageType)-1) {} static WsControlMessage fromVariant(const Variant &in, bool *ok = 0, QString *errorMessage = 0); }; diff --git a/src/proxy/wscontrolsession.cpp b/src/proxy/wscontrolsession.cpp index 7c1eb57b..a4c5b1d2 100644 --- a/src/proxy/wscontrolsession.cpp +++ b/src/proxy/wscontrolsession.cpp @@ -35,6 +35,17 @@ using Connection = boost::signals2::scoped_connection; +static WebSocket::Frame::Type contentTypeToFrameType(const QString &contentType) { + if (contentType == "binary") + return WebSocket::Frame::Binary; + else if (contentType == "ping") + return WebSocket::Frame::Ping; + else if (contentType == "pong") + return WebSocket::Frame::Pong; + else + return WebSocket::Frame::Text; +} + class WsControlSession::Private { public: WsControlSession *q; @@ -200,15 +211,7 @@ class WsControlSession::Private { } if (item.type == WsControlPacket::Item::Send) { - WebSocket::Frame::Type type; - if (item.contentType == "binary") - type = WebSocket::Frame::Binary; - else if (item.contentType == "ping") - type = WebSocket::Frame::Ping; - else if (item.contentType == "pong") - type = WebSocket::Frame::Pong; - else - type = WebSocket::Frame::Text; + WebSocket::Frame::Type type = contentTypeToFrameType(item.contentType); // For sends, don't ack until written @@ -230,6 +233,21 @@ class WsControlSession::Private { q->keepAliveSetupEventReceived(WsControl::NoKeepAlive, -1); } else if (item.type == WsControlPacket::Item::Refresh) { q->refreshEventReceived(); + } else if (item.type == WsControlPacket::Item::AutoRespond) { + WsControl::AutoRespondConfig config; + + if (!item.matchContentType.isEmpty()) + config.matchType = contentTypeToFrameType(item.matchContentType); + + config.matchContent = item.matchContent; + config.matchContentPtr = item.matchContentPtr; + + if (!item.contentType.isEmpty()) + config.type = contentTypeToFrameType(item.contentType); + + config.content = item.message; + + q->autoRespondEventReceived(config); } else if (item.type == WsControlPacket::Item::Close) { q->closeEventReceived(item.code, item.reason); } else if (item.type == WsControlPacket::Item::Detach) { diff --git a/src/proxy/wscontrolsession.h b/src/proxy/wscontrolsession.h index bcd51f75..7322a969 100644 --- a/src/proxy/wscontrolsession.h +++ b/src/proxy/wscontrolsession.h @@ -58,6 +58,7 @@ class WsControlSession { sendEventReceived; boost::signals2::signal keepAliveSetupEventReceived; Signal refreshEventReceived; + boost::signals2::signal autoRespondEventReceived; boost::signals2::signal closeEventReceived; // Use -1 for no code Signal detachEventReceived; Signal cancelEventReceived; diff --git a/src/proxy/wsproxysession.cpp b/src/proxy/wsproxysession.cpp index f0fa090e..831f5066 100644 --- a/src/proxy/wsproxysession.cpp +++ b/src/proxy/wsproxysession.cpp @@ -26,6 +26,7 @@ #include "connectionmanager.h" #include "defercall.h" #include "inspectdata.h" +#include "jsonpointer.h" #include "jwt.h" #include "log.h" #include "packet/httprequestdata.h" @@ -44,6 +45,7 @@ #include "zwebsocket.h" #include #include +#include #include #include #include @@ -51,6 +53,7 @@ #define ACTIVITY_TIMEOUT 60000 #define KEEPALIVE_RAND_MAX 1000 +#define AUTO_RESPOND_CONFIGS_MAX 2 class HttpExtension { public: @@ -187,6 +190,59 @@ static HttpExtension getExtension(const QList &extStrings, const QBy return e; } +static bool autoRespondMatch(const WsControl::AutoRespondConfig &config, + const WebSocket::Frame &f) { + if (((int)config.matchType) >= 0 && config.matchType != f.type) + return false; + + if (!config.matchContent.isNull()) { + QByteArray content = f.data; + + if (!config.matchContentPtr.isEmpty()) { + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(f.data, &error); + if (error.error != QJsonParseError::NoError) + return false; + + if (!doc.isObject() && !doc.isArray()) + return false; + + Variant data; + if (doc.isObject()) + data = doc.object().toVariantMap(); + else + data = doc.array().toVariantList(); + + QString errorMessage; + JsonPointer ptr = JsonPointer::resolve(&data, config.matchContentPtr, &errorMessage); + if (ptr.isNull()) { + log_debug("failed to resolve json pointer [%s]: %s", + qPrintable(config.matchContentPtr), qPrintable(errorMessage)); + return false; + } + + Variant value = ptr.value(); + if (typeId(value) != VariantType::String) { + log_debug("no string value at json pointer [%s]", + qPrintable(config.matchContentPtr)); + return false; + } + + QString s = value.toString(); + + log_debug("found value at json pointer [%s]: [%s]", qPrintable(config.matchContentPtr), + qPrintable(s)); + + content = s.toUtf8(); + } + + if (config.matchContent != content) + return false; + } + + return true; +} + class WsProxySession::Private { public: enum State { Idle, Connecting, Connected, Closing }; @@ -215,6 +271,7 @@ class WsProxySession::Private { Connection sendEventReceivedConnection; Connection keepAliveSetupEventReceivedConnection; Connection refreshEventReceivedConnection; + Connection autoRespondEventReceivedConnection; Connection closeEventReceivedConnection; Connection detachEventReceivedConnection; Connection cancelEventReceivedConnection; @@ -273,6 +330,7 @@ class WsProxySession::Private { map wsProxyConnectionMap; WSConnections outWSConnection; InWSConnections inWSConnection; + std::vector autoRespondConfigs; Private(WsProxySession *_q, ZRoutes *_zroutes, ConnectionManager *_connectionManager, const LogUtil::Config &_logConfig, StatsManager *_statsManager, @@ -594,6 +652,9 @@ class WsProxySession::Private { if (!f.more) incCounter(Stats::ClientMessagesReceived); + if (autoRespond(f)) + continue; + if (detached) continue; @@ -746,6 +807,53 @@ class WsProxySession::Private { woh->setHeaders(requestData.headers); } + bool autoRespond(const WebSocket::Frame &f) { + // We don't buffer full multi-part messages, so only respond to single-part messages + if (f.type == WebSocket::Frame::Continuation) + return false; + + std::optional index; + for (size_t i = 0; i < autoRespondConfigs.size(); ++i) { + if (autoRespondMatch(autoRespondConfigs[i], f)) { + index = i; + break; + } + } + + // No matching config + if (!index.has_value()) + return false; + + const auto &config = autoRespondConfigs[*index]; + + // If auto response specifies a frame type then use it, else default to either the + // same type as the sender, or PONG if the sender used PING. + WebSocket::Frame::Type type; + if (((int)config.type) >= 0) { + type = config.type; + } else { + if (f.type == WebSocket::Frame::Ping) { + type = WebSocket::Frame::Pong; + } else { + type = f.type; + } + } + + QByteArray content = config.content; + if (content.isNull()) + content = f.data; + + WebSocket::Frame resp(type, content, false); + + if (outReadInProgress != -1) { + queuedInFrames += QueuedFrame(resp, false); + } else { + writeInFrame(resp, false); + } + + return true; + } + void incCounter(Stats::Counter c, int count = 1) { if (statsManager) statsManager->incCounter(route.statsRoute(), c, count); @@ -850,6 +958,9 @@ class WsProxySession::Private { boost::placeholders::_1, boost::placeholders::_2)), wsControl->refreshEventReceived.connect( boost::bind(&Private::wsControl_refreshEventReceived, this)), + wsControl->autoRespondEventReceived.connect( + boost::bind(&Private::wsControl_autoRespondEventReceived, this, + boost::placeholders::_1)), wsControl->closeEventReceived.connect( boost::bind(&Private::wsControl_closeEventReceived, this, boost::placeholders::_1, boost::placeholders::_2)), @@ -1005,6 +1116,20 @@ class WsProxySession::Private { woh->refresh(); } + void wsControl_autoRespondEventReceived(const WsControl::AutoRespondConfig &config) { + // Remove any matching config + for (auto it = autoRespondConfigs.begin(); it != autoRespondConfigs.end();) { + if ((*it).matches(config)) { + it = autoRespondConfigs.erase(it); + } else { + ++it; + } + } + + if (autoRespondConfigs.size() < AUTO_RESPOND_CONFIGS_MAX && config.isEnabled()) + autoRespondConfigs.push_back(config); + } + void wsControl_closeEventReceived(int code, const QByteArray &reason) { if (!detached && outSock && outSock->state() != WebSocket::Closing) outSock->close();