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