dvander@2562: /** dvander@2562: * vim: set ts=4 : dvander@2562: * ============================================================================= dvander@2562: * SourceMod Updater Extension dvander@2562: * Copyright (C) 2004-2009 AlliedModders LLC. All rights reserved. dvander@2562: * ============================================================================= dvander@2562: * dvander@2562: * This program is free software; you can redistribute it and/or modify it under dvander@2562: * the terms of the GNU General Public License, version 3.0, as published by the dvander@2562: * Free Software Foundation. dvander@2562: * dvander@2562: * This program is distributed in the hope that it will be useful, but WITHOUT dvander@2562: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS dvander@2562: * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more dvander@2562: * details. dvander@2562: * dvander@2562: * You should have received a copy of the GNU General Public License along with dvander@2562: * this program. If not, see . dvander@2562: * dvander@2562: * As a special exception, AlliedModders LLC gives you permission to link the dvander@2562: * code of this program (as well as its derivative works) to "Half-Life 2," the dvander@2562: * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software dvander@2562: * by the Valve Corporation. You must obey the GNU General Public License in dvander@2562: * all respects for all other code used. Additionally, AlliedModders LLC grants dvander@2562: * this exception to all derivative works. AlliedModders LLC defines further dvander@2562: * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), dvander@2562: * or . dvander@2562: * dvander@2562: * Version: $Id$ dvander@2562: */ dvander@2562: dvander@2783: #include dvander@2562: #include dvander@2562: #include "extension.h" dvander@2562: #include "Updater.h" dvander@2562: #include "md5.h" dvander@2562: dvander@2562: #define USTATE_NONE 0 dvander@2562: #define USTATE_FOLDERS 1 dvander@2562: #define USTATE_CHANGED 2 dvander@2562: #define USTATE_CHANGE_FILE 3 dvander@2630: #define USTATE_ERRORS 4 dvander@2562: dvander@2562: using namespace SourceMod; dvander@2562: dvander@2573: UpdateReader::UpdateReader() : partFirst(NULL), partLast(NULL) dvander@2562: { dvander@2562: } dvander@2562: dvander@2562: UpdateReader::~UpdateReader() dvander@2562: { dvander@2562: } dvander@2562: dvander@2562: void UpdateReader::ReadSMC_ParseStart() dvander@2562: { dvander@2562: ignoreLevel = 0; dvander@2562: ustate = USTATE_NONE; dvander@2562: } dvander@2562: dvander@2562: SMCResult UpdateReader::ReadSMC_NewSection(const SMCStates *states, const char *name) dvander@2562: { dvander@2562: if (ignoreLevel) dvander@2562: { dvander@2562: ignoreLevel++; dvander@2562: return SMCResult_Continue; dvander@2562: } dvander@2562: dvander@2562: switch (ustate) dvander@2562: { dvander@2562: case USTATE_NONE: dvander@2562: { dvander@2562: if (strcmp(name, "Folders") == 0) dvander@2562: { dvander@2562: ustate = USTATE_FOLDERS; dvander@2562: } dvander@2562: else if (strcmp(name, "Changed") == 0) dvander@2562: { dvander@2562: ustate = USTATE_CHANGED; dvander@2562: } dvander@2630: else if (strcmp(name, "Errors") == 0) dvander@2630: { dvander@2630: ustate = USTATE_ERRORS; dvander@2630: } dvander@2562: else dvander@2562: { dvander@2562: ignoreLevel++; dvander@2562: } dvander@2562: break; dvander@2562: } dvander@2562: case USTATE_FOLDERS: dvander@2562: case USTATE_CHANGE_FILE: dvander@2562: { dvander@2562: ignoreLevel++; dvander@2562: break; dvander@2562: } dvander@2562: case USTATE_CHANGED: dvander@2562: { dvander@2562: curfile.assign(name); dvander@2562: url.clear(); dvander@2562: checksum[0] = '\0'; dvander@2562: ustate = USTATE_CHANGE_FILE; dvander@2562: break; dvander@2562: } dvander@2562: } dvander@2562: dvander@2562: return SMCResult_Continue; dvander@2562: } dvander@2562: dvander@2562: SMCResult UpdateReader::ReadSMC_KeyValue(const SMCStates *states, dvander@2562: const char *key, dvander@2562: const char *value) dvander@2562: { dvander@2562: if (ignoreLevel) dvander@2562: { dvander@2562: return SMCResult_Continue; dvander@2562: } dvander@2562: dvander@2562: switch (ustate) dvander@2562: { dvander@2562: case USTATE_CHANGE_FILE: dvander@2562: { dvander@2562: if (strcmp(key, "md5sum") == 0) dvander@2562: { dvander@2562: if (strlen(value) != 32) dvander@2562: { dvander@2562: return SMCResult_Continue; dvander@2562: } dvander@2562: strcpy(checksum, value); dvander@2562: } dvander@2562: else if (strcmp(key, "location") == 0) dvander@2562: { dvander@2575: url.assign(update_url); dvander@2562: url.append(value); dvander@2562: } dvander@2562: break; dvander@2562: } dvander@2630: case USTATE_ERRORS: dvander@2630: { dvander@2630: if (strcmp(key, "error") == 0) dvander@2630: { dvander@2630: AddUpdateError("%s", value); dvander@2630: } dvander@2630: break; dvander@2630: } dvander@2562: case USTATE_FOLDERS: dvander@2562: { dvander@2562: HandleFolder(value); dvander@2562: break; dvander@2562: } dvander@2562: } dvander@2562: dvander@2562: return SMCResult_Continue; dvander@2562: } dvander@2562: dvander@2562: SMCResult UpdateReader::ReadSMC_LeavingSection(const SMCStates *states) dvander@2562: { dvander@2562: if (ignoreLevel) dvander@2562: { dvander@2562: ignoreLevel--; dvander@2562: return SMCResult_Continue; dvander@2562: } dvander@2562: dvander@2562: switch (ustate) dvander@2562: { dvander@2562: case USTATE_FOLDERS: dvander@2562: case USTATE_CHANGED: dvander@2630: case USTATE_ERRORS: dvander@2562: { dvander@2562: ustate = USTATE_NONE; dvander@2562: break; dvander@2562: } dvander@2562: case USTATE_CHANGE_FILE: dvander@2562: { dvander@2562: if (url.size() != 0 && checksum[0] != '\0') dvander@2562: { dvander@2562: HandleFile(); dvander@2562: } dvander@2562: else dvander@2562: { dvander@2562: AddUpdateError("Incomplete file definition in update manifest"); dvander@2562: } dvander@2562: ustate = USTATE_CHANGED; dvander@2562: break; dvander@2562: } dvander@2562: } dvander@2562: dvander@2562: return SMCResult_Continue; dvander@2562: } dvander@2562: dvander@2573: void UpdateReader::LinkPart(UpdatePart *part) dvander@2573: { dvander@2573: part->next = NULL; dvander@2573: if (partFirst == NULL) dvander@2573: { dvander@2573: partFirst = part; dvander@2573: partLast = part; dvander@2573: } dvander@2573: else dvander@2573: { dvander@2573: partLast->next = part; dvander@2573: partLast = part; dvander@2573: } dvander@2573: } dvander@2573: dvander@2562: void UpdateReader::HandleFile() dvander@2562: { dvander@2562: MD5 md5; dvander@2562: char real_checksum[33]; dvander@2562: dvander@2562: mdl.Reset(); dvander@2562: dvander@2562: if (!xfer->Download(url.c_str(), &mdl, NULL)) dvander@2562: { dvander@2562: AddUpdateError("Could not download \"%s\"", url.c_str()); dvander@2562: AddUpdateError("Error: %s", xfer->LastErrorMessage()); dvander@2562: return; dvander@2562: } dvander@2562: dvander@2562: md5.update((unsigned char *)mdl.GetBuffer(), mdl.GetSize()); dvander@2562: md5.finalize(); dvander@2562: md5.hex_digest(real_checksum); dvander@2562: dvander@2573: if (mdl.GetSize() == 0) dvander@2573: { dvander@2573: AddUpdateError("Zero-length file returned for \"%s\"", curfile.c_str()); dvander@2573: return; dvander@2573: } dvander@2573: dvander@2562: if (strcasecmp(checksum, real_checksum) != 0) dvander@2562: { dvander@2562: AddUpdateError("Checksums for file \"%s\" do not match:", curfile.c_str()); dvander@2562: AddUpdateError("Expected: %s Real: %s", checksum, real_checksum); dvander@2562: return; dvander@2562: } dvander@2562: dvander@2573: UpdatePart *part = new UpdatePart; dvander@2573: part->data = (char*)malloc(mdl.GetSize()); dvander@2630: memcpy(part->data, mdl.GetBuffer(), mdl.GetSize()); dvander@2573: part->file = strdup(curfile.c_str()); dvander@2573: part->length = mdl.GetSize(); dvander@2573: LinkPart(part); dvander@2562: } dvander@2562: dvander@2562: void UpdateReader::HandleFolder(const char *folder) dvander@2562: { dvander@2573: UpdatePart *part = new UpdatePart; dvander@2573: part->data = NULL; dvander@2573: part->length = 0; dvander@2573: part->file = strdup(folder); dvander@2573: LinkPart(part); dvander@2562: } dvander@2562: dvander@2562: static bool md5_file(const char *file, char checksum[33]) dvander@2562: { dvander@2562: MD5 md5; dvander@2562: FILE *fp; dvander@2562: long length; dvander@2562: void *fdata; dvander@2562: dvander@2630: if ((fp = fopen(file, "rb")) == NULL) dvander@2562: { dvander@2562: return false; dvander@2562: } dvander@2562: dvander@2562: fseek(fp, 0, SEEK_END); dvander@2562: length = ftell(fp); dvander@2562: fseek(fp, 0, SEEK_SET); dvander@2562: fdata = malloc(length); dvander@2562: if (fread(fdata, 1, length, fp) != size_t(length)) dvander@2562: { dvander@2562: free(fdata); dvander@2562: fclose(fp); dvander@2562: return false; dvander@2562: } dvander@2562: fclose(fp); dvander@2562: dvander@2562: md5.update((unsigned char*)fdata, length); dvander@2562: md5.finalize(); dvander@2562: md5.hex_digest(checksum); dvander@2562: dvander@2562: free(fdata); dvander@2562: dvander@2562: return true; dvander@2562: } dvander@2562: dvander@2562: /* Path should be sourcemod relative, not gamedata relative */ dvander@2562: static bool add_file(IWebForm *form, const char *file, unsigned int &num_files) dvander@2562: { dvander@2562: char path[PLATFORM_MAX_PATH]; dvander@2562: dvander@2562: smutils->BuildPath(Path_SM, path, sizeof(path), "%s", file); dvander@2562: dvander@2562: char checksum[33]; dvander@2562: if (!md5_file(path, checksum)) dvander@2562: { dvander@2562: return false; dvander@2562: } dvander@2562: dvander@2562: char name[32]; dvander@2562: smutils->Format(name, sizeof(name), "file_%d_name", num_files); dvander@2562: form->AddString(name, file); dvander@2562: smutils->Format(name, sizeof(name), "file_%d_md5", num_files); dvander@2562: form->AddString(name, checksum); dvander@2562: dvander@2562: num_files++; dvander@2562: dvander@2562: return true; dvander@2562: } dvander@2562: dvander@2562: static void add_folders(IWebForm *form, const char *root, unsigned int &num_files) dvander@2562: { dvander@2562: IDirectory *dir; dvander@2562: char path[PLATFORM_MAX_PATH]; dvander@2562: char name[PLATFORM_MAX_PATH]; dvander@2562: dvander@2562: smutils->BuildPath(Path_SM, path, sizeof(path), "%s", root); dvander@2562: dir = libsys->OpenDirectory(path); dvander@2562: if (dir == NULL) dvander@2562: { dvander@2562: AddUpdateError("Could not open folder: %s", path); dvander@2562: return; dvander@2562: } dvander@2562: dvander@2562: while (dir->MoreFiles()) dvander@2562: { dvander@2562: if (strcmp(dir->GetEntryName(), ".") == 0 || dvander@2562: strcmp(dir->GetEntryName(), "..") == 0) dvander@2562: { dvander@2562: dir->NextEntry(); dvander@2562: continue; dvander@2562: } dvander@2562: smutils->Format(name, sizeof(name), "%s/%s", root, dir->GetEntryName()); dvander@2562: if (dir->IsEntryDirectory()) dvander@2562: { dvander@2562: add_folders(form, name, num_files); dvander@2562: } dvander@2562: else if (dir->IsEntryFile()) dvander@2562: { dvander@2562: add_file(form, name, num_files); dvander@2562: } dvander@2562: dir->NextEntry(); dvander@2562: } dvander@2562: dvander@2562: libsys->CloseDirectory(dir); dvander@2562: } dvander@2562: dvander@2575: void UpdateReader::PerformUpdate(const char *url) dvander@2562: { dvander@2562: IWebForm *form; dvander@2562: MemoryDownloader master; dvander@2562: SMCStates states = {0, 0}; dvander@2562: dvander@2575: update_url = url; dvander@2575: dvander@2562: form = webternet->CreateForm(); dvander@2562: xfer = webternet->CreateSession(); dvander@2562: xfer->SetFailOnHTTPError(true); dvander@2562: asherkin@3630: form->AddString("version", SM_VERSION_STRING); dvander@2562: dvander@2562: unsigned int num_files = 0; dvander@2562: add_folders(form, "gamedata", num_files); dvander@2562: dvander@2562: char temp[24]; dvander@2562: smutils->Format(temp, sizeof(temp), "%d", num_files); dvander@2562: form->AddString("files", temp); dvander@2562: dvander@2575: if (!xfer->PostAndDownload(url, form, &master, NULL)) dvander@2562: { dvander@2575: AddUpdateError("Could not download \"%s\"", url); dvander@2562: AddUpdateError("Error: %s", xfer->LastErrorMessage()); dvander@2562: goto cleanup; dvander@2562: } dvander@2562: dvander@2562: SMCError error; dvander@2562: char errbuf[256]; dvander@2562: error = textparsers->ParseSMCStream(master.GetBuffer(), dvander@2562: master.GetSize(), dvander@2562: this, dvander@2562: &states, dvander@2562: errbuf, dvander@2562: sizeof(errbuf)); dvander@2562: if (error != SMCError_Okay) dvander@2562: { dvander@2562: AddUpdateError("Parse error in update manifest: %s", errbuf); dvander@2562: goto cleanup; dvander@2562: } dvander@2562: dvander@2562: cleanup: dvander@2562: delete xfer; dvander@2562: delete form; dvander@2562: } dvander@2573: dvander@2573: UpdatePart *UpdateReader::DetachParts() dvander@2573: { dvander@2573: UpdatePart *first; dvander@2573: dvander@2573: first = partFirst; dvander@2573: partFirst = NULL; dvander@2573: partLast = NULL; dvander@2573: dvander@2573: return first; dvander@2573: } dvander@2783: