1 /* OperServ core functions
2  *
3  * (C) 2003-2020 Anope Team
4  * Contact us at team@anope.org
5  *
6  * Please read COPYING and README for further details.
7  *
8  * Based on the original code of Epona by Lara.
9  * Based on the original code of Services by Andy Church.
10  */
11 
12 #include "module.h"
13 
14 static unsigned int HARDMAX = 65536;
15 
16 class CommandOSLogSearch : public Command
17 {
CreateLogName(const Anope::string & file,time_t t=Anope::CurTime)18 	static inline Anope::string CreateLogName(const Anope::string &file, time_t t = Anope::CurTime)
19 	{
20 		char timestamp[32];
21 
22 		tm *tm = localtime(&t);
23 
24 		strftime(timestamp, sizeof(timestamp), "%Y%m%d", tm);
25 
26 		return Anope::LogDir + "/" + file + "." + timestamp;
27 	}
28 
29  public:
CommandOSLogSearch(Module * creator)30 	CommandOSLogSearch(Module *creator) : Command(creator, "operserv/logsearch", 1, 3)
31 	{
32 		this->SetDesc(_("Searches logs for a matching pattern"));
33 		this->SetSyntax(_("[+\037days\037d] [+\037limit\037l] \037pattern\037"));
34 	}
35 
Execute(CommandSource & source,const std::vector<Anope::string> & params)36 	void Execute(CommandSource &source, const std::vector<Anope::string> &params) anope_override
37 	{
38 		int days = 7, replies = 50;
39 
40 		unsigned i;
41 		for (i = 0; i < params.size() && params[i][0] == '+'; ++i)
42 		{
43 			switch (params[i][params[i].length() - 1])
44 			{
45 				case 'd':
46 					if (params[i].length() > 2)
47 					{
48 						Anope::string dur = params[i].substr(1, params[i].length() - 2);
49 						try
50 						{
51 							days = convertTo<int>(dur);
52 							if (days <= 0)
53 								throw ConvertException();
54 						}
55 						catch (const ConvertException &)
56 						{
57 							source.Reply(_("Invalid duration %s, using %d days."), dur.c_str(), days);
58 						}
59 					}
60 					break;
61 				case 'l':
62 					if (params[i].length() > 2)
63 					{
64 						Anope::string dur = params[i].substr(1, params[i].length() - 2);
65 						try
66 						{
67 							replies = convertTo<int>(dur);
68 							if (replies <= 0)
69 								throw ConvertException();
70 						}
71 						catch (const ConvertException &)
72 						{
73 							source.Reply(_("Invalid limit %s, using %d."), dur.c_str(), replies);
74 						}
75 					}
76 					break;
77 				default:
78 					source.Reply(_("Unknown parameter: %s"), params[i].c_str());
79 			}
80 		}
81 
82 		if (i >= params.size())
83 		{
84 			this->OnSyntaxError(source, "");
85 			return;
86 		}
87 
88 		Anope::string search_string = params[i++];
89 		for (; i < params.size(); ++i)
90 			search_string += " " + params[i];
91 
92 		Log(LOG_ADMIN, source, this) << "for " << search_string;
93 
94 		bool wildcard = search_string.find_first_of("?*") != Anope::string::npos;
95 		bool regex = search_string.empty() == false && search_string[0] == '/' && search_string[search_string.length() - 1] == '/';
96 
97 		const Anope::string &logfile_name = Config->GetModule(this->owner)->Get<const Anope::string>("logname");
98 		std::vector<Anope::string> matches;
99 		for (int d = days - 1; d >= 0; --d)
100 		{
101 			Anope::string lf_name = CreateLogName(logfile_name, Anope::CurTime - (d * 86400));
102 			Log(LOG_DEBUG) << "Searching " << lf_name;
103 			std::fstream fd(lf_name.c_str(), std::ios_base::in);
104 			if (!fd.is_open())
105 				continue;
106 
107 			for (Anope::string buf, token; std::getline(fd, buf.str());)
108 			{
109 				bool match = false;
110 
111 				if (regex)
112 					match = Anope::Match(buf, search_string, false, true);
113 				else if (wildcard)
114 					match = Anope::Match(buf, "*" + search_string + "*");
115 				else
116 					match = buf.find_ci(search_string) != Anope::string::npos;
117 
118 				if (match)
119 				{
120 					matches.push_back(buf);
121 
122 					if (matches.size() >= HARDMAX)
123 						break;
124 				}
125 			}
126 
127 			fd.close();
128 		}
129 
130 		unsigned int found = matches.size();
131 		if (!found)
132 		{
133 			source.Reply(_("No matches for \002%s\002 found."), search_string.c_str());
134 			return;
135 		}
136 
137 		if (matches.size() >= HARDMAX)
138 		{
139 			source.Reply(_("Too many results for \002%s\002."), search_string.c_str());
140 			return;
141 		}
142 
143 		if (matches.size() > static_cast<unsigned int>(replies))
144 		{
145 			matches.erase(matches.begin(), matches.begin() + (matches.size() - static_cast<unsigned int>(replies)));
146 		}
147 
148 		source.Reply(_("Matches for \002%s\002:"), search_string.c_str());
149 		unsigned int count = 0;
150 		for (std::vector<Anope::string>::iterator it = matches.begin(), it_end = matches.end(); it != it_end; ++it)
151 			source.Reply("#%d: %s", ++count, it->c_str());
152 		source.Reply(_("Showed %d/%d matches for \002%s\002."), matches.size(), found, search_string.c_str());
153 	}
154 
OnHelp(CommandSource & source,const Anope::string & subcommand)155 	bool OnHelp(CommandSource &source, const Anope::string &subcommand) anope_override
156 	{
157 		this->SendSyntax(source);
158 		source.Reply(" ");
159 		source.Reply(_("This command searches the Services logfiles for messages\n"
160 				"that match the given pattern. The day and limit argument\n"
161 				"may be used to specify how many days of logs to search\n"
162 				"and the number of replies to limit to. By default this\n"
163 				"command searches one week of logs, and limits replies\n"
164 				"to 50.\n"
165 				" \n"
166 				"For example:\n"
167 				"    \002LOGSEARCH +21d +500l Anope\002\n"
168 				"      Searches the last 21 days worth of logs for messages\n"
169 				"      containing Anope and lists the most recent 500 of them."));
170 		return true;
171 	}
172 };
173 
174 class OSLogSearch : public Module
175 {
176 	CommandOSLogSearch commandoslogsearch;
177 
178  public:
OSLogSearch(const Anope::string & modname,const Anope::string & creator)179 	OSLogSearch(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR),
180 		commandoslogsearch(this)
181 	{
182 	}
183 };
184 
185 MODULE_INIT(OSLogSearch)
186