extensions/updater/Updater.cpp
author Asher Baker <asherkin@gmail.com>
Sun Aug 26 01:33:54 2012 +0100 (2012-08-26)
changeset 3630 b72450a8be42
parent 2783 5f333027ef04
child 4188 89782d5fc91b
permissions -rw-r--r--
Overhauled versioning information (bug 5453, r=dvander).
     1 /**
     2  * vim: set ts=4 :
     3  * =============================================================================
     4  * SourceMod Updater Extension
     5  * Copyright (C) 2004-2009 AlliedModders LLC.  All rights reserved.
     6  * =============================================================================
     7  *
     8  * This program is free software; you can redistribute it and/or modify it under
     9  * the terms of the GNU General Public License, version 3.0, as published by the
    10  * Free Software Foundation.
    11  * 
    12  * This program is distributed in the hope that it will be useful, but WITHOUT
    13  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    14  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
    15  * details.
    16  *
    17  * You should have received a copy of the GNU General Public License along with
    18  * this program.  If not, see <http://www.gnu.org/licenses/>.
    19  *
    20  * As a special exception, AlliedModders LLC gives you permission to link the
    21  * code of this program (as well as its derivative works) to "Half-Life 2," the
    22  * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
    23  * by the Valve Corporation.  You must obey the GNU General Public License in
    24  * all respects for all other code used.  Additionally, AlliedModders LLC grants
    25  * this exception to all derivative works.  AlliedModders LLC defines further
    26  * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
    27  * or <http://www.sourcemod.net/license.php>.
    28  *
    29  * Version: $Id$
    30  */
    31 
    32 #include <sourcemod_version.h>
    33 #include <stdlib.h>
    34 #include "extension.h"
    35 #include "Updater.h"
    36 #include "md5.h"
    37 
    38 #define USTATE_NONE			0
    39 #define USTATE_FOLDERS		1
    40 #define USTATE_CHANGED		2
    41 #define USTATE_CHANGE_FILE	3
    42 #define USTATE_ERRORS		4
    43 
    44 using namespace SourceMod;
    45 
    46 UpdateReader::UpdateReader() : partFirst(NULL), partLast(NULL)
    47 {
    48 }
    49 
    50 UpdateReader::~UpdateReader()
    51 {
    52 }
    53 
    54 void UpdateReader::ReadSMC_ParseStart()
    55 {
    56 	ignoreLevel = 0;
    57 	ustate = USTATE_NONE;
    58 }
    59 
    60 SMCResult UpdateReader::ReadSMC_NewSection(const SMCStates *states, const char *name)
    61 {
    62 	if (ignoreLevel)
    63 	{
    64 		ignoreLevel++;
    65 		return SMCResult_Continue;
    66 	}
    67 
    68 	switch (ustate)
    69 	{
    70 	case USTATE_NONE:
    71 		{
    72 			if (strcmp(name, "Folders") == 0)
    73 			{
    74 				ustate = USTATE_FOLDERS;
    75 			}
    76 			else if (strcmp(name, "Changed") == 0)
    77 			{
    78 				ustate = USTATE_CHANGED;
    79 			}
    80 			else if (strcmp(name, "Errors") == 0)
    81 			{
    82 				ustate = USTATE_ERRORS;
    83 			}
    84 			else
    85 			{
    86 				ignoreLevel++;
    87 			}
    88 			break;
    89 		}
    90 	case USTATE_FOLDERS:
    91 	case USTATE_CHANGE_FILE:
    92 		{
    93 			ignoreLevel++;
    94 			break;
    95 		}
    96 	case USTATE_CHANGED:
    97 		{
    98 			curfile.assign(name);
    99 			url.clear();
   100 			checksum[0] = '\0';
   101 			ustate = USTATE_CHANGE_FILE;
   102 			break;
   103 		}
   104 	}
   105 
   106 	return SMCResult_Continue;
   107 }
   108 
   109 SMCResult UpdateReader::ReadSMC_KeyValue(const SMCStates *states,
   110 										 const char *key,
   111 										 const char *value)
   112 {
   113 	if (ignoreLevel)
   114 	{
   115 		return SMCResult_Continue;
   116 	}
   117 
   118 	switch (ustate)
   119 	{
   120 	case USTATE_CHANGE_FILE:
   121 		{
   122 			if (strcmp(key, "md5sum") == 0)
   123 			{
   124 				if (strlen(value) != 32)
   125 				{
   126 					return SMCResult_Continue;
   127 				}
   128 				strcpy(checksum, value);
   129 			}
   130 			else if (strcmp(key, "location") == 0)
   131 			{
   132 				url.assign(update_url);
   133 				url.append(value);
   134 			}
   135 			break;
   136 		}
   137 	case USTATE_ERRORS:
   138 		{
   139 			if (strcmp(key, "error") == 0)
   140 			{
   141 				AddUpdateError("%s", value);
   142 			}
   143 			break;
   144 		}
   145 	case USTATE_FOLDERS:
   146 		{
   147 			HandleFolder(value);
   148 			break;
   149 		}
   150 	}
   151 
   152 	return SMCResult_Continue;
   153 }
   154 
   155 SMCResult UpdateReader::ReadSMC_LeavingSection(const SMCStates *states)
   156 {
   157 	if (ignoreLevel)
   158 	{
   159 		ignoreLevel--;
   160 		return SMCResult_Continue;
   161 	}
   162 
   163 	switch (ustate)
   164 	{
   165 	case USTATE_FOLDERS:
   166 	case USTATE_CHANGED:
   167 	case USTATE_ERRORS:
   168 		{
   169 			ustate = USTATE_NONE;
   170 			break;
   171 		}
   172 	case USTATE_CHANGE_FILE:
   173 		{
   174 			if (url.size() != 0 && checksum[0] != '\0')
   175 			{
   176 				HandleFile();
   177 			}
   178 			else
   179 			{
   180 				AddUpdateError("Incomplete file definition in update manifest");
   181 			}
   182 			ustate = USTATE_CHANGED;
   183 			break;
   184 		}
   185 	}
   186 
   187 	return SMCResult_Continue;
   188 }
   189 
   190 void UpdateReader::LinkPart(UpdatePart *part)
   191 {
   192 	part->next = NULL;
   193 	if (partFirst == NULL)
   194 	{
   195 		partFirst = part;
   196 		partLast = part;
   197 	}
   198 	else
   199 	{
   200 		partLast->next = part;
   201 		partLast = part;
   202 	}
   203 }
   204 
   205 void UpdateReader::HandleFile()
   206 {
   207 	MD5 md5;
   208 	char real_checksum[33];
   209 
   210 	mdl.Reset();
   211 	
   212 	if (!xfer->Download(url.c_str(), &mdl, NULL))
   213 	{
   214 		AddUpdateError("Could not download \"%s\"", url.c_str());
   215 		AddUpdateError("Error: %s", xfer->LastErrorMessage());
   216 		return;
   217 	}
   218 
   219 	md5.update((unsigned char *)mdl.GetBuffer(), mdl.GetSize());
   220 	md5.finalize();
   221 	md5.hex_digest(real_checksum);
   222 
   223 	if (mdl.GetSize() == 0)
   224 	{
   225 		AddUpdateError("Zero-length file returned for \"%s\"", curfile.c_str());
   226 		return;
   227 	}
   228 
   229 	if (strcasecmp(checksum, real_checksum) != 0)
   230 	{
   231 		AddUpdateError("Checksums for file \"%s\" do not match:", curfile.c_str());
   232 		AddUpdateError("Expected: %s Real: %s", checksum, real_checksum);
   233 		return;
   234 	}
   235 
   236 	UpdatePart *part = new UpdatePart;
   237 	part->data = (char*)malloc(mdl.GetSize());
   238 	memcpy(part->data, mdl.GetBuffer(), mdl.GetSize());
   239 	part->file = strdup(curfile.c_str());
   240 	part->length = mdl.GetSize();
   241 	LinkPart(part);
   242 }
   243 
   244 void UpdateReader::HandleFolder(const char *folder)
   245 {
   246 	UpdatePart *part = new UpdatePart;
   247 	part->data = NULL;
   248 	part->length = 0;
   249 	part->file = strdup(folder);
   250 	LinkPart(part);
   251 }
   252 
   253 static bool md5_file(const char *file, char checksum[33])
   254 {
   255 	MD5 md5;
   256 	FILE *fp;
   257 	long length;
   258 	void *fdata;
   259 
   260 	if ((fp = fopen(file, "rb")) == NULL)
   261 	{
   262 		return false;
   263 	}
   264 
   265 	fseek(fp, 0, SEEK_END);
   266 	length = ftell(fp);
   267 	fseek(fp, 0, SEEK_SET);
   268 	fdata = malloc(length);
   269 	if (fread(fdata, 1, length, fp) != size_t(length))
   270 	{
   271 		free(fdata);
   272 		fclose(fp);
   273 		return false;
   274 	}
   275 	fclose(fp);
   276 
   277 	md5.update((unsigned char*)fdata, length);
   278 	md5.finalize();
   279 	md5.hex_digest(checksum);
   280 
   281 	free(fdata);
   282 
   283 	return true;
   284 }
   285 
   286 /* Path should be sourcemod relative, not gamedata relative */
   287 static bool add_file(IWebForm *form, const char *file, unsigned int &num_files)
   288 {
   289 	char path[PLATFORM_MAX_PATH];
   290 
   291 	smutils->BuildPath(Path_SM, path, sizeof(path), "%s", file);
   292 
   293 	char checksum[33];
   294 	if (!md5_file(path, checksum))
   295 	{
   296 		return false;
   297 	}
   298 
   299 	char name[32];
   300 	smutils->Format(name, sizeof(name), "file_%d_name", num_files);
   301 	form->AddString(name, file);
   302 	smutils->Format(name, sizeof(name), "file_%d_md5", num_files);
   303 	form->AddString(name, checksum);
   304 
   305 	num_files++;
   306 
   307 	return true;
   308 }
   309 
   310 static void add_folders(IWebForm *form, const char *root, unsigned int &num_files)
   311 {
   312 	IDirectory *dir;
   313 	char path[PLATFORM_MAX_PATH];
   314 	char name[PLATFORM_MAX_PATH];
   315 
   316 	smutils->BuildPath(Path_SM, path, sizeof(path), "%s", root);
   317 	dir = libsys->OpenDirectory(path);
   318 	if (dir == NULL)
   319 	{
   320 		AddUpdateError("Could not open folder: %s", path);
   321 		return;
   322 	}
   323 
   324 	while (dir->MoreFiles())
   325 	{
   326 		if (strcmp(dir->GetEntryName(), ".") == 0 ||
   327 			strcmp(dir->GetEntryName(), "..") == 0)
   328 		{
   329 			dir->NextEntry();
   330 			continue;
   331 		}
   332 		smutils->Format(name, sizeof(name), "%s/%s", root, dir->GetEntryName());
   333 		if (dir->IsEntryDirectory())
   334 		{
   335 			add_folders(form, name, num_files);
   336 		}
   337 		else if (dir->IsEntryFile())
   338 		{
   339 			add_file(form, name, num_files);
   340 		}
   341 		dir->NextEntry();
   342 	}
   343 
   344 	libsys->CloseDirectory(dir);
   345 }
   346 
   347 void UpdateReader::PerformUpdate(const char *url)
   348 {
   349 	IWebForm *form;
   350 	MemoryDownloader master;
   351 	SMCStates states = {0, 0};
   352 
   353 	update_url = url;
   354 
   355 	form = webternet->CreateForm();
   356 	xfer = webternet->CreateSession();
   357 	xfer->SetFailOnHTTPError(true);
   358 
   359 	form->AddString("version", SM_VERSION_STRING);
   360 
   361 	unsigned int num_files = 0;
   362 	add_folders(form, "gamedata", num_files);
   363 
   364 	char temp[24];
   365 	smutils->Format(temp, sizeof(temp), "%d", num_files);
   366 	form->AddString("files", temp);
   367 
   368 	if (!xfer->PostAndDownload(url, form, &master, NULL))
   369 	{
   370 		AddUpdateError("Could not download \"%s\"", url);
   371 		AddUpdateError("Error: %s", xfer->LastErrorMessage());
   372 		goto cleanup;
   373 	}
   374 	
   375 	SMCError error;
   376 	char errbuf[256];
   377 	error = textparsers->ParseSMCStream(master.GetBuffer(),
   378 										master.GetSize(),
   379 										this,
   380 										&states,
   381 										errbuf,
   382 										sizeof(errbuf));
   383 	if (error != SMCError_Okay)
   384 	{
   385 		AddUpdateError("Parse error in update manifest: %s", errbuf);
   386 		goto cleanup;
   387 	}
   388 
   389 cleanup:
   390 	delete xfer;
   391 	delete form;
   392 }
   393 
   394 UpdatePart *UpdateReader::DetachParts()
   395 {
   396 	UpdatePart *first;
   397 
   398 	first = partFirst;
   399 	partFirst = NULL;
   400 	partLast = NULL;
   401 
   402 	return first;
   403 }
   404