diff --git a/src/client/ClientTelnet.cpp b/src/client/ClientTelnet.cpp index 005cde465..59f60209f 100644 --- a/src/client/ClientTelnet.cpp +++ b/src/client/ClientTelnet.cpp @@ -6,6 +6,7 @@ #include "ClientTelnet.h" #include "../configuration/configuration.h" +#include "../global/Version.h" #include "../global/io.h" #include "../global/utils.h" #include "../proxy/TextCodec.h" @@ -118,3 +119,47 @@ void ClientTelnet::virt_receiveEchoMode(const bool mode) { m_output.echoModeChanged(mode); } + +void ClientTelnet::virt_receiveNewEnvironSend(const QList &vars, + const QList &userVars) +{ + const bool sendAll = vars.isEmpty() && userVars.isEmpty(); + + QMap myVars; + QMap myUserVars; + + bool clientNameRequested = sendAll; + bool clientVersionRequested = sendAll; + bool charsetRequested = sendAll; + bool mttsRequested = sendAll; + + for (const auto &v : vars) { + if (v == RawBytes("CLIENT_NAME")) { + clientNameRequested = true; + } else if (v == RawBytes("CLIENT_VERSION")) { + clientVersionRequested = true; + } else if (v == RawBytes("CHARSET")) { + charsetRequested = true; + } else if (v == RawBytes("MTTS")) { + mttsRequested = true; + } + } + + if (clientNameRequested) { + myVars[RawBytes("CLIENT_NAME")] = RawBytes("MMapper"); + } + if (clientVersionRequested) { + myVars[RawBytes("CLIENT_VERSION")] = RawBytes(getMMapperVersion()); + } + if (charsetRequested) { + myVars[RawBytes("CHARSET")] = RawBytes("UTF-8"); + } + if (mttsRequested) { + // bitvector = ANSI(1) | UTF-8(4) | 256 COLORS(8) | MNES(512) = 525 + myVars[RawBytes("MTTS")] = RawBytes("525"); + } + + if (!myVars.isEmpty() || !myUserVars.isEmpty()) { + sendNewEnvironIs(myVars, myUserVars); + } +} diff --git a/src/client/ClientTelnet.h b/src/client/ClientTelnet.h index 695d733c2..a347ed68f 100644 --- a/src/client/ClientTelnet.h +++ b/src/client/ClientTelnet.h @@ -63,6 +63,8 @@ class NODISCARD ClientTelnet final : AbstractTelnet private: void virt_sendToMapper(const RawBytes &, bool goAhead) final; void virt_receiveEchoMode(bool) final; + void virt_receiveNewEnvironSend(const QList &vars, + const QList &userVars) final; void virt_sendRawData(const TelnetIacBytes &data) final; public: diff --git a/src/proxy/AbstractSocket.h b/src/proxy/AbstractSocket.h index 097b75540..4a7731223 100644 --- a/src/proxy/AbstractSocket.h +++ b/src/proxy/AbstractSocket.h @@ -21,11 +21,13 @@ class NODISCARD_QOBJECT AbstractSocket : public QIODevice void flush() { virt_flush(); } void disconnectFromHost() { virt_disconnectFromHost(); } NODISCARD bool isConnected() const { return virt_isConnected(); } + NODISCARD QString peerAddress() const { return virt_peerAddress(); } private: virtual void virt_flush() = 0; virtual void virt_disconnectFromHost() = 0; NODISCARD virtual bool virt_isConnected() const = 0; + NODISCARD virtual QString virt_peerAddress() const = 0; signals: void sig_connected(); diff --git a/src/proxy/AbstractTelnet.cpp b/src/proxy/AbstractTelnet.cpp index 90b6b6584..453c34b92 100644 --- a/src/proxy/AbstractTelnet.cpp +++ b/src/proxy/AbstractTelnet.cpp @@ -70,6 +70,7 @@ NODISCARD static QString telnetOptionName(uint8_t opt) CASE(TERMINAL_TYPE); CASE(NAWS); CASE(CHARSET); + CASE(NEW_ENVIRON); CASE(COMPRESS2); CASE(GMCP); CASE(MSSP); @@ -79,23 +80,57 @@ NODISCARD static QString telnetOptionName(uint8_t opt) return QString::asprintf("%u", opt); #undef CASE } -NODISCARD static QString telnetSubnegName(uint8_t opt) +NODISCARD static QString telnetSubnegName(uint8_t option, uint8_t opt) { -#define CASE(x) \ - case TNSB_##x: \ - return #x switch (opt) { - CASE(IS); - CASE(SEND); // TODO: conflict between SEND, REQUEST, EDIT, MODE - CASE(ACCEPTED); - CASE(REJECTED); - CASE(TTABLE_IS); - CASE(TTABLE_REJECTED); - CASE(TTABLE_ACK); - CASE(TTABLE_NAK); + case 0: // TNSB_IS / TNSB_VAR + if (option == OPT_NEW_ENVIRON) { + return "IS/VAR"; + } + return "IS"; + case 1: // TNSB_SEND / TNSB_VAL / TNSB_REQUEST / TNSB_MODE / TNSB_EDIT / TNSB_MSSP_VAR + if (option == OPT_NEW_ENVIRON) { + return "SEND/VAL"; + } + if (option == OPT_CHARSET) { + return "REQUEST"; + } + if (option == OPT_LINEMODE) { + return "MODE/EDIT"; + } + if (option == OPT_MSSP) { + return "MSSP_VAR"; + } + return "SEND"; + case 2: // TNSB_ESC / TNSB_INFO / TNSB_ACCEPTED / TNSB_MSSP_VAL + if (option == OPT_NEW_ENVIRON) { + return "ESC/INFO"; + } + if (option == OPT_CHARSET) { + return "ACCEPTED"; + } + if (option == OPT_MSSP) { + return "MSSP_VAL"; + } + return "ESC/ACCEPTED"; + case 3: // TNSB_USERVAR / TNSB_REJECTED + if (option == OPT_NEW_ENVIRON) { + return "USERVAR"; + } + if (option == OPT_CHARSET) { + return "REJECTED"; + } + return "USERVAR/REJECTED"; + case 4: + return "TTABLE_IS"; + case 5: + return "TTABLE_REJECTED"; + case 6: + return "TTABLE_ACK"; + case 7: + return "TTABLE_NAK"; } return QString::asprintf("%u", opt); -#undef CASE } NODISCARD static bool containsIAC(const std::string_view arr) @@ -474,6 +509,12 @@ void AbstractTelnet::sendTerminalType(const TelnetTermTypeBytes &terminalType) s.addEscaped(TNSB_IS); /* NOTE: "IS" will never actually be escaped */ s.addEscapedBytes(terminalType.getQByteArray()); s.addSubnegEnd(); + + if (m_options.myOptionState[OPT_NEW_ENVIRON]) { + QMap vars; + vars[RawBytes("TERMINAL_TYPE")] = RawBytes(terminalType.getQByteArray()); + sendNewEnvironInfo(vars, {}); + } } void AbstractTelnet::sendCharsetRejected() @@ -495,6 +536,12 @@ void AbstractTelnet::sendCharsetAccepted(const TelnetCharsetBytes &characterSet) s.addRaw(TNSB_ACCEPTED); s.addEscapedBytes(characterSet.getQByteArray()); s.addSubnegEnd(); + + if (m_options.myOptionState[OPT_NEW_ENVIRON]) { + QMap vars; + vars[RawBytes("CHARSET")] = RawBytes(characterSet.getQByteArray()); + sendNewEnvironInfo(vars, {}); + } } void AbstractTelnet::sendOptionStatus() @@ -529,6 +576,73 @@ void AbstractTelnet::sendTerminalTypeRequest() s.addSubnegEnd(); } +void AbstractTelnet::sendNewEnvironIs(const QMap &vars, + const QMap &userVars) +{ + if (m_debug) { + qDebug() << "Sending NEW-ENVIRON IS" << vars << userVars; + } + TelnetFormatter s{*this}; + s.addSubnegBegin(OPT_NEW_ENVIRON); + s.addRaw(TNSB_IS); + for (auto it = vars.begin(); it != vars.end(); ++it) { + s.addRaw(TNSB_VAR); + s.addEscapedBytes(it.key().getQByteArray()); + s.addRaw(TNSB_VAL); + s.addEscapedBytes(it.value().getQByteArray()); + } + for (auto it = userVars.begin(); it != userVars.end(); ++it) { + s.addRaw(TNSB_USERVAR); + s.addEscapedBytes(it.key().getQByteArray()); + s.addRaw(TNSB_VAL); + s.addEscapedBytes(it.value().getQByteArray()); + } + s.addSubnegEnd(); +} + +void AbstractTelnet::sendNewEnvironInfo(const QMap &vars, + const QMap &userVars) +{ + if (m_debug) { + qDebug() << "Sending NEW-ENVIRON INFO" << vars << userVars; + } + TelnetFormatter s{*this}; + s.addSubnegBegin(OPT_NEW_ENVIRON); + s.addRaw(TNSB_INFO); + for (auto it = vars.begin(); it != vars.end(); ++it) { + s.addRaw(TNSB_VAR); + s.addEscapedBytes(it.key().getQByteArray()); + s.addRaw(TNSB_VAL); + s.addEscapedBytes(it.value().getQByteArray()); + } + for (auto it = userVars.begin(); it != userVars.end(); ++it) { + s.addRaw(TNSB_USERVAR); + s.addEscapedBytes(it.key().getQByteArray()); + s.addRaw(TNSB_VAL); + s.addEscapedBytes(it.value().getQByteArray()); + } + s.addSubnegEnd(); +} + +void AbstractTelnet::sendNewEnvironSend(const QList &vars, const QList &userVars) +{ + if (m_debug) { + qDebug() << "Sending NEW-ENVIRON SEND" << vars << userVars; + } + TelnetFormatter s{*this}; + s.addSubnegBegin(OPT_NEW_ENVIRON); + s.addRaw(TNSB_SEND); + for (const auto &v : vars) { + s.addRaw(TNSB_VAR); + s.addEscapedBytes(v.getQByteArray()); + } + for (const auto &v : userVars) { + s.addRaw(TNSB_USERVAR); + s.addEscapedBytes(v.getQByteArray()); + } + s.addSubnegEnd(); +} + void AbstractTelnet::processTelnetCommand(const AppendBuffer &command) { assert(!command.isEmpty()); @@ -588,12 +702,18 @@ void AbstractTelnet::processTelnetCommand(const AppendBuffer &command) } else if (option == OPT_CHARSET) { sendCharsetRequest(); } + } else if (option == OPT_NEW_ENVIRON) { + sendTelnetOption(TN_DO, option); + hisOptionState[option] = true; } else { sendTelnetOption(TN_DONT, option); hisOptionState[option] = false; } } heAnnouncedState[option] = true; + if (option == OPT_NEW_ENVIRON) { + virt_receiveNewEnvironWill(); + } break; case TN_WONT: // peer refuses to enable some option... @@ -606,6 +726,21 @@ void AbstractTelnet::processTelnetCommand(const AppendBuffer &command) if (option == OPT_ECHO) { receiveEchoMode(true); } + if (option == OPT_NEW_ENVIRON) { + virt_receiveNewEnvironWont(); + } + if (option == OPT_TERMINAL_TYPE || option == OPT_CHARSET) { + if (myOptionState[OPT_NEW_ENVIRON]) { + QMap vars; + if (option == OPT_TERMINAL_TYPE) { + vars[RawBytes("TERMINAL_TYPE")] = RawBytes(m_termType.getQByteArray()); + } else { + vars[RawBytes("CHARSET")] = RawBytes( + mmqt::toQByteArrayRaw(std::string(m_textCodec.getName()))); + } + sendNewEnvironInfo(vars, {}); + } + } break; case TN_DO: // peer allows us to enable some option @@ -642,12 +777,18 @@ void AbstractTelnet::processTelnetCommand(const AppendBuffer &command) // REVISIT: Start deflating after sending IAC SB COMPRESS2 IAC SE } else if (option == OPT_CHARSET && heAnnouncedState[option]) { sendCharsetRequest(); + } else if (option == OPT_NEW_ENVIRON) { + // sendTelnetOption(TN_WILL, option); + // myOptionState[option] = true; } } else { sendTelnetOption(TN_WONT, option); myOptionState[option] = false; } announcedState[option] = true; + if (option == OPT_NEW_ENVIRON) { + virt_receiveNewEnvironDo(); + } } break; case TN_DONT: @@ -681,7 +822,7 @@ void AbstractTelnet::processTelnetSubnegotiation(const AppendBuffer &payload) } else if (payload.length() >= 2) { qDebug() << "* Processing Telnet Subnegotiation:" << telnetOptionName(payload.unsigned_at(0)) - << telnetSubnegName(payload.unsigned_at(1)); + << telnetSubnegName(payload.unsigned_at(0), payload.unsigned_at(1)); } } @@ -822,6 +963,91 @@ void AbstractTelnet::processTelnetSubnegotiation(const AppendBuffer &payload) } break; + case OPT_NEW_ENVIRON: + if (myOptionState[OPT_NEW_ENVIRON] || hisOptionState[OPT_NEW_ENVIRON]) { + if (payload.length() < 2) { + break; + } + const uint8_t type = payload.unsigned_at(1); + QList sendVars; + QList sendUserVars; + QMap isVars; + QMap isUserVars; + + RawBytes currentVar; + RawBytes currentVal; + bool inVal = false; + bool isUserVar = false; + + for (int i = 2; i < payload.length(); ++i) { + const uint8_t c = payload.unsigned_at(i); + if (c == TNSB_VAR || c == TNSB_USERVAR) { + if (inVal) { + if (isUserVar) { + isUserVars[currentVar] = currentVal; + } else { + isVars[currentVar] = currentVal; + } + } else if (!currentVar.isEmpty()) { + if (type == TNSB_SEND) { + if (isUserVar) { + sendUserVars.append(currentVar); + } else { + sendVars.append(currentVar); + } + } + } + currentVar.clear(); + currentVal.clear(); + inVal = false; + isUserVar = (c == TNSB_USERVAR); + } else if (c == TNSB_VAL) { + inVal = true; + currentVal.clear(); + } else if (c == TNSB_ESC) { + if (i + 1 < payload.length()) { + const uint8_t next = payload.unsigned_at(++i); + if (inVal) { + currentVal.append(static_cast(next)); + } else { + currentVar.append(static_cast(next)); + } + } + } else { + if (inVal) { + currentVal.append(static_cast(c)); + } else { + currentVar.append(static_cast(c)); + } + } + } + // Add last one + if (inVal) { + if (isUserVar) { + isUserVars[currentVar] = currentVal; + } else { + isVars[currentVar] = currentVal; + } + } else if (!currentVar.isEmpty()) { + if (type == TNSB_SEND) { + if (isUserVar) { + sendUserVars.append(currentVar); + } else { + sendVars.append(currentVar); + } + } + } + + if (type == TNSB_SEND) { + virt_receiveNewEnvironSend(sendVars, sendUserVars); + } else if (type == TNSB_IS) { + virt_receiveNewEnvironIs(isVars, isUserVars); + } else if (type == TNSB_INFO) { + virt_receiveNewEnvironInfo(isVars, isUserVars); + } + } + break; + case OPT_NAWS: if (myOptionState[OPT_NAWS] || hisOptionState[OPT_NAWS]) { // NAWS <16-bit value> <16-bit value> diff --git a/src/proxy/AbstractTelnet.h b/src/proxy/AbstractTelnet.h index 34edb27b4..9cfede91b 100644 --- a/src/proxy/AbstractTelnet.h +++ b/src/proxy/AbstractTelnet.h @@ -46,6 +46,7 @@ static constexpr const uint8_t OPT_TERMINAL_TYPE = 24; static constexpr const uint8_t OPT_EOR = 25; static constexpr const uint8_t OPT_NAWS = 31; static constexpr const uint8_t OPT_LINEMODE = 34; +static constexpr const uint8_t OPT_NEW_ENVIRON = 39; static constexpr const uint8_t OPT_CHARSET = 42; static constexpr const uint8_t OPT_MSSP = 70; static constexpr const uint8_t OPT_COMPRESS2 = 86; @@ -53,7 +54,12 @@ static constexpr const uint8_t OPT_GMCP = 201; // telnet SB suboption types static constexpr const uint8_t TNSB_IS = 0; +static constexpr const uint8_t TNSB_VAR = 0; static constexpr const uint8_t TNSB_SEND = 1; +static constexpr const uint8_t TNSB_VAL = 1; +static constexpr const uint8_t TNSB_ESC = 2; +static constexpr const uint8_t TNSB_INFO = 2; +static constexpr const uint8_t TNSB_USERVAR = 3; static constexpr const uint8_t TNSB_REQUEST = 1; static constexpr const uint8_t TNSB_MODE = 1; static constexpr const uint8_t TNSB_EDIT = 1; @@ -225,6 +231,16 @@ class NODISCARD AbstractTelnet virtual void virt_receiveTerminalType(const TelnetTermTypeBytes &) {} virtual void virt_receiveMudServerStatus(const TelnetMsspBytes &) {} virtual void virt_receiveWindowSize(int, int) {} + virtual void virt_receiveNewEnvironSend(const QList &, const QList &) {} + virtual void virt_receiveNewEnvironIs(const QMap &, + const QMap &) + {} + virtual void virt_receiveNewEnvironInfo(const QMap &, + const QMap &) + {} + virtual void virt_receiveNewEnvironDo() {} + virtual void virt_receiveNewEnvironWill() {} + virtual void virt_receiveNewEnvironWont() {} virtual void virt_sendRawData(const TelnetIacBytes &data) = 0; virtual void virt_sendToMapper(const RawBytes &, bool goAhead) = 0; @@ -264,9 +280,17 @@ class NODISCARD AbstractTelnet void reset(); void onReadInternal(const TelnetIacBytes &); void setTerminalType(const TelnetTermTypeBytes &terminalType) { m_termType = terminalType; } + void setEncodingForName(const std::string &name) { m_textCodec.setEncodingForName(name); } NODISCARD CharacterEncodingEnum getEncoding() const { return m_textCodec.getEncoding(); } +protected: + void sendNewEnvironIs(const QMap &vars, + const QMap &userVars); + void sendNewEnvironInfo(const QMap &vars, + const QMap &userVars); + void sendNewEnvironSend(const QList &vars, const QList &userVars); + private: void onReadInternal2(AppendBuffer &, uint8_t); diff --git a/src/proxy/MudTelnet.cpp b/src/proxy/MudTelnet.cpp index d0a4c68ea..25b4e2f6e 100644 --- a/src/proxy/MudTelnet.cpp +++ b/src/proxy/MudTelnet.cpp @@ -417,6 +417,33 @@ void MudTelnet::onRelayTermType(const TelnetTermTypeBytes &terminalType) } } +void MudTelnet::onRelayNewEnvironIs(const QMap &vars, + const QMap &userVars) +{ + if (getOptions().myOptionState[OPT_NEW_ENVIRON]) { + sendNewEnvironIs(vars, userVars); + } +} + +void MudTelnet::onRelayNewEnvironInfo(const QMap &vars, + const QMap &userVars) +{ + if (getOptions().myOptionState[OPT_NEW_ENVIRON]) { + sendNewEnvironInfo(vars, userVars); + } +} + +void MudTelnet::onUserNewEnvironNegotiated(const bool supported) +{ + m_userSupportsNewEnviron = supported; + if (supported) { + if (m_serverRequestedNewEnviron && !getOptions().myOptionState[OPT_NEW_ENVIRON]) { + sendTelnetOption(TN_WILL, OPT_NEW_ENVIRON); + m_options.myOptionState[OPT_NEW_ENVIRON] = true; + } + } +} + void MudTelnet::onLoginCredentials(const QString &name, const QString &password) { sendGmcpMessage( @@ -512,6 +539,74 @@ void MudTelnet::virt_receiveMudServerStatus(const TelnetMsspBytes &ba) m_outputs.onSendMSSPToUser(ba); } +void MudTelnet::virt_receiveNewEnvironSend(const QList &vars, + const QList &userVars) +{ + if (!m_userSupportsNewEnviron) { + // We wait until we get data from the client. + // But for now, we just relay the request. + m_outputs.onRelayNewEnvironSendFromMudToUser(vars, userVars); + return; + } + + const bool sendAll = vars.isEmpty() && userVars.isEmpty(); + + QMap myVars; + QMap myUserVars; + + bool mttsRequested = sendAll; + bool ipRequested = sendAll; + + for (const auto &v : vars) { + if (v == RawBytes("MTTS")) { + mttsRequested = true; + } else if (v == RawBytes("IPADDRESS")) { + ipRequested = true; + } + } + + if (mttsRequested) { + // MMapper capabilities: ANSI(1) | UTF-8(4) | 256 COLORS(8) | PROXY(128) | MNES(512) + // bitvector = 1 + 4 + 8 + 128 + 512 = 653 + myVars[RawBytes("MTTS")] = RawBytes("653"); + } + + if (ipRequested) { + myVars[RawBytes("IPADDRESS")] = RawBytes(m_outputs.onGetPeerAddress().toUtf8()); + } + + if (!myVars.isEmpty() || !myUserVars.isEmpty()) { + sendNewEnvironIs(myVars, myUserVars); + } + + // Relay the request to the user client as well + m_outputs.onRelayNewEnvironSendFromMudToUser(vars, userVars); +} + +void MudTelnet::virt_receiveNewEnvironIs(const QMap & /*vars*/, + const QMap & /*userVars*/) +{ + // A server shouldn't send IS to a client, but if it does, ignore it. +} + +void MudTelnet::virt_receiveNewEnvironInfo(const QMap & /*vars*/, + const QMap & /*userVars*/) +{ + // A server shouldn't send INFO to a client, but if it does, ignore it. +} + +void MudTelnet::virt_receiveNewEnvironDo() +{ + m_serverRequestedNewEnviron = true; + // If the mud sends DO NEW-ENVIRON, we check if the user supports it. + if (m_userSupportsNewEnviron) { + if (!getOptions().myOptionState[OPT_NEW_ENVIRON]) { + sendTelnetOption(TN_WILL, OPT_NEW_ENVIRON); + m_options.myOptionState[OPT_NEW_ENVIRON] = true; + } + } +} + void MudTelnet::virt_onGmcpEnabled() { if (getDebug()) { diff --git a/src/proxy/MudTelnet.h b/src/proxy/MudTelnet.h index f54df5dd9..be7d2640d 100644 --- a/src/proxy/MudTelnet.h +++ b/src/proxy/MudTelnet.h @@ -42,6 +42,12 @@ struct NODISCARD MudTelnetOutputs virt_onMumeClientEdit(id, title, body); } void onMumeClientError(const QString &errmsg) { virt_onMumeClientError(errmsg); } + void onRelayNewEnvironSendFromMudToUser(const QList &vars, + const QList &userVars) + { + virt_onRelayNewEnvironSendFromMudToUser(vars, userVars); + } + NODISCARD QString onGetPeerAddress() const { return virt_onGetPeerAddress(); } private: virtual void virt_onAnalyzeMudStream(const RawBytes &, bool goAhead) = 0; @@ -57,6 +63,10 @@ struct NODISCARD MudTelnetOutputs const QString &body) = 0; virtual void virt_onMumeClientError(const QString &errmsg) = 0; + virtual void virt_onRelayNewEnvironSendFromMudToUser(const QList &vars, + const QList &userVars) + = 0; + NODISCARD virtual QString virt_onGetPeerAddress() const = 0; }; class NODISCARD MudTelnet final : public AbstractTelnet @@ -67,6 +77,8 @@ class NODISCARD MudTelnet final : public AbstractTelnet GmcpModuleSet m_gmcp; QString m_lineBuffer; bool m_receivedExternalDiscordHello = false; + bool m_userSupportsNewEnviron = false; + bool m_serverRequestedNewEnviron = false; public: explicit MudTelnet(MudTelnetOutputs &outputs); @@ -77,6 +89,13 @@ class NODISCARD MudTelnet final : public AbstractTelnet void virt_receiveEchoMode(bool toggle) final; void virt_receiveGmcpMessage(const GmcpMessage &) final; void virt_receiveMudServerStatus(const TelnetMsspBytes &) final; + void virt_receiveNewEnvironSend(const QList &vars, + const QList &userVars) final; + void virt_receiveNewEnvironIs(const QMap &vars, + const QMap &userVars) final; + void virt_receiveNewEnvironInfo(const QMap &vars, + const QMap &userVars) final; + void virt_receiveNewEnvironDo() final; void virt_onGmcpEnabled() final; void virt_sendRawData(const TelnetIacBytes &data) final; @@ -98,6 +117,11 @@ class NODISCARD MudTelnet final : public AbstractTelnet void onSendToMud(const QString &); void onRelayNaws(int, int); void onRelayTermType(const TelnetTermTypeBytes &); + void onRelayNewEnvironIs(const QMap &vars, + const QMap &userVars); + void onRelayNewEnvironInfo(const QMap &vars, + const QMap &userVars); + void onUserNewEnvironNegotiated(bool supported); void onGmcpToMud(const GmcpMessage &); void onLoginCredentials(const QString &, const QString &); }; diff --git a/src/proxy/TaggedBytes.h b/src/proxy/TaggedBytes.h index a2a169a40..7f9d20974 100644 --- a/src/proxy/TaggedBytes.h +++ b/src/proxy/TaggedBytes.h @@ -80,6 +80,10 @@ struct NODISCARD TaggedBytes : private QByteArray { return !(a == b); } + NODISCARD friend bool operator<(const TaggedBytes &a, const TaggedBytes &b) + { + return a.getQByteArray() < b.getQByteArray(); + } friend QDebug operator<<(QDebug debug, const TaggedBytes &tagged) { return debug << tagged.getQByteArray(); diff --git a/src/proxy/TcpSocket.cpp b/src/proxy/TcpSocket.cpp index b4c99ce27..18b857fbc 100644 --- a/src/proxy/TcpSocket.cpp +++ b/src/proxy/TcpSocket.cpp @@ -43,6 +43,11 @@ bool TcpSocket::virt_isConnected() const return m_socket.state() == QAbstractSocket::ConnectedState; } +QString TcpSocket::virt_peerAddress() const +{ + return m_socket.peerAddress().toString(); +} + qint64 TcpSocket::bytesAvailable() const { return m_socket.bytesAvailable(); diff --git a/src/proxy/TcpSocket.h b/src/proxy/TcpSocket.h index 6d0f005e2..949181915 100644 --- a/src/proxy/TcpSocket.h +++ b/src/proxy/TcpSocket.h @@ -21,6 +21,7 @@ class NODISCARD_QOBJECT TcpSocket final : public AbstractSocket void virt_flush() override; void virt_disconnectFromHost() override; NODISCARD bool virt_isConnected() const override; + NODISCARD QString virt_peerAddress() const override; NODISCARD qint64 bytesAvailable() const override; diff --git a/src/proxy/UserTelnet.cpp b/src/proxy/UserTelnet.cpp index e02e2fdb3..0e5767749 100644 --- a/src/proxy/UserTelnet.cpp +++ b/src/proxy/UserTelnet.cpp @@ -101,6 +101,7 @@ void UserTelnet::onConnected() requestTelnetOption(TN_WILL, OPT_GMCP); // Request permission to replace IAC GA with IAC EOR requestTelnetOption(TN_WILL, OPT_EOR); + requestTelnetOption(TN_DO, OPT_NEW_ENVIRON); } void UserTelnet::onAnalyzeUserStream(const TelnetIacBytes &data) @@ -142,6 +143,15 @@ void UserTelnet::onSendMSSPToUser(const TelnetMsspBytes &data) sendMudServerStatus(data); } +void UserTelnet::onRelayNewEnvironSend(const QList &vars, + const QList &userVars) +{ + if (!getOptions().myOptionState[OPT_NEW_ENVIRON]) { + return; + } + sendNewEnvironSend(vars, userVars); +} + void UserTelnet::virt_sendToMapper(const RawBytes &data, const bool goAhead) { m_outputs.onAnalyzeUserStream(decodeFromUser(getEncoding(), data), goAhead); @@ -238,6 +248,65 @@ void UserTelnet::virt_receiveWindowSize(const int x, const int y) m_outputs.onRelayNawsFromUserToMud(x, y); } +void UserTelnet::virt_receiveNewEnvironSend(const QList & /*vars*/, + const QList & /*userVars*/) +{ + // A client shouldn't send SEND to a server, but if it does, ignore it for now as a proxy. +} + +void UserTelnet::virt_receiveNewEnvironIs(const QMap &vars, + const QMap &userVars) +{ + auto it = vars.find(RawBytes("MTTS")); + if (it != vars.end()) { + bool ok = false; + int mtts = it.value().getQByteArray().toInt(&ok); + if (ok && (mtts & 4)) { // UTF-8 bit + setEncodingForName(ENCODING_UTF_8); + } + } + it = vars.find(RawBytes("CHARSET")); + if (it != vars.end()) { + setEncodingForName(mmqt::toStdStringUtf8(it.value().getQByteArray())); + } + + m_outputs.onRelayNewEnvironIsFromUserToMud(vars, userVars); +} + +void UserTelnet::virt_receiveNewEnvironInfo(const QMap &vars, + const QMap &userVars) +{ + auto it = vars.find(RawBytes("MTTS")); + if (it != vars.end()) { + bool ok = false; + int mtts = it.value().getQByteArray().toInt(&ok); + if (ok && (mtts & 4)) { // UTF-8 bit + setEncodingForName(ENCODING_UTF_8); + } + } + it = vars.find(RawBytes("CHARSET")); + if (it != vars.end()) { + setEncodingForName(mmqt::toStdStringUtf8(it.value().getQByteArray())); + } + + m_outputs.onRelayNewEnvironInfoFromUserToMud(vars, userVars); +} + +void UserTelnet::virt_receiveNewEnvironDo() +{ + m_outputs.onNewEnvironNegotiated(true); +} + +void UserTelnet::virt_receiveNewEnvironWill() +{ + m_outputs.onNewEnvironNegotiated(true); +} + +void UserTelnet::virt_receiveNewEnvironWont() +{ + m_outputs.onNewEnvironNegotiated(false); +} + void UserTelnet::virt_sendRawData(const TelnetIacBytes &data) { m_outputs.onSendToSocket(data); diff --git a/src/proxy/UserTelnet.h b/src/proxy/UserTelnet.h index 8cf7f4fbd..5d95fb834 100644 --- a/src/proxy/UserTelnet.h +++ b/src/proxy/UserTelnet.h @@ -29,6 +29,17 @@ struct NODISCARD UserTelnetOutputs { virt_onRelayTermTypeFromUserToMud(bytes); } + void onRelayNewEnvironIsFromUserToMud(const QMap &vars, + const QMap &userVars) + { + virt_onRelayNewEnvironIsFromUserToMud(vars, userVars); + } + void onRelayNewEnvironInfoFromUserToMud(const QMap &vars, + const QMap &userVars) + { + virt_onRelayNewEnvironInfoFromUserToMud(vars, userVars); + } + void onNewEnvironNegotiated(bool supported) { virt_onNewEnvironNegotiated(supported); } private: virtual void virt_onAnalyzeUserStream(const RawBytes &, bool) = 0; @@ -36,6 +47,13 @@ struct NODISCARD UserTelnetOutputs virtual void virt_onRelayGmcpFromUserToMud(const GmcpMessage &) = 0; virtual void virt_onRelayNawsFromUserToMud(int, int) = 0; virtual void virt_onRelayTermTypeFromUserToMud(const TelnetTermTypeBytes &) = 0; + virtual void virt_onRelayNewEnvironIsFromUserToMud(const QMap &vars, + const QMap &userVars) + = 0; + virtual void virt_onRelayNewEnvironInfoFromUserToMud(const QMap &vars, + const QMap &userVars) + = 0; + virtual void virt_onNewEnvironNegotiated(bool supported) = 0; }; class NODISCARD UserTelnet final : public AbstractTelnet @@ -63,6 +81,15 @@ class NODISCARD UserTelnet final : public AbstractTelnet void virt_receiveGmcpMessage(const GmcpMessage &) final; void virt_receiveTerminalType(const TelnetTermTypeBytes &) final; void virt_receiveWindowSize(int, int) final; + void virt_receiveNewEnvironSend(const QList &vars, + const QList &userVars) final; + void virt_receiveNewEnvironIs(const QMap &vars, + const QMap &userVars) final; + void virt_receiveNewEnvironInfo(const QMap &vars, + const QMap &userVars) final; + void virt_receiveNewEnvironDo() final; + void virt_receiveNewEnvironWill() final; + void virt_receiveNewEnvironWont() final; void virt_sendRawData(const TelnetIacBytes &data) final; private: @@ -78,4 +105,5 @@ class NODISCARD UserTelnet final : public AbstractTelnet void onRelayEchoMode(bool); void onGmcpToUser(const GmcpMessage &); void onSendMSSPToUser(const TelnetMsspBytes &); + void onRelayNewEnvironSend(const QList &vars, const QList &userVars); }; diff --git a/src/proxy/VirtualSocket.cpp b/src/proxy/VirtualSocket.cpp index 2900717fe..b13b596f0 100644 --- a/src/proxy/VirtualSocket.cpp +++ b/src/proxy/VirtualSocket.cpp @@ -61,6 +61,11 @@ bool VirtualSocket::virt_isConnected() const return m_peer != nullptr && m_peer->m_peer != nullptr; } +QString VirtualSocket::virt_peerAddress() const +{ + return "127.0.0.1"; +} + void VirtualSocket::slot_onPeerDestroyed() { if (m_peer) { diff --git a/src/proxy/VirtualSocket.h b/src/proxy/VirtualSocket.h index 531b8c28a..5210b4d18 100644 --- a/src/proxy/VirtualSocket.h +++ b/src/proxy/VirtualSocket.h @@ -24,6 +24,7 @@ class NODISCARD_QOBJECT VirtualSocket final : public AbstractSocket void virt_flush() override; void virt_disconnectFromHost() override; NODISCARD bool virt_isConnected() const override; + NODISCARD QString virt_peerAddress() const override; void connectToPeer(VirtualSocket *peer); diff --git a/src/proxy/proxy.cpp b/src/proxy/proxy.cpp index cd93da563..5be079689 100644 --- a/src/proxy/proxy.cpp +++ b/src/proxy/proxy.cpp @@ -411,6 +411,22 @@ void Proxy::allocUserTelnet() // forwarded (to mud) getMudTelnet().onRelayTermType(bytes); } + void virt_onRelayNewEnvironIsFromUserToMud(const QMap &vars, + const QMap &userVars) final + { + // forwarded (to mud) + getMudTelnet().onRelayNewEnvironIs(vars, userVars); + } + void virt_onRelayNewEnvironInfoFromUserToMud(const QMap &vars, + const QMap &userVars) final + { + // forwarded (to mud) + getMudTelnet().onRelayNewEnvironInfo(vars, userVars); + } + void virt_onNewEnvironNegotiated(bool supported) final + { + getMudTelnet().onUserNewEnvironNegotiated(supported); + } }; auto &pipe = getPipeline(); @@ -443,7 +459,7 @@ void Proxy::allocMudTelnet() {} private: - NODISCARD Proxy &getProxy() { return m_proxy; } + NODISCARD Proxy &getProxy() const { return m_proxy; } NODISCARD TelnetLineFilter &getMudTelnetFilter() { return getProxy().getMudTelnetFilter(); } NODISCARD UserTelnet &getUserTelnet() { return getProxy().getUserTelnet(); } NODISCARD MumeXmlParser &getMudParser() { return getProxy().getMudParser(); } @@ -528,6 +544,17 @@ void Proxy::allocMudTelnet() getProxy().sendToUser(SendToUserSourceEnum::FromMMapper, QString("MUME.Client protocol error: %1").arg(errmsg)); } + + void virt_onRelayNewEnvironSendFromMudToUser(const QList &vars, + const QList &userVars) final + { + getUserTelnet().onRelayNewEnvironSend(vars, userVars); + } + + NODISCARD QString virt_onGetPeerAddress() const final + { + return getProxy().getUserSocketAddress(); + } }; auto &pipe = getPipeline(); @@ -1185,3 +1212,8 @@ RemoteEdit &Proxy::getRemoteEdit() { return deref(m_remoteEdit); } + +QString Proxy::getUserSocketAddress() const +{ + return m_userSocket->peerAddress(); +} diff --git a/src/proxy/proxy.h b/src/proxy/proxy.h index 580f9d3ac..264d6886e 100644 --- a/src/proxy/proxy.h +++ b/src/proxy/proxy.h @@ -329,4 +329,5 @@ class NODISCARD_QOBJECT Proxy final : public QObject private: NODISCARD bool hasConnectedUserSocket() const; + NODISCARD QString getUserSocketAddress() const; }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c6d58ad24..63fd22cc0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -129,7 +129,11 @@ set(proxy_SRCS ../src/proxy/GmcpUtils.h ../src/proxy/telnetfilter.cpp ../src/proxy/telnetfilter.h - ) + ../src/proxy/AbstractTelnet.cpp + ../src/proxy/AbstractTelnet.h + ../src/proxy/TextCodec.cpp + ../src/proxy/TextCodec.h +) set(TestProxy_SRCS TestProxy.cpp) add_executable(TestProxy ${TestProxy_SRCS} ${proxy_SRCS}) add_dependencies(TestProxy mm_test mm_global) diff --git a/tests/TestProxy.cpp b/tests/TestProxy.cpp index 077d9b0e4..ad5d63f19 100644 --- a/tests/TestProxy.cpp +++ b/tests/TestProxy.cpp @@ -4,6 +4,7 @@ #include "TestProxy.h" #include "../src/global/TextUtils.h" +#include "../src/proxy/AbstractTelnet.h" #include "../src/proxy/GmcpMessage.h" #include "../src/proxy/GmcpModule.h" #include "../src/proxy/GmcpUtils.h" @@ -68,4 +69,103 @@ void TestProxy::telnetFilterTest() test::test_telnetfilter(); } +class TestTelnet final : public AbstractTelnet +{ +public: + TestTelnet() + : AbstractTelnet(TextCodecStrategyEnum::FORCE_UTF_8, TelnetTermTypeBytes{"test"}) + {} + ~TestTelnet() override; + + void virt_sendRawData(const TelnetIacBytes &data) override { lastRaw = data; } + void virt_sendToMapper(const RawBytes & /*data*/, bool /*goAhead*/) override {} + void virt_receiveNewEnvironSend(const QList &vars, + const QList &userVars) override + { + receivedVars = vars; + receivedUserVars = userVars; + receivedType = 1; // SEND + } + void virt_receiveNewEnvironIs(const QMap &vars, + const QMap &userVars) override + { + receivedIsVars = vars; + receivedIsUserVars = userVars; + receivedType = 0; // IS + } + void virt_receiveNewEnvironInfo(const QMap &vars, + const QMap &userVars) override + { + receivedIsVars = vars; + receivedIsUserVars = userVars; + receivedType = 2; // INFO + } + + void onRead(const QByteArray &data) { onReadInternal(TelnetIacBytes{data}); } + void enableNewEnviron() { m_options.myOptionState[OPT_NEW_ENVIRON] = true; } + + TelnetIacBytes lastRaw; + QList receivedVars; + QList receivedUserVars; + QMap receivedIsVars; + QMap receivedIsUserVars; + int receivedType = -1; +}; + +TestTelnet::~TestTelnet() = default; + +void TestProxy::mnesTest() +{ + TestTelnet telnet; + telnet.enableNewEnviron(); + + // Test SEND ALL + // IAC SB NEW-ENVIRON SEND IAC SE -> 255 250 39 1 255 240 + QByteArray sendAll; + sendAll.append(static_cast(255u)); + sendAll.append(static_cast(250u)); + sendAll.append(static_cast(39u)); + sendAll.append(static_cast(1u)); + sendAll.append(static_cast(255u)); + sendAll.append(static_cast(240u)); + telnet.onRead(sendAll); + QCOMPARE(telnet.receivedType, 1); + QVERIFY(telnet.receivedVars.isEmpty()); + QVERIFY(telnet.receivedUserVars.isEmpty()); + + // Test SEND VAR + // IAC SB NEW-ENVIRON SEND VAR "MTTS" IAC SE + QByteArray sendVar; + sendVar.append(static_cast(255u)); + sendVar.append(static_cast(250u)); + sendVar.append(static_cast(39u)); + sendVar.append(static_cast(1u)); + sendVar.append(static_cast(0u)); + sendVar.append("MTTS"); + sendVar.append(static_cast(255u)); + sendVar.append(static_cast(240u)); + telnet.onRead(sendVar); + QCOMPARE(telnet.receivedType, 1); + QCOMPARE(telnet.receivedVars.size(), 1); + QCOMPARE(telnet.receivedVars[0].getQByteArray(), QByteArray("MTTS")); + + // Test IS + // IAC SB NEW-ENVIRON IS VAR "CHARSET" VAL "UTF-8" IAC SE + QByteArray isVar; + isVar.append(static_cast(255u)); + isVar.append(static_cast(250u)); + isVar.append(static_cast(39u)); + isVar.append(static_cast(0u)); + isVar.append(static_cast(0u)); + isVar.append("CHARSET"); + isVar.append(static_cast(1u)); + isVar.append("UTF-8"); + isVar.append(static_cast(255u)); + isVar.append(static_cast(240u)); + telnet.onRead(isVar); + QCOMPARE(telnet.receivedType, 0); + QCOMPARE(telnet.receivedIsVars.size(), 1); + QCOMPARE(telnet.receivedIsVars[RawBytes("CHARSET")].getQByteArray(), QByteArray("UTF-8")); +} + QTEST_MAIN(TestProxy) diff --git a/tests/TestProxy.h b/tests/TestProxy.h index 1f10279ce..1a6b92686 100644 --- a/tests/TestProxy.h +++ b/tests/TestProxy.h @@ -20,4 +20,5 @@ private Q_SLOTS: static void gmcpMessageSerializeTest(); static void gmcpModuleTest(); static void telnetFilterTest(); + static void mnesTest(); };