/*==LICENSE==*
CyanWorlds.com Engine - MMOG client, server and tools
Copyright (C) 2011 Cyan Worlds, Inc.
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 3 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, see .
Additional permissions under GNU GPL version 3 section 7
If you modify this Program, or any covered work, by linking or
combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK,
NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent
JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK
(or a modified version of those libraries),
containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA,
PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG
JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the
licensors of this Program grant you additional
permission to convey the resulting work. Corresponding Source for a
non-source form of such a combination shall include the source code for
the parts of OpenSSL and IJG JPEG Library used as well as that of the covered
work.
You can contact Cyan Worlds, Inc. by email legal@cyan.com
or by snail mail at:
Cyan Worlds, Inc.
14617 N Newport Hwy
Mead, WA 99021
*==LICENSE==*/
#include "hsSTLStream.h"
#include "hsResMgr.h"
#include "plgDispatch.h"
#include "../pnUtils/pnUtils.h"
#include "../pnNetBase/pnNetBase.h"
#include "../pnAsyncCore/pnAsyncCore.h"
#include "../pnNetCli/pnNetCli.h"
#include "../plNetGameLib/plNetGameLib.h"
#include "../plFile/plFileUtils.h"
#include "../plFile/plStreamSource.h"
#include "../plNetCommon/plNetCommon.h"
#include "../plProgressMgr/plProgressMgr.h"
#include "../plMessage/plPreloaderMsg.h"
#include "../plMessage/plNetCommMsgs.h"
#include "pfSecurePreloader.h"
#include "../plNetClientComm/plNetClientComm.h"
extern hsBool gDataServerLocal;
// Max number of concurrent file downloads
static const unsigned kMaxConcurrency = 1;
pfSecurePreloader * pfSecurePreloader::fInstance;
///////////////////////////////////////////////////////////////////////////////
// Callback routines for the network code
// Called when a file's info is retrieved from the server
static void DefaultFileListRequestCallback(ENetError result, void* param, const NetCliAuthFileInfo infoArr[], unsigned infoCount)
{
bool success = !IS_NET_ERROR(result);
std::vector filenames;
std::vector sizes;
if (success)
{
filenames.reserve(infoCount);
sizes.reserve(infoCount);
for (unsigned curFile = 0; curFile < infoCount; curFile++)
{
filenames.push_back(infoArr[curFile].filename);
sizes.push_back(infoArr[curFile].filesize);
}
}
((pfSecurePreloader*)param)->RequestFinished(filenames, sizes, success);
}
// Called when a file download is either finished, or failed
static void DefaultFileRequestCallback(ENetError result, void* param, const wchar filename[], hsStream* stream)
{
// Retry download unless shutting down or file not found
switch (result) {
case kNetSuccess:
((pfSecurePreloader*)param)->FinishedDownload(filename, true);
break;
case kNetErrFileNotFound:
case kNetErrRemoteShutdown:
((pfSecurePreloader*)param)->FinishedDownload(filename, false);
break;
default:
stream->Rewind();
NetCliAuthFileRequest(
filename,
stream,
&DefaultFileRequestCallback,
param
);
break;
}
}
///////////////////////////////////////////////////////////////////////////////
// Our custom stream for writing directly to disk securely, and updating the
// progress bar. Does NOT support reading (cause it doesn't need to)
class Direct2DiskStream : public hsUNIXStream
{
protected:
wchar * fWriteFileName;
pfSecurePreloader* fPreloader;
public:
Direct2DiskStream(pfSecurePreloader* preloader);
~Direct2DiskStream();
virtual hsBool Open(const char* name, const char* mode = "wb");
virtual hsBool Open(const wchar* name, const wchar* mode = L"wb");
virtual hsBool Close();
virtual UInt32 Read(UInt32 byteCount, void* buffer);
virtual UInt32 Write(UInt32 byteCount, const void* buffer);
};
Direct2DiskStream::Direct2DiskStream(pfSecurePreloader* preloader) :
fWriteFileName(nil),
fPreloader(preloader)
{}
Direct2DiskStream::~Direct2DiskStream()
{
Close();
}
hsBool Direct2DiskStream::Open(const char* name, const char* mode)
{
wchar* wName = hsStringToWString(name);
wchar* wMode = hsStringToWString(mode);
hsBool ret = Open(wName, wMode);
delete [] wName;
delete [] wMode;
return ret;
}
hsBool Direct2DiskStream::Open(const wchar* name, const wchar* mode)
{
if (0 != wcscmp(mode, L"wb")) {
hsAssert(0, "Unsupported open mode");
return false;
}
fWriteFileName = TRACKED_NEW(wchar[wcslen(name) + 1]);
wcscpy(fWriteFileName, name);
// LogMsg(kLogPerf, L"Opening disk file %S", fWriteFileName);
return hsUNIXStream::Open(name, mode);
}
hsBool Direct2DiskStream::Close()
{
delete [] fWriteFileName;
fWriteFileName = nil;
return hsUNIXStream::Close();
}
UInt32 Direct2DiskStream::Read(UInt32 bytes, void* buffer)
{
hsAssert(0, "not implemented");
return 0; // we don't read
}
UInt32 Direct2DiskStream::Write(UInt32 bytes, const void* buffer)
{
// LogMsg(kLogPerf, L"Writing %u bytes to disk file %S", bytes, fWriteFileName);
fPreloader->UpdateProgressBar(bytes);
return hsUNIXStream::Write(bytes, buffer);
}
///////////////////////////////////////////////////////////////////////////////
// secure preloader class implementation
// closes and deletes all streams
void pfSecurePreloader::ICleanupStreams()
{
if (fD2DStreams.size() > 0)
{
std::map::iterator curStream;
for (curStream = fD2DStreams.begin(); curStream != fD2DStreams.end(); curStream++)
{
curStream->second->Close();
delete curStream->second;
curStream->second = nil;
}
fD2DStreams.clear();
}
}
// queues a single file to be preloaded (does nothing if already preloaded)
void pfSecurePreloader::RequestSingleFile(std::wstring filename)
{
fileRequest request;
ZERO(request);
request.fType = fileRequest::kSingleFile;
request.fPath = filename;
request.fExt = L"";
fRequests.push_back(request);
}
// queues a group of files to be preloaded (does nothing if already preloaded)
void pfSecurePreloader::RequestFileGroup(std::wstring dir, std::wstring ext)
{
fileRequest request;
ZERO(request);
request.fType = fileRequest::kFileList;
request.fPath = dir;
request.fExt = ext;
fRequests.push_back(request);
}
// preloads all requested files from the server (does nothing if already preloaded)
void pfSecurePreloader::Start()
{
if (gDataServerLocal) {
// using local data, don't do anything
plPreloaderMsg * msg = TRACKED_NEW plPreloaderMsg();
msg->fSuccess = true;
msg->Send();
return;
}
NetCliAuthGetEncryptionKey(fEncryptionKey, 4); // grab the encryption key from the server
fNetError = false;
// make sure we are all cleaned up
ICleanupStreams();
fTotalDataReceived = 0;
// update the progress bar for downloading
if (!fProgressBar)
fProgressBar = plProgressMgr::GetInstance()->RegisterOperation((hsScalar)(fRequests.size()), "Getting file info...", plProgressMgr::kUpdateText, false, true);
for (unsigned curRequest = 0; curRequest < fRequests.size(); curRequest++)
{
fNumInfoRequestsRemaining++; // increment the counter
if (fRequests[curRequest].fType == fileRequest::kSingleFile)
{
#ifndef PLASMA_EXTERNAL_RELEASE
// in internal releases, we can use on-disk files if they exist
if (plFileUtils::FileExists(fRequests[curRequest].fPath.c_str()))
{
fileInfo info;
info.fOriginalNameAndPath = fRequests[curRequest].fPath;
info.fSizeInBytes = plFileUtils::GetFileSize(info.fOriginalNameAndPath.c_str());
info.fDownloading = false;
info.fDownloaded = false;
info.fLocal = true;
// generate garbled name
wchar_t pathBuffer[MAX_PATH + 1];
wchar_t filename[arrsize(pathBuffer)];
GetTempPathW(arrsize(pathBuffer), pathBuffer);
GetTempFileNameW(pathBuffer, L"CYN", 0, filename);
info.fGarbledNameAndPath = filename;
fTotalDataDownload += info.fSizeInBytes;
fFileInfoMap[info.fOriginalNameAndPath] = info;
}
// internal client will still request it, even if it exists locally,
// so that things get updated properly
#endif // PLASMA_EXTERNAL_RELEASE
NetCliAuthFileListRequest(
fRequests[curRequest].fPath.c_str(),
nil,
&DefaultFileListRequestCallback,
(void*)this
);
}
else
{
#ifndef PLASMA_EXTERNAL_RELEASE
// in internal releases, we can use on-disk files if they exist
// Build the search string as "dir\\*.ext"
wchar searchStr[MAX_PATH];
PathAddFilename(searchStr, fRequests[curRequest].fPath.c_str(), L"*", arrsize(searchStr));
PathSetExtension(searchStr, searchStr, fRequests[curRequest].fExt.c_str(), arrsize(searchStr));
ARRAY(PathFind) paths;
PathFindFiles(&paths, searchStr, kPathFlagFile); // find all files that match
// convert it to our little file info array
PathFind* curFile = paths.Ptr();
PathFind* lastFile = paths.Term();
while (curFile != lastFile) {
fileInfo info;
info.fOriginalNameAndPath = curFile->name;
info.fSizeInBytes = (UInt32)curFile->fileLength;
info.fDownloading = false;
info.fDownloaded = false;
info.fLocal = true;
// generate garbled name
wchar_t pathBuffer[MAX_PATH + 1];
wchar_t filename[arrsize(pathBuffer)];
GetTempPathW(arrsize(pathBuffer), pathBuffer);
GetTempFileNameW(pathBuffer, L"CYN", 0, filename);
info.fGarbledNameAndPath = filename;
fTotalDataDownload += info.fSizeInBytes;
fFileInfoMap[info.fOriginalNameAndPath] = info;
curFile++;
}
#endif // PLASMA_EXTERNAL_RELEASE
NetCliAuthFileListRequest(
fRequests[curRequest].fPath.c_str(),
fRequests[curRequest].fExt.c_str(),
&DefaultFileListRequestCallback,
(void*)this
);
}
}
}
// closes all file pointers and cleans up after itself
void pfSecurePreloader::Cleanup()
{
ICleanupStreams();
fRequests.clear();
fFileInfoMap.clear();
fNumInfoRequestsRemaining = 0;
fTotalDataDownload = 0;
fTotalDataReceived = 0;
DEL(fProgressBar);
fProgressBar = nil;
}
//============================================================================
void pfSecurePreloader::RequestFinished(const std::vector & filenames, const std::vector & sizes, bool succeeded)
{
fNetError |= !succeeded;
if (succeeded)
{
unsigned count = 0;
for (int curFile = 0; curFile < filenames.size(); curFile++)
{
if (fFileInfoMap.find(filenames[curFile]) != fFileInfoMap.end())
continue; // if it is a duplicate, ignore it (the duplicate is probably one we found locally)
fileInfo info;
info.fOriginalNameAndPath = filenames[curFile];
info.fSizeInBytes = sizes[curFile];
info.fDownloading = false;
info.fDownloaded = false;
info.fLocal = false; // if we get here, it was retrieved remotely
// generate garbled name
wchar_t pathBuffer[MAX_PATH + 1];
wchar_t filename[arrsize(pathBuffer)];
GetTempPathW(arrsize(pathBuffer), pathBuffer);
GetTempFileNameW(pathBuffer, L"CYN", 0, filename);
info.fGarbledNameAndPath = filename;
fTotalDataDownload += info.fSizeInBytes;
fFileInfoMap[info.fOriginalNameAndPath] = info;
++count;
}
LogMsg(kLogPerf, "Added %u files to secure download queue", count);
}
if (fProgressBar)
fProgressBar->Increment(1.f);
--fNumInfoRequestsRemaining; // even if we fail, decrement the counter
if (succeeded) {
DEL(fProgressBar);
fProgressBar = plProgressMgr::GetInstance()->RegisterOperation((hsScalar)(fTotalDataDownload), "Downloading...", plProgressMgr::kUpdateText, false, true);
// Issue some file download requests (up to kMaxConcurrency)
IIssueDownloadRequests();
}
else {
IPreloadComplete();
}
}
//============================================================================
void pfSecurePreloader::IIssueDownloadRequests () {
std::map::iterator curFile;
for (curFile = fFileInfoMap.begin(); curFile != fFileInfoMap.end(); curFile++)
{
// Skip files already downloaded or currently downloading
if (curFile->second.fDownloaded || curFile->second.fDownloading)
continue;
std::wstring filename = curFile->second.fOriginalNameAndPath;
#ifndef PLASMA_EXTERNAL_RELEASE
// in internal releases, we can use on-disk files if they exist
if (plFileUtils::FileExists(filename.c_str()))
{
// don't bother streaming, just make the secure stream using the local file
// a local key overrides the server-downloaded key
UInt32 localKey[4];
bool hasLocalKey = plFileUtils::GetSecureEncryptionKey(filename.c_str(), localKey, arrsize(localKey));
hsStream* stream = nil;
if (hasLocalKey)
stream = plSecureStream::OpenSecureFile(filename.c_str(), 0, localKey);
else
stream = plSecureStream::OpenSecureFile(filename.c_str(), 0, fEncryptionKey);
// add it to the stream source
bool added = plStreamSource::GetInstance()->InsertFile(filename.c_str(), stream);
if (!added)
DEL(stream); // wasn't added, so nuke our local copy
// and make sure the vars are set up right
curFile->second.fDownloaded = true;
curFile->second.fLocal = true;
}
else
#endif
{
// Enforce concurrency limit
if (fNumDownloadRequestsRemaining >= kMaxConcurrency)
break;
curFile->second.fDownloading = true;
curFile->second.fDownloaded = false;
curFile->second.fLocal = false;
// create and setup the stream
Direct2DiskStream* fileStream = TRACKED_NEW Direct2DiskStream(this);
fileStream->Open(curFile->second.fGarbledNameAndPath.c_str(), L"wb");
fD2DStreams[filename] = (hsStream*)fileStream;
// request the file from the server
LogMsg(kLogPerf, L"Requesting secure file:%s", filename.c_str());
++fNumDownloadRequestsRemaining;
NetCliAuthFileRequest(
filename.c_str(),
(hsStream*)fileStream,
&DefaultFileRequestCallback,
this
);
}
}
if (!fNumDownloadRequestsRemaining)
IPreloadComplete();
}
void pfSecurePreloader::UpdateProgressBar(UInt32 bytesReceived)
{
fTotalDataReceived += bytesReceived;
if (fTotalDataReceived > fTotalDataDownload)
fTotalDataReceived = fTotalDataDownload; // shouldn't happen... but just in case
if (fProgressBar)
fProgressBar->Increment((hsScalar)bytesReceived);
}
void pfSecurePreloader::FinishedDownload(std::wstring filename, bool succeeded)
{
for (;;)
{
if (fFileInfoMap.find(filename) == fFileInfoMap.end())
{
// file doesn't exist... abort
succeeded = false;
break;
}
fFileInfoMap[filename].fDownloading = false;
// close and delete the writer stream (even if we failed)
fD2DStreams[filename]->Close();
delete fD2DStreams[filename];
fD2DStreams.erase(fD2DStreams.find(filename));
if (succeeded)
{
// open a secure stream to that file
hsStream* stream = plSecureStream::OpenSecureFile(
fFileInfoMap[filename].fGarbledNameAndPath.c_str(),
plSecureStream::kRequireEncryption | plSecureStream::kDeleteOnExit, // force delete and encryption
fEncryptionKey
);
bool addedToSource = plStreamSource::GetInstance()->InsertFile(filename.c_str(), stream);
if (!addedToSource)
DEL(stream); // cleanup if it wasn't added
fFileInfoMap[filename].fDownloaded = true;
break;
}
// file download failed, clean up after it
// delete the temporary file
if (plFileUtils::FileExists(fFileInfoMap[filename].fGarbledNameAndPath.c_str()))
plFileUtils::RemoveFile(fFileInfoMap[filename].fGarbledNameAndPath.c_str(), true);
// and remove it from the info map
fFileInfoMap.erase(fFileInfoMap.find(filename));
break;
}
fNetError |= !succeeded;
--fNumDownloadRequestsRemaining;
LogMsg(kLogPerf, L"Received secure file:%s, success:%s", filename.c_str(), succeeded ? L"Yep" : L"Nope");
if (!succeeded)
IPreloadComplete();
else
// Issue some file download requests (up to kMaxConcurrency)
IIssueDownloadRequests();
}
//============================================================================
void pfSecurePreloader::INotifyAuthReconnected () {
// The secure file download network protocol will now just pick up downloading
// where it left off before the reconnect, so no need to reset in-progress files.
/*
std::map::iterator curFile;
for (curFile = fFileInfoMap.begin(); curFile != fFileInfoMap.end(); curFile++) {
// Reset files that were currently downloading
if (curFile->second.fDownloading)
curFile->second.fDownloading = false;
}
if (fNumDownloadRequestsRemaining > 0) {
LogMsg(kLogPerf, L"pfSecurePreloader: Auth reconnected, resetting in-progress file downloads");
// Issue some file download requests (up to kMaxConcurrency)
IIssueDownloadRequests();
}
*/
}
//============================================================================
void pfSecurePreloader::IPreloadComplete () {
DEL(fProgressBar);
fProgressBar = nil;
plPreloaderMsg * msg = TRACKED_NEW plPreloaderMsg();
msg->fSuccess = !fNetError;
msg->Send();
}
//============================================================================
hsBool pfSecurePreloader::MsgReceive (plMessage * msg) {
if (plNetCommAuthConnectedMsg * authMsg = plNetCommAuthConnectedMsg::ConvertNoRef(msg)) {
INotifyAuthReconnected();
return true;
}
return hsKeyedObject::MsgReceive(msg);
}
//============================================================================
pfSecurePreloader * pfSecurePreloader::GetInstance () {
if (!fInstance) {
fInstance = NEWZERO(pfSecurePreloader);
fInstance->RegisterAs(kSecurePreloader_KEY);
}
return fInstance;
}
//============================================================================
bool pfSecurePreloader::IsInstanced () {
return fInstance != nil;
}
//============================================================================
void pfSecurePreloader::Init () {
if (!fInitialized) {
fInitialized = true;
plgDispatch::Dispatch()->RegisterForExactType(plNetCommAuthConnectedMsg::Index(), GetKey());
}
}
//============================================================================
void pfSecurePreloader::Shutdown () {
if (fInitialized) {
fInitialized = false;
plgDispatch::Dispatch()->UnRegisterForExactType(plNetCommAuthConnectedMsg::Index(), GetKey());
}
if (fInstance) {
fInstance->UnRegister();
fInstance = nil;
}
}
//============================================================================
pfSecurePreloader::pfSecurePreloader () {
}
//============================================================================
pfSecurePreloader::~pfSecurePreloader () {
Cleanup();
}