source: nscp/modules/NRPEListener/NRPEListener.cpp @ 846bbe4

0.4.00.4.10.4.2stable
Last change on this file since 846bbe4 was 846bbe4, checked in by Michael Medin <michael@…>, 5 years ago

2008-08-09 MickeM

+ Added ChangeWindowMessageFilter? so systray should not work on vista and beyond!

2008-07-28 MickeM

  • Improved the error handling for the check proc state.
  • Removed all (I think) asserts replacing them with exceptions (should I hope reduce crashes and instead leave some form of errors)
  • Property mode set to 100644
File size: 17.8 KB
Line 
1/**************************************************************************
2*   Copyright (C) 2004-2007 by Michael Medin <michael@medin.name>         *
3*                                                                         *
4*   This code is part of NSClient++ - http://trac.nakednuns.org/nscp      *
5*                                                                         *
6*   This program is free software; you can redistribute it and/or modify  *
7*   it under the terms of the GNU General Public License as published by  *
8*   the Free Software Foundation; either version 2 of the License, or     *
9*   (at your option) any later version.                                   *
10*                                                                         *
11*   This program is distributed in the hope that it will be useful,       *
12*   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
13*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
14*   GNU General Public License for more details.                          *
15*                                                                         *
16*   You should have received a copy of the GNU General Public License     *
17*   along with this program; if not, write to the                         *
18*   Free Software Foundation, Inc.,                                       *
19*   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
20***************************************************************************/
21#include "stdafx.h"
22#include "NRPEListener.h"
23#include <strEx.h>
24#include <time.h>
25#include <config.h>
26#include <msvc_wrappers.h>
27
28NRPEListener gNRPEListener;
29
30BOOL APIENTRY DllMain( HANDLE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
31{
32        NSCModuleWrapper::wrapDllMain(hModule, ul_reason_for_call);
33        return TRUE;
34}
35
36NRPEListener::NRPEListener() : noPerfData_(false), buffer_length_(0) {
37}
38NRPEListener::~NRPEListener() {
39}
40
41std::wstring getAllowedHosts() {
42        std::wstring ret = NSCModuleHelper::getSettingsString(NRPE_SECTION_TITLE, MAIN_ALLOWED_HOSTS, _T(""));
43        if (ret.empty())
44                ret = NSCModuleHelper::getSettingsString(MAIN_SECTION_TITLE, MAIN_ALLOWED_HOSTS, MAIN_ALLOWED_HOSTS_DEFAULT);
45        return ret;
46}
47bool getCacheAllowedHosts() {
48        int val = NSCModuleHelper::getSettingsInt(NRPE_SECTION_TITLE, MAIN_ALLOWED_HOSTS_CACHE, -1);
49        if (val == -1)
50                val = NSCModuleHelper::getSettingsInt(MAIN_SECTION_TITLE, MAIN_ALLOWED_HOSTS_CACHE, MAIN_ALLOWED_HOSTS_CACHE_DEFAULT);
51        return val==1?true:false;
52}
53
54
55void NRPEListener::addAllScriptsFrom(std::wstring path) {
56        std::wstring baseDir;
57        std::wstring::size_type pos = path.find_last_of('*');
58        if (pos == std::wstring::npos) {
59                path += _T("*.*");
60        }
61        WIN32_FIND_DATA wfd;
62        HANDLE hFind = FindFirstFile(path.c_str(), &wfd);
63        if (hFind != INVALID_HANDLE_VALUE) {
64                do {
65                        if ((wfd.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY) != FILE_ATTRIBUTE_DIRECTORY) {
66                                addCommand(script_dir, wfd.cFileName);
67                        }
68                } while (FindNextFile(hFind, &wfd));
69        } else {
70                NSC_LOG_ERROR_STD(_T("No scripts found in path: ") + path);
71                return;
72        }
73        FindClose(hFind);
74}
75
76bool NRPEListener::loadModule() {
77        bUseSSL_ = NSCModuleHelper::getSettingsInt(NRPE_SECTION_TITLE, NRPE_SETTINGS_USE_SSL ,NRPE_SETTINGS_USE_SSL_DEFAULT)==1;
78        noPerfData_ = NSCModuleHelper::getSettingsInt(NRPE_SECTION_TITLE, NRPE_SETTINGS_PERFDATA,NRPE_SETTINGS_PERFDATA_DEFAULT)==0;
79        timeout = NSCModuleHelper::getSettingsInt(NRPE_SECTION_TITLE, NRPE_SETTINGS_TIMEOUT ,NRPE_SETTINGS_TIMEOUT_DEFAULT);
80        socketTimeout_ = NSCModuleHelper::getSettingsInt(NRPE_SECTION_TITLE, NRPE_SETTINGS_READ_TIMEOUT ,NRPE_SETTINGS_READ_TIMEOUT_DEFAULT);
81        scriptDirectory_ = NSCModuleHelper::getSettingsString(NRPE_SECTION_TITLE, NRPE_SETTINGS_SCRIPTDIR ,NRPE_SETTINGS_SCRIPTDIR_DEFAULT);
82        buffer_length_ = NSCModuleHelper::getSettingsInt(NRPE_SECTION_TITLE, NRPE_SETTINGS_STRLEN, NRPE_SETTINGS_STRLEN_DEFAULT);
83        if (buffer_length_ != 1024)
84                NSC_DEBUG_MSG_STD(_T("Non-standard buffer length (hope you have recompiled check_nrpe changing #define MAX_PACKETBUFFER_LENGTH = ") + strEx::itos(buffer_length_));
85        std::list<std::wstring> commands = NSCModuleHelper::getSettingsSection(NRPE_HANDLER_SECTION_TITLE);
86        std::list<std::wstring>::const_iterator it;
87        for (it = commands.begin(); it != commands.end(); ++it) {
88                std::wstring command_name;
89                if (((*it).length() > 7)&&((*it).substr(0,7) == _T("command"))) {
90                        strEx::token t = strEx::getToken((*it), '[');
91                        t = strEx::getToken(t.second, ']');
92                        command_name = t.first;
93                } else {
94                        command_name = (*it);
95                }
96                std::wstring s = NSCModuleHelper::getSettingsString(NRPE_HANDLER_SECTION_TITLE, (*it), _T(""));
97                if (command_name.empty() || s.empty()) {
98                        NSC_LOG_ERROR_STD(_T("Invalid command definition: ") + (*it));
99                } else {
100                        if ((s.length() > 7)&&(s.substr(0,6) == _T("inject"))) {
101                                addCommand(inject, command_name.c_str(), s.substr(7));
102                        } else {
103                                addCommand(script, command_name.c_str(), s);
104                        }
105                }
106        }
107
108        if (!scriptDirectory_.empty()) {
109                addAllScriptsFrom(scriptDirectory_);
110        }
111
112        allowedHosts.setAllowedHosts(strEx::splitEx(getAllowedHosts(), _T(",")), getCacheAllowedHosts());
113        try {
114                unsigned short port = NSCModuleHelper::getSettingsInt(NRPE_SECTION_TITLE, NRPE_SETTINGS_PORT, NRPE_SETTINGS_PORT_DEFAULT);
115                std::wstring host = NSCModuleHelper::getSettingsString(NRPE_SECTION_TITLE, NRPE_SETTINGS_BINDADDR, NRPE_SETTINGS_BINDADDR_DEFAULT);
116                unsigned int backLog = NSCModuleHelper::getSettingsInt(NRPE_SECTION_TITLE, NRPE_SETTINGS_LISTENQUE, NRPE_SETTINGS_LISTENQUE_DEFAULT);
117                if (bUseSSL_) {
118                        socket_ssl_.setHandler(this);
119                        socket_ssl_.StartListener(host, port, backLog);
120                } else {
121                        socket_.setHandler(this);
122                        socket_.StartListener(host, port, backLog);
123                }
124        } catch (simpleSocket::SocketException e) {
125                NSC_LOG_ERROR_STD(_T("Exception caught: ") + e.getMessage());
126                return false;
127        } catch (simpleSSL::SSLException e) {
128                NSC_LOG_ERROR_STD(_T("Exception caught: ") + e.getMessage());
129                return false;
130        }
131        root_ = NSCModuleHelper::getBasePath();
132
133        return true;
134}
135bool NRPEListener::unloadModule() {
136        try {
137                if (bUseSSL_) {
138                        socket_ssl_.removeHandler(this);
139                        if (socket_ssl_.hasListener())
140                                socket_ssl_.StopListener();
141                } else {
142                        socket_.removeHandler(this);
143                        if (socket_.hasListener())
144                                socket_.StopListener();
145                }
146        } catch (simpleSocket::SocketException e) {
147                NSC_LOG_ERROR_STD(_T("Exception caught: ") + e.getMessage());
148                return false;
149        } catch (simpleSSL::SSLException e) {
150                NSC_LOG_ERROR_STD(_T("Exception caught: ") + e.getMessage());
151                return false;
152        }
153        return true;
154}
155
156
157bool NRPEListener::hasCommandHandler() {
158        return true;
159}
160bool NRPEListener::hasMessageHandler() {
161        return false;
162}
163
164
165NSCAPI::nagiosReturn NRPEListener::handleCommand(const strEx::blindstr command, const unsigned int argLen, TCHAR **char_args, std::wstring &message, std::wstring &perf) {
166        command_list::const_iterator cit = commands.find(command);
167        if (cit == commands.end())
168                return NSCAPI::returnIgnored;
169
170        const command_data cd = (*cit).second;
171        std::wstring args = cd.arguments;
172        if (NSCModuleHelper::getSettingsInt(NRPE_SECTION_TITLE, NRPE_SETTINGS_ALLOW_ARGUMENTS, NRPE_SETTINGS_ALLOW_ARGUMENTS_DEFAULT) == 1) {
173                arrayBuffer::arrayList arr = arrayBuffer::arrayBuffer2list(argLen, char_args);
174                arrayBuffer::arrayList::const_iterator cit2 = arr.begin();
175                int i=1;
176
177                for (;cit2!=arr.end();cit2++,i++) {
178                        if (NSCModuleHelper::getSettingsInt(NRPE_SECTION_TITLE, NRPE_SETTINGS_ALLOW_NASTY_META, NRPE_SETTINGS_ALLOW_NASTY_META_DEFAULT) == 0) {
179                                if ((*cit2).find_first_of(NASTY_METACHARS) != std::wstring::npos) {
180                                        NSC_LOG_ERROR(_T("Request string contained illegal metachars!"));
181                                        return NSCAPI::returnIgnored;
182                                }
183                        }
184                        strEx::replace(args, _T("$ARG") + strEx::itos(i) + _T("$"), (*cit2));
185                }
186        }
187        if (cd.type == inject) {
188                strEx::token t = strEx::getToken(args, ' ');
189                std::wstring s = t.second;
190                std::wstring sTarget;
191
192                std::wstring::size_type p = 0;
193                while(true) {
194                        std::wstring::size_type pStart = p;
195                        std::wstring::size_type pEnd = std::wstring::npos;
196                        if (s[p] == '\"') {
197                                pStart++;
198                                while (true) {
199                                        p = s.find(' ', ++p);
200                                        if (p == std::wstring::npos)
201                                                break;
202                                        if ((p>1)&&(s[p-1]=='\"')&&(((p>2)&&(s[p-2]!='\\'))||(p==2)))
203                                                break;
204                                }
205                                if (p != std::wstring::npos)
206                                        pEnd = p-1;
207                                else
208                                        pEnd = s.length()-1;
209                                if (p != std::wstring::npos) {
210                                        p++;
211                                }
212                        } else {
213                                pEnd = p = s.find(' ', ++p);
214                                if (p != std::wstring::npos) {
215                                        p = s.find_first_not_of(' ', p);
216                                }
217                        }
218                        if (!sTarget.empty())
219                                sTarget += _T("!");
220                        if (p == std::wstring::npos) {
221                                if (pEnd == std::wstring::npos)
222                                        sTarget += s.substr(pStart);
223                                else
224                                        sTarget += s.substr(pStart, pEnd-pStart);
225                                break;
226                        }
227                        sTarget += s.substr(pStart,pEnd-pStart);
228                        //p++;
229                }
230                try {
231                        return NSCModuleHelper::InjectSplitAndCommand(t.first, sTarget, '!', message, perf);
232                } catch (NSCModuleHelper::NSCMHExcpetion e) {
233                        NSC_LOG_ERROR_STD(_T("Failed to inject command (") + command.c_str() + _T("): ") + e.msg_);
234                } catch (...) {
235                        NSC_LOG_ERROR_STD(_T("Failed to inject command (") + command.c_str() + _T("): Unknown error REPORT THIS"));
236                }
237        } else if (cd.type == script) {
238                int result = process::executeProcess(root_, args, message, perf, timeout);
239                if (!NSCHelper::isNagiosReturnCode(result)) {
240                        NSC_LOG_ERROR_STD(_T("The command (") + command.c_str() + _T(") returned an invalid return code: ") + strEx::itos(result));
241                        return NSCAPI::returnUNKNOWN;
242                }
243                return NSCHelper::int2nagios(result);
244        } else if (cd.type == script_dir) {
245                std::wstring args = arrayBuffer::arrayBuffer2string(char_args, argLen, _T(" "));
246                std::wstring cmd = scriptDirectory_ + command.c_str() + _T(" ") +args;
247                int result = process::executeProcess(root_, cmd, message, perf, timeout);
248                if (!NSCHelper::isNagiosReturnCode(result)) {
249                        NSC_LOG_ERROR_STD(_T("The command (") + command.c_str() + _T(") returned an invalid return code: ") + strEx::itos(result));
250                        return NSCAPI::returnUNKNOWN;
251                }
252                return NSCHelper::int2nagios(result);
253        } else {
254                NSC_LOG_ERROR_STD(_T("Unknown script type: ") + command.c_str());
255                return NSCAPI::critical;
256        }
257        return NSCAPI::returnIgnored;
258}
259
260void NRPEListener::onClose()
261{}
262
263void NRPEListener::onAccept(simpleSocket::Socket *client)
264{
265        if (!allowedHosts.inAllowedHosts(client->getAddr())) {
266                NSC_LOG_ERROR(_T("Unauthorize access from: ") + client->getAddrString());
267                client->close();
268                return;
269        }
270        try {
271                simpleSocket::DataBuffer block;
272                int i;
273                int maxWait = socketTimeout_*10;
274                for (i=0;i<maxWait;i++) {
275                        bool lastReadHasMore = false;
276                        try {
277                                lastReadHasMore = client->readAll(block, 1048);
278                        } catch (simpleSocket::SocketException e) {
279                                NSC_LOG_MESSAGE(_T("Could not read NRPE packet from socket :") + e.getMessage());
280                                client->close();
281                                return;
282                        }
283                        if (block.getLength() >= NRPEPacket::getBufferLength(buffer_length_))
284                                break;
285                        if (!lastReadHasMore) {
286                                NSC_LOG_MESSAGE(_T("Could not read a full NRPE packet from socket, only got: ") + strEx::itos(block.getLength()));
287                                client->close();
288                                return;
289                        }
290                        Sleep(100);
291                }
292                if (i >= maxWait) {
293                        NSC_LOG_ERROR_STD(_T("Timeout reading NRPE-packet (increase socket_timeout), we only got: ") + strEx::itos(block.getLength()));
294                        client->close();
295                        return;
296                }
297                if (block.getLength() == NRPEPacket::getBufferLength(buffer_length_)) {
298                        try {
299                                NRPEPacket out = handlePacket(NRPEPacket(block.getBuffer(), block.getLength(), buffer_length_));
300                                block.copyFrom(out.getBuffer(), out.getBufferLength());
301                        } catch (NRPEPacket::NRPEPacketException e) {
302                                NSC_LOG_ERROR_STD(_T("NRPESocketException: ") + e.getMessage());
303                                client->close();
304                                return;
305                        }
306                        int maxWait = socketTimeout_*10;
307                        for (i=0;i<maxWait;i++) {
308                                bool lastReadHasMore = false;
309                                try {
310                                        if (client->canWrite())
311                                                lastReadHasMore = client->sendAll(block);
312                                } catch (simpleSocket::SocketException e) {
313                                        NSC_LOG_MESSAGE(_T("Could not send NRPE packet from socket :") + e.getMessage());
314                                        client->close();
315                                        return;
316                                }
317                                if (!lastReadHasMore) {
318                                        client->close();
319                                        return;
320                                }
321                                Sleep(100);
322                        }
323                        if (i >= maxWait) {
324                                NSC_LOG_ERROR_STD(_T("Timeout reading NRPE-packet (increase socket_timeout)"));
325                                client->close();
326                                return;
327                        }
328                } else {
329                        NSC_LOG_ERROR_STD(_T("We got more then we wanted ") + strEx::itos(NRPEPacket::getBufferLength(buffer_length_)) + _T(", we only got: ") + strEx::itos(block.getLength()));
330                        client->close();
331                        return;
332                }
333        } catch (simpleSocket::SocketException e) {
334                NSC_LOG_ERROR_STD(_T("SocketException: ") + e.getMessage());
335        } catch (NRPEException e) {
336                NSC_LOG_ERROR_STD(_T("NRPEException: ") + e.getMessage());
337        } catch (...) {
338                NSC_LOG_ERROR_STD(_T("Unhandled Exception in NRPE listner..."));
339        }
340        client->close();
341}
342
343NRPEPacket NRPEListener::handlePacket(NRPEPacket p) {
344        if (p.getType() != NRPEPacket::queryPacket) {
345                NSC_LOG_ERROR(_T("Request is not a query."));
346                throw NRPEException(_T("Invalid query type: ") + strEx::itos(p.getType()));
347        }
348        if (p.getVersion() != NRPEPacket::version2) {
349                NSC_LOG_ERROR(_T("Request had unsupported version."));
350                throw NRPEException(_T("Invalid version"));
351        }
352        if (!p.verifyCRC()) {
353                NSC_LOG_ERROR(_T("Request had invalid checksum."));
354                throw NRPEException(_T("Invalid checksum"));
355        }
356        strEx::token cmd = strEx::getToken(p.getPayload(), '!');
357        if (cmd.first == _T("_NRPE_CHECK")) {
358                return NRPEPacket(NRPEPacket::responsePacket, NRPEPacket::version2, NSCAPI::returnOK, _T("I (") SZVERSION _T(") seem to be doing fine..."), buffer_length_);
359        }
360        std::wstring msg, perf;
361
362        if (NSCModuleHelper::getSettingsInt(NRPE_SECTION_TITLE, NRPE_SETTINGS_ALLOW_ARGUMENTS, NRPE_SETTINGS_ALLOW_ARGUMENTS_DEFAULT) == 0) {
363                if (!cmd.second.empty()) {
364                        NSC_LOG_ERROR(_T("Request contained arguments (not currently allowed, check the allow_arguments option)."));
365                        throw NRPEException(_T("Request contained arguments (not currently allowed, check the allow_arguments option)."));
366                }
367        }
368        if (NSCModuleHelper::getSettingsInt(NRPE_SECTION_TITLE, NRPE_SETTINGS_ALLOW_NASTY_META, NRPE_SETTINGS_ALLOW_NASTY_META_DEFAULT) == 0) {
369                if (cmd.first.find_first_of(NASTY_METACHARS) != std::wstring::npos) {
370                        NSC_LOG_ERROR(_T("Request command contained illegal metachars!"));
371                        throw NRPEException(_T("Request command contained illegal metachars!"));
372                }
373                if (cmd.second.find_first_of(NASTY_METACHARS) != std::wstring::npos) {
374                        NSC_LOG_ERROR(_T("Request arguments contained illegal metachars!"));
375                        throw NRPEException(_T("Request command contained illegal metachars!"));
376                }
377        }
378
379        NSCAPI::nagiosReturn ret = -3;
380        try {
381                ret = NSCModuleHelper::InjectSplitAndCommand(cmd.first, cmd.second, '!', msg, perf);
382        } catch (...) {
383                return NRPEPacket(NRPEPacket::responsePacket, NRPEPacket::version2, NSCAPI::returnUNKNOWN, _T("UNKNOWN: Internal exception"), buffer_length_);
384        }
385        switch (ret) {
386                case NSCAPI::returnInvalidBufferLen:
387                        msg = _T("UNKNOWN: Return buffer to small to handle this command.");
388                        ret = NSCAPI::returnUNKNOWN;
389                        break;
390                case NSCAPI::returnIgnored:
391                        msg = _T("UNKNOWN: No handler for that command");
392                        ret = NSCAPI::returnUNKNOWN;
393                        break;
394                case NSCAPI::returnOK:
395                case NSCAPI::returnWARN:
396                case NSCAPI::returnCRIT:
397                case NSCAPI::returnUNKNOWN:
398                        break;
399                default:
400                        msg = _T("UNKNOWN: Internal error.");
401                        ret = NSCAPI::returnUNKNOWN;
402        }
403        if (msg.length() >= buffer_length_-1) {
404                NSC_LOG_ERROR(_T("Truncating returndata as it is bigger then NRPE allowes :("));
405                msg = msg.substr(0,buffer_length_-2);
406        }
407        if (perf.empty()||noPerfData_) {
408                return NRPEPacket(NRPEPacket::responsePacket, NRPEPacket::version2, ret, msg, buffer_length_);
409        } else {
410                return NRPEPacket(NRPEPacket::responsePacket, NRPEPacket::version2, ret, msg + _T("|") + perf, buffer_length_);
411        }
412}
413
414NSC_WRAPPERS_MAIN_DEF(gNRPEListener);
415NSC_WRAPPERS_IGNORE_MSG_DEF();
416NSC_WRAPPERS_HANDLE_CMD_DEF(gNRPEListener);
417NSC_WRAPPERS_HANDLE_CONFIGURATION(gNRPEListener);
418
419
420MODULE_SETTINGS_START(NRPEListener, _T("NRPE Listener configuration"), _T("..."))
421
422PAGE(_T("NRPE Listsner configuration"))
423
424ITEM_EDIT_TEXT(_T("port"), _T("This is the port the NRPEListener.dll will listen to."))
425ITEM_MAP_TO(_T("basic_ini_text_mapper"))
426OPTION(_T("section"), _T("NRPE"))
427OPTION(_T("key"), _T("port"))
428OPTION(_T("default"), _T("5666"))
429ITEM_END()
430
431ITEM_CHECK_BOOL(_T("allow_arguments"), _T("This option determines whether or not the NRPE daemon will allow clients to specify arguments to commands that are executed."))
432ITEM_MAP_TO(_T("basic_ini_bool_mapper"))
433OPTION(_T("section"), _T("NRPE"))
434OPTION(_T("key"), _T("allow_arguments"))
435OPTION(_T("default"), _T("false"))
436OPTION(_T("true_value"), _T("1"))
437OPTION(_T("false_value"), _T("0"))
438ITEM_END()
439
440ITEM_CHECK_BOOL(_T("allow_nasty_meta_chars"), _T("This might have security implications (depending on what you do with the options)"))
441ITEM_MAP_TO(_T("basic_ini_bool_mapper"))
442OPTION(_T("section"), _T("NRPE"))
443OPTION(_T("key"), _T("allow_nasty_meta_chars"))
444OPTION(_T("default"), _T("false"))
445OPTION(_T("true_value"), _T("1"))
446OPTION(_T("false_value"), _T("0"))
447ITEM_END()
448
449ITEM_CHECK_BOOL(_T("use_ssl"), _T("This option will enable SSL encryption on the NRPE data socket (this increases security somwhat."))
450ITEM_MAP_TO(_T("basic_ini_bool_mapper"))
451OPTION(_T("section"), _T("NRPE"))
452OPTION(_T("key"), _T("use_ssl"))
453OPTION(_T("default"), _T("true"))
454OPTION(_T("true_value"), _T("1"))
455OPTION(_T("false_value"), _T("0"))
456ITEM_END()
457
458PAGE_END()
459ADVANCED_PAGE(_T("Access configuration"))
460
461ITEM_EDIT_OPTIONAL_LIST(_T("Allow connection from:"), _T("This is the hosts that will be allowed to poll performance data from the NRPE server."))
462OPTION(_T("disabledCaption"), _T("Use global settings (defined previously)"))
463OPTION(_T("enabledCaption"), _T("Specify hosts for NRPE server"))
464OPTION(_T("listCaption"), _T("Add all IP addresses (not hosts) which should be able to connect:"))
465OPTION(_T("separator"), _T(","))
466OPTION(_T("disabled"), _T(""))
467ITEM_MAP_TO(_T("basic_ini_text_mapper"))
468OPTION(_T("section"), _T("NRPE"))
469OPTION(_T("key"), _T("allowed_hosts"))
470OPTION(_T("default"), _T(""))
471ITEM_END()
472
473PAGE_END()
474MODULE_SETTINGS_END()
Note: See TracBrowser for help on using the repository browser.