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