diff --git a/AUTHORS b/AUTHORS index c23b032b..9841c216 100644 --- a/AUTHORS +++ b/AUTHORS @@ -49,4 +49,5 @@ SilverLeo - Several patches kroimon - 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 index 00000000..e64a485d --- /dev/null +++ b/modules/q.cpp @@ -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 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 [ ]"); + 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 "); + 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: "); + 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 "); + } 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.")