dvander@1322: /** dvander@1322: * admin-sql-threaded.sp dvander@1322: * Fetches admins from an SQL database dynamically. Groups and overrides dvander@1322: * are fetched once, but also using threads. dvander@1322: * dvander@1322: * This file is part of SourceMod, Copyright (C) 2004-2007 AlliedModders LLC dvander@1322: * dvander@1322: * This program is free software; you can redistribute it and/or dvander@1322: * modify it under the terms of the GNU General Public License dvander@1322: * as published by the Free Software Foundation; either version 2 dvander@1322: * of the License, or (at your option) any later version. dvander@1322: * dvander@1322: * This program is distributed in the hope that it will be useful, dvander@1322: * but WITHOUT ANY WARRANTY; without even the implied warranty of dvander@1322: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the dvander@1322: * GNU General Public License for more details. dvander@1322: * dvander@1322: * You should have received a copy of the GNU General Public License dvander@1322: * along with this program; if not, write to the Free Software dvander@1322: * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. dvander@1322: * dvander@1322: * Version: $Id$ dvander@1322: */ dvander@1322: dvander@1322: /* We like semicolons */ dvander@1322: #pragma semicolon 1 dvander@1322: dvander@1322: #include dvander@1322: dvander@1322: public Plugin:myinfo = dvander@1322: { dvander@1322: name = "SQL Admins (Threaded)", dvander@1322: author = "AlliedModders LLC", dvander@1322: description = "Reads admins from SQL dynamically", dvander@1322: version = SOURCEMOD_VERSION, dvander@1322: url = "http://www.sourcemod.net/" dvander@1322: }; dvander@1322: dvander@1322: /** dvander@1322: * Notes: dvander@1322: * dvander@1322: * 1) All queries in here are high priority. This is because the admin stuff dvander@1322: * is very important. Do not take this to mean that in your script, dvander@1322: * everything should be high priority. dvander@1322: * dvander@1322: * 2) All callbacks are locked with "sequence numbers." This is to make sure dvander@1322: * that multiple calls to sm_reloadadmins and the like do not make us dvander@1322: * store the results from two or more callbacks accidentally. Instead, we dvander@1322: * check the sequence number in each callback with the current "allowed" dvander@1322: * sequence number, and if it doesn't match, the callback is cancelled. dvander@1322: * dvander@1322: * 3) Sequence numbers for groups and overrides are not cleared unless there dvander@1322: * was a 100% success in the fetch. This is so we can potentially implement dvander@1322: * connection retries in the future. dvander@1322: * dvander@1322: * 4) Sequence numbers for the admin cache are ignored except for being dvander@1322: * non-zero, which means players in-game should be re-checked for admin \ dvander@1322: * powers. dvander@1322: */ dvander@1322: dvander@1322: new Handle:hDatabase = INVALID_HANDLE; /** Database connection */ dvander@1322: new g_sequence = 0; /** Global unique sequence number */ dvander@1322: new ConnectLock = 0; /** Connect sequence number */ dvander@1322: new RebuildCachePart[3] = {0}; /** Cache part sequence numbers */ dvander@1322: new PlayerSeq[MAXPLAYERS+1]; /** Player-specific sequence numbers */ dvander@1322: new bool:PlayerAuth[MAXPLAYERS+1]; /** Whether a player has been "pre-authed" */ dvander@1322: dvander@1322: #define _DEBUG dvander@1322: dvander@1322: public OnMapEnd() dvander@1322: { dvander@1322: /** dvander@1322: * Clean up on map end just so we can start a fresh connection when we need it later. dvander@1322: */ dvander@1322: if (hDatabase != INVALID_HANDLE) dvander@1322: { dvander@1322: CloseHandle(hDatabase); dvander@1322: hDatabase = INVALID_HANDLE; dvander@1322: } dvander@1322: } dvander@1322: dvander@1322: public bool:OnClientConnect(client, String:rejectmsg[], maxlen) dvander@1322: { dvander@1322: PlayerSeq[client] = 0; dvander@1322: PlayerAuth[client] = false; dvander@1322: return true; dvander@1322: } dvander@1322: dvander@1322: public OnClientDisconnect(client) dvander@1322: { dvander@1322: PlayerSeq[client] = 0; dvander@1322: PlayerAuth[client] = false; dvander@1322: } dvander@1322: dvander@1322: public OnDatabaseConnect(Handle:owner, Handle:hndl, const String:error[], any:data) dvander@1322: { dvander@1322: #if defined _DEBUG dvander@1322: PrintToServer("OnDatabaseConnect(%x,%x,%d) ConnectLock=%d", owner, hndl, data, ConnectLock); dvander@1322: #endif dvander@1322: dvander@1322: /** dvander@1322: * If this happens to be an old connection request, ignore it. dvander@1322: */ dvander@1322: if (data != ConnectLock || hDatabase != INVALID_HANDLE) dvander@1322: { dvander@1322: if (hndl != INVALID_HANDLE) dvander@1322: { dvander@1322: CloseHandle(hndl); dvander@1322: } dvander@1322: return; dvander@1322: } dvander@1322: dvander@1322: ConnectLock = 0; dvander@1322: hDatabase = hndl; dvander@1322: dvander@1322: /** dvander@1322: * See if the connection is valid. If not, don't un-mark the caches dvander@1322: * as needing rebuilding, in case the next connection request works. dvander@1322: */ dvander@1322: if (hDatabase == INVALID_HANDLE) dvander@1322: { dvander@1322: LogError("Failed to connect to database: %s", error); dvander@1322: return; dvander@1322: } dvander@1322: dvander@1322: /** dvander@1322: * See if we need to get any of the cache stuff now. dvander@1322: */ dvander@1322: new sequence; dvander@1322: if ((sequence = RebuildCachePart[_:AdminCache_Overrides]) != 0) dvander@1322: { dvander@1322: FetchOverrides(hDatabase, sequence); dvander@1322: } dvander@1322: if ((sequence = RebuildCachePart[_:AdminCache_Groups]) != 0) dvander@1322: { dvander@1322: FetchGroups(hDatabase, sequence); dvander@1322: } dvander@1322: if ((sequence = RebuildCachePart[_:AdminCache_Admins]) != 0) dvander@1322: { dvander@1322: FetchUsersWeCan(hDatabase); dvander@1322: } dvander@1322: } dvander@1322: dvander@1322: RequestDatabaseConnection() dvander@1322: { dvander@1322: ConnectLock = ++g_sequence; dvander@1324: if (SQL_CheckConfig("admins")) dvander@1324: { dvander@1324: SQL_TConnect(OnDatabaseConnect, "admins", ConnectLock); dvander@1324: } else { dvander@1324: SQL_TConnect(OnDatabaseConnect, "default", ConnectLock); dvander@1324: } dvander@1322: } dvander@1322: dvander@1322: public OnRebuildAdminCache(AdminCachePart:part) dvander@1322: { dvander@1322: /** dvander@1322: * Mark this part of the cache as being rebuilt. This is used by the dvander@1322: * callback system to determine whether the results should still be dvander@1322: * used. dvander@1322: */ dvander@1322: new sequence = ++g_sequence; dvander@1322: RebuildCachePart[_:part] = sequence; dvander@1322: dvander@1322: /** dvander@1322: * If we don't have a database connection, we can't do any lookups just yet. dvander@1322: */ dvander@1322: if (!hDatabase) dvander@1322: { dvander@1322: /** dvander@1322: * Ask for a new connection if we need it. dvander@1322: */ dvander@1322: if (!ConnectLock) dvander@1322: { dvander@1322: RequestDatabaseConnection(); dvander@1322: } dvander@1322: return; dvander@1322: } dvander@1322: dvander@1322: if (part == AdminCache_Overrides) dvander@1322: { dvander@1322: FetchOverrides(hDatabase, sequence); dvander@1322: } else if (part == AdminCache_Groups) { dvander@1322: FetchGroups(hDatabase, sequence); dvander@1322: } else if (part == AdminCache_Admins) { dvander@1322: FetchUsersWeCan(hDatabase); dvander@1322: } dvander@1322: } dvander@1322: dvander@1322: public Action:OnClientPreAdminCheck(client) dvander@1322: { dvander@1322: PlayerAuth[client] = true; dvander@1322: dvander@1322: /** dvander@1322: * Play nice with other plugins. If there's no database, don't delay the dvander@1322: * connection process. Unfortunately, we can't attempt anything else and dvander@1322: * we just have to hope either the database is waiting or someone will type dvander@1322: * sm_reloadadmins. dvander@1322: */ dvander@1322: if (hDatabase == INVALID_HANDLE) dvander@1322: { dvander@1322: return Plugin_Continue; dvander@1322: } dvander@1322: dvander@1322: /** dvander@1322: * Similarly, if the cache is in the process of being rebuilt, don't delay dvander@1322: * the user's normal connection flow. The database will soon auth the user dvander@1322: * normally. dvander@1322: */ dvander@1322: if (RebuildCachePart[_:AdminCache_Admins] != 0) dvander@1322: { dvander@1322: return Plugin_Continue; dvander@1322: } dvander@1322: dvander@1322: /** dvander@1322: * If someone has already assigned an admin ID (bad bad bad), don't dvander@1322: * bother waiting. dvander@1322: */ dvander@1322: if (GetUserAdmin(client) != INVALID_ADMIN_ID) dvander@1322: { dvander@1322: return Plugin_Continue; dvander@1322: } dvander@1322: dvander@1322: FetchUser(hDatabase, client); dvander@1322: dvander@1322: return Plugin_Handled; dvander@1322: } dvander@1322: dvander@1322: public OnReceiveUserGroups(Handle:owner, Handle:hndl, const String:error[], any:data) dvander@1322: { dvander@1322: new Handle:pk = Handle:data; dvander@1322: ResetPack(pk); dvander@1322: dvander@1322: new client = ReadPackCell(pk); dvander@1322: new sequence = ReadPackCell(pk); dvander@1322: dvander@1322: /** dvander@1322: * Make sure it's the same client. dvander@1322: */ dvander@1322: if (PlayerSeq[client] != sequence) dvander@1322: { dvander@1322: CloseHandle(pk); dvander@1322: return; dvander@1322: } dvander@1322: dvander@1322: new AdminId:adm = AdminId:ReadPackCell(pk); dvander@1322: dvander@1322: /** dvander@1322: * Someone could have sneakily changed the admin id while we waited. dvander@1322: */ dvander@1322: if (GetUserAdmin(client) != adm) dvander@1322: { dvander@1322: NotifyPostAdminCheck(client); dvander@1322: CloseHandle(pk); dvander@1322: return; dvander@1322: } dvander@1322: dvander@1322: /** dvander@1322: * See if we got results. dvander@1322: */ dvander@1322: if (hndl == INVALID_HANDLE) dvander@1322: { dvander@1322: decl String:query[255]; dvander@1322: ReadPackString(pk, query, sizeof(query)); dvander@1322: LogError("SQL error receiving user: %s", error); dvander@1322: LogError("Query dump: %s", query); dvander@1322: NotifyPostAdminCheck(client); dvander@1322: CloseHandle(pk); dvander@1322: return; dvander@1322: } dvander@1322: dvander@1322: decl String:name[80]; dvander@1322: new GroupId:gid; dvander@1322: dvander@1322: while (SQL_FetchRow(hndl)) dvander@1322: { dvander@1322: SQL_FetchString(hndl, 0, name, sizeof(name)); dvander@1322: dvander@1322: if ((gid = FindAdmGroup(name)) == INVALID_GROUP_ID) dvander@1322: { dvander@1322: continue; dvander@1322: } dvander@1322: dvander@1322: #if defined _DEBUG dvander@1322: PrintToServer("Binding user group (%d, %d, %d, %s, %d)", client, sequence, adm, name, gid); dvander@1322: #endif dvander@1322: dvander@1322: AdminInheritGroup(adm, gid); dvander@1322: } dvander@1322: dvander@1322: /** dvander@1322: * We're DONE! Omg. dvander@1322: */ dvander@1322: NotifyPostAdminCheck(client); dvander@1322: CloseHandle(pk); dvander@1322: } dvander@1322: dvander@1322: public OnReceiveUser(Handle:owner, Handle:hndl, const String:error[], any:data) dvander@1322: { dvander@1322: new Handle:pk = Handle:data; dvander@1322: ResetPack(pk); dvander@1322: dvander@1322: new client = ReadPackCell(pk); dvander@1322: dvander@1322: /** dvander@1322: * Check if this is the latest result request. dvander@1322: */ dvander@1322: new sequence = ReadPackCell(pk); dvander@1322: if (PlayerSeq[client] != sequence) dvander@1322: { dvander@1322: /* Discard everything, since we're out of sequence. */ dvander@1322: CloseHandle(pk); dvander@1322: return; dvander@1322: } dvander@1322: dvander@1322: /** dvander@1322: * If we need to use the results, make sure they succeeded. dvander@1322: */ dvander@1322: if (hndl == INVALID_HANDLE) dvander@1322: { dvander@1322: decl String:query[255]; dvander@1322: ReadPackString(pk, query, sizeof(query)); dvander@1322: LogError("SQL error receiving user: %s", error); dvander@1322: LogError("Query dump: %s", query); dvander@1322: RunAdminCacheChecks(client); dvander@1322: NotifyPostAdminCheck(client); dvander@1322: CloseHandle(pk); dvander@1322: return; dvander@1322: } dvander@1322: dvander@1322: new num_accounts = SQL_GetRowCount(hndl); dvander@1322: if (num_accounts == 0) dvander@1322: { dvander@1322: RunAdminCacheChecks(client); dvander@1322: NotifyPostAdminCheck(client); dvander@1322: CloseHandle(pk); dvander@1322: return; dvander@1322: } dvander@1322: dvander@1322: decl String:authtype[16]; dvander@1322: decl String:identity[80]; dvander@1322: decl String:password[80]; dvander@1322: decl String:flags[32]; dvander@1322: decl String:name[80]; dvander@1322: new AdminId:adm, id; dvander@1322: dvander@1322: /** dvander@1322: * Cache user info -- [0] = db id, [1] = cache id, [2] = groups dvander@1322: */ dvander@1322: decl user_lookup[num_accounts][3]; dvander@1322: new total_users = 0; dvander@1322: dvander@1322: while (SQL_FetchRow(hndl)) dvander@1322: { dvander@1322: id = SQL_FetchInt(hndl, 0); dvander@1322: SQL_FetchString(hndl, 1, authtype, sizeof(authtype)); dvander@1322: SQL_FetchString(hndl, 2, identity, sizeof(identity)); dvander@1322: SQL_FetchString(hndl, 3, password, sizeof(password)); dvander@1322: SQL_FetchString(hndl, 4, flags, sizeof(flags)); dvander@1322: SQL_FetchString(hndl, 5, name, sizeof(name)); dvander@1322: dvander@1322: /* For dynamic admins we clear anything already in the cache. */ dvander@1322: if ((adm = FindAdminByIdentity(authtype, identity)) != INVALID_ADMIN_ID) dvander@1322: { dvander@1322: RemoveAdmin(adm); dvander@1322: } dvander@1322: dvander@1322: adm = CreateAdmin(name); dvander@1322: if (!BindAdminIdentity(adm, authtype, identity)) dvander@1322: { dvander@1322: LogError("Could not bind prefetched SQL admin (authtype \"%s\") (identity \"%s\")", authtype, identity); dvander@1322: continue; dvander@1322: } dvander@1322: dvander@1322: user_lookup[total_users][0] = id; dvander@1322: user_lookup[total_users][1] = _:adm; dvander@1322: user_lookup[total_users][2] = SQL_FetchInt(hndl, 6); dvander@1322: total_users++; dvander@1322: dvander@1322: #if defined _DEBUG dvander@1322: PrintToServer("Found SQL admin (%d,%s,%s,%s,%s,%s):%d:%d", id, authtype, identity, password, flags, name, adm, user_lookup[total_users-1][2]); dvander@1322: #endif dvander@1322: dvander@1322: /* See if this admin wants a password */ dvander@1322: if (password[0] != '\0') dvander@1322: { dvander@1322: SetAdminPassword(adm, password); dvander@1322: } dvander@1322: dvander@1322: /* Apply each flag */ dvander@1322: new len = strlen(flags); dvander@1322: new AdminFlag:flag; dvander@1322: for (new i=0; i