From e3d5140dcc936ff411c438b7e3997104cb5f085a Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 1 Sep 2012 18:54:51 -0400 Subject: [PATCH] Added a web panel module + a default template --- data/modules.example.conf | 55 +++ include/access.h | 5 + include/anope.h | 2 +- include/logger.h | 1 + include/services.h | 2 + include/sockets.h | 9 +- modules/CMakeLists.txt | 7 +- modules/commands/ns_register.cpp | 9 +- modules/encryption/enc_sha256.cpp | 2 +- modules/extra/httpd.h | 224 ++++++++++ modules/extra/m_httpd.cpp | 415 ++++++++++++++++++ modules/extra/webcpanel/CMakeLists.txt | 5 + .../extra/webcpanel/pages/chanserv/access.cpp | 147 +++++++ .../extra/webcpanel/pages/chanserv/access.h | 27 ++ .../extra/webcpanel/pages/chanserv/akick.cpp | 81 ++++ .../extra/webcpanel/pages/chanserv/akick.h | 27 ++ .../extra/webcpanel/pages/chanserv/info.cpp | 32 ++ modules/extra/webcpanel/pages/chanserv/info.h | 25 ++ .../extra/webcpanel/pages/chanserv/set.cpp | 136 ++++++ modules/extra/webcpanel/pages/chanserv/set.h | 27 ++ modules/extra/webcpanel/pages/confirm.cpp | 31 ++ modules/extra/webcpanel/pages/confirm.h | 22 + modules/extra/webcpanel/pages/index.cpp | 73 +++ modules/extra/webcpanel/pages/index.h | 22 + modules/extra/webcpanel/pages/logout.cpp | 22 + modules/extra/webcpanel/pages/logout.h | 20 + .../extra/webcpanel/pages/memoserv/memos.cpp | 121 +++++ .../extra/webcpanel/pages/memoserv/memos.h | 25 ++ .../extra/webcpanel/pages/nickserv/access.cpp | 39 ++ .../extra/webcpanel/pages/nickserv/access.h | 25 ++ .../extra/webcpanel/pages/nickserv/alist.cpp | 49 +++ .../extra/webcpanel/pages/nickserv/alist.h | 25 ++ .../extra/webcpanel/pages/nickserv/cert.cpp | 39 ++ modules/extra/webcpanel/pages/nickserv/cert.h | 25 ++ .../extra/webcpanel/pages/nickserv/info.cpp | 111 +++++ modules/extra/webcpanel/pages/nickserv/info.h | 25 ++ modules/extra/webcpanel/pages/register.cpp | 23 + modules/extra/webcpanel/pages/register.h | 22 + modules/extra/webcpanel/static_fileserver.cpp | 42 ++ modules/extra/webcpanel/static_fileserver.h | 19 + .../extra/webcpanel/template_fileserver.cpp | 244 ++++++++++ modules/extra/webcpanel/template_fileserver.h | 27 ++ .../templates/default/chanserv/access.html | 52 +++ .../templates/default/chanserv/akick.html | 42 ++ .../templates/default/chanserv/main.html | 6 + .../templates/default/chanserv/set.html | 100 +++++ .../webcpanel/templates/default/confirm.html | 30 ++ .../webcpanel/templates/default/favicon.ico | Bin 0 -> 3774 bytes .../webcpanel/templates/default/footer.html | 7 + .../webcpanel/templates/default/header.html | 24 + .../webcpanel/templates/default/login.html | 47 ++ .../webcpanel/templates/default/logo.png | Bin 0 -> 16385 bytes .../templates/default/memoserv/memos.html | 50 +++ .../templates/default/nickserv/access.html | 24 + .../templates/default/nickserv/alist.html | 16 + .../templates/default/nickserv/cert.html | 19 + .../templates/default/nickserv/info.html | 82 ++++ .../webcpanel/templates/default/register.html | 51 +++ .../webcpanel/templates/default/style.css | 97 ++++ modules/extra/webcpanel/webcpanel.cpp | 209 +++++++++ modules/extra/webcpanel/webcpanel.h | 163 +++++++ src/access.cpp | 11 + src/bots.cpp | 1 - src/command.cpp | 7 +- src/logger.cpp | 6 +- src/misc.cpp | 19 +- src/socket_clients.cpp | 2 +- src/socket_transport.cpp | 40 +- 68 files changed, 3360 insertions(+), 34 deletions(-) create mode 100644 modules/extra/httpd.h create mode 100644 modules/extra/m_httpd.cpp create mode 100644 modules/extra/webcpanel/CMakeLists.txt create mode 100644 modules/extra/webcpanel/pages/chanserv/access.cpp create mode 100644 modules/extra/webcpanel/pages/chanserv/access.h create mode 100644 modules/extra/webcpanel/pages/chanserv/akick.cpp create mode 100644 modules/extra/webcpanel/pages/chanserv/akick.h create mode 100644 modules/extra/webcpanel/pages/chanserv/info.cpp create mode 100644 modules/extra/webcpanel/pages/chanserv/info.h create mode 100644 modules/extra/webcpanel/pages/chanserv/set.cpp create mode 100644 modules/extra/webcpanel/pages/chanserv/set.h create mode 100644 modules/extra/webcpanel/pages/confirm.cpp create mode 100644 modules/extra/webcpanel/pages/confirm.h create mode 100644 modules/extra/webcpanel/pages/index.cpp create mode 100644 modules/extra/webcpanel/pages/index.h create mode 100644 modules/extra/webcpanel/pages/logout.cpp create mode 100644 modules/extra/webcpanel/pages/logout.h create mode 100644 modules/extra/webcpanel/pages/memoserv/memos.cpp create mode 100644 modules/extra/webcpanel/pages/memoserv/memos.h create mode 100644 modules/extra/webcpanel/pages/nickserv/access.cpp create mode 100644 modules/extra/webcpanel/pages/nickserv/access.h create mode 100644 modules/extra/webcpanel/pages/nickserv/alist.cpp create mode 100644 modules/extra/webcpanel/pages/nickserv/alist.h create mode 100644 modules/extra/webcpanel/pages/nickserv/cert.cpp create mode 100644 modules/extra/webcpanel/pages/nickserv/cert.h create mode 100644 modules/extra/webcpanel/pages/nickserv/info.cpp create mode 100644 modules/extra/webcpanel/pages/nickserv/info.h create mode 100644 modules/extra/webcpanel/pages/register.cpp create mode 100644 modules/extra/webcpanel/pages/register.h create mode 100644 modules/extra/webcpanel/static_fileserver.cpp create mode 100644 modules/extra/webcpanel/static_fileserver.h create mode 100644 modules/extra/webcpanel/template_fileserver.cpp create mode 100644 modules/extra/webcpanel/template_fileserver.h create mode 100644 modules/extra/webcpanel/templates/default/chanserv/access.html create mode 100644 modules/extra/webcpanel/templates/default/chanserv/akick.html create mode 100644 modules/extra/webcpanel/templates/default/chanserv/main.html create mode 100644 modules/extra/webcpanel/templates/default/chanserv/set.html create mode 100644 modules/extra/webcpanel/templates/default/confirm.html create mode 100644 modules/extra/webcpanel/templates/default/favicon.ico create mode 100644 modules/extra/webcpanel/templates/default/footer.html create mode 100644 modules/extra/webcpanel/templates/default/header.html create mode 100644 modules/extra/webcpanel/templates/default/login.html create mode 100644 modules/extra/webcpanel/templates/default/logo.png create mode 100644 modules/extra/webcpanel/templates/default/memoserv/memos.html create mode 100644 modules/extra/webcpanel/templates/default/nickserv/access.html create mode 100644 modules/extra/webcpanel/templates/default/nickserv/alist.html create mode 100644 modules/extra/webcpanel/templates/default/nickserv/cert.html create mode 100644 modules/extra/webcpanel/templates/default/nickserv/info.html create mode 100644 modules/extra/webcpanel/templates/default/register.html create mode 100644 modules/extra/webcpanel/templates/default/style.css create mode 100644 modules/extra/webcpanel/webcpanel.cpp create mode 100644 modules/extra/webcpanel/webcpanel.h diff --git a/data/modules.example.conf b/data/modules.example.conf index 2a06b1a99..d4d86c0ba 100644 --- a/data/modules.example.conf +++ b/data/modules.example.conf @@ -91,6 +91,38 @@ m_helpchan helpchannel = "#help" } +/* + * m_httpd + * + * Allows services to serve web pages. By itself, this module does nothing useful. + * + * Note that using this will allow users to get the IP of your services. + * To prevent this we recommend using a reverse proxy or a tunnel. + */ +#module { name = "m_httpd" } +httpd +{ + /* Name of this service */ + name = "httpd/main" + + /* IP to listen on */ + ip = "0.0.0.0" + /* Port to listen on */ + port = 8080 + /* Time before connections to this server are timed out */ + timeout = 30 + + /* If you are using a reverse proxy that sends one of the + * extforward_headers set below, set this to its IP. + * This allows services to obtain the real IP of users by + * reading the forwarded-for HTTP header. + */ + #extforward_ip = "192.168.0.255" + + /* The header to look for. These probably work as is. */ + extforward_header = "X-Forwarded-For Forwarded-For" +} + /* * m_ldap * @@ -440,3 +472,26 @@ ns_maxemail #maxemails = 1 } +/* + * webcpanel + * + * This module creates a web configuration panel that allows users and operators to perform any task + * as they could over IRC. If you are using the default configuration you should be able to access + * this panel by visiting http://127.0.0.1:8080 in your web browser from the machine Anope is running on. + * + * This module requires m_httpd. + */ +#module { name = "webcpanel" } +webcpanel +{ + /* Web server to use */ + server = "httpd/main"; + + /* Template to use */ + template = "default"; + + /* Page title */ + title = "Anope IRC Services"; +} + + diff --git a/include/access.h b/include/access.h index 18499b602..b29572dfc 100644 --- a/include/access.h +++ b/include/access.h @@ -53,6 +53,11 @@ class CoreExport AccessProvider : public Service AccessProvider(Module *o, const Anope::string &n); virtual ~AccessProvider(); virtual ChanAccess *Create() = 0; + + private: + static std::list providers; + public: + static const std::list& GetProviders(); }; class CoreExport ChanAccess : public Serializable diff --git a/include/anope.h b/include/anope.h index 2a51514e0..2ec6602ea 100644 --- a/include/anope.h +++ b/include/anope.h @@ -342,7 +342,7 @@ namespace Anope * @param dest The destination string */ extern CoreExport void Unhex(const string &src, string &dest); - extern CoreExport void Unhex(const string &src, char *dest); + extern CoreExport void Unhex(const string &src, char *dest, size_t sz); /** Base 64 encode a string * @param src The string to encode diff --git a/include/logger.h b/include/logger.h index 9364cfb7c..eea86f890 100644 --- a/include/logger.h +++ b/include/logger.h @@ -49,6 +49,7 @@ class CoreExport Log { public: const BotInfo *bi; + Anope::string nick; const User *u; const NickCore *nc; Command *c; diff --git a/include/services.h b/include/services.h index c03991a13..31ab1cf37 100644 --- a/include/services.h +++ b/include/services.h @@ -55,8 +55,10 @@ #ifdef __GXX_EXPERIMENTAL_CXX0X__ # define anope_override override +# define anope_final final #else # define anope_override +# define anope_final #endif /** diff --git a/include/sockets.h b/include/sockets.h index 16cd10a1a..8f863b59c 100644 --- a/include/sockets.h +++ b/include/sockets.h @@ -288,6 +288,9 @@ class CoreExport BufferedSocket : public virtual Socket /** Write to the socket * @param message The message */ + protected: + virtual void Write(const char *buffer, size_t l); + public: void Write(const char *message, ...); void Write(const Anope::string &message); @@ -304,8 +307,10 @@ class CoreExport BufferedSocket : public virtual Socket class CoreExport BinarySocket : public virtual Socket { + protected: struct DataBlock { + char *orig; char *buf; size_t len; @@ -338,7 +343,9 @@ class CoreExport BinarySocket : public virtual Socket * @param buffer The data to write * @param l The length of the data */ - void Write(const char *buffer, size_t l); + virtual void Write(const char *buffer, size_t l); + void Write(const char *message, ...); + void Write(const Anope::string &message); /** Called with data from the socket * @param buffer The data diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index 3653a59f1..13eb83d5a 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -105,7 +105,7 @@ foreach(MODULE_FOLDER ${MODULES_FOLDERS}) foreach(SUBDIR ${SUBMODULE_DIRS}) if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIR}") - file(GLOB MODULES_SUBDIR_SRCS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "${SUBDIR}/*.cpp") + file(GLOB_RECURSE MODULES_SUBDIR_SRCS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "${SUBDIR}/*.cpp") sort_list(MODULES_SUBDIR_SRCS) # Set all the files to use C++ as well as set their compile flags (use the module-specific compile flags, though) @@ -195,6 +195,11 @@ foreach(MODULE_FOLDER ${MODULES_FOLDERS}) else(NOT SKIP_DEPENDS AND NOT SKIP_LIBRARIES AND HAS_FUNCTION) message(" This is not a fatal error - ${SUBDIR} will not be built.") endif(NOT SKIP_DEPENDS AND NOT SKIP_LIBRARIES AND HAS_FUNCTION) + + # Run the directories CMakeLists.txt if there is one + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIR}/CMakeLists.txt") + add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIR}") + endif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIR}/CMakeLists.txt") endif(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIR}") endforeach(SUBDIR) endif(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${MODULE_FOLDER}") diff --git a/modules/commands/ns_register.cpp b/modules/commands/ns_register.cpp index 0d51ef20c..1a219c8ba 100644 --- a/modules/commands/ns_register.cpp +++ b/modules/commands/ns_register.cpp @@ -224,9 +224,12 @@ class CommandNSRegister : public Command } else if (Config->NSRegistration.equals_ci("none")) { - ircdproto->SendLogin(u); - if (!Config->NoNicknameOwnership && u && na->nc == u->Account() && na->nc->HasFlag(NI_UNCONFIRMED) == false) - u->SetMode(findbot(Config->NickServ), UMODE_REGISTERED); + if (u) + { + ircdproto->SendLogin(u); + if (!Config->NoNicknameOwnership && na->nc == u->Account() && na->nc->HasFlag(NI_UNCONFIRMED) == false) + u->SetMode(findbot(Config->NickServ), UMODE_REGISTERED); + } } if (u) diff --git a/modules/encryption/enc_sha256.cpp b/modules/encryption/enc_sha256.cpp index b119eeba6..cee21b01d 100644 --- a/modules/encryption/enc_sha256.cpp +++ b/modules/encryption/enc_sha256.cpp @@ -148,7 +148,7 @@ class ESHA256 : public Module size_t pos = password.find(':'); Anope::string buf = password.substr(password.find(':', pos + 1) + 1, password.length()); char buf2[33]; - Anope::Unhex(buf, buf2); + Anope::Unhex(buf, buf2, sizeof(buf2)); for (int i = 0 ; i < 8; ++i) PACK32(reinterpret_cast(&buf2[i << 2]), iv[i]); } diff --git a/modules/extra/httpd.h b/modules/extra/httpd.h new file mode 100644 index 000000000..684c11512 --- /dev/null +++ b/modules/extra/httpd.h @@ -0,0 +1,224 @@ +#ifndef ANOPE_HTTPD_H +#define ANOPE_HTTPD_H + +enum HTTPError +{ + HTTP_ERROR_OK = 200, + HTTP_FOUND = 302, + HTTP_BAD_REQUEST = 400, + HTTP_PAGE_NOT_FOUND = 404, + HTTP_NOT_SUPPORTED = 505 +}; + +/* A message to someone */ +struct HTTPReply +{ + HTTPError error; + Anope::string content_type; + std::map headers; + typedef std::list > cookie; + std::vector cookies; + + HTTPReply() : error(HTTP_ERROR_OK), length(0) { } + + struct Data + { + char *buf; + size_t len; + + Data(const char *b, size_t l) + { + this->buf = new char[l]; + memcpy(this->buf, b, l); + this->len = l; + } + + ~Data() + { + delete [] buf; + } + }; + + std::deque out; + size_t length; + + void Write(const Anope::string &message) + { + this->out.push_back(new Data(message.c_str(), message.length())); + this->length += message.length(); + } + + void Write(const char *b, size_t l) + { + this->out.push_back(new Data(b, l)); + this->length += l; + } +}; + +/* A message from soneone */ +struct HTTPMessage +{ + std::map headers; + std::map cookies; + std::map get_data; + std::map post_data; + Anope::string content; +}; + +class HTTPClient; +class HTTPProvider; + +class HTTPPage : public Base +{ + Anope::string url; + Anope::string content_type; + + public: + HTTPPage(const Anope::string &u, const Anope::string &ct = "text/html") : url(u), content_type(ct) { } + + const Anope::string &GetURL() const { return this->url; } + + const Anope::string &GetContentType() const { return this->content_type; } + + /** Called when this page is requested + * @param The server this page is on + * @param The page name + * @param The client requesting the page + * @param The HTTP header sent from the client to request the page + * @param The HTTP header that will be sent back to the client + */ + virtual void OnRequest(HTTPProvider *, const Anope::string &, HTTPClient *, HTTPMessage &, HTTPReply &) = 0; +}; + +class HTTPClient : public ClientSocket, public BufferedSocket, public BinarySocket +{ + protected: + void WriteClient(const Anope::string &message) + { + BinarySocket::Write(message + "\r\n"); + } + + public: + HTTPClient(ListenSocket *l, int f, const sockaddrs &a) : ClientSocket(l, a), BufferedSocket(), BinarySocket() { } + + virtual const Anope::string GetIP() + { + return this->clientaddr.addr(); + } + + bool ProcessRead() anope_override + { + return BufferedSocket::ProcessRead(); + } + + bool ProcessWrite() anope_override + { + return !BinarySocket::ProcessWrite() || BinarySocket::WriteBuffer.empty() ? false : true; + } + + void Write(const char *buffer, size_t l) anope_override + { + BinarySocket::Write(buffer, l); + } + + virtual void SendError(HTTPError err, const Anope::string &msg) = 0; + virtual void SendReply(HTTPReply *) = 0; +}; + +class HTTPProvider : public Service, public ListenSocket +{ + Anope::string ip; + unsigned short port; + public: + Anope::string ext_ip; + std::vector ext_headers; + + HTTPProvider(Module *c, const Anope::string &n, const Anope::string &i, const unsigned short p) : Service(c, "HTTPProvider", n), ListenSocket(i, p, i.find(':') != Anope::string::npos), ip(i), port(p) { } + + const Anope::string &GetIP() const + { + return this->ip; + } + + unsigned short GetPort() const + { + return this->port; + } + + virtual bool RegisterPage(HTTPPage *page) = 0; + virtual void UnregisterPage(HTTPPage *page) = 0; + virtual HTTPPage* FindPage(const Anope::string &name) = 0; +}; + +namespace HTTPUtils +{ + inline Anope::string URLDecode(const Anope::string &url) + { + Anope::string decoded; + + for (unsigned i = 0; i < url.length(); ++i) + { + const char& c = url[i]; + + if (c == '%' && i + 2 < url.length()) + { + Anope::string dest; + Anope::Unhex(url.substr(i + 1, 2), dest); + decoded += dest; + i += 2; + } + else if (c == '+') + decoded += ' '; + else + decoded += c; + } + + return decoded; + } + + inline Anope::string URLEncode(const Anope::string &url) + { + Anope::string encoded; + + for (unsigned i = 0; i < url.length(); ++i) + { + const char& c = url[i]; + + if (isalnum(c) || c == '.' || c == '-' || c == '*' || c == '_') + encoded += c; + else if (c == ' ') + encoded += '+'; + else + encoded += "%" + Anope::Hex(c); + } + + return encoded; + } + + inline Anope::string Escape(const Anope::string &src) + { + Anope::string dst; + + for (unsigned i = 0; i < src.length(); ++i) + { + switch (src[i]) + { + case '<': + dst += "<"; + break; + case '>': + dst += ">"; + break; + case '"': + dst += """; + break; + default: + dst += src[i]; + } + } + + return dst; + } +} + +#endif // ANOPE_HTTPD_H diff --git a/modules/extra/m_httpd.cpp b/modules/extra/m_httpd.cpp new file mode 100644 index 000000000..aec3ce4a1 --- /dev/null +++ b/modules/extra/m_httpd.cpp @@ -0,0 +1,415 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "module.h" +#include "httpd.h" + +static Anope::string BuildDate() +{ + char timebuf[64]; + struct tm *tm = localtime(&Anope::CurTime); + strftime(timebuf, sizeof(timebuf), "%a, %d %b %Y %T %Z", tm); + return timebuf; +} + +static Anope::string GetStatusFromCode(HTTPError err) +{ + switch (err) + { + case HTTP_ERROR_OK: + return "200 OK"; + case HTTP_FOUND: + return "302 Found"; + case HTTP_BAD_REQUEST: + return "400 Bad Request"; + case HTTP_PAGE_NOT_FOUND: + return "404 Not Found"; + case HTTP_NOT_SUPPORTED: + return "505 HTTP Version Not Supported"; + } + + return "501 Not Implemented"; +} + +class MyHTTPClient : public HTTPClient, public Base +{ + HTTPProvider *provider; + HTTPMessage header; + bool header_done; + Anope::string page_name; + dynamic_reference page; + Anope::string ip; + + enum + { + ACTION_NONE, + ACTION_GET, + ACTION_POST + } action; + + void Serve() + { + if (!this->page) + { + this->SendError(HTTP_PAGE_NOT_FOUND, "Page not found"); + return; + } + + if (this->ip == this->provider->ext_ip) + { + for (unsigned i = 0; i < this->provider->ext_headers.size(); ++i) + { + const Anope::string &token = this->provider->ext_headers[i]; + + if (this->header.headers.count(token)) + { + Log(LOG_DEBUG, "httpd") << "m_httpd: IP for connection " << this->GetFD() << " changed to " << this->ip; + this->ip = this->header.headers[token]; + break; + } + } + } + + Log(LOG_DEBUG, "httpd") << "m_httpd: Serving page " << this->page_name << " to " << this->ip; + + HTTPReply reply; + + this->page->OnRequest(this->provider, this->page_name, this, this->header, reply); + + this->SendReply(&reply); + } + + public: + time_t created; + + MyHTTPClient(HTTPProvider *l, int f, const sockaddrs &a) : Socket(f, l->IsIPv6()), HTTPClient(l, f, a), provider(l), header_done(false), ip(a.addr()), action(ACTION_NONE), created(Anope::CurTime) + { + Log(LOG_DEBUG, "httpd") << "Accepted connection " << f << " from " << a.addr(); + } + + ~MyHTTPClient() + { + Log(LOG_DEBUG, "httpd") << "Closing connection " << this->GetFD() << " from " << this->ip; + } + + const Anope::string GetIP() anope_override + { + return this->ip; + } + + bool Read(const Anope::string &buf) anope_override + { + Log(LOG_DEBUG_2) << "HTTP from " << this->clientaddr.addr() << ": " << buf; + + if (!this->header_done) + { + if (this->action == ACTION_NONE) + { + std::vector params = BuildStringVector(buf); + + if (params.empty() || (params[0] != "GET" && params[0] != "POST")) + { + this->SendError(HTTP_BAD_REQUEST, "Unknown operation"); + return true; + } + + if (params.size() != 3) + { + this->SendError(HTTP_BAD_REQUEST, "Invalid parameters"); + return true; + } + + if (params[0] == "GET") + this->action = ACTION_GET; + else if (params[0] == "POST") + this->action = ACTION_POST; + + Anope::string targ = params[1]; + size_t q = targ.find('?'); + if (q != Anope::string::npos) + { + sepstream sep(targ.substr(q + 1), '&'); + targ = targ.substr(0, q); + + Anope::string token; + while (sep.GetToken(token)) + { + size_t sz = token.find('='); + if (sz == Anope::string::npos || !sz || sz + 1 >= token.length()) + continue; + this->header.get_data[token.substr(0, sz)] = HTTPUtils::URLDecode(token.substr(sz + 1)); + } + } + + this->page = this->provider->FindPage(targ); + this->page_name = targ; + } + else if (buf.find("Cookie: ") == 0) + { + spacesepstream sep(buf.substr(8)); + Anope::string token; + + while (sep.GetToken(token)) + { + size_t sz = token.find('='); + if (sz == Anope::string::npos || !sz || sz + 1 >= token.length()) + continue; + size_t end = token.length() - (sz + 1); + if (!sep.StreamEnd()) + --end; // Remove trailing ; + this->header.cookies[token.substr(0, sz)] = token.substr(sz + 1, end); + } + } + else if (buf.find(':') != Anope::string::npos) + { + size_t sz = buf.find(':'); + if (sz + 2 < buf.length()) + this->header.headers[buf.substr(0, sz)] = buf.substr(sz + 2); + } + else if (buf.empty()) + { + this->header_done = true; + + if (this->action == ACTION_POST) + { + Log(LOG_DEBUG_2) << "HTTP POST from " << this->clientaddr.addr() << ": " << this->extrabuf; + + sepstream sep(this->extrabuf, '&'); + Anope::string token; + + while (sep.GetToken(token)) + { + size_t sz = token.find('='); + if (sz == Anope::string::npos || !sz || sz + 1 >= token.length()) + continue; + this->header.post_data[token.substr(0, sz)] = HTTPUtils::URLDecode(token.substr(sz + 1)); + } + } + + this->Serve(); + } + } + + return true; + } + + void SendError(HTTPError err, const Anope::string &msg) anope_override + { + HTTPReply h; + + h.error = err; + + h.Write(msg); + + this->SendReply(&h); + } + + void SendReply(HTTPReply *message) anope_override + { + this->WriteClient("HTTP/1.1 " + GetStatusFromCode(message->error)); + this->WriteClient("Date: " + BuildDate()); + this->WriteClient("Server: Anope-" + Anope::VersionShort()); + if (message->content_type.empty()) + this->WriteClient("Content-Type: text/html"); + else + this->WriteClient("Content-Type: " + message->content_type); + this->WriteClient("Content-Length: " + stringify(message->length)); + + for (unsigned i = 0; i < message->cookies.size(); ++i) + { + Anope::string buf = "Set-Cookie:"; + + for (HTTPReply::cookie::iterator it = message->cookies[i].begin(), it_end = message->cookies[i].end(); it != it_end; ++it) + buf += " " + it->first + "=" + it->second + ";"; + + buf.erase(buf.length() - 1); + + this->WriteClient(buf); + } + + typedef std::map map; + for (map::iterator it = message->headers.begin(), it_end = message->headers.end(); it != it_end; ++it) + this->WriteClient(it->first + ": " + it->second); + + this->WriteClient("Connection: Close"); + this->WriteClient(""); + + for (unsigned i = 0; i < message->out.size(); ++i) + { + HTTPReply::Data* d = message->out[i]; + + this->Write(d->buf, d->len); + + delete d; + } + + message->out.clear(); + } +}; + +class MyHTTPProvider : public HTTPProvider, public CallBack +{ + int timeout; + std::map pages; + std::list > clients; + + public: + MyHTTPProvider(Module *c, const Anope::string &n, const Anope::string &i, const unsigned short p, const int t) : HTTPProvider(c, n, i, p), CallBack(c, 10, Anope::CurTime, true), timeout(t) { } + + void Tick(time_t) anope_override + { + while (!this->clients.empty()) + { + dynamic_reference& c = this->clients.front(); + if (c && c->created + this->timeout >= Anope::CurTime) + break; + + delete c; + this->clients.pop_front(); + } + } + + ClientSocket* OnAccept(int fd, const sockaddrs &addr) anope_override + { + MyHTTPClient *c = new MyHTTPClient(this, fd, addr); + this->clients.push_back(c); + return c; + } + + bool RegisterPage(HTTPPage *page) anope_override + { + return this->pages.insert(std::make_pair(page->GetURL(), page)).second; + } + + void UnregisterPage(HTTPPage *page) anope_override + { + this->pages.erase(page->GetURL()); + } + + HTTPPage* FindPage(const Anope::string &pname) + { + if (this->pages.count(pname) == 0) + return NULL; + return this->pages[pname]; + } +}; + +class HTTPD : public Module +{ + std::map providers; + public: + HTTPD(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, SUPPORTED) + { + this->SetAuthor("Anope"); + + Implementation i[] = { I_OnReload }; + ModuleManager::Attach(i, this, sizeof(i) / sizeof(Implementation)); + + this->OnReload(); + } + + ~HTTPD() + { + for (std::map::const_iterator it = SocketEngine::Sockets.begin(), it_end = SocketEngine::Sockets.end(); it != it_end;) + { + Socket *s = it->second; + ++it; + + if (dynamic_cast(s) || dynamic_cast(s)) + delete s; + } + + this->providers.clear(); + } + + void OnReload() anope_override + { + ConfigReader config; + std::set existing; + + for (int i = 0, num = config.Enumerate("httpd"); i < num; ++i) + { + Anope::string hname = config.ReadValue("httpd", "name", "httpd/main", i); + existing.insert(hname); + + Anope::string ip = config.ReadValue("httpd", "ip", "", i); + int port = config.ReadInteger("httpd", "port", "8080", i, true); + int timeout = config.ReadInteger("httpd", "timeout", "30", i, true); + Anope::string ext_ip = config.ReadValue("httpd", "extforward_ip", "", i); + Anope::string ext_header = config.ReadValue("httpd", "extforward_header", "", i); + + if (ip.empty()) + { + Log(LOG_NORMAL, "httpd") << "You must configure a bind IP for HTTP server " << hname; + continue; + } + else if (port <= 0 || port > 65535) + { + Log(LOG_NORMAL, "httpd") << "You must configure a (valid) listen port for HTTP server " << hname; + continue; + } + + HTTPProvider *p; + if (this->providers.count(hname) == 0) + { + try + { + p = new MyHTTPProvider(this, hname, ip, port, timeout); + } + catch (const SocketException &ex) + { + Log(LOG_NORMAL, "httpd") << "Unable to create HTTP server " << hname << ": " << ex.GetReason(); + continue; + } + this->providers[hname] = p; + + Log(LOG_NORMAL, "httpd") << "Created HTTP server " << hname; + } + else + { + p = this->providers[hname]; + + if (p->GetIP() != ip || p->GetPort() != port) + { + delete p; + this->providers.erase(hname); + + try + { + p = new MyHTTPProvider(this, hname, ip, port, timeout); + } + catch (const SocketException &ex) + { + Log(LOG_NORMAL, "httpd") << "Unable to create HTTP server " << hname << ": " << ex.GetReason(); + continue; + } + + this->providers[hname] = p; + } + } + + + p->ext_ip = ext_ip; + p->ext_headers = BuildStringVector(ext_header); + } + + for (std::map::iterator it = this->providers.begin(), it_end = this->providers.end(); it != it_end;) + { + HTTPProvider *p = it->second; + ++it; + + if (existing.count(p->name) == 0) + { + Log(LOG_NORMAL, "httpd") << "Removing HTTP server " << p->name; + this->providers.erase(p->name); + delete p; + } + } + } +}; + +MODULE_INIT(HTTPD) diff --git a/modules/extra/webcpanel/CMakeLists.txt b/modules/extra/webcpanel/CMakeLists.txt new file mode 100644 index 000000000..4437156ad --- /dev/null +++ b/modules/extra/webcpanel/CMakeLists.txt @@ -0,0 +1,5 @@ + +install(DIRECTORY templates + DESTINATION "${DB_DIR}/modules/webcpanel" +) + diff --git a/modules/extra/webcpanel/pages/chanserv/access.cpp b/modules/extra/webcpanel/pages/chanserv/access.cpp new file mode 100644 index 000000000..d3ec2ad99 --- /dev/null +++ b/modules/extra/webcpanel/pages/chanserv/access.cpp @@ -0,0 +1,147 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "../../webcpanel.h" + +WebCPanel::ChanServ::Access::Access(const Anope::string &cat, const Anope::string &u) : WebPanelProtectedPage(cat, u) +{ +} + +void WebCPanel::ChanServ::Access::OnRequest(HTTPProvider *server, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply, NickAlias *na, TemplateFileServer::Replacements &replacements) +{ + const Anope::string &chname = message.get_data["channel"]; + + if (chname.empty()) + { + reply.error = HTTP_FOUND; + reply.headers["Location"] = "http://" + message.headers["Host"] + "/chanserv/info"; + return; + } + + ChannelInfo *ci = cs_findchan(chname); + + if (!ci) + return; + + AccessGroup u_access = ci->AccessFor(na->nc); + bool has_priv = na->nc->IsServicesOper() && na->nc->o->ot->HasPriv("chanserv/access/modify"); + + if (!u_access.HasPriv("ACCESS_LIST") && !has_priv) + return; + + const ChanAccess *highest = u_access.Highest(); + + if (ci->AccessFor(na->nc).HasPriv("ACCESS_CHANGE") || has_priv) + { + if (message.get_data["del"].empty() == false && message.get_data["mask"].empty() == false) + { + std::vector params; + params.push_back(ci->name); + params.push_back("DEL"); + params.push_back(message.get_data["mask"]); + + WebPanel::RunCommand(na->nc->display, na->nc, Config->ChanServ, "chanserv/access", params, replacements); + } + else if (message.post_data["mask"].empty() == false && message.post_data["access"].empty() == false && message.post_data["provider"].empty() == false) + { + // Generic access add code here, works with any provider (so we can't call a command exactly) + AccessProvider *a = NULL; + for (std::list::const_iterator it = AccessProvider::GetProviders().begin(); it != AccessProvider::GetProviders().end(); ++it) + if ((*it)->name == message.post_data["provider"]) + a = *it; + + if (a) + { + bool denied = false; + + for (unsigned i = 0, end = ci->GetAccessCount(); i < end; ++i) + { + ChanAccess *acc = ci->GetAccess(i); + + if (acc->mask == message.post_data["mask"]) + { + if ((!highest || *acc >= *highest) && !u_access.Founder && !has_priv) + { + replacements["MESSAGES"] = "Access denied"; + denied = true; + } + else + ci->EraseAccess(acc); + break; + } + } + + if (ci->GetAccessCount() >= Config->CSAccessMax) + replacements["MESSAGES"] = "Sorry, you can only have " + stringify(Config->CSAccessMax) + " access entries on a channel."; + else if (!denied) + { + ChanAccess *new_acc = a->Create(); + new_acc->ci = ci; + new_acc->mask = message.post_data["mask"]; + new_acc->creator = na->nc->display; + try + { + new_acc->Unserialize(message.post_data["access"]); + } + catch (...) + { + replacements["MESSAGES"] = "Invalid access expression for the given type"; + delete new_acc; + new_acc = NULL; + } + if (new_acc) + { + new_acc->last_seen = 0; + new_acc->created = Anope::CurTime; + + if ((!highest || *highest <= *new_acc) && !u_access.Founder && !has_priv) + delete new_acc; + else if (new_acc->Serialize().empty()) + { + replacements["MESSAGES"] = "Invalid access expression for the given type"; + delete new_acc; + } + else + { + ci->AddAccess(new_acc); + replacements["MESSAGES"] = "Access for " + new_acc->mask + " set to " + new_acc->Serialize(); + } + } + } + } + } + } + + replacements["ESCAPED_CHANNEL"] = HTTPUtils::URLEncode(chname); + + for (unsigned i = 0; i < ci->GetAccessCount(); ++i) + { + ChanAccess *access = ci->GetAccess(i); + + replacements["MASKS"] = HTTPUtils::Escape(access->mask); + replacements["ACCESSES"] = HTTPUtils::Escape(access->Serialize()); + replacements["CREATORS"] = HTTPUtils::Escape(access->creator); + replacements["ACCESS_CHANGES"] = ci->AccessFor(na->nc).HasPriv("ACCESS_CHANGE") ? "YES" : "NO"; + } + + for (std::list::const_iterator it = AccessProvider::GetProviders().begin(); it != AccessProvider::GetProviders().end(); ++it) + { + const AccessProvider *a = *it; + replacements["PROVIDERS"] = a->name; + } + + TemplateFileServer page("chanserv/access.html"); + page.Serve(server, page_name, client, message, reply, replacements); +} + +std::set WebCPanel::ChanServ::Access::GetData() anope_override +{ + std::set v; + v.insert("channel"); + return v; +} + diff --git a/modules/extra/webcpanel/pages/chanserv/access.h b/modules/extra/webcpanel/pages/chanserv/access.h new file mode 100644 index 000000000..50425aa40 --- /dev/null +++ b/modules/extra/webcpanel/pages/chanserv/access.h @@ -0,0 +1,27 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +namespace WebCPanel +{ + +namespace ChanServ +{ + +class Access : public WebPanelProtectedPage +{ + public: + Access(const Anope::string &cat, const Anope::string &u); + + void OnRequest(HTTPProvider *, const Anope::string &, HTTPClient *, HTTPMessage &, HTTPReply &, NickAlias *, TemplateFileServer::Replacements &) anope_override; + + std::set GetData() anope_override; +}; + +} + +} + diff --git a/modules/extra/webcpanel/pages/chanserv/akick.cpp b/modules/extra/webcpanel/pages/chanserv/akick.cpp new file mode 100644 index 000000000..bc80437cc --- /dev/null +++ b/modules/extra/webcpanel/pages/chanserv/akick.cpp @@ -0,0 +1,81 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "../../webcpanel.h" + +WebCPanel::ChanServ::Akick::Akick(const Anope::string &cat, const Anope::string &u) : WebPanelProtectedPage(cat, u) +{ +} + +void WebCPanel::ChanServ::Akick::OnRequest(HTTPProvider *server, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply, NickAlias *na, TemplateFileServer::Replacements &replacements) +{ + const Anope::string &chname = message.get_data["channel"]; + + if (chname.empty()) + { + reply.error = HTTP_FOUND; + reply.headers["Location"] = "http://" + message.headers["Host"] + "/chanserv/info"; + return; + } + + ChannelInfo *ci = cs_findchan(chname); + + if (!ci) + return; + + AccessGroup u_access = ci->AccessFor(na->nc); + bool has_priv = na->nc->IsServicesOper() && na->nc->o->ot->HasPriv("chanserv/access/modify"); + + if (!u_access.HasPriv("akick") && !has_priv) + return; + + if (message.get_data["del"].empty() == false && message.get_data["mask"].empty() == false) + { + std::vector params; + params.push_back(ci->name); + params.push_back("DEL"); + params.push_back(message.get_data["mask"]); + + WebPanel::RunCommand(na->nc->display, na->nc, Config->ChanServ, "chanserv/akick", params, replacements); + } + else if (message.post_data["mask"].empty() == false) + { + std::vector params; + params.push_back(ci->name); + params.push_back("ADD"); + params.push_back(message.post_data["mask"]); + if (message.post_data["reason"].empty() == false) + params.push_back(message.get_data["reason"]); + + WebPanel::RunCommand(na->nc->display, na->nc, Config->ChanServ, "chanserv/akick", params, replacements); + } + + replacements["ESCAPED_CHANNEL"] = HTTPUtils::URLEncode(chname); + + for (unsigned i = 0; i < ci->GetAkickCount(); ++i) + { + AutoKick *akick = ci->GetAkick(i); + + if (akick->nc) + replacements["MASKS"] = HTTPUtils::Escape(akick->nc->display); + else + replacements["MASKS"] = HTTPUtils::Escape(akick->mask); + replacements["CREATORS"] = HTTPUtils::Escape(akick->creator); + replacements["REASONS"] = HTTPUtils::Escape(akick->reason); + } + + TemplateFileServer page("chanserv/akick.html"); + page.Serve(server, page_name, client, message, reply, replacements); +} + +std::set WebCPanel::ChanServ::Akick::GetData() anope_override +{ + std::set v; + v.insert("channel"); + return v; +} + diff --git a/modules/extra/webcpanel/pages/chanserv/akick.h b/modules/extra/webcpanel/pages/chanserv/akick.h new file mode 100644 index 000000000..ff4a4653e --- /dev/null +++ b/modules/extra/webcpanel/pages/chanserv/akick.h @@ -0,0 +1,27 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +namespace WebCPanel +{ + +namespace ChanServ +{ + +class Akick : public WebPanelProtectedPage +{ + public: + Akick(const Anope::string &cat, const Anope::string &u); + + void OnRequest(HTTPProvider *, const Anope::string &, HTTPClient *, HTTPMessage &, HTTPReply &, NickAlias *, TemplateFileServer::Replacements &) anope_override; + + std::set GetData() anope_override; +}; + +} + +} + diff --git a/modules/extra/webcpanel/pages/chanserv/info.cpp b/modules/extra/webcpanel/pages/chanserv/info.cpp new file mode 100644 index 000000000..8db88f496 --- /dev/null +++ b/modules/extra/webcpanel/pages/chanserv/info.cpp @@ -0,0 +1,32 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "../../webcpanel.h" + +WebCPanel::ChanServ::Info::Info(const Anope::string &cat, const Anope::string &u) : WebPanelProtectedPage(cat, u) +{ +} + +void WebCPanel::ChanServ::Info::OnRequest(HTTPProvider *server, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply, NickAlias *na, TemplateFileServer::Replacements &replacements) +{ + // XXX this is slightly inefficient + for (registered_channel_map::const_iterator it = RegisteredChannelList->begin(), it_end = RegisteredChannelList->end(); it != it_end; ++it) + { + ChannelInfo *ci = it->second; + + if (ci->AccessFor(na->nc).HasPriv("SET") || ci->AccessFor(na->nc).HasPriv("ACCESS_LIST")) + { + replacements["CHANNEL_NAMES"] = ci->name; + replacements["ESCAPED_CHANNEL_NAMES"] = HTTPUtils::URLEncode(ci->name); + } + } + + + TemplateFileServer page("chanserv/main.html"); + page.Serve(server, page_name, client, message, reply, replacements); +} + diff --git a/modules/extra/webcpanel/pages/chanserv/info.h b/modules/extra/webcpanel/pages/chanserv/info.h new file mode 100644 index 000000000..87dc9836b --- /dev/null +++ b/modules/extra/webcpanel/pages/chanserv/info.h @@ -0,0 +1,25 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +namespace WebCPanel +{ + +namespace ChanServ +{ + +class Info : public WebPanelProtectedPage +{ + public: + Info(const Anope::string &cat, const Anope::string &u); + + void OnRequest(HTTPProvider *, const Anope::string &, HTTPClient *, HTTPMessage &, HTTPReply &, NickAlias *, TemplateFileServer::Replacements &) anope_override; +}; + +} + +} + diff --git a/modules/extra/webcpanel/pages/chanserv/set.cpp b/modules/extra/webcpanel/pages/chanserv/set.cpp new file mode 100644 index 000000000..764eb13ac --- /dev/null +++ b/modules/extra/webcpanel/pages/chanserv/set.cpp @@ -0,0 +1,136 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "../../webcpanel.h" + +WebCPanel::ChanServ::Set::Set(const Anope::string &cat, const Anope::string &u) : WebPanelProtectedPage(cat, u) +{ +} + +void WebCPanel::ChanServ::Set::OnRequest(HTTPProvider *server, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply, NickAlias *na, TemplateFileServer::Replacements &replacements) +{ + const Anope::string &chname = message.get_data["channel"]; + + if (chname.empty()) + { + reply.error = HTTP_FOUND; + reply.headers["Location"] = "http://" + message.headers["Host"] + "/chanserv/info"; + return; + } + + ChannelInfo *ci = cs_findchan(chname); + + if (!ci || !ci->AccessFor(na->nc).HasPriv("SET")) + return; + + if (message.post_data.empty() == false) + { + if (ci->HasFlag(CI_KEEPTOPIC) != message.post_data.count("keeptopic")) + { + if (!ci->HasFlag(CI_KEEPTOPIC)) + ci->SetFlag(CI_KEEPTOPIC); + else + ci->UnsetFlag(CI_KEEPTOPIC); + replacements["MESSAGES"] = "Secure updated"; + } + if (ci->HasFlag(CI_PEACE) != message.post_data.count("peace")) + { + if (!ci->HasFlag(CI_PEACE)) + ci->SetFlag(CI_PEACE); + else + ci->UnsetFlag(CI_PEACE); + replacements["MESSAGES"] = "Peace updated"; + } + if (ci->HasFlag(CI_PRIVATE) != message.post_data.count("private")) + { + if (!ci->HasFlag(CI_PRIVATE)) + ci->SetFlag(CI_PRIVATE); + else + ci->UnsetFlag(CI_PRIVATE); + replacements["MESSAGES"] = "Private updated"; + } + if (ci->HasFlag(CI_RESTRICTED) != message.post_data.count("restricted")) + { + if (!ci->HasFlag(CI_RESTRICTED)) + ci->SetFlag(CI_RESTRICTED); + else + ci->UnsetFlag(CI_RESTRICTED); + replacements["MESSAGES"] = "Restricted updated"; + } + if (ci->HasFlag(CI_SECURE) != message.post_data.count("secure")) + { + if (!ci->HasFlag(CI_SECURE)) + ci->SetFlag(CI_SECURE); + else + ci->UnsetFlag(CI_SECURE); + replacements["MESSAGES"] = "Secure updated"; + } + if (ci->HasFlag(CI_SECUREOPS) != message.post_data.count("secureops")) + { + if (!ci->HasFlag(CI_SECUREOPS)) + ci->SetFlag(CI_SECUREOPS); + else + ci->UnsetFlag(CI_SECUREOPS); + replacements["MESSAGES"] = "Secureops updated"; + } + if (ci->HasFlag(CI_TOPICLOCK) != message.post_data.count("topiclock")) + { + if (!ci->HasFlag(CI_TOPICLOCK)) + ci->SetFlag(CI_TOPICLOCK); + else + ci->UnsetFlag(CI_TOPICLOCK); + replacements["MESSAGES"] = "Topiclock updated"; + } + } + + replacements["CHANNEL"] = HTTPUtils::Escape(ci->name); + replacements["CHANNEL_ESCAPED"] = HTTPUtils::URLEncode(ci->name); + if (ci->GetFounder()) + replacements["FOUNDER"] = ci->GetFounder()->display; + if (ci->successor) + replacements["SUCCESSOR"] = ci->successor->display; + replacements["TIME_REGISTERED"] = do_strftime(ci->time_registered, na->nc); + replacements["LAST_USED"] = do_strftime(ci->last_used, na->nc); + + if (!ci->last_topic.empty()) + { + replacements["LAST_TOPIC"] = HTTPUtils::Escape(ci->last_topic); + replacements["LAST_TOPIC_SETTER"] = HTTPUtils::Escape(ci->last_topic_setter); + } + + if (ci->HasFlag(CI_KEEPTOPIC)) + replacements["KEEPTOPIC"]; + + if (ci->HasFlag(CI_PEACE)) + replacements["PEACE"]; + + if (ci->HasFlag(CI_PRIVATE)) + replacements["PRIVATE"]; + + if (ci->HasFlag(CI_RESTRICTED)) + replacements["RESTRICTED"]; + + if (ci->HasFlag(CI_SECURE)) + replacements["SECURE"]; + + if (ci->HasFlag(CI_SECUREOPS)) + replacements["SECUREOPS"]; + + if (ci->HasFlag(CI_TOPICLOCK)) + replacements["TOPICLOCK"]; + + TemplateFileServer page("chanserv/set.html"); + page.Serve(server, page_name, client, message, reply, replacements); +} + +std::set WebCPanel::ChanServ::Set::GetData() anope_override +{ + std::set v; + v.insert("channel"); + return v; +} + diff --git a/modules/extra/webcpanel/pages/chanserv/set.h b/modules/extra/webcpanel/pages/chanserv/set.h new file mode 100644 index 000000000..67e817493 --- /dev/null +++ b/modules/extra/webcpanel/pages/chanserv/set.h @@ -0,0 +1,27 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +namespace WebCPanel +{ + +namespace ChanServ +{ + +class Set : public WebPanelProtectedPage +{ + public: + Set(const Anope::string &cat, const Anope::string &u); + + void OnRequest(HTTPProvider *, const Anope::string &, HTTPClient *, HTTPMessage &, HTTPReply &, NickAlias *, TemplateFileServer::Replacements &) anope_override; + + std::set GetData() anope_override; +}; + +} + +} + diff --git a/modules/extra/webcpanel/pages/confirm.cpp b/modules/extra/webcpanel/pages/confirm.cpp new file mode 100644 index 000000000..f18d28129 --- /dev/null +++ b/modules/extra/webcpanel/pages/confirm.cpp @@ -0,0 +1,31 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "../webcpanel.h" + +void WebCPanel::Confirm::OnRequest(HTTPProvider *server, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply) +{ + TemplateFileServer::Replacements replacements; + const Anope::string &user = message.post_data["username"], &pass = message.post_data["password"], &email = message.post_data["email"]; + + replacements["TITLE"] = page_title; + + if (!user.empty() && !pass.empty()) + { + std::vector params; + params.push_back(pass); + if (!email.empty()) + params.push_back(email); + + WebPanel::RunCommand(user, NULL, Config->NickServ, "nickserv/register", params, replacements); + } + + TemplateFileServer page("confirm.html"); + + page.Serve(server, page_name, client, message, reply, replacements); +} + diff --git a/modules/extra/webcpanel/pages/confirm.h b/modules/extra/webcpanel/pages/confirm.h new file mode 100644 index 000000000..d4d19d9c6 --- /dev/null +++ b/modules/extra/webcpanel/pages/confirm.h @@ -0,0 +1,22 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "../../httpd.h" + +namespace WebCPanel +{ + +class Confirm : public WebPanelPage +{ + public: + Confirm(const Anope::string &u) : WebPanelPage(u) { } + + void OnRequest(HTTPProvider *, const Anope::string &, HTTPClient *, HTTPMessage &, HTTPReply &) anope_override; +}; + +} + diff --git a/modules/extra/webcpanel/pages/index.cpp b/modules/extra/webcpanel/pages/index.cpp new file mode 100644 index 000000000..525e564f6 --- /dev/null +++ b/modules/extra/webcpanel/pages/index.cpp @@ -0,0 +1,73 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "../webcpanel.h" + +void WebCPanel::Index::OnRequest(HTTPProvider *server, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply) +{ + TemplateFileServer::Replacements replacements; + const Anope::string &user = message.post_data["username"], &pass = message.post_data["password"]; + + replacements["TITLE"] = page_title; + + if (!user.empty() && !pass.empty()) + { + // Rate limit check. + + NickAlias *na = findnick(user); + + EventReturn MOD_RESULT = EVENT_CONTINUE; + + if (na) + { + FOREACH_RESULT(I_OnCheckAuthentication, OnCheckAuthentication(NULL, NULL, std::vector(), na->nc->display, pass)); + } + + if (MOD_RESULT == EVENT_ALLOW) + { + Anope::string id; + for (int i = 0; i < 64; ++i) + { + char c; + do + c = 48 + (rand() % 123); + while (!isalnum(c)); + id += c; + } + + na->Extend("webcpanel_id", new ExtensibleItemClass(id)); + na->Extend("webcpanel_ip", new ExtensibleItemClass(client->GetIP())); + + { + HTTPReply::cookie c; + c.push_back(std::make_pair("account", na->nick)); + c.push_back(std::make_pair("Path", "/")); + reply.cookies.push_back(c); + } + + { + HTTPReply::cookie c; + c.push_back(std::make_pair("id", id)); + c.push_back(std::make_pair("Path", "/")); + reply.cookies.push_back(c); + } + + reply.error = HTTP_FOUND; + reply.headers["Location"] = "http://" + message.headers["Host"] + "/nickserv/info"; + return; + } + else + { + replacements["INVALID_LOGIN"] = "Invalid username or password"; + } + } + + TemplateFileServer page("login.html"); + + page.Serve(server, page_name, client, message, reply, replacements); +} + diff --git a/modules/extra/webcpanel/pages/index.h b/modules/extra/webcpanel/pages/index.h new file mode 100644 index 000000000..2f4d9c400 --- /dev/null +++ b/modules/extra/webcpanel/pages/index.h @@ -0,0 +1,22 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "../../httpd.h" + +namespace WebCPanel +{ + +class Index : public WebPanelPage +{ + public: + Index(const Anope::string &u) : WebPanelPage(u) { } + + void OnRequest(HTTPProvider *, const Anope::string &, HTTPClient *, HTTPMessage &, HTTPReply &) anope_override; +}; + +} + diff --git a/modules/extra/webcpanel/pages/logout.cpp b/modules/extra/webcpanel/pages/logout.cpp new file mode 100644 index 000000000..90a6a8a9b --- /dev/null +++ b/modules/extra/webcpanel/pages/logout.cpp @@ -0,0 +1,22 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "../webcpanel.h" + +WebCPanel::Logout::Logout(const Anope::string &u) : WebPanelProtectedPage("", u) +{ +} + +void WebCPanel::Logout::OnRequest(HTTPProvider *server, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply, NickAlias *na, TemplateFileServer::Replacements &replacements) +{ + na->Shrink("webcpanel_id"); + na->Shrink("webcpanel_ip"); + + reply.error = HTTP_FOUND; + reply.headers["Location"] = "http://" + message.headers["Host"]; +} + diff --git a/modules/extra/webcpanel/pages/logout.h b/modules/extra/webcpanel/pages/logout.h new file mode 100644 index 000000000..4f31e4a00 --- /dev/null +++ b/modules/extra/webcpanel/pages/logout.h @@ -0,0 +1,20 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +namespace WebCPanel +{ + +class Logout : public WebPanelProtectedPage +{ + public: + Logout(const Anope::string &u); + + void OnRequest(HTTPProvider *, const Anope::string &, HTTPClient *, HTTPMessage &, HTTPReply &, NickAlias *, TemplateFileServer::Replacements &) anope_override; +}; + +} + diff --git a/modules/extra/webcpanel/pages/memoserv/memos.cpp b/modules/extra/webcpanel/pages/memoserv/memos.cpp new file mode 100644 index 000000000..eafbde3a5 --- /dev/null +++ b/modules/extra/webcpanel/pages/memoserv/memos.cpp @@ -0,0 +1,121 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "../../webcpanel.h" + +WebCPanel::MemoServ::Memos::Memos(const Anope::string &cat, const Anope::string &u) : WebPanelProtectedPage(cat, u) +{ +} + +void WebCPanel::MemoServ::Memos::OnRequest(HTTPProvider *server, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply, NickAlias *na, TemplateFileServer::Replacements &replacements) +{ + const Anope::string &chname = message.get_data["channel"]; + ChannelInfo *ci; + const MemoInfo *mi; + Memo *m; + + for (registered_channel_map::const_iterator it = RegisteredChannelList->begin(), it_end = RegisteredChannelList->end(); it != it_end; ++it) + { + ci = it->second; + + if (ci->AccessFor(na->nc).HasPriv("MEMO")) + { + replacements["CHANNEL_NAMES"] = ci->name; + replacements["ESCAPED_CHANNEL_NAMES"] = HTTPUtils::URLEncode(ci->name); + } + } + + if (chname.empty()) + { + replacements["MESSAGES"] = "No Channel specified, displaying the memos for your Nick"; + mi = &na->nc->memos; + } + else + { + ci = cs_findchan(chname); + if (ci) + { + replacements["MESSAGES"] = "Displaying the memos for " + chname + "."; + mi = &ci->memos; + } + else + { + replacements["MESSAGES"] = "Channel " + chname + " not found, displaying the memos for your nick"; + mi = &na->nc->memos; + } + + replacements["CHANNEL_NAME"] = ci->name; + replacements["ESCAPED_CHANNEL_NAME"] = HTTPUtils::URLEncode(ci->name); + } + if (message.post_data.count("receiver") > 0 && message.post_data.count("message") > 0) + { + std::vector params; + params.push_back(HTTPUtils::URLDecode(message.post_data["receiver"])); + params.push_back(HTTPUtils::URLDecode(message.post_data["message"])); + + WebPanel::RunCommand(na->nc->display, na->nc, Config->MemoServ, "memoserv/send", params, replacements); + } + if (message.get_data.count("del") > 0 && message.get_data.count("number") > 0) + { + std::vector params; + if (!chname.empty()) + params.push_back(chname); + params.push_back(message.get_data["number"]); + + WebPanel::RunCommand(na->nc->display, na->nc, Config->MemoServ, "memoserv/del", params, replacements); + } + if (message.get_data.count("read") > 0 && message.get_data.count("number") > 0) + { + std::vector params; + int number; + bool error = false; + + try + { + number = convertTo(message.get_data["number"]); + } + catch (const ConvertException &ex) + { + replacements["MESSAGES"] = "ERROR - invalid parameter for NUMBER"; + error = true; + } + + m = mi->GetMemo(number-1); + + if (!error && !m) + { + replacements["MESSAGES"] = "ERROR - invalid memo number."; + error = true; + } + + if (!error && message.get_data["read"] == "1") + { + m->UnsetFlag(MF_UNREAD); + } + else if (!error && message.get_data["read"] == "2") + { + m->SetFlag(MF_UNREAD); + } + } + + for (unsigned i = 0; i < mi->memos->size(); ++i) + { + m = mi->GetMemo(i); + replacements["NUMBER"] = stringify(i+1); + replacements["SENDER"] = m->sender; + replacements["TIME"] = do_strftime(m->time); + replacements["TEXT"] = m->text; + if (m->HasFlag(MF_UNREAD)) + replacements["UNREAD"] = "YES"; + else + replacements["UNREAD"] = "NO"; + } + + TemplateFileServer page("memoserv/memos.html"); + page.Serve(server, page_name, client, message, reply, replacements); +} + diff --git a/modules/extra/webcpanel/pages/memoserv/memos.h b/modules/extra/webcpanel/pages/memoserv/memos.h new file mode 100644 index 000000000..00c4c346b --- /dev/null +++ b/modules/extra/webcpanel/pages/memoserv/memos.h @@ -0,0 +1,25 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +namespace WebCPanel +{ + +namespace MemoServ +{ + +class Memos : public WebPanelProtectedPage +{ + public: + Memos(const Anope::string &cat, const Anope::string &u); + + void OnRequest(HTTPProvider *, const Anope::string &, HTTPClient *, HTTPMessage &, HTTPReply &, NickAlias *, TemplateFileServer::Replacements &) anope_override; +}; + +} + +} + diff --git a/modules/extra/webcpanel/pages/nickserv/access.cpp b/modules/extra/webcpanel/pages/nickserv/access.cpp new file mode 100644 index 000000000..c18c5ccb0 --- /dev/null +++ b/modules/extra/webcpanel/pages/nickserv/access.cpp @@ -0,0 +1,39 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "../../webcpanel.h" + +WebCPanel::NickServ::Access::Access(const Anope::string &cat, const Anope::string &u) : WebPanelProtectedPage(cat, u) +{ +} + +void WebCPanel::NickServ::Access::OnRequest(HTTPProvider *server, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply, NickAlias *na, TemplateFileServer::Replacements &replacements) +{ + if (message.post_data.count("access") > 0) + { + std::vector params; + params.push_back("ADD"); + params.push_back(message.post_data["access"]); + + WebPanel::RunCommand(na->nc->display, na->nc, Config->NickServ, "nickserv/access", params, replacements); + } + else if (message.get_data.count("del") > 0 && message.get_data.count("mask") > 0) + { + std::vector params; + params.push_back("DEL"); + params.push_back(message.get_data["mask"]); + + WebPanel::RunCommand(na->nc->display, na->nc, Config->NickServ, "nickserv/access", params, replacements); + } + + for (unsigned i = 0; i < na->nc->access.size(); ++i) + replacements["ACCESS"] = na->nc->access[i]; + + TemplateFileServer page("nickserv/access.html"); + page.Serve(server, page_name, client, message, reply, replacements); +} + diff --git a/modules/extra/webcpanel/pages/nickserv/access.h b/modules/extra/webcpanel/pages/nickserv/access.h new file mode 100644 index 000000000..3f4059aa4 --- /dev/null +++ b/modules/extra/webcpanel/pages/nickserv/access.h @@ -0,0 +1,25 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +namespace WebCPanel +{ + +namespace NickServ +{ + +class Access : public WebPanelProtectedPage +{ + public: + Access(const Anope::string &cat, const Anope::string &u); + + void OnRequest(HTTPProvider *, const Anope::string &, HTTPClient *, HTTPMessage &, HTTPReply &, NickAlias *, TemplateFileServer::Replacements &) anope_override; +}; + +} + +} + diff --git a/modules/extra/webcpanel/pages/nickserv/alist.cpp b/modules/extra/webcpanel/pages/nickserv/alist.cpp new file mode 100644 index 000000000..d3c92dcf4 --- /dev/null +++ b/modules/extra/webcpanel/pages/nickserv/alist.cpp @@ -0,0 +1,49 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "../../webcpanel.h" + +WebCPanel::NickServ::Alist::Alist(const Anope::string &cat, const Anope::string &u) : WebPanelProtectedPage(cat, u) +{ +} + +void WebCPanel::NickServ::Alist::OnRequest(HTTPProvider *server, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply, NickAlias *na, TemplateFileServer::Replacements &replacements) +{ + int chan_count = 0; + + for (registered_channel_map::const_iterator it = RegisteredChannelList->begin(), it_end = RegisteredChannelList->end(); it != it_end; ++it) + { + ChannelInfo *ci = it->second; + + if (ci->GetFounder() && ci->GetFounder() == na->nc) + { + ++chan_count; + + replacements["NUMBERS"] = stringify(chan_count); + replacements["CHANNELS"] = (ci->HasFlag(CI_NO_EXPIRE) ? "!" : "") + ci->name; + replacements["ACCESSES"] = "Founder"; + continue; + } + + AccessGroup access = ci->AccessFor(na->nc); + if (access.empty()) + continue; + + ++chan_count; + + replacements["NUMBERS"] = stringify(chan_count); + replacements["CHANNELS"] = (ci->HasFlag(CI_NO_EXPIRE) ? "!" : "") + ci->name; + Anope::string access_str; + for (unsigned i = 0; i < access.size(); ++i) + access_str += ", " + access[i]->Serialize(); + replacements["ACCESSES"] = access_str.substr(2); + } + + TemplateFileServer page("nickserv/alist.html"); + page.Serve(server, page_name, client, message, reply, replacements); +} + diff --git a/modules/extra/webcpanel/pages/nickserv/alist.h b/modules/extra/webcpanel/pages/nickserv/alist.h new file mode 100644 index 000000000..5e4b31724 --- /dev/null +++ b/modules/extra/webcpanel/pages/nickserv/alist.h @@ -0,0 +1,25 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +namespace WebCPanel +{ + +namespace NickServ +{ + +class Alist : public WebPanelProtectedPage +{ + public: + Alist(const Anope::string &cat, const Anope::string &u); + + void OnRequest(HTTPProvider *, const Anope::string &, HTTPClient *, HTTPMessage &, HTTPReply &, NickAlias *, TemplateFileServer::Replacements &) anope_override; +}; + +} + +} + diff --git a/modules/extra/webcpanel/pages/nickserv/cert.cpp b/modules/extra/webcpanel/pages/nickserv/cert.cpp new file mode 100644 index 000000000..e354addca --- /dev/null +++ b/modules/extra/webcpanel/pages/nickserv/cert.cpp @@ -0,0 +1,39 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "../../webcpanel.h" + +WebCPanel::NickServ::Cert::Cert(const Anope::string &cat, const Anope::string &u) : WebPanelProtectedPage(cat, u) +{ +} + +void WebCPanel::NickServ::Cert::OnRequest(HTTPProvider *server, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply, NickAlias *na, TemplateFileServer::Replacements &replacements) +{ + if (message.post_data.count("certfp") > 0) + { + std::vector params; + params.push_back("ADD"); + params.push_back(message.post_data["certfp"]); + + WebPanel::RunCommand(na->nc->display, na->nc, Config->NickServ, "nickserv/cert", params, replacements); + } + else if (message.get_data.count("del") > 0 && message.get_data.count("mask") > 0) + { + std::vector params; + params.push_back("DEL"); + params.push_back(message.get_data["mask"]); + + WebPanel::RunCommand(na->nc->display, na->nc, Config->NickServ, "nickserv/cert", params, replacements); + } + + for (unsigned i = 0; i < na->nc->cert.size(); ++i) + replacements["CERTS"] = na->nc->cert[i]; + + TemplateFileServer page("nickserv/cert.html"); + page.Serve(server, page_name, client, message, reply, replacements); +} + diff --git a/modules/extra/webcpanel/pages/nickserv/cert.h b/modules/extra/webcpanel/pages/nickserv/cert.h new file mode 100644 index 000000000..1f9f1b4ad --- /dev/null +++ b/modules/extra/webcpanel/pages/nickserv/cert.h @@ -0,0 +1,25 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +namespace WebCPanel +{ + +namespace NickServ +{ + +class Cert : public WebPanelProtectedPage +{ + public: + Cert(const Anope::string &cat, const Anope::string &u); + + void OnRequest(HTTPProvider *, const Anope::string &, HTTPClient *, HTTPMessage &, HTTPReply &, NickAlias *, TemplateFileServer::Replacements &) anope_override; +}; + +} + +} + diff --git a/modules/extra/webcpanel/pages/nickserv/info.cpp b/modules/extra/webcpanel/pages/nickserv/info.cpp new file mode 100644 index 000000000..7ae07ad28 --- /dev/null +++ b/modules/extra/webcpanel/pages/nickserv/info.cpp @@ -0,0 +1,111 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "../../webcpanel.h" + +WebCPanel::NickServ::Info::Info(const Anope::string &cat, const Anope::string &u) : WebPanelProtectedPage(cat, u) +{ +} + +void WebCPanel::NickServ::Info::OnRequest(HTTPProvider *server, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply, NickAlias *na, TemplateFileServer::Replacements &replacements) +{ + if (message.post_data.empty() == false) + { + if (message.post_data.count("email") > 0) + { + if (message.post_data["email"] != na->nc->email) + { + if (!message.post_data["email"].empty() && !MailValidate(message.post_data["email"])) + replacements["ERRORS"] = "Invalid email"; + else + { + na->nc->email = message.post_data["email"]; + replacements["MESSAGES"] = "Email updated"; + } + } + } + if (message.post_data.count("greet") > 0) + { + if (message.post_data["greet"].replace_all_cs("+", " ") != na->nc->greet) + { + na->nc->greet = HTTPUtils::URLDecode(message.post_data["greet"]); + replacements["MESSAGES"] = "Greet updated"; + } + } + if (na->nc->HasFlag(NI_AUTOOP) != message.post_data.count("autoop")) + { + if (!na->nc->HasFlag(NI_AUTOOP)) + na->nc->SetFlag(NI_AUTOOP); + else + na->nc->UnsetFlag(NI_AUTOOP); + replacements["MESSAGES"] = "Autoop updated"; + } + if (na->nc->HasFlag(NI_PRIVATE) != message.post_data.count("private")) + { + if (!na->nc->HasFlag(NI_PRIVATE)) + na->nc->SetFlag(NI_PRIVATE); + else + na->nc->UnsetFlag(NI_PRIVATE); + replacements["MESSAGES"] = "Private updated"; + } + if (na->nc->HasFlag(NI_SECURE) != message.post_data.count("secure")) + { + if (!na->nc->HasFlag(NI_SECURE)) + na->nc->SetFlag(NI_SECURE); + else + na->nc->UnsetFlag(NI_SECURE); + replacements["MESSAGES"] = "Secure updated"; + } + if (message.post_data["kill"] == "on" && !na->nc->HasFlag(NI_KILLPROTECT)) + { + na->nc->SetFlag(NI_KILLPROTECT); + na->nc->UnsetFlag(NI_KILL_QUICK); + replacements["MESSAGES"] = "Kill updated"; + } + else if (message.post_data["kill"] == "quick" && !na->nc->HasFlag(NI_KILL_QUICK)) + { + na->nc->UnsetFlag(NI_KILLPROTECT); + na->nc->SetFlag(NI_KILL_QUICK); + replacements["MESSAGES"] = "Kill updated"; + } + else if (message.post_data["kill"] == "off" && (na->nc->HasFlag(NI_KILLPROTECT) || na->nc->HasFlag(NI_KILL_QUICK))) + { + na->nc->UnsetFlag(NI_KILLPROTECT); + na->nc->UnsetFlag(NI_KILL_QUICK); + replacements["MESSAGES"] = "Kill updated"; + } + } + + replacements["DISPLAY"] = HTTPUtils::Escape(na->nc->display); + if (na->nc->email.empty() == false) + replacements["EMAIL"] = HTTPUtils::Escape(na->nc->email); + replacements["TIME_REGISTERED"] = do_strftime(na->time_registered, na->nc); + if (na->HasVhost()) + { + if (na->GetVhostIdent().empty() == false) + replacements["VHOST"] = na->GetVhostIdent() + "@" + na->GetVhostHost(); + else + replacements["VHOST"] = na->GetVhostHost(); + } + replacements["GREET"] = HTTPUtils::Escape(na->nc->greet); + if (na->nc->HasFlag(NI_AUTOOP)) + replacements["AUTOOP"]; + if (na->nc->HasFlag(NI_PRIVATE)) + replacements["PRIVATE"]; + if (na->nc->HasFlag(NI_SECURE)) + replacements["SECURE"]; + if (na->nc->HasFlag(NI_KILLPROTECT)) + replacements["KILL_ON"]; + if (na->nc->HasFlag(NI_KILL_QUICK)) + replacements["KILL_QUICK"]; + if (!na->nc->HasFlag(NI_KILLPROTECT) && !na->nc->HasFlag(NI_KILL_QUICK)) + replacements["KILL_OFF"]; + + TemplateFileServer page("nickserv/info.html"); + page.Serve(server, page_name, client, message, reply, replacements); +} + diff --git a/modules/extra/webcpanel/pages/nickserv/info.h b/modules/extra/webcpanel/pages/nickserv/info.h new file mode 100644 index 000000000..05fc981ac --- /dev/null +++ b/modules/extra/webcpanel/pages/nickserv/info.h @@ -0,0 +1,25 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +namespace WebCPanel +{ + +namespace NickServ +{ + +class Info : public WebPanelProtectedPage +{ + public: + Info(const Anope::string &cat, const Anope::string &u); + + void OnRequest(HTTPProvider *, const Anope::string &, HTTPClient *, HTTPMessage &, HTTPReply &, NickAlias *, TemplateFileServer::Replacements &) anope_override; +}; + +} + +} + diff --git a/modules/extra/webcpanel/pages/register.cpp b/modules/extra/webcpanel/pages/register.cpp new file mode 100644 index 000000000..84efb3fc0 --- /dev/null +++ b/modules/extra/webcpanel/pages/register.cpp @@ -0,0 +1,23 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "../webcpanel.h" + +void WebCPanel::Register::OnRequest(HTTPProvider *server, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply) +{ + TemplateFileServer::Replacements replacements; + + replacements["TITLE"] = page_title; + + if (!Config->NSForceEmail) + replacements["EMAIL_TYPE"] = "hidden"; + + TemplateFileServer page("register.html"); + + page.Serve(server, page_name, client, message, reply, replacements); +} + diff --git a/modules/extra/webcpanel/pages/register.h b/modules/extra/webcpanel/pages/register.h new file mode 100644 index 000000000..0651bfba6 --- /dev/null +++ b/modules/extra/webcpanel/pages/register.h @@ -0,0 +1,22 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "../../httpd.h" + +namespace WebCPanel +{ + +class Register : public WebPanelPage +{ + public: + Register(const Anope::string &u) : WebPanelPage(u) { } + + void OnRequest(HTTPProvider *, const Anope::string &, HTTPClient *, HTTPMessage &, HTTPReply &) anope_override; +}; + +} + diff --git a/modules/extra/webcpanel/static_fileserver.cpp b/modules/extra/webcpanel/static_fileserver.cpp new file mode 100644 index 000000000..3f51c8b40 --- /dev/null +++ b/modules/extra/webcpanel/static_fileserver.cpp @@ -0,0 +1,42 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "webcpanel.h" +#include +#include + +#include +#include +#include +#include + +StaticFileServer::StaticFileServer(const Anope::string &f_n, const Anope::string &u, const Anope::string &c_t) : HTTPPage(u, c_t), file_name(f_n) +{ +} + +void StaticFileServer::OnRequest(HTTPProvider *server, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply) +{ + int fd = open((template_base + "/" + this->file_name).c_str(), O_RDONLY); + if (fd < 0) + { + Log(LOG_NORMAL, "httpd") << "Error serving file " << page_name << " (" << (template_base + "/" + this->file_name) << "): " << strerror(errno); + + client->SendError(HTTP_PAGE_NOT_FOUND, "Page not found"); + return; + } + + reply.content_type = this->GetContentType(); + reply.headers["Cache-Control"] = "public"; + + int i; + char buffer[BUFSIZE]; + while ((i = read(fd, buffer, sizeof(buffer))) > 0) + reply.Write(buffer, i); + + close(fd); +} + diff --git a/modules/extra/webcpanel/static_fileserver.h b/modules/extra/webcpanel/static_fileserver.h new file mode 100644 index 000000000..17fa7d6b2 --- /dev/null +++ b/modules/extra/webcpanel/static_fileserver.h @@ -0,0 +1,19 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "../httpd.h" + +/* A basic file server. Used for serving static content on disk. */ +class StaticFileServer : public HTTPPage +{ + Anope::string file_name; + public: + StaticFileServer(const Anope::string &f_n, const Anope::string &u, const Anope::string &c_t); + + void OnRequest(HTTPProvider *, const Anope::string &, HTTPClient *, HTTPMessage &, HTTPReply &) anope_override; +}; + diff --git a/modules/extra/webcpanel/template_fileserver.cpp b/modules/extra/webcpanel/template_fileserver.cpp new file mode 100644 index 000000000..d3a974391 --- /dev/null +++ b/modules/extra/webcpanel/template_fileserver.cpp @@ -0,0 +1,244 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "webcpanel.h" +#include +#include +#include + +#include +#include +#include +#include + +struct ForLoop +{ + static std::vector Stack; + + size_t start; /* Index of start of this loop */ + std::vector vars; /* User defined variables */ + typedef std::pair range; + std::vector ranges; /* iterator ranges for each variable */ + + ForLoop(size_t s, TemplateFileServer::Replacements &r, const std::vector &v, const std::vector &r_names) : start(s), vars(v) + { + for (unsigned i = 0; i < r_names.size(); ++i) + ranges.push_back(r.equal_range(r_names[i])); + } + + void increment(const TemplateFileServer::Replacements &r) + { + for (unsigned i = 0; i < ranges.size(); ++i) + { + range &ra = ranges[i]; + + if (ra.first != r.end() && ra.first != ra.second) + ++ra.first; + } + } + + bool finished(const TemplateFileServer::Replacements &r) const + { + for (unsigned i = 0; i < ranges.size(); ++i) + { + const range &ra = ranges[i]; + + if (ra.first != r.end() && ra.first != ra.second) + return false; + } + + return true; + } +}; +std::vector ForLoop::Stack; + +std::stack IfStack; + +static Anope::string FindReplacement(const TemplateFileServer::Replacements &r, const Anope::string &key) +{ + /* Search first through for loop stack then global replacements */ + for (unsigned i = ForLoop::Stack.size(); i > 0; --i) + { + ForLoop &fl = ForLoop::Stack[i - 1]; + + for (unsigned j = 0; j < fl.vars.size(); ++j) + { + const Anope::string &var_name = fl.vars[j]; + + if (key == var_name) + { + const ForLoop::range &range = fl.ranges[j]; + + if (range.first != r.end() && range.first != range.second) + { + return range.first->second; + } + } + } + } + + TemplateFileServer::Replacements::const_iterator it = r.find(key); + if (it != r.end()) + return it->second; + return ""; +} + +TemplateFileServer::TemplateFileServer(const Anope::string &f_n) : file_name(f_n) +{ +} + +void TemplateFileServer::Serve(HTTPProvider *server, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply, Replacements &r) +{ + int fd = open((template_base + "/" + this->file_name).c_str(), O_RDONLY); + if (fd < 0) + { + Log(LOG_NORMAL, "httpd") << "Error serving file " << page_name << " (" << (template_base + "/" + this->file_name) << "): " << strerror(errno); + + client->SendError(HTTP_PAGE_NOT_FOUND, "Page not found"); + return; + } + + Anope::string buf; + + int i; + char buffer[BUFSIZE]; + while ((i = read(fd, buffer, sizeof(buffer) - 1)) > 0) + { + buffer[i] = 0; + buf += buffer; + } + + close(fd); + + Anope::string finished; + + for (unsigned j = 0; j < buf.length(); ++j) + { + if (buf[j] == '{') + { + size_t f = buf.substr(j).find('}'); + if (f == Anope::string::npos) + break; + const Anope::string &content = buf.substr(j + 1, f - 1); + + if (content.find("IF ") == 0) + { + std::vector tokens = BuildStringVector(content); + + if (tokens.size() == 4 && tokens[1] == "EQ") + { + Anope::string first = FindReplacement(r, tokens[2]), second = FindReplacement(r, tokens[3]); + if (first.empty()) + first = tokens[2]; + if (second.empty()) + second = tokens[3]; + + IfStack.push(first == second); + } + else if (tokens.size() == 3 && tokens[1] == "EXISTS") + IfStack.push(r.count(tokens[2]) > 0); + else + Log() << "Invalid IF in web template " << this->file_name; + } + else if (content == "ELSE") + { + if (IfStack.empty()) + Log() << "Invalid ELSE with no stack in web template" << this->file_name; + else + { + bool old = IfStack.top(); + IfStack.pop(); // Pop off previous if() + IfStack.push(!old); // Push back the opposite of what was popped + } + } + else if (content == "END IF") + { + if (IfStack.empty()) + Log() << "END IF with empty stack?"; + else + IfStack.pop(); + } + else if (content.find("FOR ") == 0) + { + std::vector tokens = BuildStringVector(content); + if (tokens.size() != 4 || tokens[2] != "IN") + Log() << "Invalid FOR in web template " << this->file_name; + else + { + std::vector temp_variables = BuildStringVector(tokens[1], ','), + real_variables = BuildStringVector(tokens[3], ','); + if (temp_variables.size() != real_variables.size()) + Log() << "Invalid FOR in web template " << this->file_name << " variable mismatch"; + else + ForLoop::Stack.push_back(ForLoop(j + f, r, temp_variables, real_variables)); + } + } + else if (content == "END FOR") + { + if (ForLoop::Stack.empty()) + Log() << "END FOR with empty stack?"; + else + { + ForLoop &fl = ForLoop::Stack.back(); + if (fl.finished(r)) + ForLoop::Stack.pop_back(); + else + { + fl.increment(r); + if (fl.finished(r)) + ForLoop::Stack.pop_back(); + else + { + j = fl.start; // Move pointer back to start of the loop + continue; // To prevent skipping over this block which doesn't exist anymore + } + } + } + } + else if (content.find("INCLUDE ") == 0) + { + std::vector tokens = BuildStringVector(content); + if (tokens.size() != 2) + Log() << "Invalid INCLUDE in web template " << this->file_name; + else + { + reply.Write(finished); // Write out what we have currently so we insert this files contents here + finished.clear(); + + TemplateFileServer tfs(tokens[1]); + tfs.Serve(server, page_name, client, message, reply, r); + } + } + else + { + // If the if stack is empty or we are in a true statement + bool ifok = IfStack.empty() || IfStack.top(); + bool forok = ForLoop::Stack.empty() || !ForLoop::Stack.back().finished(r); + + if (ifok && forok) + { + const Anope::string &replacement = FindReplacement(r, content.substr(0, f - 1)); + finished += replacement; + } + } + + j += f; // Skip over this whole block + } + else + { + // If the if stack is empty or we are in a true statement + bool ifok = IfStack.empty() || IfStack.top(); + bool forok = ForLoop::Stack.empty() || !ForLoop::Stack.back().finished(r); + + if (ifok && forok) + finished += buf[j]; + } + } + + reply.Write(finished); +} + diff --git a/modules/extra/webcpanel/template_fileserver.h b/modules/extra/webcpanel/template_fileserver.h new file mode 100644 index 000000000..58d6a4872 --- /dev/null +++ b/modules/extra/webcpanel/template_fileserver.h @@ -0,0 +1,27 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "../httpd.h" + +/* A basic file server. Used for serving non-static non-binary content on disk. */ +class TemplateFileServer +{ + Anope::string file_name; + public: + struct Replacements : std::multimap + { + Anope::string& operator[](const Anope::string &key) + { + return insert(std::make_pair(key, ""))->second; + } + }; + + TemplateFileServer(const Anope::string &f_n); + + void Serve(HTTPProvider *, const Anope::string &, HTTPClient *, HTTPMessage &, HTTPReply &, Replacements &); +}; + diff --git a/modules/extra/webcpanel/templates/default/chanserv/access.html b/modules/extra/webcpanel/templates/default/chanserv/access.html new file mode 100644 index 000000000..13c0ba82b --- /dev/null +++ b/modules/extra/webcpanel/templates/default/chanserv/access.html @@ -0,0 +1,52 @@ +{INCLUDE header.html} + {FOR M IN MESSAGES} + {M}
+ {END FOR} + {IF EXISTS ACCESSES} + Access List
+ + + + + + + {FOR MASK,ACCESS,CREATOR,ACCESS_CHANGE IN MASKS,ACCESSES,CREATORS,ACCESS_CHANGES} + + + + + {IF EQ ACCESS_CHANGE YES} + + {END IF} + + {END FOR} +
MaskAccessCreator
{MASK}{ACCESS}{CREATOR}Delete
+ {ELSE} + Access list is empty. + {END IF} + +

