dvander@1322: /**
ds@1330: * vim: set ts=4 :
ds@1330: * =============================================================================
ds@1330: * SourceMod SQL Admins Plugin (Threaded)
ds@1330: * Fetches admins from an SQL database dynamically.
dvander@1322: *
ds@1330: * SourceMod (C)2004-2007 AlliedModders LLC. All rights reserved.
ds@1330: * =============================================================================
dvander@1322: *
ds@1330: * This program is free software; you can redistribute it and/or modify it under
ds@1330: * the terms of the GNU General Public License, version 3.0, as published by the
ds@1330: * Free Software Foundation.
ds@1330: *
ds@1330: * This program is distributed in the hope that it will be useful, but WITHOUT
ds@1330: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
ds@1330: * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
ds@1330: * details.
dvander@1322: *
ds@1330: * You should have received a copy of the GNU General Public License along with
ds@1330: * this program. If not, see .
ds@1330: *
ds@1330: * As a special exception, AlliedModders LLC gives you permission to link the
ds@1330: * code of this program (as well as its derivative works) to "Half-Life 2," the
ds@1330: * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
ds@1330: * by the Valve Corporation. You must obey the GNU General Public License in
ds@1330: * all respects for all other code used. Additionally, AlliedModders LLC grants
ds@1330: * this exception to all derivative works. AlliedModders LLC defines further
ds@1330: * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
ds@1330: * or .
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@1326: * 4) Sequence numbers for the user cache are ignored except for being
dvander@1326: * 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@1403: new immunity;
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@1403: immunity = SQL_FetchInt(hndl, 7);
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@1403: 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]);
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@1403:
dvander@1403: SetAdminImmunityLevel(adm, immunity);
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