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: