1 /*
2  * Copyright (C) 2004-2020 ZNC, see the NOTICE file for details.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #include <znc/IRCNetwork.h>
18 #include <znc/IRCSock.h>
19 #include <algorithm>
20 
21 #define NV_REQUIRE_AUTH "require_auth"
22 #define NV_MECHANISMS "mechanisms"
23 
24 class Mechanisms : public VCString {
25   public:
SetIndex(unsigned int uiIndex)26     void SetIndex(unsigned int uiIndex) { m_uiIndex = uiIndex; }
27 
GetIndex() const28     unsigned int GetIndex() const { return m_uiIndex; }
29 
HasNext() const30     bool HasNext() const { return size() > (m_uiIndex + 1); }
31 
IncrementIndex()32     void IncrementIndex() { m_uiIndex++; }
33 
GetCurrent() const34     CString GetCurrent() const { return at(m_uiIndex); }
35 
GetNext() const36     CString GetNext() const {
37         if (HasNext()) {
38             return at(m_uiIndex + 1);
39         }
40 
41         return "";
42     }
43 
44   private:
45     unsigned int m_uiIndex = 0;
46 };
47 
48 class CSASLMod : public CModule {
49     const struct {
50         const char* szName;
51         CDelayedTranslation sDescription;
52         bool bDefault;
53     } SupportedMechanisms[2] = {
54         {"EXTERNAL", t_d("TLS certificate, for use with the *cert module"),
55          true},
56         {"PLAIN", t_d("Plain text negotiation, this should work always if the "
57                       "network supports SASL"),
58          true}};
59 
60   public:
MODCONSTRUCTOR(CSASLMod)61     MODCONSTRUCTOR(CSASLMod) {
62         AddCommand("Help", t_d("search"), t_d("Generate this output"),
63                    [=](const CString& sLine) { PrintHelp(sLine); });
64         AddCommand("Set", t_d("[<username> [<password>]]"),
65                    t_d("Set username and password for the mechanisms that need "
66                        "them. Password is optional. Without parameters, "
67                        "returns information about current settings."),
68                    [=](const CString& sLine) { Set(sLine); });
69         AddCommand("Mechanism", t_d("[mechanism[ ...]]"),
70                    t_d("Set the mechanisms to be attempted (in order)"),
71                    [=](const CString& sLine) { SetMechanismCommand(sLine); });
72         AddCommand("RequireAuth", t_d("[yes|no]"),
73                    t_d("Don't connect unless SASL authentication succeeds"),
74                    [=](const CString& sLine) { RequireAuthCommand(sLine); });
75         AddCommand("Verbose", "yes|no", "Set verbosity level, useful to debug",
76                    [&](const CString& sLine) {
77                        m_bVerbose = sLine.Token(1, true).ToBool();
78                        PutModule("Verbose: " + CString(m_bVerbose));
79                    });
80 
81         m_bAuthenticated = false;
82     }
83 
PrintHelp(const CString & sLine)84     void PrintHelp(const CString& sLine) {
85         HandleHelpCommand(sLine);
86 
87         CTable Mechanisms;
88         Mechanisms.AddColumn(t_s("Mechanism"));
89         Mechanisms.AddColumn(t_s("Description"));
90         Mechanisms.SetStyle(CTable::ListStyle);
91 
92         for (const auto& it : SupportedMechanisms) {
93             Mechanisms.AddRow();
94             Mechanisms.SetCell(t_s("Mechanism"), it.szName);
95             Mechanisms.SetCell(t_s("Description"), it.sDescription.Resolve());
96         }
97 
98         PutModule("");
99         PutModule(t_s("The following mechanisms are available:"));
100         PutModule(Mechanisms);
101     }
102 
Set(const CString & sLine)103     void Set(const CString& sLine) {
104         if (sLine.Token(1).empty()) {
105             CString sUsername = GetNV("username");
106             CString sPassword = GetNV("password");
107 
108             if (sUsername.empty()) {
109                 PutModule(t_s("Username is currently not set"));
110             } else {
111                 PutModule(t_f("Username is currently set to '{1}'")(sUsername));
112             }
113             if (sPassword.empty()) {
114                 PutModule(t_s("Password was not supplied"));
115             } else {
116                 PutModule(t_s("Password was supplied"));
117             }
118             return;
119         }
120 
121         SetNV("username", sLine.Token(1));
122         SetNV("password", sLine.Token(2));
123 
124         PutModule(t_f("Username has been set to [{1}]")(GetNV("username")));
125         PutModule(t_f("Password has been set to [{1}]")(GetNV("password")));
126     }
127 
SetMechanismCommand(const CString & sLine)128     void SetMechanismCommand(const CString& sLine) {
129         CString sMechanisms = sLine.Token(1, true).AsUpper();
130 
131         if (!sMechanisms.empty()) {
132             VCString vsMechanisms;
133             sMechanisms.Split(" ", vsMechanisms);
134 
135             for (const CString& sMechanism : vsMechanisms) {
136                 if (!SupportsMechanism(sMechanism)) {
137                     PutModule("Unsupported mechanism: " + sMechanism);
138                     return;
139                 }
140             }
141 
142             SetNV(NV_MECHANISMS, sMechanisms);
143         }
144 
145         PutModule(t_f("Current mechanisms set: {1}")(GetMechanismsString()));
146     }
147 
RequireAuthCommand(const CString & sLine)148     void RequireAuthCommand(const CString& sLine) {
149         if (!sLine.Token(1).empty()) {
150             SetNV(NV_REQUIRE_AUTH, sLine.Token(1));
151         }
152 
153         if (GetNV(NV_REQUIRE_AUTH).ToBool()) {
154             PutModule(t_s("We require SASL negotiation to connect"));
155         } else {
156             PutModule(t_s("We will connect even if SASL fails"));
157         }
158     }
159 
SupportsMechanism(const CString & sMechanism) const160     bool SupportsMechanism(const CString& sMechanism) const {
161         for (const auto& it : SupportedMechanisms) {
162             if (sMechanism.Equals(it.szName)) {
163                 return true;
164             }
165         }
166 
167         return false;
168     }
169 
GetMechanismsString() const170     CString GetMechanismsString() const {
171         if (GetNV(NV_MECHANISMS).empty()) {
172             CString sDefaults = "";
173 
174             for (const auto& it : SupportedMechanisms) {
175                 if (it.bDefault) {
176                     if (!sDefaults.empty()) {
177                         sDefaults += " ";
178                     }
179 
180                     sDefaults += it.szName;
181                 }
182             }
183 
184             return sDefaults;
185         }
186 
187         return GetNV(NV_MECHANISMS);
188     }
189 
CheckRequireAuth()190     void CheckRequireAuth() {
191         if (!m_bAuthenticated && GetNV(NV_REQUIRE_AUTH).ToBool()) {
192             GetNetwork()->SetIRCConnectEnabled(false);
193             PutModule(t_s("Disabling network, we require authentication."));
194             PutModule(t_s("Use 'RequireAuth no' to disable."));
195         }
196     }
197 
Authenticate(const CString & sLine)198     void Authenticate(const CString& sLine) {
199         /* Send blank authenticate for other mechanisms (like EXTERNAL). */
200         CString sAuthLine;
201         if (m_Mechanisms.GetCurrent().Equals("PLAIN") && sLine.Equals("+")) {
202             sAuthLine = GetNV("username") + '\0' + GetNV("username") +
203                                 '\0' + GetNV("password");
204             sAuthLine.Base64Encode();
205         }
206 
207         /* The spec requires authentication data to be sent in chunks */
208         const size_t chunkSize = 400;
209         for (size_t offset = 0; offset < sAuthLine.length(); offset += chunkSize) {
210             size_t size = std::min(chunkSize, sAuthLine.length() - offset);
211             PutIRC("AUTHENTICATE " + sAuthLine.substr(offset, size));
212         }
213         if (sAuthLine.length() % chunkSize == 0) {
214             /* Signal end if we have a multiple of the chunk size */
215             PutIRC("AUTHENTICATE +");
216         }
217     }
218 
OnServerCapAvailable(const CString & sCap)219     bool OnServerCapAvailable(const CString& sCap) override {
220         return sCap.Equals("sasl");
221     }
222 
OnServerCapResult(const CString & sCap,bool bSuccess)223     void OnServerCapResult(const CString& sCap, bool bSuccess) override {
224         if (sCap.Equals("sasl")) {
225             if (bSuccess) {
226                 GetMechanismsString().Split(" ", m_Mechanisms);
227 
228                 if (m_Mechanisms.empty()) {
229                     CheckRequireAuth();
230                     return;
231                 }
232 
233                 GetNetwork()->GetIRCSock()->PauseCap();
234 
235                 m_Mechanisms.SetIndex(0);
236                 PutIRC("AUTHENTICATE " + m_Mechanisms.GetCurrent());
237             } else {
238                 CheckRequireAuth();
239             }
240         }
241     }
242 
OnRawMessage(CMessage & msg)243     EModRet OnRawMessage(CMessage& msg) override {
244         if (msg.GetCommand().Equals("AUTHENTICATE")) {
245             Authenticate(msg.GetParam(0));
246             return HALT;
247         }
248         return CONTINUE;
249     }
250 
OnNumericMessage(CNumericMessage & msg)251     EModRet OnNumericMessage(CNumericMessage& msg) override {
252         if (msg.GetCode() == 903) {
253             /* SASL success! */
254             if (m_bVerbose) {
255                 PutModule(
256                     t_f("{1} mechanism succeeded.")(m_Mechanisms.GetCurrent()));
257             }
258             GetNetwork()->GetIRCSock()->ResumeCap();
259             m_bAuthenticated = true;
260             DEBUG("sasl: Authenticated with mechanism ["
261                   << m_Mechanisms.GetCurrent() << "]");
262         } else if (msg.GetCode() == 904 ||
263                    msg.GetCode() == 905) {
264             DEBUG("sasl: Mechanism [" << m_Mechanisms.GetCurrent()
265                                       << "] failed.");
266             if (m_bVerbose) {
267                 PutModule(
268                     t_f("{1} mechanism failed.")(m_Mechanisms.GetCurrent()));
269             }
270 
271             if (m_Mechanisms.HasNext()) {
272                 m_Mechanisms.IncrementIndex();
273                 PutIRC("AUTHENTICATE " + m_Mechanisms.GetCurrent());
274             } else {
275                 CheckRequireAuth();
276                 GetNetwork()->GetIRCSock()->ResumeCap();
277             }
278         } else if (msg.GetCode() == 906) {
279             /* CAP wasn't paused? */
280             DEBUG("sasl: Reached 906.");
281             CheckRequireAuth();
282         } else if (msg.GetCode() == 907) {
283             m_bAuthenticated = true;
284             GetNetwork()->GetIRCSock()->ResumeCap();
285             DEBUG("sasl: Received 907 -- We are already registered");
286         } else {
287             return CONTINUE;
288         }
289         return HALT;
290     }
291 
OnIRCConnected()292     void OnIRCConnected() override {
293         /* Just incase something slipped through, perhaps the server doesn't
294          * respond to our CAP negotiation. */
295 
296         CheckRequireAuth();
297     }
298 
OnIRCDisconnected()299     void OnIRCDisconnected() override { m_bAuthenticated = false; }
300 
GetWebMenuTitle()301     CString GetWebMenuTitle() override { return t_s("SASL"); }
302 
OnWebRequest(CWebSock & WebSock,const CString & sPageName,CTemplate & Tmpl)303     bool OnWebRequest(CWebSock& WebSock, const CString& sPageName,
304                       CTemplate& Tmpl) override {
305         if (sPageName != "index") {
306             // only accept requests to index
307             return false;
308         }
309 
310         if (WebSock.IsPost()) {
311             SetNV("username", WebSock.GetParam("username"));
312             CString sPassword = WebSock.GetParam("password");
313             if (!sPassword.empty()) {
314                 SetNV("password", sPassword);
315             }
316             SetNV(NV_REQUIRE_AUTH, WebSock.GetParam("require_auth"));
317             SetNV(NV_MECHANISMS, WebSock.GetParam("mechanisms"));
318         }
319 
320         Tmpl["Username"] = GetNV("username");
321         Tmpl["Password"] = GetNV("password");
322         Tmpl["RequireAuth"] = GetNV(NV_REQUIRE_AUTH);
323         Tmpl["Mechanisms"] = GetMechanismsString();
324 
325         for (const auto& it : SupportedMechanisms) {
326             CTemplate& Row = Tmpl.AddRow("MechanismLoop");
327             CString sName(it.szName);
328             Row["Name"] = sName;
329             Row["Description"] = it.sDescription.Resolve();
330         }
331 
332         return true;
333     }
334 
335   private:
336     Mechanisms m_Mechanisms;
337     bool m_bAuthenticated;
338     bool m_bVerbose = false;
339 };
340 
341 template <>
TModInfo(CModInfo & Info)342 void TModInfo<CSASLMod>(CModInfo& Info) {
343     Info.SetWikiPage("sasl");
344 }
345 
346 NETWORKMODULEDEFS(CSASLMod, t_s("Adds support for sasl authentication "
347                                 "capability to authenticate to an IRC server"))
348