/**
* vim: set ts=4 :
* =============================================================================
* SourceMod Webternet Extension
* Copyright (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$
*/
#include
#include "extension.h"
#include
#include
#include
#include "curlapi.h"
#include "FileDownloader.h"
#include "MemoryDownloader.h"
/**
* @file extension.cpp
* @brief Implement extension code here.
*/
CurlExt curl_ext; /**< Global singleton for extension's main interface */
SMEXT_LINK(&curl_ext);
// Natives
extern sp_nativeinfo_t curlext_natives[];
HandleType_t g_SessionHandle = 0;
HandleType_t g_FormHandle = 0;
HandleType_t g_DownloadHandle = 0;
HTTPHandleDispatcher g_HTTPHandler;
HTTPSessionManager& g_SessionManager = HTTPSessionManager::instance();
bool CurlExt::SDK_OnLoad(char *error, size_t maxlength, bool late)
{
long flags;
CURLcode code;
flags = CURL_GLOBAL_NOTHING;
#if defined PLATFORM_WINDOWS
flags = CURL_GLOBAL_WIN32;
#endif
code = curl_global_init(flags);
if (code)
{
smutils->Format(error, maxlength, "%s", curl_easy_strerror(code));
return false;
}
if (!sharesys->AddInterface(myself, &g_webternet))
{
smutils->Format(error, maxlength, "Could not add IWebternet interface");
return false;
}
// Register natives
g_pShareSys->AddNatives(myself, curlext_natives);
// Register session handle handler
HandleAccess hacc;
g_pHandleSys->InitAccessDefaults(NULL, &hacc);
hacc.access[HandleAccess_Clone] = HANDLE_RESTRICT_IDENTITY;
g_SessionHandle = g_pHandleSys->CreateType("HTTPSession",
&g_HTTPHandler, 0, 0, &hacc, myself->GetIdentity(), NULL);
// Register web form handle handler
g_pHandleSys->InitAccessDefaults(NULL, &hacc);
hacc.access[HandleAccess_Delete] = HANDLE_RESTRICT_IDENTITY;
g_FormHandle = g_pHandleSys->CreateType("HTTPWebForm",
&g_HTTPHandler, 0, 0, &hacc, myself->GetIdentity(), NULL);
// Register download handle handler
g_pHandleSys->InitAccessDefaults(NULL, &hacc);
hacc.access[HandleAccess_Clone] = HANDLE_RESTRICT_IDENTITY;
g_DownloadHandle = g_pHandleSys->CreateType("HTTPDownloader",
&g_HTTPHandler, 0, 0, &hacc, myself->GetIdentity(), NULL);
// Register game listeners
plsys->AddPluginsListener(this);
g_SessionManager.Initialize();
smutils->AddGameFrameHook(&OnGameFrame);
return true;
}
void CurlExt::SDK_OnUnload()
{
g_pHandleSys->RemoveType(g_SessionHandle, myself->GetIdentity());
g_pHandleSys->RemoveType(g_FormHandle, myself->GetIdentity());
g_pHandleSys->RemoveType(g_DownloadHandle, myself->GetIdentity());
plsys->RemovePluginsListener(this);
smutils->RemoveGameFrameHook(&OnGameFrame);
g_SessionManager.Shutdown();
curl_global_cleanup();
}
const char *CurlExt::GetExtensionVerString()
{
return SOURCEMOD_VERSION;
}
const char *CurlExt::GetExtensionDateString()
{
return SOURCEMOD_BUILD_TIME;
}
void OnGameFrame(bool simulating)
{
g_SessionManager.RunFrame();
}
void CurlExt::OnPluginUnloaded(IPlugin *plugin)
{
g_SessionManager.PluginUnloaded(plugin);
}
void HTTPSessionManager::PluginUnloaded(IPlugin *plugin)
{
// Wait for running requests to finish
/* NOTE: when a plugin gets unloaded all associated handles get
* automatically destroyed, so running threads may access invalid
* memory and crash the host process. This blocking "solution" may
* have to be re factored to a better solution.
*/
{
ke::AutoLock lock(&threads_);
if (!threads.empty())
{
for (ke::LinkedList::iterator i(threads.begin()), end(threads.end()); i != end; ++i)
{
if ((*i) != NULL)
{
(*i)->WaitForThread();
(*i)->DestroyThis();
i = this->threads.erase(i);
}
}
}
}
}
void HTTPSessionManager::PostAndDownload(IPluginContext *pCtx,
HTTPRequestHandleSet handles, const char *url,
HTTPRequestCompletedContext &reqCtx)
{
HTTPRequest request = {};
BurnSessionHandle(pCtx, handles);
request.plugin = FindPluginByContext(pCtx)->GetMyHandle();
request.handles = handles;
request.method = HTTP_POST;
request.url = url;
request.context = reqCtx;
m_pWorker->MakeThread(new HTTPAsyncRequestHandler(request), Thread_AutoRelease);
}
void HTTPSessionManager::Download(IPluginContext *pCtx,
HTTPRequestHandleSet handles, const char *url,
HTTPRequestCompletedContext &reqCtx)
{
HTTPRequest request = {};
BurnSessionHandle(pCtx, handles);
request.plugin = FindPluginByContext(pCtx)->GetMyHandle();
request.handles = handles;
request.method = HTTP_GET;
request.url = url;
request.context = reqCtx;
m_pWorker->MakeThread(new HTTPAsyncRequestHandler(request), Thread_AutoRelease);
}
void HTTPSessionManager::BurnSessionHandle(IPluginContext *pCtx,
HTTPRequestHandleSet &handles)
{
HandleSecurity sec;
sec.pOwner = pCtx->GetIdentity();
sec.pIdentity = myself->GetIdentity();
// TODO: maybe better way to do this?
// Make session handle inaccessible to the user
Handle_t hndlNew = g_pHandleSys->FastCloneHandle(handles.hndlSession);
if (g_pHandleSys->FreeHandle(handles.hndlSession, &sec) !=
HandleError_None)
{
pCtx->ThrowNativeError("Couldn't free HTTP session handle");
return;
}
handles.hndlSession = hndlNew;
}
void HTTPSessionManager::RunFrame()
{
// Try to execute pending callbacks
if (callbacks_.DoTryLock())
{
if (!callbacks.empty())
{
HTTPRequest request = this->callbacks[0];
HandleError herr;
IPlugin *parent = plsys->PluginFromHandle(request.plugin, &herr);
// Is the requesting plugin still alive?
if (parent != NULL && herr != HandleError_Freed)
{
IPluginContext *pCtx = parent->GetBaseContext();
funcid_t id = request.context.GetPluginFunction();
IPluginFunction *pFunction = pCtx->GetFunctionById(id);
if (pFunction != NULL)
{
// Push data and execute callback
pFunction->PushCell(request.handles.hndlSession);
pFunction->PushCell(request.result);
pFunction->PushCell(request.handles.hndlDownloader);
if (request.context.HasContextValue())
{
pFunction->PushCell(request.context.GetContextValue());
}
pFunction->Execute(NULL);
}
}
callbacks.remove(0);
}
callbacks_.DoUnlock();
}
// Do some quick "garbage collection" on finished threads
this->RemoveFinishedThreads();
}
void HTTPSessionManager::Shutdown()
{
// Block until all running threads have finished
this->RemoveFinishedThreads();
// Destroy all remaining callback calls
{
ke::AutoLock lock(&callbacks_);
this->callbacks.clear();
}
threader->DestroyWorker(m_pWorker);
}
void HTTPSessionManager::AddCallback(HTTPRequest request)
{
ke::AutoLock lock(&callbacks_);
this->callbacks.append(request);
}
void HTTPSessionManager::RemoveFinishedThreads()
{
if (threads_.DoTryLock())
{
if (!this->threads.empty())
{
for (ke::LinkedList::iterator i(threads.begin()), end(threads.end()); i != end; ++i)
{
if ((*i) != NULL)
{
if ((*i)->GetState() == Thread_Done)
{
(*i)->DestroyThis();
i = this->threads.erase(i);
}
}
}
}
threads_.DoUnlock();
}
}
void HTTPSessionManager::Initialize()
{
m_pWorker = threader->MakeWorker(NULL, true);
m_pWorker->Start();
}
void HTTPSessionManager::HTTPAsyncRequestHandler::RunThread(IThreadHandle *pHandle)
{
HandleError herr;
IPlugin *parent = plsys->PluginFromHandle(request.plugin, &herr);
if (parent == NULL || herr == HandleError_Freed)
{
// The parent plugin got unloaded, no more work to do
return;
}
HandleSecurity sec;
sec.pOwner = parent->GetIdentity();
sec.pIdentity = myself->GetIdentity();
IWebTransfer *xfer = NULL;
g_pHandleSys->ReadHandle(this->request.handles.hndlSession,
g_SessionHandle, &sec, (void **)&xfer);
IBaseDownloader *downldr = NULL;
g_pHandleSys->ReadHandle(this->request.handles.hndlDownloader,
g_DownloadHandle, &sec, (void **)&downldr);
switch (this->request.method)
{
case HTTP_GET:
this->request.result =
xfer->Download(this->request.url, downldr, NULL);
break;
case HTTP_POST:
IWebForm *form = NULL;
g_pHandleSys->ReadHandle(this->request.handles.hndlForm,
g_FormHandle, &sec, (void **)&form);
this->request.result =
xfer->PostAndDownload(this->request.url, form, downldr, NULL);
break;
}
g_SessionManager.AddCallback(request);
}
void HTTPHandleDispatcher::OnHandleDestroy(HandleType_t type, void *object)
{
if (type == g_SessionHandle)
{
delete ((IWebTransfer *)object);
}
else if (type == g_DownloadHandle)
{
delete ((IBaseDownloader *)object);
}
else if (type == g_FormHandle)
{
delete ((IWebForm *)object);
}
}
IPlugin *FindPluginByContext(IPluginContext *pContext)
{
IPlugin *pFoundPlugin;
IPluginIterator *pPluginIterator = plsys->GetPluginIterator();
while (pPluginIterator->MorePlugins())
{
IPlugin *pPlugin = pPluginIterator->GetPlugin();
if (pPlugin->GetBaseContext() == pContext)
{
pFoundPlugin = pPlugin;
break;
}
pPluginIterator->NextPlugin();
}
pPluginIterator->Release();
return pFoundPlugin;
}