]> jfr.im git - irc/rizon/znc.git/commitdiff
Added the Q module which allows the user to auth with QuakeNet's Q bot.
authorkroimon <redacted>
Sat, 27 Sep 2008 09:52:00 +0000 (09:52 +0000)
committerkroimon <redacted>
Sat, 27 Sep 2008 09:52:00 +0000 (09:52 +0000)
git-svn-id: https://znc.svn.sourceforge.net/svnroot/znc/trunk@1217 726aef4b-f618-498e-8847-2d620e286838

AUTHORS
modules/q.cpp [new file with mode: 0755]

diff --git a/AUTHORS b/AUTHORS
index c23b032b7d4d65a6de96d07f18953ea8ad06de9b..9841c216acacc6a83d9405178572a35e8b8fd0e3 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -49,4 +49,5 @@ SilverLeo - Several patches
 
 kroimon <znc@kroimon.de> - Patches, Documentation
        Some enhancements, some cleanups, some other stuff ;)
+       Modules: q
        German and english wiki maintenance
diff --git a/modules/q.cpp b/modules/q.cpp
new file mode 100755 (executable)
index 0000000..e64a485
--- /dev/null
@@ -0,0 +1,486 @@
+/*
+ * Copyright (C) 2008  See the AUTHORS file for details.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 as published
+ * by the Free Software Foundation.
+ */
+
+#include "Modules.h"
+#include "User.h"
+#include "IRCSock.h"
+#include "Nick.h"
+#include "Chan.h"
+
+#ifndef Q_DEBUG_COMMUNICATION
+       #define Q_DEBUG_COMMUNICATION 0
+#endif
+
+class CQModule : public CModule {
+public:
+       MODCONSTRUCTOR(CQModule) {}
+       virtual ~CQModule() {}
+
+       virtual bool OnLoad(const CString& sArgs, CString& sMessage) {
+               if (!sArgs.empty()) {
+                       SetUsername(sArgs.Token(0));
+                       SetPassword(sArgs.Token(1));
+               } else {
+                       m_sUsername = GetNV("Username");
+                       m_sPassword = GetNV("Password");
+               }
+
+               CString sTmp;
+               m_bUseHiddenHost = (sTmp = GetNV("UseHiddenHost")).empty() ? true : sTmp.ToBool();
+               m_bUseChallenge  = (sTmp = GetNV("UseChallenge")).empty()  ? true : sTmp.ToBool();
+               m_bRequestPerms  = GetNV("RequestPerms").ToBool();
+
+               OnIRCDisconnected(); // reset module's state
+
+               if (IsIRCConnected()) {
+                       // check for usermode +x if we are already connected
+                       set<unsigned char> scUserModes = GetUser()->GetIRCSock()->GetUserModes();
+                       if (scUserModes.find('x') != scUserModes.end())
+                               m_bCloaked = true;
+
+                       OnIRCConnected();
+               }
+
+               return true;
+       }
+
+       virtual void OnIRCDisconnected() {
+               m_bCloaked = false;
+               m_bAuthed  = false;
+               m_bRequestedWhoami    = false;
+               m_bRequestedChallenge = false;
+               m_bCatchResponse = false;
+       }
+
+       virtual void OnIRCConnected() {
+               if (m_bUseHiddenHost)
+                       Cloak();
+               WhoAmI();
+       }
+
+       virtual void OnModCommand(const CString& sLine) {
+               CString sCommand = sLine.Token(0).AsLower();
+
+               if (sCommand == "help") {
+                       PutModule("The following commands are available:");
+                       CTable Table;
+                       Table.AddColumn("Command");
+                       Table.AddColumn("Description");
+                       Table.AddRow();
+                       Table.SetCell("Command", "Auth [<username> <password>]");
+                       Table.SetCell("Description", "Tries to authenticate you with Q. Both parameters are optional.");
+                       Table.AddRow();
+                       Table.SetCell("Command", "Cloak");
+                       Table.SetCell("Description", "Tries to set usermode +x to hide your real hostname.");
+                       Table.AddRow();
+                       Table.SetCell("Command", "Status");
+                       Table.SetCell("Description", "Prints the current status of the module.");
+                       Table.AddRow();
+                       Table.SetCell("Command", "Update");
+                       Table.SetCell("Description", "Re-requests the current user information from Q.");
+                       Table.AddRow();
+                       Table.SetCell("Command", "Set <setting> <value>");
+                       Table.SetCell("Description", "Changes the value of the given setting. See the list of settings below.");
+                       Table.AddRow();
+                       Table.SetCell("Command", "Get");
+                       Table.SetCell("Description", "Prints out the current configuration. See the list of settings below.");
+                       PutModule(Table);
+
+                       PutModule("The following settings are available:");
+                       CTable Table2;
+                       Table2.AddColumn("Setting");
+                       Table2.AddColumn("Type");
+                       Table2.AddColumn("Description");
+                       Table2.AddRow();
+                       Table2.SetCell("Setting", "Username");
+                       Table2.SetCell("Type", "String");
+                       Table2.SetCell("Description", "Your Q username.");
+                       Table2.AddRow();
+                       Table2.SetCell("Setting", "Password");
+                       Table2.SetCell("Type", "String");
+                       Table2.SetCell("Description", "Your Q password.");
+                       Table2.AddRow();
+                       Table2.SetCell("Setting", "UseHiddenHost");
+                       Table2.SetCell("Type", "Boolean");
+                       Table2.SetCell("Description", "Whether to cloak your hostname (+x) automatically on connect.");
+                       Table2.AddRow();
+                       Table2.SetCell("Setting", "UseChallenge");
+                       Table2.SetCell("Type", "Boolean");
+                       Table2.SetCell("Description", "Whether to use the CHALLENGEAUTH mechanism to avoid sending passwords in cleartext.");
+                       Table2.AddRow();
+                       Table2.SetCell("Setting", "RequestPerms");
+                       Table2.SetCell("Type", "Boolean");
+                       Table2.SetCell("Description", "Whether to request voice/op from Q on join/devoice/deop.");
+                       PutModule(Table2);
+
+                       PutModule("This module takes 2 optional parameters: <username> <password>");
+                       PutModule("Module settings are stored between restarts.");
+
+               } else if (sCommand == "set") {
+                       CString sSetting = sLine.Token(1).AsLower();
+                       CString sValue   = sLine.Token(2);
+                       if (sSetting.empty() || sValue.empty()) {
+                               PutModule("Syntax: Set <setting> <value>");
+                       } else if (sSetting == "username") {
+                               SetUsername(sValue);
+                               PutModule("Username set");
+                       } else if (sSetting == "password") {
+                               SetPassword(sValue);
+                               PutModule("Password set");
+                       } else if (sSetting == "usehiddenhost") {
+                               SetUseHiddenHost(sValue.ToBool());
+                               PutModule("UseHiddenHost set");
+                               if (m_bUseHiddenHost && IsIRCConnected())
+                                       Cloak();
+                       } else if (sSetting == "usechallenge") {
+                               SetUseChallenge(sValue.ToBool());
+                               PutModule("UseChallenge set");
+                       } else if (sSetting == "requestperms") {
+                               SetRequestPerms(sValue.ToBool());
+                               PutModule("RequestPerms set");
+                       } else
+                               PutModule("Unknown setting: " + sSetting);
+
+               } else if (sCommand == "get" || sCommand == "list") {
+                       CTable Table;
+                       Table.AddColumn("Setting");
+                       Table.AddColumn("Value");
+                       Table.AddRow();
+                       Table.SetCell("Setting", "Username");
+                       Table.SetCell("Value", m_sUsername);
+                       Table.AddRow();
+                       Table.SetCell("Setting", "Password");
+                       Table.SetCell("Value", "*****"); // m_sPassword
+                       Table.AddRow();
+                       Table.SetCell("Setting", "UseHiddenHost");
+                       Table.SetCell("Value", CString(m_bUseHiddenHost ? "true" : "false"));
+                       Table.AddRow();
+                       Table.SetCell("Setting", "UseChallenge");
+                       Table.SetCell("Value", CString(m_bUseChallenge ? "true" : "false"));
+                       Table.AddRow();
+                       Table.SetCell("Setting", "RequestPerms");
+                       Table.SetCell("Value", CString(m_bRequestPerms ? "true" : "false"));
+                       PutModule(Table);
+
+               } else if (sCommand == "status") {
+                       PutModule("Connected: " + CString(IsIRCConnected() ? "yes" : "no"));
+                       PutModule("Cloaked: "   + CString(m_bCloaked       ? "yes" : "no"));
+                       PutModule("Authed: "    + CString(m_bAuthed        ? "yes" : "no"));
+
+               } else {
+                       // The following commands require an IRC connection.
+                       if (!IsIRCConnected()) {
+                               PutModule("Error: You are not connected to IRC.");
+                               return;
+                       }
+
+                       if (sCommand == "cloak") {
+                               if (!m_bCloaked)
+                                       Cloak();
+                               else
+                                       PutModule("Error: You are already cloaked!");
+
+                       } else if (sCommand == "auth") {
+                               if (!m_bAuthed)
+                                       Auth(sLine.Token(1), sLine.Token(2));
+                               else
+                                       PutModule("Error: You are already authed!");
+
+                       } else if (sCommand == "update") {
+                               WhoAmI();
+                               PutModule("Update requested.");
+
+                       } else {
+                               PutModule("Unknown command. Try 'help'.");
+                       }
+               }
+       }
+
+       virtual EModRet OnRaw(CString& sLine) {
+               // use OnRaw because OnUserMode is not defined (yet?)
+               if (sLine.Token(1) == "396" && sLine.Token(3).find("users.quakenet.org") != CString::npos) {
+                       m_bCloaked = true;
+                       PutModule("Cloak successful: Your hostname is now cloaked.");
+               }
+               return CONTINUE;
+       }
+
+       virtual EModRet OnPrivMsg(CNick& Nick, CString& sMessage) {
+               return HandleMessage(Nick, sMessage);
+       }
+
+       virtual EModRet OnPrivNotice(CNick& Nick, CString& sMessage) {
+               return HandleMessage(Nick, sMessage);
+       }
+
+       virtual void OnJoin(const CNick& Nick, CChan& Channel) {
+               if (m_bRequestPerms && IsSelf(Nick))
+                       HandleNeed(Channel, "ov");
+       }
+
+       virtual void OnDeop(const CNick& OpNick, const CNick& Nick, CChan& Channel, bool bNoChange) {
+               if (m_bRequestPerms && IsSelf(Nick) && !IsSelf(OpNick))
+                       HandleNeed(Channel, "o");
+       }
+
+       virtual void OnDevoice(const CNick& OpNick, const CNick& Nick, CChan& Channel, bool bNoChange) {
+               if (m_bRequestPerms && IsSelf(Nick) && !IsSelf(OpNick))
+                       HandleNeed(Channel, "v");
+       }
+
+
+private:
+       bool m_bCloaked;
+       bool m_bAuthed;
+       bool m_bRequestedWhoami;
+       bool m_bRequestedChallenge;
+       bool m_bCatchResponse;
+       MCString m_msChanModes;
+
+       void PutQ(const CString& sMessage) {
+               PutIRC("PRIVMSG Q@CServe.quakenet.org :" + sMessage);
+#if Q_DEBUG_COMMUNICATION
+               PutModule("[ZNC --> Q] " + sMessage);
+#endif
+       }
+
+       void Cloak() {
+               if (m_bCloaked)
+                       return;
+
+               PutModule("Cloak: Trying to cloak your hostname, setting +x...");
+               PutIRC("MODE " + GetUser()->GetIRCSock()->GetNick() + " +x");
+       }
+
+       void WhoAmI() {
+               m_bRequestedWhoami = true;
+               PutQ("WHOAMI");
+       }
+
+       void Auth(const CString& sUsername = "", const CString& sPassword = "") {
+               if (m_bAuthed)
+                       return;
+
+               if (!sUsername.empty())
+                       SetUsername(sUsername);
+               if (!sPassword.empty())
+                       SetPassword(sPassword);
+
+               if (m_sUsername.empty() || m_sPassword.empty()) {
+                       PutModule("You have to set a username and password to use this module! See 'help' for details.");
+                       return;
+               }
+
+               if (m_bUseChallenge) {
+                       PutModule("Auth: Requesting CHALLENGE...");
+                       m_bRequestedChallenge = true;
+                       PutQ("CHALLENGE");
+               } else {
+                       PutModule("Auth: Sending AUTH request...");
+                       PutQ("AUTH " + m_sUsername + " " + m_sPassword);
+               }
+       }
+
+       void ChallengeAuth(CString sChallenge) {
+               if (m_bAuthed)
+                       return;
+
+               CString sUsername     = m_sUsername.AsLower()
+                                                  .Replace_n("[",  "{")
+                                                  .Replace_n("]",  "}")
+                                                  .Replace_n("\\", "|");
+               CString sPasswordHash = m_sPassword.Left(10).MD5();
+               CString sKey          = CString(sUsername + ":" + sPasswordHash).MD5();
+               CString sResponse     = HMAC_MD5(sKey, sChallenge);
+
+               PutModule("Auth: Received challenge, sending CHALLENGEAUTH request...");
+               PutQ("CHALLENGEAUTH " + m_sUsername + " " + sResponse + " HMAC-MD5");
+       }
+
+       EModRet HandleMessage(const CNick& Nick, CString sMessage) {
+               if (Nick.GetNick().CaseCmp("Q") != 0 || Nick.GetHost().CaseCmp("CServe.quakenet.org") != 0)
+                       return CONTINUE;
+
+               sMessage.Trim();
+
+#if Q_DEBUG_COMMUNICATION
+               PutModule("[ZNC <-- Q] " + sMessage);
+#endif
+
+               // WHOAMI
+               if (sMessage.find("WHOAMI is only available to authed users") != CString::npos) {
+                       m_bAuthed = false;
+                       Auth();
+                       m_bCatchResponse = m_bRequestedWhoami;
+               }
+               else if (sMessage.find("Information for user") != CString::npos) {
+                       m_bAuthed = true;
+                       m_msChanModes.clear();
+                       m_bCatchResponse = m_bRequestedWhoami;
+                       m_bRequestedWhoami = true;
+               }
+               else if (m_bRequestedWhoami && sMessage.WildCmp("#*")) {
+                       CString sChannel = sMessage.Token(0);
+                       CString sFlags   = sMessage.Token(1, true).Trim_n().TrimLeft_n("+");
+                       m_msChanModes[sChannel] = sFlags;
+               }
+               else if (m_bRequestedWhoami && m_bCatchResponse
+                               && (sMessage.CaseCmp("End of list.") == 0
+                               ||  sMessage.CaseCmp("account, or HELLO to create an account.") == 0)) {
+                       m_bRequestedWhoami = m_bCatchResponse = false;
+                       return HALT;
+               }
+
+               // AUTH
+               else if (sMessage.CaseCmp("Username or password incorrect.") == 0) {
+                       m_bAuthed = false;
+                       PutModule("Auth failed: " + sMessage);
+                       return HALT;
+               }
+               else if (sMessage.WildCmp("You are now logged in as *.")) {
+                       m_bAuthed = true;
+                       PutModule("Auth successful: " + sMessage);
+                       WhoAmI();
+                       return HALT;
+               }
+               else if (m_bRequestedChallenge && sMessage.Token(0).CaseCmp("CHALLENGE") == 0) {
+                       m_bRequestedChallenge = false;
+                       if (sMessage.find("not available once you have authed") != CString::npos) {
+                               m_bAuthed = true;
+                       } else {
+                               if (sMessage.find("HMAC-MD5") != CString::npos) {
+                                       ChallengeAuth(sMessage.Token(1));
+                               } else {
+                                       PutModule("Auth failed: Q does not support HMAC-MD5 for CHALLENGEAUTH, falling back to standard AUTH.");
+                                       SetUseChallenge(false);
+                                       Auth();
+                               }
+                       }
+                       return HALT;
+               }
+
+               // prevent buffering of Q's responses
+               return !m_bCatchResponse && GetUser()->IsUserAttached() ? CONTINUE : HALT;
+       }
+
+       void HandleNeed(const CChan& Channel, const CString& sPerms) {
+               MCString::iterator it = m_msChanModes.find(Channel.GetName());
+               if (it == m_msChanModes.end())
+                       return;
+               CString sModes = it->second;
+
+               bool bMaster = (sModes.find("m") != CString::npos) || (sModes.find("n") != CString::npos);
+
+               if (sPerms.find("o") != CString::npos) {
+                       bool bOp     = (sModes.find("o") != CString::npos);
+                       bool bAutoOp = (sModes.find("a") != CString::npos);
+                       if (bMaster || bOp) {
+                               if (!bAutoOp) {
+                                       PutModule("RequestPerms: Requesting op on " + Channel.GetName());
+                                       PutQ("OP " + Channel.GetName());
+                               }
+                               return;
+                       }
+               }
+
+               if (sPerms.find("v") != CString::npos) {
+                       bool bVoice     = (sModes.find("v") != CString::npos);
+                       bool bAutoVoice = (sModes.find("g") != CString::npos);
+                       if (bMaster || bVoice) {
+                               if (!bAutoVoice) {
+                                       PutModule("RequestPerms: Requesting voice on " + Channel.GetName());
+                                       PutQ("VOICE " + Channel.GetName());
+                               }
+                               return;
+                       }
+               }
+       }
+
+
+/* Utility Functions */
+       bool IsIRCConnected() {
+               CIRCSock* pIRCSock = GetUser()->GetIRCSock();
+               return pIRCSock && pIRCSock->IsAuthed();
+       }
+
+       bool IsSelf(const CNick& Nick) {
+               return Nick.GetNick().CaseCmp(m_pUser->GetCurNick()) == 0;
+       }
+
+       bool PackHex(const CString& sHex, CString& sPackedHex) {
+               if (sHex.length() % 2)
+                       return false;
+
+               sPackedHex.clear();
+
+               unsigned int len = sHex.length() / 2;
+               for (unsigned int i = 0; i < len; i++) {
+                       unsigned int value;
+                       int n = sscanf(&sHex[i*2], "%02x", &value);
+                       if (n != 1 || value > 0xff)
+                               return false;
+                       sPackedHex += (unsigned char) value;
+               }
+
+               return true;
+       }
+
+       CString HMAC_MD5(const CString& sKey, const CString& sData) {
+               CString sRealKey;
+               if (sKey.length() > 64)
+                       PackHex(sKey.MD5(), sRealKey);
+               else
+                       sRealKey = sKey;
+
+               CString sOuterKey, sInnerKey;
+               unsigned int iKeyLength = sRealKey.length();
+               for (unsigned int i = 0; i < 64; i++) {
+                       int r = (i < iKeyLength ? sRealKey[i] : 0);
+                       sOuterKey += r ^ 0x5c;
+                       sInnerKey += r ^ 0x36;
+               }
+
+               CString sInnerHash;
+               PackHex(CString(sInnerKey + sData).MD5(), sInnerHash);
+               return CString(sOuterKey + sInnerHash).MD5();
+       }
+
+/* Settings */
+       CString m_sUsername;
+       CString m_sPassword;
+       bool m_bUseHiddenHost;
+       bool m_bUseChallenge;
+       bool m_bRequestPerms;
+
+       void SetUsername(const CString& sUsername) {
+               m_sUsername = sUsername;
+               SetNV("Username", sUsername);
+       }
+
+       void SetPassword(const CString& sPassword) {
+               m_sPassword = sPassword;
+               SetNV("Password", sPassword);
+       }
+
+       void SetUseHiddenHost(const bool bUseHiddenHost) {
+               m_bUseHiddenHost = bUseHiddenHost;
+               SetNV("UseHiddenHost", CString(bUseHiddenHost ? "true" : "false"));
+       }
+
+       void SetUseChallenge(const bool bUseChallenge) {
+               m_bUseChallenge = bUseChallenge;
+               SetNV("UseChallenge", CString(bUseChallenge ? "true" : "false"));
+       }
+
+       void SetRequestPerms(const bool bRequestPerms) {
+               m_bRequestPerms = bRequestPerms;
+               SetNV("RequestPerms", CString(bRequestPerms ? "true" : "false"));
+       }
+};
+
+MODULEDEFS(CQModule, "Auths you with QuakeNet's Q bot.")