]> git.netwichtig.de Git - user/henk/code/inspircd.git/blob - src/modules/extra/m_ldapauth.cpp
Change Windows libraries to be dynamically linked
[user/henk/code/inspircd.git] / src / modules / extra / m_ldapauth.cpp
1 /*
2  * InspIRCd -- Internet Relay Chat Daemon
3  *
4  *   Copyright (C) 2011 Pierre Carrier <pierre@spotify.com>
5  *   Copyright (C) 2009-2010 Robin Burchell <robin+git@viroteck.net>
6  *   Copyright (C) 2009 Daniel De Graaf <danieldg@inspircd.org>
7  *   Copyright (C) 2008 Pippijn van Steenhoven <pip88nl@gmail.com>
8  *   Copyright (C) 2008 Craig Edwards <craigedwards@brainbox.cc>
9  *   Copyright (C) 2008 Dennis Friis <peavey@inspircd.org>
10  *   Copyright (C) 2007 Carsten Valdemar Munk <carsten.munk+inspircd@gmail.com>
11  *
12  * This file is part of InspIRCd.  InspIRCd is free software: you can
13  * redistribute it and/or modify it under the terms of the GNU General Public
14  * License as published by the Free Software Foundation, version 2.
15  *
16  * This program is distributed in the hope that it will be useful, but WITHOUT
17  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
19  * details.
20  *
21  * You should have received a copy of the GNU General Public License
22  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23  */
24
25
26 #include "inspircd.h"
27 #include "users.h"
28 #include "channels.h"
29 #include "modules.h"
30
31 #include <ldap.h>
32
33 #ifdef _WIN32
34 # pragma comment(lib, "libldap.lib")
35 # pragma comment(lib, "liblber.lib")
36 #endif
37
38 /* $ModDesc: Allow/Deny connections based upon answer from LDAP server */
39 /* $LinkerFlags: -lldap */
40
41 struct RAIILDAPString
42 {
43         char *str;
44
45         RAIILDAPString(char *Str)
46                 : str(Str)
47         {
48         }
49
50         ~RAIILDAPString()
51         {
52                 ldap_memfree(str);
53         }
54
55         operator char*()
56         {
57                 return str;
58         }
59
60         operator std::string()
61         {
62                 return str;
63         }
64 };
65
66 struct RAIILDAPMessage
67 {
68         RAIILDAPMessage()
69         {
70         }
71
72         ~RAIILDAPMessage()
73         {
74                 dealloc();
75         }
76
77         void dealloc()
78         {
79                 ldap_msgfree(msg);
80         }
81
82         operator LDAPMessage*()
83         {
84                 return msg;
85         }
86
87         LDAPMessage **operator &()
88         {
89                 return &msg;
90         }
91
92         LDAPMessage *msg;
93 };
94
95 class ModuleLDAPAuth : public Module
96 {
97         LocalIntExt ldapAuthed;
98         LocalStringExt ldapVhost;
99         std::string base;
100         std::string attribute;
101         std::string ldapserver;
102         std::string allowpattern;
103         std::string killreason;
104         std::string username;
105         std::string password;
106         std::string vhost;
107         std::vector<std::string> whitelistedcidrs;
108         std::vector<std::pair<std::string, std::string> > requiredattributes;
109         int searchscope;
110         bool verbose;
111         bool useusername;
112         LDAP *conn;
113
114 public:
115         ModuleLDAPAuth()
116                 : ldapAuthed("ldapauth", this)
117                 , ldapVhost("ldapauth_vhost", this)
118         {
119                 conn = NULL;
120         }
121
122         void init()
123         {
124                 ServerInstance->Modules->AddService(ldapAuthed);
125                 ServerInstance->Modules->AddService(ldapVhost);
126                 Implementation eventlist[] = { I_OnCheckReady, I_OnRehash,I_OnUserRegister, I_OnUserConnect };
127                 ServerInstance->Modules->Attach(eventlist, this, sizeof(eventlist)/sizeof(Implementation));
128                 OnRehash(NULL);
129         }
130
131         ~ModuleLDAPAuth()
132         {
133                 if (conn)
134                         ldap_unbind_ext(conn, NULL, NULL);
135         }
136
137         void OnRehash(User* user)
138         {
139                 ConfigTag* tag = ServerInstance->Config->ConfValue("ldapauth");
140                 whitelistedcidrs.clear();
141                 requiredattributes.clear();
142
143                 base                    = tag->getString("baserdn");
144                 attribute               = tag->getString("attribute");
145                 ldapserver              = tag->getString("server");
146                 allowpattern    = tag->getString("allowpattern");
147                 killreason              = tag->getString("killreason");
148                 std::string scope       = tag->getString("searchscope");
149                 username                = tag->getString("binddn");
150                 password                = tag->getString("bindauth");
151                 vhost                   = tag->getString("host");
152                 verbose                 = tag->getBool("verbose");              /* Set to true if failed connects should be reported to operators */
153                 useusername             = tag->getBool("userfield");
154
155                 ConfigTagList whitelisttags = ServerInstance->Config->ConfTags("ldapwhitelist");
156
157                 for (ConfigIter i = whitelisttags.first; i != whitelisttags.second; ++i)
158                 {
159                         std::string cidr = i->second->getString("cidr");
160                         if (!cidr.empty()) {
161                                 whitelistedcidrs.push_back(cidr);
162                         }
163                 }
164
165                 ConfigTagList attributetags = ServerInstance->Config->ConfTags("ldaprequire");
166
167                 for (ConfigIter i = attributetags.first; i != attributetags.second; ++i)
168                 {
169                         const std::string attr = i->second->getString("attribute");
170                         const std::string val = i->second->getString("value");
171
172                         if (!attr.empty() && !val.empty())
173                                 requiredattributes.push_back(make_pair(attr, val));
174                 }
175
176                 if (scope == "base")
177                         searchscope = LDAP_SCOPE_BASE;
178                 else if (scope == "onelevel")
179                         searchscope = LDAP_SCOPE_ONELEVEL;
180                 else searchscope = LDAP_SCOPE_SUBTREE;
181
182                 Connect();
183         }
184
185         bool Connect()
186         {
187                 if (conn != NULL)
188                         ldap_unbind_ext(conn, NULL, NULL);
189                 int res, v = LDAP_VERSION3;
190                 res = ldap_initialize(&conn, ldapserver.c_str());
191                 if (res != LDAP_SUCCESS)
192                 {
193                         if (verbose)
194                                 ServerInstance->SNO->WriteToSnoMask('c', "LDAP connection failed: %s", ldap_err2string(res));
195                         conn = NULL;
196                         return false;
197                 }
198
199                 res = ldap_set_option(conn, LDAP_OPT_PROTOCOL_VERSION, (void *)&v);
200                 if (res != LDAP_SUCCESS)
201                 {
202                         if (verbose)
203                                 ServerInstance->SNO->WriteToSnoMask('c', "LDAP set protocol to v3 failed: %s", ldap_err2string(res));
204                         ldap_unbind_ext(conn, NULL, NULL);
205                         conn = NULL;
206                         return false;
207                 }
208                 return true;
209         }
210
211         std::string SafeReplace(const std::string &text, std::map<std::string,
212                         std::string> &replacements)
213         {
214                 std::string result;
215                 result.reserve(MAXBUF);
216
217                 for (unsigned int i = 0; i < text.length(); ++i) {
218                         char c = text[i];
219                         if (c == '$') {
220                                 // find the first nonalpha
221                                 i++;
222                                 unsigned int start = i;
223
224                                 while (i < text.length() - 1 && isalpha(text[i + 1]))
225                                         ++i;
226
227                                 std::string key = text.substr(start, (i - start) + 1);
228                                 result.append(replacements[key]);
229                         } else {
230                                 result.push_back(c);
231                         }
232                 }
233
234            return result;
235         }
236
237         virtual void OnUserConnect(LocalUser *user)
238         {
239                 std::string* cc = ldapVhost.get(user);
240                 if (cc)
241                 {
242                         user->ChangeDisplayedHost(cc->c_str());
243                         ldapVhost.unset(user);
244                 }
245         }
246
247         ModResult OnUserRegister(LocalUser* user)
248         {
249                 if ((!allowpattern.empty()) && (InspIRCd::Match(user->nick,allowpattern)))
250                 {
251                         ldapAuthed.set(user,1);
252                         return MOD_RES_PASSTHRU;
253                 }
254
255                 for (std::vector<std::string>::iterator i = whitelistedcidrs.begin(); i != whitelistedcidrs.end(); i++)
256                 {
257                         if (InspIRCd::MatchCIDR(user->GetIPString(), *i, ascii_case_insensitive_map))
258                         {
259                                 ldapAuthed.set(user,1);
260                                 return MOD_RES_PASSTHRU;
261                         }
262                 }
263
264                 if (!CheckCredentials(user))
265                 {
266                         ServerInstance->Users->QuitUser(user, killreason);
267                         return MOD_RES_DENY;
268                 }
269                 return MOD_RES_PASSTHRU;
270         }
271
272         bool CheckCredentials(LocalUser* user)
273         {
274                 if (conn == NULL)
275                         if (!Connect())
276                                 return false;
277
278                 if (user->password.empty())
279                 {
280                         if (verbose)
281                                 ServerInstance->SNO->WriteToSnoMask('c', "Forbidden connection from %s (No password provided)", user->GetFullRealHost().c_str());
282                         return false;
283                 }
284
285                 int res;
286                 // bind anonymously if no bind DN and authentication are given in the config
287                 struct berval cred;
288                 cred.bv_val = const_cast<char*>(password.c_str());
289                 cred.bv_len = password.length();
290
291                 if ((res = ldap_sasl_bind_s(conn, username.c_str(), LDAP_SASL_SIMPLE, &cred, NULL, NULL, NULL)) != LDAP_SUCCESS)
292                 {
293                         if (res == LDAP_SERVER_DOWN)
294                         {
295                                 // Attempt to reconnect if the connection dropped
296                                 if (verbose)
297                                         ServerInstance->SNO->WriteToSnoMask('a', "LDAP server has gone away - reconnecting...");
298                                 Connect();
299                                 res = ldap_sasl_bind_s(conn, username.c_str(), LDAP_SASL_SIMPLE, &cred, NULL, NULL, NULL);
300                         }
301
302                         if (res != LDAP_SUCCESS)
303                         {
304                                 if (verbose)
305                                         ServerInstance->SNO->WriteToSnoMask('c', "Forbidden connection from %s (LDAP bind failed: %s)", user->GetFullRealHost().c_str(), ldap_err2string(res));
306                                 ldap_unbind_ext(conn, NULL, NULL);
307                                 conn = NULL;
308                                 return false;
309                         }
310                 }
311
312                 RAIILDAPMessage msg;
313                 std::string what = (attribute + "=" + (useusername ? user->ident : user->nick));
314                 if ((res = ldap_search_ext_s(conn, base.c_str(), searchscope, what.c_str(), NULL, 0, NULL, NULL, NULL, 0, &msg)) != LDAP_SUCCESS)
315                 {
316                         // Do a second search, based on password, if it contains a :
317                         // That is, PASS <user>:<password> will work.
318                         size_t pos = user->password.find(":");
319                         if (pos != std::string::npos)
320                         {
321                                 // manpage says we must deallocate regardless of success or failure
322                                 // since we're about to do another query (and reset msg), first
323                                 // free the old one.
324                                 msg.dealloc();
325
326                                 std::string cutpassword = user->password.substr(0, pos);
327                                 res = ldap_search_ext_s(conn, base.c_str(), searchscope, cutpassword.c_str(), NULL, 0, NULL, NULL, NULL, 0, &msg);
328
329                                 if (res == LDAP_SUCCESS)
330                                 {
331                                         // Trim the user: prefix, leaving just 'pass' for later password check
332                                         user->password = user->password.substr(pos + 1);
333                                 }
334                         }
335
336                         // It may have found based on user:pass check above.
337                         if (res != LDAP_SUCCESS)
338                         {
339                                 if (verbose)
340                                         ServerInstance->SNO->WriteToSnoMask('c', "Forbidden connection from %s (LDAP search failed: %s)", user->GetFullRealHost().c_str(), ldap_err2string(res));
341                                 return false;
342                         }
343                 }
344                 if (ldap_count_entries(conn, msg) > 1)
345                 {
346                         if (verbose)
347                                 ServerInstance->SNO->WriteToSnoMask('c', "Forbidden connection from %s (LDAP search returned more than one result: %s)", user->GetFullRealHost().c_str(), ldap_err2string(res));
348                         return false;
349                 }
350
351                 LDAPMessage *entry;
352                 if ((entry = ldap_first_entry(conn, msg)) == NULL)
353                 {
354                         if (verbose)
355                                 ServerInstance->SNO->WriteToSnoMask('c', "Forbidden connection from %s (LDAP search returned no results: %s)", user->GetFullRealHost().c_str(), ldap_err2string(res));
356                         return false;
357                 }
358                 cred.bv_val = (char*)user->password.data();
359                 cred.bv_len = user->password.length();
360                 RAIILDAPString DN(ldap_get_dn(conn, entry));
361                 if ((res = ldap_sasl_bind_s(conn, DN, LDAP_SASL_SIMPLE, &cred, NULL, NULL, NULL)) != LDAP_SUCCESS)
362                 {
363                         if (verbose)
364                                 ServerInstance->SNO->WriteToSnoMask('c', "Forbidden connection from %s (%s)", user->GetFullRealHost().c_str(), ldap_err2string(res));
365                         return false;
366                 }
367
368                 if (!requiredattributes.empty())
369                 {
370                         bool authed = false;
371
372                         for (std::vector<std::pair<std::string, std::string> >::const_iterator it = requiredattributes.begin(); it != requiredattributes.end(); ++it)
373                         {
374                                 const std::string &attr = it->first;
375                                 const std::string &val = it->second;
376
377                                 struct berval attr_value;
378                                 attr_value.bv_val = const_cast<char*>(val.c_str());
379                                 attr_value.bv_len = val.length();
380
381                                 ServerInstance->Logs->Log("m_ldapauth", DEBUG, "LDAP compare: %s=%s", attr.c_str(), val.c_str());
382
383                                 authed = (ldap_compare_ext_s(conn, DN, attr.c_str(), &attr_value, NULL, NULL) == LDAP_COMPARE_TRUE);
384
385                                 if (authed)
386                                         break;
387                         }
388
389                         if (!authed)
390                         {
391                                 if (verbose)
392                                         ServerInstance->SNO->WriteToSnoMask('c', "Forbidden connection from %s (Lacks required LDAP attributes)", user->GetFullRealHost().c_str());
393                                 return false;
394                         }
395                 }
396
397                 if (!vhost.empty())
398                 {
399                         irc::commasepstream stream(DN);
400
401                         // mashed map of key:value parts of the DN
402                         std::map<std::string, std::string> dnParts;
403
404                         std::string dnPart;
405                         while (stream.GetToken(dnPart))
406                         {
407                                 std::string::size_type pos = dnPart.find('=');
408                                 if (pos == std::string::npos) // malformed
409                                         continue;
410
411                                 std::string key = dnPart.substr(0, pos);
412                                 std::string value = dnPart.substr(pos + 1, dnPart.length() - pos + 1); // +1s to skip the = itself
413                                 dnParts[key] = value;
414                         }
415
416                         // change host according to config key
417                         ldapVhost.set(user, SafeReplace(vhost, dnParts));
418                 }
419
420                 ldapAuthed.set(user,1);
421                 return true;
422         }
423
424         ModResult OnCheckReady(LocalUser* user)
425         {
426                 return ldapAuthed.get(user) ? MOD_RES_PASSTHRU : MOD_RES_DENY;
427         }
428
429         Version GetVersion()
430         {
431                 return Version("Allow/Deny connections based upon answer from LDAP server", VF_VENDOR);
432         }
433
434 };
435
436 MODULE_INIT(ModuleLDAPAuth)