diff --git a/Makefile.in b/Makefile.in index 1fc0fc67..5fefe0a6 100644 --- a/Makefile.in +++ b/Makefile.in @@ -44,12 +44,12 @@ LIB_SRCS := ZNCString.cpp Csocket.cpp znc.cpp IRCNetwork.cpp User.cpp IRCSock.c Client.cpp Chan.cpp Nick.cpp Server.cpp Modules.cpp MD5.cpp Buffer.cpp Utils.cpp \ FileUtils.cpp HTTPSock.cpp Template.cpp ClientCommand.cpp Socket.cpp SHA256.cpp \ WebModules.cpp Listener.cpp Config.cpp ZNCDebug.cpp Threads.cpp version.cpp Query.cpp \ - SSLVerifyHost.cpp + SSLVerifyHost.cpp Message.cpp LIB_SRCS := $(addprefix src/,$(LIB_SRCS)) BIN_SRCS := src/main.cpp LIB_OBJS := $(patsubst %cpp,%o,$(LIB_SRCS)) BIN_OBJS := $(patsubst %cpp,%o,$(BIN_SRCS)) -TESTS := StringTest ConfigTest UtilsTest ThreadTest NickTest ClientTest NetworkTest +TESTS := StringTest ConfigTest UtilsTest ThreadTest NickTest ClientTest NetworkTest MessageTest TESTS := $(addprefix test/,$(addsuffix .o,$(TESTS))) CLEAN := znc src/*.o test/*.o core core.* .version_extra .depend modules/.depend \ unittest $(LIBZNC) diff --git a/configure.ac b/configure.ac index 86116074..34f0e7a1 100644 --- a/configure.ac +++ b/configure.ac @@ -292,7 +292,7 @@ if test "$POLL" = "yes"; then fi AC_CHECK_LIB( gnugetopt, getopt_long,) -AC_CHECK_FUNCS([lstat getopt_long getpassphrase]) +AC_CHECK_FUNCS([lstat getopt_long getpassphrase clock_gettime]) # ----- Check for dlopen diff --git a/include/znc/Message.h b/include/znc/Message.h new file mode 100644 index 00000000..b31a1c02 --- /dev/null +++ b/include/znc/Message.h @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2004-2015 ZNC, see the NOTICE file for details. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MESSAGE_H +#define _MESSAGE_H + +#include +#include +#include +#include + +class CChan; +class CClient; +class CIRCNetwork; + +class CMessage { +public: + explicit CMessage(const CString& sMessage = ""); + CMessage(const CNick& Nick, const CString& sCommand, const VCString& vsParams, const MCString& mssTags = MCString::EmptyMap); + + // ZNC <-> IRC + CIRCNetwork* GetNetwork() const { return m_pNetwork; } + void SetNetwork(CIRCNetwork* pNetwork) { m_pNetwork = pNetwork; } + + // ZNC <-> CLI + CClient* GetClient() const { return m_pClient; } + void SetClient(CClient* pClient) { m_pClient = pClient; } + + CChan* GetChan() const { return m_pChan; } + void SetChan(CChan* pChan) { m_pChan = pChan; } + + CNick& GetNick() { return m_Nick; } + const CNick& GetNick() const { return m_Nick; } + void SetNick(const CNick& Nick) { m_Nick = Nick; } + + const CString& GetCommand() const { return m_sCommand; } + void SetCommand(const CString& sCommand) { m_sCommand = sCommand; } + + const VCString& GetParams() const { return m_vsParams; } + CString GetParams(unsigned int uIdx, unsigned int uLen = -1) const; + void SetParams(const VCString& vsParams) { m_vsParams = vsParams; } + + CString GetParam(unsigned int uIdx) const; + void SetParam(unsigned int uIdx, const CString& sParam); + + const timeval& GetTime() const { return m_time; } + void SetTime(const timeval& ts) { m_time = ts; } + + const MCString& GetTags() const { return m_mssTags; } + void SetTags(const MCString& mssTags) { m_mssTags = mssTags; } + + CString GetTag(const CString& sKey) const; + void SetTag(const CString& sKey, const CString& sValue); + + enum FormatFlags { + IncludeAll = 0x0, + ExcludePrefix = 0x1, + ExcludeTags = 0x2 + }; + + CString ToString(unsigned int uFlags = IncludeAll) const; + void Parse(CString sMessage); + +private: + void InitTime(); + + CNick m_Nick; + CString m_sCommand; + VCString m_vsParams; + MCString m_mssTags; + timeval m_time; + CIRCNetwork* m_pNetwork = nullptr; + CClient* m_pClient = nullptr; + CChan* m_pChan = nullptr; +}; + +#endif // !_MESSAGE_H diff --git a/src/Message.cpp b/src/Message.cpp new file mode 100644 index 00000000..d8615841 --- /dev/null +++ b/src/Message.cpp @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2004-2015 ZNC, see the NOTICE file for details. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +CMessage::CMessage(const CString& sMessage) +{ + Parse(sMessage); + InitTime(); +} + +CMessage::CMessage(const CNick& Nick, const CString& sCommand, const VCString& vsParams, const MCString& mssTags) + : m_Nick(Nick), m_sCommand(sCommand), m_vsParams(vsParams), m_mssTags(mssTags) +{ + InitTime(); +} + +CString CMessage::GetParams(unsigned int uIdx, unsigned int uLen) const +{ + VCString vsParams; + if (uLen > m_vsParams.size() - uIdx - 1) { + uLen = m_vsParams.size() - uIdx - 1; + } + for (unsigned int i = uIdx; i <= uIdx + uLen; ++i) { + CString sParam = m_vsParams[i]; + if (sParam.Contains(" ")) { + sParam = ":" + sParam; + } + vsParams.push_back(sParam); + } + return CString(" ").Join(vsParams.begin(), vsParams.end()); +} + +CString CMessage::GetParam(unsigned int uIdx) const +{ + if (uIdx >= m_vsParams.size()) { + return ""; + } + return m_vsParams[uIdx]; +} + +void CMessage::SetParam(unsigned int uIdx, const CString& sParam) +{ + if (uIdx >= m_vsParams.size()) { + m_vsParams.resize(uIdx + 1); + } + m_vsParams[uIdx] = sParam; +} + +CString CMessage::GetTag(const CString& sKey) const +{ + MCString::const_iterator it = m_mssTags.find(sKey); + if (it != m_mssTags.end()) { + return it->second; + } + return ""; +} + +void CMessage::SetTag(const CString& sKey, const CString& sValue) +{ + m_mssTags[sKey] = sValue; +} + +CString CMessage::ToString(unsigned int uFlags) const +{ + CString sMessage; + + // + if (!(uFlags & ExcludePrefix)) { + CString sPrefix = m_Nick.GetHostMask(); + if (!sPrefix.empty()) { + sMessage += ":" + sPrefix; + } + } + + // + if (!m_sCommand.empty()) { + if (!sMessage.empty()) { + sMessage += " "; + } + sMessage += m_sCommand; + } + + // + for (const CString& sParam : m_vsParams) { + sMessage += " "; + if (sParam.Contains(" ")) { + sMessage += ":"; + } + sMessage += sParam; + } + + // + if (!(uFlags & ExcludeTags)) { + CUtils::SetMessageTags(sMessage, m_mssTags); + } + + return sMessage; +} + +void CMessage::Parse(CString sMessage) +{ + // + if (sMessage.StartsWith("@")) { + m_mssTags = CUtils::GetMessageTags(sMessage); + sMessage = sMessage.Token(1, true); + } + + // ::= [':' ] + // ::= | [ '!' ] [ '@' ] + // ::= { } | + // ::= ' ' { ' ' } + // ::= [ ':' | ] + // ::= + // ::= + + // + if (sMessage.TrimLeft(":")) { + m_Nick.Parse(sMessage.Token(0)); + sMessage = sMessage.Token(1, true); + } + + // + m_sCommand = sMessage.Token(0); + sMessage = sMessage.Token(1, true); + + // + m_vsParams.clear(); + while (!sMessage.empty()) { + if (sMessage.TrimLeft(":")) { + m_vsParams.push_back(sMessage); + sMessage.clear(); + } else { + m_vsParams.push_back(sMessage.Token(0)); + sMessage = sMessage.Token(1, true); + } + } +} + +void CMessage::InitTime() +{ + auto it = m_mssTags.find("time"); + if (it != m_mssTags.end()) { + m_time = CUtils::ParseServerTime(it->second); + return; + } + +#ifdef HAVE_CLOCK_GETTIME + timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { + m_time.tv_sec = ts.tv_sec; + m_time.tv_usec = ts.tv_nsec / 1000; + return; + } +#endif + + if (!gettimeofday(&m_time, nullptr)) { + m_time.tv_sec = time(nullptr); + m_time.tv_usec = 0; + } +} diff --git a/test/MessageTest.cpp b/test/MessageTest.cpp new file mode 100644 index 00000000..f9a73ec7 --- /dev/null +++ b/test/MessageTest.cpp @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2004-2015 ZNC, see the NOTICE file for details. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +TEST(MessageTest, SetParam) { + CMessage msg; + + msg.SetParam(1, "bar"); + msg.SetParam(0, "foo"); + + VCString params = {"foo", "bar"}; + EXPECT_EQ(params, msg.GetParams()); + + msg.SetParam(3, "baz"); + + params = {"foo", "bar", "", "baz"}; + EXPECT_EQ(params, msg.GetParams()); +} + +TEST(MessageTest, ToString) { + EXPECT_EQ("CMD", CMessage("CMD").ToString()); + EXPECT_EQ("CMD p1", CMessage("CMD p1").ToString()); + EXPECT_EQ("CMD p1 p2", CMessage("CMD p1 p2").ToString()); + EXPECT_EQ("CMD :p p p", CMessage("CMD :p p p").ToString()); + EXPECT_EQ(":irc.znc.in", CMessage(":irc.znc.in").ToString()); + EXPECT_EQ(":irc.znc.in CMD", CMessage(":irc.znc.in CMD").ToString()); + EXPECT_EQ(":irc.znc.in CMD p1", CMessage(":irc.znc.in CMD p1").ToString()); + EXPECT_EQ(":irc.znc.in CMD p1 p2", CMessage(":irc.znc.in CMD p1 p2").ToString()); + EXPECT_EQ(":irc.znc.in CMD :p p p", CMessage(":irc.znc.in CMD :p p p").ToString()); +} + +TEST(MessageTest, FormatFlags) { + const CString line = "@foo=bar :irc.example.com COMMAND param"; + + CMessage msg(line); + EXPECT_EQ(line, msg.ToString()); + EXPECT_EQ(":irc.example.com COMMAND param", msg.ToString(CMessage::ExcludeTags)); + EXPECT_EQ("@foo=bar COMMAND param", msg.ToString(CMessage::ExcludePrefix)); + EXPECT_EQ("COMMAND param", msg.ToString(CMessage::ExcludePrefix|CMessage::ExcludeTags)); +} + +// The test data for MessageTest.Parse originates from https://github.com/SaberUK/ircparser +// +// IRCParser - Internet Relay Chat Message Parser +// +// Copyright (C) 2015 Peter "SaberUK" Powell +// +// Permission to use, copy, modify, and/or distribute this software for any purpose with or without +// fee is hereby granted, provided that the above copyright notice and this permission notice appear +// in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +// SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +// AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +// NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +// OF THIS SOFTWARE. + +// when checking a valid message with tags and a source +TEST(MessageTest, ParseWithTags) { + const CString line = "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 :irc.example.com COMMAND param1 param2 :param3 param3"; + + MCString tags; + tags["tag1"] = "value1"; + tags["tag2"] = ""; + tags["vendor1/tag3"] = "value2"; + tags["vendor2/tag4"] = ""; + + VCString params = {"param1", "param2", "param3 param3"}; + + CMessage msg(line); + EXPECT_EQ(line, msg.ToString()); + EXPECT_EQ(tags, msg.GetTags()); + EXPECT_EQ("irc.example.com", msg.GetNick().GetNick()); + EXPECT_EQ("COMMAND", msg.GetCommand()); + EXPECT_EQ(params, msg.GetParams()); +} + +// when checking a valid message with a source but no tags +TEST(MessageTest, ParseWithoutTags) { + const CString line = ":irc.example.com COMMAND param1 param2 :param3 param3"; + + VCString params = {"param1", "param2", "param3 param3"}; + + CMessage msg(line); + EXPECT_EQ(line, msg.ToString()); + EXPECT_EQ(MCString(), msg.GetTags()); + EXPECT_EQ("irc.example.com", msg.GetNick().GetNick()); + EXPECT_EQ("COMMAND", msg.GetCommand()); + EXPECT_EQ(params, msg.GetParams()); +} + +// when checking a valid message with tags but no source +TEST(MessageTest, ParseWithoutSource) { + const CString line = "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 COMMAND param1 param2 :param3 param3"; + + MCString tags; + tags["tag1"] = "value1"; + tags["tag2"] = ""; + tags["vendor1/tag3"] = "value2"; + tags["vendor2/tag4"] = ""; + + VCString params = {"param1", "param2", "param3 param3"}; + + CMessage msg(line); + EXPECT_EQ(line, msg.ToString()); + EXPECT_EQ(tags, msg.GetTags()); + EXPECT_EQ("", msg.GetNick().GetNick()); + EXPECT_EQ("COMMAND", msg.GetCommand()); + EXPECT_EQ(params, msg.GetParams()); +} + +// when checking a valid message with no tags, source or parameters +TEST(MessageTest, ParseWithoutSourceAndTags) { + const CString line = "COMMAND"; + + CMessage msg(line); + EXPECT_EQ(line, msg.ToString()); + EXPECT_EQ(MCString(), msg.GetTags()); + EXPECT_EQ("", msg.GetNick().GetNick()); + EXPECT_EQ("COMMAND", msg.GetCommand()); + EXPECT_EQ(VCString(), msg.GetParams()); +}