/** * vim: set ts=4 : * ============================================================================= * SourceMod SQL Admins Plugin (Threaded) * Fetches admins from an SQL database dynamically. * * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. * ============================================================================= * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 3.0, as published by the * Free Software Foundation. * * 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, see . * * As a special exception, AlliedModders LLC gives you permission to link the * code of this program (as well as its derivative works) to "Half-Life 2," the * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software * by the Valve Corporation. You must obey the GNU General Public License in * all respects for all other code used. Additionally, AlliedModders LLC grants * this exception to all derivative works. AlliedModders LLC defines further * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), * or . * * 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 user cache are ignored except for being * non-zero, which means players in-game should be re-checked for admin * powers. */ Database hDatabase = null; /** 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. */ delete hDatabase; } 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 void OnDatabaseConnect(Database db, const char[] error, any data) { #if defined _DEBUG PrintToServer("OnDatabaseConnect(%x, %d) ConnectLock=%d", db, data, ConnectLock); #endif /** * If this happens to be an old connection request, ignore it. */ if (data != ConnectLock || hDatabase != null) { delete db; return; } ConnectLock = 0; hDatabase = db; /** * 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 == null) { 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; if (SQL_CheckConfig("admins")) { Database.Connect(OnDatabaseConnect, "admins", ConnectLock); } else { Database.Connect(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. */ int 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 == null) { 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 void OnReceiveUserGroups(Database db, DBResultSet rs, const char[] error, any data) { DataPack pk = view_as(data); pk.Reset(); int client = pk.ReadCell(); int sequence = pk.ReadCell(); /** * Make sure it's the same client. */ if (PlayerSeq[client] != sequence) { delete pk; return; } AdminId adm = view_as(pk.ReadCell()); /** * Someone could have sneakily changed the admin id while we waited. */ if (GetUserAdmin(client) != adm) { NotifyPostAdminCheck(client); delete pk; return; } /** * See if we got results. */ if (rs == null) { char query[255]; pk.ReadString(query, sizeof(query)); LogError("SQL error receiving user: %s", error); LogError("Query dump: %s", query); NotifyPostAdminCheck(client); delete pk; return; } char name[80]; GroupId gid; while (rs.FetchRow()) { rs.FetchString(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); delete pk; } public void OnReceiveUser(Database db, DBResultSet rs, const char[] error, any data) { DataPack pk = view_as(data); pk.Reset(); int client = pk.ReadCell(); /** * Check if this is the latest result request. */ int sequence = pk.ReadCell(); if (PlayerSeq[client] != sequence) { /* Discard everything, since we're out of sequence. */ delete pk; return; } /** * If we need to use the results, make sure they succeeded. */ if (rs == null) { char query[255]; pk.ReadString(query, sizeof(query)); LogError("SQL error receiving user: %s", error); LogError("Query dump: %s", query); RunAdminCacheChecks(client); NotifyPostAdminCheck(client); delete pk; return; } int num_accounts = rs.RowCount; if (num_accounts == 0) { RunAdminCacheChecks(client); NotifyPostAdminCheck(client); delete pk; return; } char authtype[16]; char identity[80]; char password[80]; char flags[32]; char name[80]; int immunity, id; AdminId adm; /** * Cache user info -- [0] = db id, [1] = cache id, [2] = groups */ char[][] user_lookup = new char[num_accounts][3]; int total_users = 0; while (rs.FetchRow()) { id = rs.FetchInt(0); rs.FetchString(1, authtype, sizeof(authtype)); rs.FetchString(2, identity, sizeof(identity)); rs.FetchString(3, password, sizeof(password)); rs.FetchString(4, flags, sizeof(flags)); rs.FetchString(5, name, sizeof(name)); immunity = rs.FetchInt(7); /* 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] = rs.FetchInt(6); total_users++; #if defined _DEBUG 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]); #endif /* See if this admin wants a password */ if (password[0] != '\0') { SetAdminPassword(adm, password); } SetAdminImmunityLevel(adm, immunity); /* Apply each flag */ int len = strlen(flags); AdminFlag flag; for (new i=0; i(data); pk.Reset(); /** * Check if this is the latest result request. */ int sequence = pk.ReadCell(); if (RebuildCachePart[_:AdminCache_Groups] != sequence) { /* Discard everything, since we're out of sequence. */ delete pk; return; } /** * If we need to use the results, make sure they succeeded. */ if (rs == null) { char query[255]; pk.ReadString(query, sizeof(query)); LogError("SQL error receiving group immunity: %s", error); LogError("Query dump: %s", query); delete pk; return; } /* We're done with the pack forever. */ delete pk; while (rs.FetchRow()) { char group1[80]; char group2[80]; GroupId gid1, gid2; rs.FetchString(0, group1, sizeof(group1)); rs.FetchString(1, group2, sizeof(group2)); if (((gid1 = FindAdmGroup(group1)) == INVALID_GROUP_ID) || (gid2 = FindAdmGroup(group2)) == INVALID_GROUP_ID) { continue; } SetAdmGroupImmuneFrom(gid1, gid2); #if defined _DEBUG PrintToServer("SetAdmGroupImmuneFrom(%d, %d)", gid1, gid2); #endif } /* Clear the sequence so another connect doesn't refetch */ RebuildCachePart[_:AdminCache_Groups] = 0; } public OnReceiveGroupOverrides(Database db, DBResultSet rs, const char[] error, any data) { DataPack pk = view_as(data); pk.Reset(); /** * Check if this is the latest result request. */ int sequence = pk.ReadCell(); if (RebuildCachePart[_:AdminCache_Groups] != sequence) { /* Discard everything, since we're out of sequence. */ delete pk; return; } /** * If we need to use the results, make sure they succeeded. */ if (rs == null) { char query[255]; pk.ReadString(query, sizeof(query)); LogError("SQL error receiving group overrides: %s", error); LogError("Query dump: %s", query); delete pk; return; } /** * Fetch the overrides. */ char name[80]; char type[16]; char command[64]; char access[16]; GroupId gid; while (rs.FetchRow()) { rs.FetchString(0, name, sizeof(name)); rs.FetchString(1, type, sizeof(type)); rs.FetchString(2, command, sizeof(command)); rs.FetchString(3, access, sizeof(access)); /* Find the group. This is actually faster than doing the ID lookup. */ if ((gid = FindAdmGroup(name)) == INVALID_GROUP_ID) { /* Oh well, just ignore it. */ continue; } OverrideType o_type = Override_Command; if (StrEqual(type, "group")) { o_type = Override_CommandGroup; } OverrideRule o_rule = Command_Deny; if (StrEqual(access, "allow")) { o_rule = Command_Allow; } #if defined _DEBUG PrintToServer("AddAdmGroupCmdOverride(%d, %s, %d, %d)", gid, command, o_type, o_rule); #endif AddAdmGroupCmdOverride(gid, command, o_type, o_rule); } /** * It's time to get the group immunity list. */ int len = 0; char query[256]; len += Format(query[len], sizeof(query)-len, "SELECT g1.name, g2.name FROM sm_group_immunity gi"); len += Format(query[len], sizeof(query)-len, " LEFT JOIN sm_groups g1 ON g1.id = gi.group_id "); len += Format(query[len], sizeof(query)-len, " LEFT JOIN sm_groups g2 ON g2.id = gi.other_id"); pk.Reset(); pk.WriteCell(sequence); pk.WriteString(query); db.Query(OnReceiveGroupImmunity, query, pk, DBPrio_High); } public OnReceiveGroups(Database db, DBResultSet rs, const char[] error, any data) { DataPack pk = view_as(data); pk.Reset(); /** * Check if this is the latest result request. */ int sequence = pk.ReadCell(); if (RebuildCachePart[_:AdminCache_Groups] != sequence) { /* Discard everything, since we're out of sequence. */ delete pk; return; } /** * If we need to use the results, make sure they succeeded. */ if (rs == null) { char query[255]; pk.ReadString(query, sizeof(query)); LogError("SQL error receiving groups: %s", error); LogError("Query dump: %s", query); delete pk; return; } /** * Now start fetching groups. */ char flags[32]; char name[128]; int immunity; while (rs.FetchRow()) { rs.FetchString(0, flags, sizeof(flags)); rs.FetchString(1, name, sizeof(name)); immunity = rs.FetchInt(2); #if defined _DEBUG PrintToServer("Adding group (%d, %s, %s)", immunity, flags, name); #endif /* Find or create the group */ GroupId gid; if ((gid = FindAdmGroup(name)) == INVALID_GROUP_ID) { gid = CreateAdmGroup(name); } /* Add flags from the database to the group */ int num_flag_chars = strlen(flags); for (int i=0; i(data); pk.Reset(); /** * Check if this is the latest result request. */ int sequence = pk.ReadCell(); if (RebuildCachePart[_:AdminCache_Overrides] != sequence) { /* Discard everything, since we're out of sequence. */ delete pk; return; } /** * If we need to use the results, make sure they succeeded. */ if (rs == null) { char query[255]; pk.ReadString(query, sizeof(query)); LogError("SQL error receiving overrides: %s", error); LogError("Query dump: %s", query); delete pk; return; } /** * We're done with you, now. */ delete pk; char type[64]; char name[64]; char flags[32]; int flag_bits; while (rs.FetchRow()) { rs.FetchString(0, type, sizeof(type)); rs.FetchString(1, name, sizeof(name)); rs.FetchString(2, flags, sizeof(flags)); #if defined _DEBUG PrintToServer("Adding override (%s, %s, %s)", type, name, flags); #endif flag_bits = ReadFlagString(flags); if (StrEqual(type, "command")) { AddCommandOverride(name, Override_Command, flag_bits); } else if (StrEqual(type, "group")) { AddCommandOverride(name, Override_CommandGroup, flag_bits); } } /* Clear the sequence so another connect doesn't refetch */ RebuildCachePart[_:AdminCache_Overrides] = 0; } void FetchOverrides(Database db, sequence) { char query[255]; Format(query, sizeof(query), "SELECT type, name, flags FROM sm_overrides"); DataPack pk = new DataPack(); pk.WriteCell(sequence); pk.WriteString(query); db.Query(OnReceiveOverrides, query, pk, DBPrio_High); }