+ + Add an access entry
+
+ + + + + + + + + + + +
MaskAccessProvider
+ +
+ +
+{INCLUDE footer.html} diff --git a/modules/extra/webcpanel/templates/default/chanserv/akick.html b/modules/extra/webcpanel/templates/default/chanserv/akick.html new file mode 100644 index 000000000..23c1c1efd --- /dev/null +++ b/modules/extra/webcpanel/templates/default/chanserv/akick.html @@ -0,0 +1,42 @@ +{INCLUDE header.html} + {FOR M IN MESSAGES} + {M}
+ {END FOR} + {IF EXISTS MASKS} + Akick List
+ + + + + + + {FOR MASK,CREATOR,REASON IN MASKS,CREATORS,REASONS} + + + + + + + {END FOR} +
MaskCreatorReason
{MASK}{CREATOR}{REASON}Delete
+ {ELSE} + Akick list is empty. + {END IF} + +

+ + Add an akick entry
+
+ + + + + + + + + +
MaskReason
+ +
+{INCLUDE footer.html} diff --git a/modules/extra/webcpanel/templates/default/chanserv/main.html b/modules/extra/webcpanel/templates/default/chanserv/main.html new file mode 100644 index 000000000..99fec7d75 --- /dev/null +++ b/modules/extra/webcpanel/templates/default/chanserv/main.html @@ -0,0 +1,6 @@ +{INCLUDE header.html} + Channels you have access in:
+ {FOR CH,ECH IN CHANNEL_NAMES,ESCAPED_CHANNEL_NAMES} + {CH}
+ {END FOR} +{INCLUDE footer.html} diff --git a/modules/extra/webcpanel/templates/default/chanserv/set.html b/modules/extra/webcpanel/templates/default/chanserv/set.html new file mode 100644 index 000000000..ec2a33929 --- /dev/null +++ b/modules/extra/webcpanel/templates/default/chanserv/set.html @@ -0,0 +1,100 @@ +{INCLUDE header.html} + {FOR M IN MESSAGES} + {M}
+ {END FOR} +
+ + + + + + {IF EXISTS FOUNDER} + + + + + {END IF} + {IF EXISTS SUCCESSOR} + + + + + {END IF} + + + + + + + + + {IF EXISTS LAST_TOPIC} + + + + + + + + + {END IF} + + + {IF EXISTS KEEPTOPIC} + + {ELSE} + + {END IF} + + + + {IF EXISTS PEACE} + + {ELSE} + + {END IF} + + + + {IF EXISTS PRIVATE} + + {ELSE} + + {END IF} + + + + {IF EXISTS RESTRICTED} + + {ELSE} + + {END IF} + + + + {IF EXISTS SECURE} + + {ELSE} + + {END IF} + + + + {IF EXISTS SECUREOPS} + + {ELSE} + + {END IF} + + + + {IF EXISTS TOPICLOCK} + + {ELSE} + + {END IF} + +
Channel Name{CHANNEL}
Founder{FOUNDER}
Succsesor{SUCCESSOR}
Time registered{TIME_REGISTERED
Last used{LAST_USED}
Last topic{LAST_TOPIC}
Set by{LAST_TOPIC_SETTER}
Keep topic
Peace
Private
Restricted
Secure
Secure Ops
Topic Lock
+ +
+{INCLUDE footer.html} diff --git a/modules/extra/webcpanel/templates/default/confirm.html b/modules/extra/webcpanel/templates/default/confirm.html new file mode 100644 index 000000000..918ccd971 --- /dev/null +++ b/modules/extra/webcpanel/templates/default/confirm.html @@ -0,0 +1,30 @@ + + + + + {TITLE} + + +
+
+ +
+ + + + +
+
+ +
+ {FOR M IN MESSAGES} + {M}
+ {END FOR} +
+
+
+ + + diff --git a/modules/extra/webcpanel/templates/default/favicon.ico b/modules/extra/webcpanel/templates/default/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..be735614a22a08899139a3dff760729e214cb933 GIT binary patch literal 3774 zcmc&$eOQ*|75{nGgq(>_yxB^$?yWVYbc!rD%A(fhvSGHZPouSIfCLDNh(Pcvf{c(* z837_8;0u|Vp{W@nDpRNA<@^9;D1@I==eL52yq)tr&nxiC)dhd;_PU>QpL74txz8uQ zKrj9`Vg&f>iqd|-002XHvk33-7WwzBC843n2?)U2g$rSenuf2^=AbGk4gV@wfF1c6 z_~+tuR4kl_RdZsI8yJ9!{Cs?sm4*C}U^H%6hNBgw_%bB{xj{k52@FI7%L>9murxdr zb*l?dl{+7wTVqhWY6VjPaFFT&RBG_>8i zh3zX=u+2c!tjt5vw5eDY9gQtnsi@3M!KJfj(Q@SqR;8q%FftNtZEeU44#r~MH(FY- zZqXuaNS}+H97Fw=OVLzb!oCGyWl|EBTdg=$_Awgv>_Ow#HhlJeH0stAU|U`~%0EoP zp7rZd8XbvGW25ljBS&!M+&L6a4@YrS6pkM}2s^)3Sx|sR)^ELW11An0Li6Rz*tj4W z^*eW>n0-Cn*a%xp47PFZN|;_|8|zYMqiJI)$5_lUet@ku8cT~+ zUt5TqY;zOaKY!{J%I3!5llXX?J8=S~GpFMk=j(Hh`Rm+V5l+cXp zXRzCrg$?r)@ol*c|1Ql&>&=@e3=hMuvNCKbD#96lr*8Xp96fLV*RNf}t=3kQXC%Qk zYZeM3B5-)uE-Ynw?czo3DftLrq^9DNm`JRMn~DE$Y->}jSecxRva~dm#KyulBLc^2 zYq4)dF3Q=bHOcX)t*YW$Ou@#yJe0)6p}Hs&x7d%Ct5>lmJsq34e(N$barMFlY{<^W zsrq{El`!o4G#3X~<)a`x9Gkh$%M%i~c7@-2*AchBqC&5;w$mSW4K9c^F9+3P? zt!1ucR-}hz?z+9)yN%R=7U~h6ruGOw($l9WcyH3Qp5TYP)f4<5S`M_c|I5h3)_&L3sZ+gZRB>0Bj7&XgbUYqBpM41$&X%r&pN9PlzGyRar!Z(lb1CI7%_7$Z@vM@#c zU7IAasyL$5i6qDAA2?>F-5u?@IUy>M<|>u+*5hPJ=H$#~E9{!+5|5$Xj%kKlY~;_op_I+DM|(VsrT5SknxNPl@Yb@LH$j4sI&cd{yLst z=DSIgwR?$1@bTS%djgrfsXV&Cdo$Xd-tbc7q4~eP-u$SBFgOh}Z6YX@VPz2feKRs8{#}rT#pDb)Mn88bkaF%?#~wE?wPC~f1zy8{(bpmBO!aw$ z@!x^m{OnPI|9l7y?Tj-yPqxl$@NJKJKlCv7x=r+}i($I%75NzB_(x^dh?JhrL}~U0q!C zj~rb2b9bK~^(DH;NKNN2t8}@3&=JPJf$a88kl#Xf`!fb@Gij?yTbyX4$vbGJ$y-ed zgSP3^%C9lsCXX_0HED}U8-+duR0?%#r&i%zfKJrm`TvpX{_y(_zbe72gWrw9b37M> Wa=dz+31-}O&M%9532kIGrr!Z3+e7pK literal 0 HcmV?d00001 diff --git a/modules/extra/webcpanel/templates/default/footer.html b/modules/extra/webcpanel/templates/default/footer.html new file mode 100644 index 000000000..65d23402a --- /dev/null +++ b/modules/extra/webcpanel/templates/default/footer.html @@ -0,0 +1,7 @@ + + + + + diff --git a/modules/extra/webcpanel/templates/default/header.html b/modules/extra/webcpanel/templates/default/header.html new file mode 100644 index 000000000..38f367acc --- /dev/null +++ b/modules/extra/webcpanel/templates/default/header.html @@ -0,0 +1,24 @@ + + + + + {TITLE} + + +
+
+
    + {FOR CATEGORY_URL,CATEGORY_NAME IN CATEGORY_URLS,CATEGORY_NAMES} +
  • {CATEGORY_NAME}
  • + {END FOR} +
+
Logged in as {ACCOUNT} (Logout)
+
+ +
diff --git a/modules/extra/webcpanel/templates/default/login.html b/modules/extra/webcpanel/templates/default/login.html new file mode 100644 index 000000000..7aa54aa9c --- /dev/null +++ b/modules/extra/webcpanel/templates/default/login.html @@ -0,0 +1,47 @@ + + + + + {TITLE} + + +
+
+ +
+ + + + +
+
+ +
+

Login

+ {INVALID_LOGIN}
+ Login to continue. +
+
+ + + + + + + + + + + + + +
Username:
Password:
+
+
+
+
+ + + diff --git a/modules/extra/webcpanel/templates/default/logo.png b/modules/extra/webcpanel/templates/default/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1ab5546c69fb3d87026fe4abc2fdacab3188c4b6 GIT binary patch literal 16385 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

F+jPl?B#=#WBQ@24>D)4RQ>#8!`-_7JRYzZ9l!o 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%0D)NUaY`e$u@&};eI@4M0!aaL+2v`3%Bl4yH z71`s2#%nBZaa2pWH}?nBq5lT!{EQw7lIEii+UV8Pm6a|#3<4b32I zwSQxqhJr~%mM%Vr{B_HuVaH%d=oUC=NxydOaOmfGhe{PE)dYnO?;ZYIdl&HkD1|Channels you have access in:
+ {FOR CH,ECH IN CHANNEL_NAMES,ESCAPED_CHANNEL_NAMES} + {CH} + {END FOR} +

+ {FOR M IN MESSAGES} + {M}
+ {END FOR} +
+ {IF EXISTS NUMBER} + Memos List: + + + + + + + {FOR I,S,T,TXT,U IN NUMBER,SENDER,TIME,TEXT,UNREAD} + + + + + {IF EQ U YES} + + {ELSE} + + {END IF} + + + + + + + + {END FOR} +
NumberSenderTime/Message
{I}{S}{T}Mark as ReadMark as UnreadDelete
{TXT}
+ {ELSE} + No memos to show. + {END IF} + +

+ + Send a new Memo +

+ Receiver: + Message: + +
+{INCLUDE footer.html} diff --git a/modules/extra/webcpanel/templates/default/nickserv/access.html b/modules/extra/webcpanel/templates/default/nickserv/access.html new file mode 100644 index 000000000..9948c3694 --- /dev/null +++ b/modules/extra/webcpanel/templates/default/nickserv/access.html @@ -0,0 +1,24 @@ +{INCLUDE header.html} + {FOR M IN MESSAGES} + {M}
+ {END FOR} + {IF EXISTS ACCESS} + Your access list: + + {FOR A IN ACCESS} + + + + + {END FOR} +
{A}Delete
+ {ELSE} + Your access list is empty. + {END IF} +

+ Add an access entry: +
+ + +
+{INCLUDE footer.html} diff --git a/modules/extra/webcpanel/templates/default/nickserv/alist.html b/modules/extra/webcpanel/templates/default/nickserv/alist.html new file mode 100644 index 000000000..e36143458 --- /dev/null +++ b/modules/extra/webcpanel/templates/default/nickserv/alist.html @@ -0,0 +1,16 @@ +{INCLUDE header.html} + + + + + + + {FOR N,C,A IN NUMBERS,CHANNELS,ACCESSES} + + + + + + {END FOR} +
NumberChannelAccess
{N}{C}{A}
+{INCLUDE footer.html} diff --git a/modules/extra/webcpanel/templates/default/nickserv/cert.html b/modules/extra/webcpanel/templates/default/nickserv/cert.html new file mode 100644 index 000000000..31f12b883 --- /dev/null +++ b/modules/extra/webcpanel/templates/default/nickserv/cert.html @@ -0,0 +1,19 @@ +{INCLUDE header.html} + {FOR M IN MESSAGES} + {M}
+ {END FOR} + Your certificate finrgerprints: + + {FOR CERT IN CERTS} + + + + + {END FOR} +
{CERT}Delete
+ Add an certificate fingerprint +
+ + +
+{INCLUDE footer.html} diff --git a/modules/extra/webcpanel/templates/default/nickserv/info.html b/modules/extra/webcpanel/templates/default/nickserv/info.html new file mode 100644 index 000000000..1dd051364 --- /dev/null +++ b/modules/extra/webcpanel/templates/default/nickserv/info.html @@ -0,0 +1,82 @@ +{INCLUDE header.html} + {FOR M IN ERRORS} + {M}
+ {END FOR} + {FOR M IN MESSAGES} + {M}
+ {END FOR} + Your account information: +
+ + + + + + {IF EXISTS EMAIL} + + + + + {END IF} + + + + + {IF EXISTS VHOST} + + + + + {END IF} + + + + + + + {IF EXISTS AUTOOP} + + {ELSE} + + {END IF} + + + + {IF EXISTS PRIVATE} + + {ELSE} + + {END IF} + + + + {IF EXISTS SECURE} + + {ELSE} + + {END IF} + + + + + +
Account:{DISPLAY}
EMail{EMAIL}
Time registered{TIME_REGISTERED}
Vhost{VHOST}
Greet
Auto op
Private
Secure
Kill
+ +
+{INCLUDE footer.html} diff --git a/modules/extra/webcpanel/templates/default/register.html b/modules/extra/webcpanel/templates/default/register.html new file mode 100644 index 000000000..8f6a5296a --- /dev/null +++ b/modules/extra/webcpanel/templates/default/register.html @@ -0,0 +1,51 @@ + + + + + {TITLE} + + +
+
+ +
+ + + + +
+
+ + {MESSAGES} +
+

Register

+
+ Fill out the following form to register. +
+ + + + + + + + + + + + + + + + + +
Username:
Password:
Email
+
+
+
+
+ + + diff --git a/modules/extra/webcpanel/templates/default/style.css b/modules/extra/webcpanel/templates/default/style.css new file mode 100644 index 000000000..78c044995 --- /dev/null +++ b/modules/extra/webcpanel/templates/default/style.css @@ -0,0 +1,97 @@ +html { +font-family: 'Cabin', Helvetica, Arial, sans-serif; +} +body { + overflow:hidden; +} +.master { + margin-left:0px; + margin-right:0px; + height:100%; + background: #FFF; + border-width: 1px; + border-style: solid; + overflow: hidden; +} +.header { + height: 33px; + padding-left: 5px; + width: auto; + background: #EEE; + border-width: 1px; + border-bottom-style: solid; +} +.sidebar { + width: 185px; + background: #EEE; + height: 500px; + border-width: 1px; + border-right-style: solid; + padding-top:15px; + padding-left:15px; +} + +.loggedinas { + right:15px; + top:15px; + position:absolute; +} + +#button { + padding: 0; + height:30px; + margin: 0px; + padding-bottom: 2px; +} +#button li { + display: inline; +} + +#button li a { + font-family: Arial; + font-size:11px; + text-decoration: none; + float:left; + padding: 10px; + background-color: #EEE; + height:13px; + color: #000; +} +#button li a:hover { + background-color: #A8BEE3; + padding-bottom:10px; +} +#a.current { + color:navy; +} + +.content { + left:220px; + top:50px; + position:absolute; +} + +.footer { + margin-left:auto; + margin-right:auto; + text-align:center; + height: 30px; + width:100%; + background: #FFF; + overflow: hidden; +} + +.sidenav { + list-style-type: none; + background: #EEE +} + +.sidenav a { + color:black; + text-decoration:none; +} +.sidenav a:hover{ + color:black; + background:#FFF; + text-decoration:underline; +} diff --git a/modules/extra/webcpanel/webcpanel.cpp b/modules/extra/webcpanel/webcpanel.cpp new file mode 100644 index 000000000..4970845cd --- /dev/null +++ b/modules/extra/webcpanel/webcpanel.cpp @@ -0,0 +1,209 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "webcpanel.h" + +Anope::string provider_name, template_name, template_base, page_title; + +class ModuleWebCPanel : public Module +{ + Panel panel; + + StaticFileServer style_css, logo_png, favicon_ico; + + WebCPanel::Index index; + WebCPanel::Logout logout; + WebCPanel::Register _register; + WebCPanel::Confirm confirm; + + WebCPanel::NickServ::Info nickserv_info; + WebCPanel::NickServ::Cert nickserv_cert; + WebCPanel::NickServ::Access nickserv_access; + WebCPanel::NickServ::Alist nickserv_alist; + + WebCPanel::ChanServ::Info chanserv_info; + WebCPanel::ChanServ::Set chanserv_set; + WebCPanel::ChanServ::Access chanserv_access; + WebCPanel::ChanServ::Akick chanserv_akick; + + WebCPanel::MemoServ::Memos memoserv_memos; + + public: + ModuleWebCPanel(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, SUPPORTED), + panel(this, "webcpanel"), + style_css("style.css", "/static/style.css", "text/css"), logo_png("logo.png", "/static/logo.png", "image/png"), favicon_ico("favicon.ico", "/favicon.ico", "image/x-icon"), + index("/"), logout("/logout"), _register("/register"), confirm("/confirm"), + nickserv_info(Config->NickServ, "/nickserv/info"), nickserv_cert(Config->NickServ, "/nickserv/cert"), nickserv_access(Config->NickServ, "/nickserv/access"), nickserv_alist(Config->NickServ, "/nickserv/alist"), + chanserv_info(Config->ChanServ, "/chanserv/info"), chanserv_set(Config->ChanServ, "/chanserv/set"), chanserv_access(Config->ChanServ, "/chanserv/access"), chanserv_akick(Config->ChanServ, "/chanserv/akick"), + memoserv_memos(Config->MemoServ, "/memoserv/memos") + { + this->SetAuthor("Anope"); + + ConfigReader reader; + provider_name = reader.ReadValue("webcpanel", "server", "httpd/main", 0); + template_name = reader.ReadValue("webcpanel", "template", "template", 0); + template_base = db_dir + "/modules/webcpanel/templates/" + template_name; + page_title = reader.ReadValue("webcpanel", "title", "Anope IRC Services", 0); + + service_reference provider("HTTPProvider", provider_name); + if (!provider) + throw ModuleException("Unable to find HTTPD provider. Is m_httpd loaded?"); + + provider->RegisterPage(&this->style_css); + provider->RegisterPage(&this->logo_png); + provider->RegisterPage(&this->favicon_ico); + + provider->RegisterPage(&this->index); + provider->RegisterPage(&this->logout); + provider->RegisterPage(&this->_register); + provider->RegisterPage(&this->confirm); + + if (Config->NickServ.empty() == false) + { + Section s; + s.name = Config->NickServ; + + SubSection ss; + ss.name = "Information"; + ss.url = "/nickserv/info"; + s.subsections.push_back(ss); + provider->RegisterPage(&this->nickserv_info); + + if (ircd && ircd->certfp) + { + ss.name = "SSL Certificates"; + ss.url = "/nickserv/cert"; + s.subsections.push_back(ss); + provider->RegisterPage(&this->nickserv_cert); + } + + ss.name = "Access"; + ss.url = "/nickserv/access"; + s.subsections.push_back(ss); + provider->RegisterPage(&this->nickserv_access); + + ss.name = "AList"; + ss.url = "/nickserv/alist"; + s.subsections.push_back(ss); + provider->RegisterPage(&this->nickserv_alist); + + panel.sections.push_back(s); + } + if (Config->ChanServ.empty() == false) + { + Section s; + s.name = Config->ChanServ; + + SubSection ss; + ss.name = "Channels"; + ss.url = "/chanserv/info"; + s.subsections.push_back(ss); + provider->RegisterPage(&this->chanserv_info); + + ss.name = "Settings"; + ss.url = "/chanserv/set"; + s.subsections.push_back(ss); + provider->RegisterPage(&this->chanserv_set); + + ss.name = "Access"; + ss.url = "/chanserv/access"; + s.subsections.push_back(ss); + provider->RegisterPage(&this->chanserv_access); + + ss.name = "Akick"; + ss.url = "/chanserv/akick"; + s.subsections.push_back(ss); + provider->RegisterPage(&this->chanserv_akick); + + panel.sections.push_back(s); + } + + if (Config->MemoServ.empty() == false) + { + Section s; + s.name = Config->MemoServ; + + SubSection ss; + ss.name = "Memos"; + ss.url = "/memoserv/memos"; + s.subsections.push_back(ss); + provider->RegisterPage(&this->memoserv_memos); + + panel.sections.push_back(s); + } + } + + ~ModuleWebCPanel() + { + service_reference provider("HTTPProvider", provider_name); + if (provider) + { + provider->UnregisterPage(&this->style_css); + provider->UnregisterPage(&this->logo_png); + provider->UnregisterPage(&this->favicon_ico); + + provider->UnregisterPage(&this->index); + provider->UnregisterPage(&this->logout); + provider->UnregisterPage(&this->_register); + provider->UnregisterPage(&this->confirm); + + provider->UnregisterPage(&this->nickserv_info); + provider->UnregisterPage(&this->nickserv_cert); + provider->UnregisterPage(&this->nickserv_access); + provider->UnregisterPage(&this->nickserv_alist); + + provider->UnregisterPage(&this->chanserv_info); + provider->UnregisterPage(&this->chanserv_set); + provider->UnregisterPage(&this->chanserv_access); + provider->UnregisterPage(&this->chanserv_akick); + + provider->UnregisterPage(&this->memoserv_memos); + } + } +}; + +namespace WebPanel +{ + void RunCommand(const Anope::string &user, NickCore *nc, const Anope::string &service, const Anope::string &c, const std::vector ¶ms, TemplateFileServer::Replacements &r) + { + service_reference cmd("Command", c); + if (!cmd) + { + r["MESSAGSE"] = "Unable to find command " + c; + return; + } + + BotInfo *bi = findbot(service); + if (!bi) + { + if (BotListByNick->empty()) + return; + bi = BotListByNick->begin()->second; // Pick one... + } + + struct MyComandReply : CommandReply + { + TemplateFileServer::Replacements &re; + + MyComandReply(TemplateFileServer::Replacements &_r) : re(_r) { } + + void SendMessage(const BotInfo *source, const Anope::string &msg) anope_override + { + re["MESSAGES"] = msg; + } + } + my_reply(r); + + CommandSource source(user, NULL, nc, &my_reply); + source.owner = bi; + source.service = bi; + + cmd->Execute(source, params); + } +} + +MODULE_INIT(ModuleWebCPanel) diff --git a/modules/extra/webcpanel/webcpanel.h b/modules/extra/webcpanel/webcpanel.h new file mode 100644 index 000000000..79f902b0f --- /dev/null +++ b/modules/extra/webcpanel/webcpanel.h @@ -0,0 +1,163 @@ +/* + * (C) 2003-2012 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "module.h" +#include "../httpd.h" + +#include "static_fileserver.h" +#include "template_fileserver.h" + +extern Anope::string provider_name, template_name, template_base, page_title; + +struct SubSection +{ + Anope::string name; + Anope::string url; +}; + +struct Section +{ + Anope::string name; + std::vector subsections; +}; + +/* An interface for this webpanel used by other modules */ +class Panel : public Section, public Service +{ + public: + Panel(Module *c, const Anope::string &n) : Service(c, "Panel", n) { } + + std::vector
sections; + + NickAlias *GetNickFromSession(HTTPClient *client, HTTPMessage &msg) + { + if (!client) + return NULL; + + const Anope::string &acc = msg.cookies["account"], &id = msg.cookies["id"]; + + if (acc.empty() || id.empty()) + return NULL; + + NickAlias *na = findnick(acc); + if (na == NULL) + return NULL; + + Anope::string *n_id = na->GetExt("webcpanel_id"), *n_ip = na->GetExt("webcpanel_ip"); + if (n_id == NULL || n_ip == NULL) + return NULL; + else if (id != *n_id) + return NULL; + else if (client->GetIP() != *n_ip) + return NULL; + else + return na; + } +}; + +class WebPanelPage : public HTTPPage +{ + public: + WebPanelPage(const Anope::string &u, const Anope::string &ct = "text/html") : HTTPPage(u, ct) + { + } + + virtual void OnRequest(HTTPProvider *, const Anope::string &, HTTPClient *, HTTPMessage &, HTTPReply &) = 0; +}; + +class WebPanelProtectedPage : public WebPanelPage +{ + Anope::string category; + + public: + WebPanelProtectedPage(const Anope::string &cat, const Anope::string &u, const Anope::string &ct = "text/html") : WebPanelPage(u, ct), category(cat) + { + } + + void OnRequest(HTTPProvider *provider, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply) anope_override anope_final + { + service_reference panel("Panel", "webcpanel"); + NickAlias *na; + + if (!panel || !(na = panel->GetNickFromSession(client, message))) + { + return; // Access denied + } + + TemplateFileServer::Replacements replacements; + + replacements["TITLE"] = page_title; + replacements["ACCOUNT"] = na->nc->display; + + Anope::string sections, get; + + for (std::map::iterator it = message.get_data.begin(), it_end = message.get_data.end(); it != it_end; ++it) + if (this->GetData().count(it->first) > 0) + get += "&" + it->first + "=" + HTTPUtils::URLEncode(it->second); + if (get.empty() == false) + get = "?" + get.substr(1); + + Section *ns = NULL; + for (unsigned i = 0; i < panel->sections.size(); ++i) + { + Section& s = panel->sections[i]; + if (s.name == this->category) + ns = &s; + replacements["CATEGORY_URLS"] = s.subsections[0].url; + replacements["CATEGORY_NAMES"] = s.name; + } + + if (ns) + { + sections = ""; + for (unsigned i = 0; i < ns->subsections.size(); ++i) + { + SubSection& ss = ns->subsections[i]; + replacements["SUBCATEGORY_URLS"] = ss.url; + replacements["SUBCATEGORY_GETS"] = get; + replacements["SUBCATEGORY_NAMES"] = ss.name; + } + } + + this->OnRequest(provider, page_name, client, message, reply, na, replacements); + } + + virtual void OnRequest(HTTPProvider *, const Anope::string &, HTTPClient *, HTTPMessage &, HTTPReply &, NickAlias *, TemplateFileServer::Replacements &) = 0; + + /* What get data should be appended to links in the navbar */ + virtual std::set GetData() { return std::set(); } +}; + +namespace WebPanel +{ + /** Run a command + * @param User name to run command as, probably nc->display unless nc == NULL + * @param nc Nick core to run command from + * @param service Service for source.owner and source.service + * @param c Command to run (as a service name) + * @param params Command parameters + * @param r Replacements, reply from command goes back into r["MESSAGES"] + */ + extern void RunCommand(const Anope::string &user, NickCore *nc, const Anope::string &service, const Anope::string &c, const std::vector ¶ms, TemplateFileServer::Replacements &r); +} + +#include "pages/index.h" +#include "pages/logout.h" +#include "pages/register.h" +#include "pages/confirm.h" + +#include "pages/nickserv/info.h" +#include "pages/nickserv/cert.h" +#include "pages/nickserv/access.h" +#include "pages/nickserv/alist.h" + +#include "pages/chanserv/info.h" +#include "pages/chanserv/set.h" +#include "pages/chanserv/access.h" +#include "pages/chanserv/akick.h" + +#include "pages/memoserv/memos.h" \ No newline at end of file diff --git a/src/access.cpp b/src/access.cpp index 40e53a305..6ff842fd8 100644 --- a/src/access.cpp +++ b/src/access.cpp @@ -73,10 +73,21 @@ void PrivilegeManager::ClearPrivileges() AccessProvider::AccessProvider(Module *o, const Anope::string &n) : Service(o, "AccessProvider", n) { + providers.push_back(this); } AccessProvider::~AccessProvider() { + std::list::iterator it = std::find(providers.begin(), providers.end(), this); + if (it != providers.end()) + providers.erase(it); +} + +std::list AccessProvider::providers; + +const std::list& AccessProvider::GetProviders() +{ + return providers; } ChanAccess::ChanAccess(AccessProvider *p) : provider(p) diff --git a/src/bots.cpp b/src/bots.cpp index 85d6e0700..53f9b78c1 100644 --- a/src/bots.cpp +++ b/src/bots.cpp @@ -239,7 +239,6 @@ void BotInfo::OnMessage(User *u, const Anope::string &message) return; CommandSource source(u->nick, u, u->Account(), u); - source.c = NULL; source.owner = this; source.service = this; diff --git a/src/command.cpp b/src/command.cpp index ae01ecda4..21901bd34 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -16,7 +16,8 @@ #include "access.h" #include "regchannel.h" -CommandSource::CommandSource(const Anope::string &n, User *user, NickCore *core, CommandReply *r) : nick(n), u(user), nc(core), reply(r) +CommandSource::CommandSource(const Anope::string &n, User *user, NickCore *core, CommandReply *r) : nick(n), u(user), nc(core), reply(r), + c(NULL), owner(NULL), service(NULL) { } @@ -90,7 +91,7 @@ void CommandSource::Reply(const char *message, ...) va_list args; char buf[4096]; // Messages can be really big. - const char *translated_message = translate(this->u, message); + const char *translated_message = translate(this->nc, message); va_start(args, message); vsnprintf(buf, sizeof(buf), translated_message, args); @@ -102,7 +103,7 @@ void CommandSource::Reply(const char *message, ...) void CommandSource::Reply(const Anope::string &message) { - const char *translated_message = translate(this->u, message.c_str()); + const char *translated_message = translate(this->nc, message.c_str()); sepstream sep(translated_message, '\n'); Anope::string tok; diff --git a/src/logger.cpp b/src/logger.cpp index d18b0731e..0624509d2 100644 --- a/src/logger.cpp +++ b/src/logger.cpp @@ -83,7 +83,7 @@ Log::Log(LogType type, const Anope::string &category, const BotInfo *b) : bi(b), this->Sources.push_back(bi->nick); } -Log::Log(LogType type, const CommandSource &source, Command *_c, const ChannelInfo *_ci) : u(source.GetUser()), nc(source.nc), c(_c), chan(NULL), ci(_ci), s(NULL), Type(type) +Log::Log(LogType type, const CommandSource &source, Command *_c, const ChannelInfo *_ci) : nick(source.GetNick()), u(source.GetUser()), nc(source.nc), c(_c), chan(NULL), ci(_ci), s(NULL), Type(type) { if (!c) throw CoreException("Invalid pointers passed to Log::Log"); @@ -210,7 +210,7 @@ Anope::string Log::BuildPrefix() const } case LOG_COMMAND: { - if (!this->c || !(this->u || this->nc)) + if (!this->c) break; buffer += "COMMAND: "; size_t sl = this->c->name.find('/'); @@ -219,6 +219,8 @@ Anope::string Log::BuildPrefix() const buffer += this->u->GetMask() + " used " + cname + " "; else if (this->nc) buffer += this->nc->display + " used " + cname + " "; + else + buffer += this->nick + " used " + cname + " "; if (this->ci) buffer += "on " + this->ci->name + " "; break; diff --git a/src/misc.cpp b/src/misc.cpp index 41f1370a1..ee091c249 100644 --- a/src/misc.cpp +++ b/src/misc.cpp @@ -758,9 +758,9 @@ void Anope::Unhex(const Anope::string &src, Anope::string &dest) { size_t len = src.length(); Anope::string rv; - for (size_t i = 0; i < len; i += 2) + for (size_t i = 0; i + 1 < len; i += 2) { - char h = src[i], l = src[i + 1]; + char h = std::tolower(src[i]), l = std::tolower(src[i + 1]); unsigned char byte = (h >= 'a' ? h - 'a' + 10 : h - '0') << 4; byte += (l >= 'a' ? l - 'a' + 10 : l - '0'); rv += byte; @@ -768,17 +768,12 @@ void Anope::Unhex(const Anope::string &src, Anope::string &dest) dest = rv; } -void Anope::Unhex(const Anope::string &src, char *dest) +void Anope::Unhex(const Anope::string &src, char *dest, size_t sz) { - size_t len = src.length(), destpos = 0; - for (size_t i = 0; i < len; i += 2) - { - char h = src[i], l = src[i + 1]; - unsigned char byte = (h >= 'a' ? h - 'a' + 10 : h - '0') << 4; - byte += (l >= 'a' ? l - 'a' + 10 : l - '0'); - dest[destpos++] = byte; - } - dest[destpos] = 0; + Anope::string d; + Anope::Unhex(src, d); + + strncpy(dest, d.c_str(), sz); } int Anope::LastErrorCode() diff --git a/src/socket_clients.cpp b/src/socket_clients.cpp index 847981510..4d3a1e3ee 100644 --- a/src/socket_clients.cpp +++ b/src/socket_clients.cpp @@ -60,7 +60,7 @@ void ConnectionSocket::OnError(const Anope::string &) { } -ClientSocket::ClientSocket(ListenSocket *ls, const sockaddrs &addr) : Socket(), LS(ls), clientaddr(addr) +ClientSocket::ClientSocket(ListenSocket *ls, const sockaddrs &addr) : LS(ls), clientaddr(addr) { } diff --git a/src/socket_transport.cpp b/src/socket_transport.cpp index b524c6a24..30af95226 100644 --- a/src/socket_transport.cpp +++ b/src/socket_transport.cpp @@ -56,7 +56,7 @@ bool BufferedSocket::ProcessRead() while (stream.GetToken(tbuf)) { tbuf.trim(); - if (!tbuf.empty() && !Read(tbuf)) + if (!Read(tbuf)) return false; } @@ -80,6 +80,12 @@ bool BufferedSocket::Read(const Anope::string &buf) return false; } +void BufferedSocket::Write(const char *buffer, size_t l) +{ + this->WriteBuffer += buffer + Anope::string("\r\n"); + SocketEngine::MarkWritable(this); +} + void BufferedSocket::Write(const char *message, ...) { va_list vi; @@ -89,17 +95,15 @@ void BufferedSocket::Write(const char *message, ...) return; va_start(vi, message); - vsnprintf(tbuffer, sizeof(tbuffer), message, vi); + int len = vsnprintf(tbuffer, sizeof(tbuffer), message, vi); va_end(vi); - Anope::string sbuf = tbuffer; - Write(sbuf); + this->Write(tbuffer, std::min(len, static_cast(sizeof(tbuffer)))); } void BufferedSocket::Write(const Anope::string &message) { - this->WriteBuffer += message + "\r\n"; - SocketEngine::MarkWritable(this); + this->Write(message.c_str(), message.length()); } int BufferedSocket::ReadBufferLen() const @@ -115,14 +119,14 @@ int BufferedSocket::WriteBufferLen() const BinarySocket::DataBlock::DataBlock(const char *b, size_t l) { - this->buf = new char[l]; + this->orig = this->buf = new char[l]; memcpy(this->buf, b, l); this->len = l; } BinarySocket::DataBlock::~DataBlock() { - delete [] this->buf; + delete [] this->orig; } BinarySocket::BinarySocket() @@ -180,6 +184,26 @@ void BinarySocket::Write(const char *buffer, size_t l) SocketEngine::MarkWritable(this); } +void BinarySocket::Write(const char *message, ...) +{ + va_list vi; + char tbuffer[BUFSIZE]; + + if (!message) + return; + + va_start(vi, message); + int len = vsnprintf(tbuffer, sizeof(tbuffer), message, vi); + va_end(vi); + + this->Write(tbuffer, std::min(len, static_cast(sizeof(tbuffer)))); +} + +void BinarySocket::Write(const Anope::string &message) +{ + this->Write(message.c_str(), message.length()); +} + bool BinarySocket::Read(const char *buffer, size_t l) { return true;