소스 검색

updater: Add windows updater module

jp9000 8 년 전
부모
커밋
86862b672e

+ 3 - 0
UI/CMakeLists.txt

@@ -252,3 +252,6 @@ if (UNIX AND UNIX_STRUCTURE AND NOT APPLE)
 endif()
 
 add_subdirectory(frontend-plugins)
+if(WIN32)
+	add_subdirectory(win-update/updater)
+endif()

+ 50 - 0
UI/win-update/updater/CMakeLists.txt

@@ -0,0 +1,50 @@
+if(NOT ENABLE_WIN_UPDATER)
+	return()
+endif()
+
+if(NOT DEFINED STATIC_ZLIB_PATH OR "${STATIC_ZLIB_PATH}" STREQUAL "")
+	message(STATUS "STATIC_ZLIB_PATH not set, windows updater disabled")
+	return()
+endif()
+
+project(updater)
+
+include_directories(${OBS_JANSSON_INCLUDE_DIRS})
+include_directories(${LIBLZMA_INCLUDE_DIRS})
+include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/libobs")
+include_directories(${BLAKE2_INCLUDE_DIR})
+
+find_package(ZLIB REQUIRED)
+
+set(updater_HEADERS
+	../win-update-helpers.hpp
+	resource.h
+	updater.hpp
+	)
+set(updater_SOURCES
+	../win-update-helpers.cpp
+	updater.cpp
+	patch.cpp
+	http.cpp
+	hash.cpp
+	updater.rc
+	)
+
+add_definitions(-DNOMINMAX -DUNICODE -D_UNICODE)
+if(MSVC)
+	add_compile_options("$<$<CONFIG:RelWithDebInfo>:/MT>")
+endif()
+
+add_executable(updater WIN32
+	${updater_HEADERS}
+	${updater_SOURCES}
+	)
+target_link_libraries(updater
+	${OBS_JANSSON_IMPORT}
+	${STATIC_ZLIB_PATH}
+	lzma
+	blake2
+	comctl32
+	shell32
+	winhttp
+	)

+ 61 - 0
UI/win-update/updater/hash.cpp

@@ -0,0 +1,61 @@
+#include "updater.hpp"
+
+#include <util/windows/WinHandle.hpp>
+#include <vector>
+
+using namespace std;
+
+void HashToString(const uint8_t *in, wchar_t *out)
+{
+	const wchar_t alphabet[] = L"0123456789abcdef";
+
+	for (int i = 0; i != BLAKE2_HASH_LENGTH; ++i) {
+		out[2 * i]     = alphabet[in[i] / 16];
+		out[2 * i + 1] = alphabet[in[i] % 16];
+	}
+
+	out[BLAKE2_HASH_LENGTH * 2] = 0;
+}
+
+void StringToHash(const wchar_t *in, BYTE *out)
+{
+	int temp;
+
+	for (int i = 0; i < BLAKE2_HASH_LENGTH; i++) {
+		swscanf_s(in + i * 2, L"%02x", &temp);
+		out[i] = (BYTE)temp;
+	}
+}
+
+bool CalculateFileHash(const wchar_t *path, BYTE *hash)
+{
+	blake2b_state blake2;
+	if (blake2b_init(&blake2, BLAKE2_HASH_LENGTH) != 0)
+		return false;
+
+	WinHandle handle = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ,
+			nullptr, OPEN_EXISTING, 0, nullptr);
+	if (handle == INVALID_HANDLE_VALUE)
+		return false;
+
+	vector<BYTE> buf;
+	buf.resize(65536);
+
+	for (;;) {
+		DWORD read = 0;
+		if (!ReadFile(handle, buf.data(), (DWORD)buf.size(), &read,
+					nullptr))
+			return false;
+
+		if (!read)
+			break;
+
+		if (blake2b_update(&blake2, buf.data(), read) != 0)
+			return false;
+	}
+
+	if (blake2b_final(&blake2, hash, BLAKE2_HASH_LENGTH) != 0)
+		return false;
+
+	return true;
+}

+ 537 - 0
UI/win-update/updater/http.cpp

@@ -0,0 +1,537 @@
+#include "Updater.hpp"
+
+#include <algorithm>
+
+using namespace std;
+
+#define MAX_BUF_SIZE  262144
+#define READ_BUF_SIZE 32768
+
+/* ------------------------------------------------------------------------ */
+
+class ZipStream {
+	z_stream strm = {};
+	bool initialized = false;
+
+public:
+	inline ~ZipStream()
+	{
+		if (initialized)
+			inflateEnd(&strm);
+	}
+
+	inline operator z_stream*() {return &strm;}
+	inline z_stream *operator->() {return &strm;}
+
+	inline bool inflate()
+	{
+		int ret = inflateInit2(&strm, 16 + MAX_WBITS);
+		initialized = (ret == Z_OK);
+		return initialized;
+	}
+};
+
+/* ------------------------------------------------------------------------ */
+
+static bool ReadZippedHTTPData(string &responseBuf, z_stream *strm,
+		string &zipBuf, const uint8_t *buffer, DWORD outSize)
+{
+	do {
+		strm->avail_in = outSize;
+		strm->next_in  = buffer;
+
+		strm->avail_out = (uInt)zipBuf.size();
+		strm->next_out  = (Bytef *)zipBuf.data();
+
+		int zret = inflate(strm, Z_NO_FLUSH);
+		if (zret != Z_STREAM_END && zret != Z_OK)
+			return false;
+
+		try {
+			responseBuf.append(zipBuf.data(),
+					zipBuf.size() - strm->avail_out);
+		} catch (...) {
+			return false;
+		}
+	} while (strm->avail_out == 0);
+
+	return true;
+}
+
+static bool ReadHTTPData(string &responseBuf, const uint8_t *buffer,
+		DWORD outSize)
+{
+	try {
+		responseBuf.append((const char *)buffer, outSize);
+	} catch (...) {
+		return false;
+	}
+	return true;
+}
+
+bool HTTPPostData(const wchar_t *url,
+                  const BYTE *   data,
+                  int            dataLen,
+                  const wchar_t *extraHeaders,
+                  int *          responseCode,
+                  string &       responseBuf)
+{
+	HttpHandle     hSession;
+	HttpHandle     hConnect;
+	HttpHandle     hRequest;
+	string         zipBuf;
+	URL_COMPONENTS urlComponents = {};
+	bool           secure        = false;
+
+	wchar_t hostName[256];
+	wchar_t path[1024];
+
+	const wchar_t *acceptTypes[] = {L"*/*", nullptr};
+
+	responseBuf.clear();
+
+	/* -------------------------------------- *
+	 * get URL components                     */
+
+	urlComponents.dwStructSize = sizeof(urlComponents);
+
+	urlComponents.lpszHostName     = hostName;
+	urlComponents.dwHostNameLength = _countof(hostName);
+
+	urlComponents.lpszUrlPath     = path;
+	urlComponents.dwUrlPathLength = _countof(path);
+
+	WinHttpCrackUrl(url, 0, 0, &urlComponents);
+
+	if (urlComponents.nPort == 443)
+		secure = true;
+
+	/* -------------------------------------- *
+	 * connect to server                      */
+
+	hSession = WinHttpOpen(L"OBS Updater/2.1",
+	                       WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
+	                       WINHTTP_NO_PROXY_NAME,
+	                       WINHTTP_NO_PROXY_BYPASS,
+	                       0);
+	if (!hSession) {
+		*responseCode = -1;
+		return false;
+	}
+
+	hConnect = WinHttpConnect(hSession, hostName,
+			secure
+			? INTERNET_DEFAULT_HTTPS_PORT
+			: INTERNET_DEFAULT_HTTP_PORT, 0);
+	if (!hConnect) {
+		*responseCode = -2;
+		return false;
+	}
+
+	/* -------------------------------------- *
+	 * request data                           */
+
+	hRequest = WinHttpOpenRequest(hConnect,
+	                              L"POST",
+	                              path,
+	                              nullptr,
+	                              WINHTTP_NO_REFERER,
+	                              acceptTypes,
+	                              secure
+	                              ? WINHTTP_FLAG_SECURE |
+	                                WINHTTP_FLAG_REFRESH
+	                              : WINHTTP_FLAG_REFRESH);
+	if (!hRequest) {
+		*responseCode = -3;
+		return false;
+	}
+
+	bool bResults = !!WinHttpSendRequest(hRequest, extraHeaders,
+			extraHeaders ? -1 : 0,
+			(void *)data, dataLen, dataLen, 0);
+
+	/* -------------------------------------- *
+	 * end request                            */
+
+	if (bResults) {
+		bResults = !!WinHttpReceiveResponse(hRequest, nullptr);
+	} else {
+		*responseCode = GetLastError();
+		return false;
+	}
+
+	/* -------------------------------------- *
+	 * get headers                            */
+
+	wchar_t encoding[64];
+	DWORD   encodingLen;
+
+	wchar_t statusCode[8];
+	DWORD   statusCodeLen;
+
+	statusCodeLen = sizeof(statusCode);
+	if (!WinHttpQueryHeaders(hRequest,
+	                         WINHTTP_QUERY_STATUS_CODE,
+	                         WINHTTP_HEADER_NAME_BY_INDEX,
+	                         &statusCode,
+	                         &statusCodeLen,
+	                         WINHTTP_NO_HEADER_INDEX)) {
+		*responseCode = -4;
+		return false;
+	} else {
+		statusCode[_countof(statusCode) - 1] = 0;
+	}
+
+	encodingLen = sizeof(encoding);
+	if (!WinHttpQueryHeaders(hRequest,
+	                         WINHTTP_QUERY_CONTENT_ENCODING,
+	                         WINHTTP_HEADER_NAME_BY_INDEX,
+	                         encoding,
+	                         &encodingLen,
+	                         WINHTTP_NO_HEADER_INDEX)) {
+		encoding[0] = 0;
+		if (GetLastError() != ERROR_WINHTTP_HEADER_NOT_FOUND) {
+			*responseCode = -5;
+			return false;
+		}
+	} else {
+		encoding[_countof(encoding) - 1] = 0;
+	}
+
+	/* -------------------------------------- *
+	 * allocate response data                 */
+
+	DWORD responseBufSize = MAX_BUF_SIZE;
+
+	try {
+		responseBuf.reserve(responseBufSize);
+	} catch (...) {
+		*responseCode = -6;
+		return false;
+	}
+
+	/* -------------------------------------- *
+	 * if zipped, initialize zip data         */
+
+	ZipStream strm;
+	bool gzip = wcscmp(encoding, L"gzip") == 0;
+
+	if (gzip) {
+		strm->zalloc   = Z_NULL;
+		strm->zfree    = Z_NULL;
+		strm->opaque   = Z_NULL;
+		strm->avail_in = 0;
+		strm->next_in  = Z_NULL;
+
+		if (!strm.inflate())
+			return false;
+
+		try {
+			zipBuf.resize(MAX_BUF_SIZE);
+		} catch (...) {
+			*responseCode = -6;
+			return false;
+		}
+	}
+
+	/* -------------------------------------- *
+	 * read data                              */
+
+	*responseCode = wcstoul(statusCode, nullptr, 10);
+
+	/* are we supposed to return true here? */
+	if (!bResults || *responseCode != 200)
+		return true;
+
+	BYTE buffer[READ_BUF_SIZE];
+	DWORD dwSize, outSize;
+
+	do {
+		/* Check for available data. */
+		dwSize = 0;
+		if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) {
+			*responseCode = -8;
+			return false;
+		}
+
+		dwSize = std::min(dwSize, (DWORD)sizeof(buffer));
+
+		if (!WinHttpReadData(hRequest, (void *)buffer, dwSize,
+					&outSize)) {
+			*responseCode = -9;
+			return false;
+		}
+
+		if (!outSize)
+			break;
+
+		if (gzip) {
+			if (!ReadZippedHTTPData(responseBuf, strm, zipBuf,
+						buffer, outSize)) {
+				*responseCode = -6;
+				return false;
+			}
+		} else {
+			if (!ReadHTTPData(responseBuf, buffer, outSize)) {
+				*responseCode = -6;
+				return false;
+			}
+		}
+
+		if (WaitForSingleObject(cancelRequested, 0) == WAIT_OBJECT_0) {
+			*responseCode = -14;
+			return false;
+		}
+	} while (dwSize > 0);
+
+	return true;
+}
+
+/* ------------------------------------------------------------------------ */
+
+static bool ReadHTTPZippedFile(z_stream *strm, HANDLE updateFile,
+		string &zipBuf, const uint8_t *buffer, DWORD outSize,
+		int *responseCode)
+{
+	do {
+		strm->avail_in = outSize;
+		strm->next_in  = buffer;
+
+		strm->avail_out = (uInt)zipBuf.size();
+		strm->next_out  = (Bytef *)zipBuf.data();
+
+		int zret = inflate(strm, Z_NO_FLUSH);
+		if (zret != Z_STREAM_END && zret != Z_OK)
+			return false;
+
+		DWORD written;
+		if (!WriteFile(updateFile,
+			       zipBuf.data(),
+			       MAX_BUF_SIZE - strm->avail_out,
+			       &written,
+			       nullptr)) {
+			*responseCode = -10;
+			return false;
+		}
+		if (written != MAX_BUF_SIZE - strm->avail_out) {
+			*responseCode = -11;
+			return false;
+		}
+
+		completedFileSize += written;
+	} while (strm->avail_out == 0);
+
+	return true;
+}
+
+static bool ReadHTTPFile(HANDLE updateFile, const uint8_t *buffer,
+		DWORD outSize, int *responseCode)
+{
+	DWORD written;
+	if (!WriteFile(updateFile, buffer, outSize, &written, nullptr)) {
+		*responseCode = -12;
+		return false;
+	}
+
+	if (written != outSize) {
+		*responseCode = -13;
+		return false;
+	}
+
+	completedFileSize += outSize;
+	return true;
+}
+
+bool HTTPGetFile(HINTERNET      hConnect,
+                 const wchar_t *url,
+                 const wchar_t *outputPath,
+                 const wchar_t *extraHeaders,
+                 int *          responseCode)
+{
+	HttpHandle hRequest;
+
+	const wchar_t *acceptTypes[] = {L"*/*", nullptr};
+
+	URL_COMPONENTS urlComponents = {};
+	bool           secure        = false;
+
+	string  zipBuf;
+	wchar_t hostName[256];
+	wchar_t path[1024];
+
+	/* -------------------------------------- *
+	 * get URL components                     */
+
+	urlComponents.dwStructSize = sizeof(urlComponents);
+
+	urlComponents.lpszHostName     = hostName;
+	urlComponents.dwHostNameLength = _countof(hostName);
+
+	urlComponents.lpszUrlPath     = path;
+	urlComponents.dwUrlPathLength = _countof(path);
+
+	WinHttpCrackUrl(url, 0, 0, &urlComponents);
+
+	if (urlComponents.nPort == 443)
+		secure = true;
+
+	/* -------------------------------------- *
+	 * request data                           */
+
+	hRequest = WinHttpOpenRequest(hConnect,
+	                              L"GET",
+	                              path,
+	                              nullptr,
+	                              WINHTTP_NO_REFERER,
+	                              acceptTypes,
+	                              secure
+	                              ? WINHTTP_FLAG_SECURE |
+	                                WINHTTP_FLAG_REFRESH
+	                              : WINHTTP_FLAG_REFRESH);
+	if (!hRequest) {
+		*responseCode = -3;
+		return false;
+	}
+
+	bool bResults = !!WinHttpSendRequest(hRequest, extraHeaders,
+			extraHeaders ? -1 : 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0);
+
+	/* -------------------------------------- *
+	 * end request                            */
+
+	if (bResults) {
+		bResults = !!WinHttpReceiveResponse(hRequest, nullptr);
+	} else {
+		*responseCode = GetLastError();
+		return false;
+	}
+
+	/* -------------------------------------- *
+	 * get headers                            */
+
+	wchar_t encoding[64];
+	DWORD   encodingLen;
+
+	wchar_t statusCode[8];
+	DWORD   statusCodeLen;
+
+	statusCodeLen = sizeof(statusCode);
+	if (!WinHttpQueryHeaders(hRequest,
+	                         WINHTTP_QUERY_STATUS_CODE,
+	                         WINHTTP_HEADER_NAME_BY_INDEX,
+	                         &statusCode,
+	                         &statusCodeLen,
+	                         WINHTTP_NO_HEADER_INDEX)) {
+		*responseCode = -4;
+		return false;
+	} else {
+		statusCode[_countof(statusCode) - 1] = 0;
+	}
+
+	encodingLen = sizeof(encoding);
+	if (!WinHttpQueryHeaders(hRequest,
+	                         WINHTTP_QUERY_CONTENT_ENCODING,
+	                         WINHTTP_HEADER_NAME_BY_INDEX,
+	                         encoding,
+	                         &encodingLen,
+	                         WINHTTP_NO_HEADER_INDEX)) {
+		encoding[0] = 0;
+		if (GetLastError() != ERROR_WINHTTP_HEADER_NOT_FOUND) {
+			*responseCode = -5;
+			return false;
+		}
+	} else {
+		encoding[_countof(encoding) - 1] = 0;
+	}
+
+	/* -------------------------------------- *
+	 * allocate response data                 */
+
+	ZipStream strm;
+	bool gzip = wcscmp(encoding, L"gzip") == 0;
+
+	if (gzip) {
+		strm->zalloc   = Z_NULL;
+		strm->zfree    = Z_NULL;
+		strm->opaque   = Z_NULL;
+		strm->avail_in = 0;
+		strm->next_in  = Z_NULL;
+
+		if (!strm.inflate())
+			return false;
+
+		try {
+			zipBuf.resize(MAX_BUF_SIZE);
+		} catch (...) {
+			*responseCode = -6;
+			return false;
+		}
+	}
+
+	/* -------------------------------------- *
+	 * read data                              */
+
+	*responseCode = wcstoul(statusCode, nullptr, 10);
+
+	/* are we supposed to return true here? */
+	if (!bResults || *responseCode != 200)
+		return true;
+
+	BYTE  buffer[READ_BUF_SIZE];
+	DWORD dwSize, outSize;
+	int   lastPosition = 0;
+
+	WinHandle updateFile = CreateFile(outputPath, GENERIC_WRITE, 0,
+			nullptr, CREATE_ALWAYS, 0, nullptr);
+	if (!updateFile.Valid()) {
+		*responseCode = -7;
+		return false;
+	}
+
+	do {
+		/* Check for available data. */
+		dwSize = 0;
+		if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) {
+			*responseCode = -8;
+			return false;
+		}
+
+		dwSize = std::min(dwSize, (DWORD)sizeof(buffer));
+
+		if (!WinHttpReadData(hRequest, (void *)buffer, dwSize,
+					&outSize)) {
+			*responseCode = -9;
+			return false;
+		} else {
+			if (!outSize)
+				break;
+
+			if (gzip) {
+				if (!ReadHTTPZippedFile(strm, updateFile,
+							zipBuf, buffer,
+							outSize, responseCode))
+					return false;
+			} else {
+				if (!ReadHTTPFile(updateFile, buffer,
+							outSize, responseCode))
+					return false;
+			}
+
+			int position = (int)(((float)completedFileSize /
+						(float)totalFileSize) * 100.0f);
+			if (position > lastPosition) {
+				lastPosition = position;
+				SendDlgItemMessage(hwndMain, IDC_PROGRESS,
+						PBM_SETPOS, position, 0);
+			}
+		}
+
+		if (WaitForSingleObject(cancelRequested, 0) == WAIT_OBJECT_0) {
+			*responseCode = -14;
+			return false;
+		}
+
+	} while (dwSize > 0);
+
+	return true;
+}

+ 301 - 0
UI/win-update/updater/patch.cpp

@@ -0,0 +1,301 @@
+#include "updater.hpp"
+
+#include <stdint.h>
+#include <vector>
+
+#include <lzma.h>
+
+using namespace std;
+
+#define MAX_BUF_SIZE  262144
+#define READ_BUF_SIZE 32768
+
+/* ------------------------------------------------------------------------ */
+
+class LZMAStream {
+	lzma_stream strm = {};
+	bool initialized = false;
+
+public:
+	inline ~LZMAStream()
+	{
+		if (initialized) {
+			lzma_end(&strm);
+		}
+	}
+
+	inline bool init_decoder()
+	{
+		lzma_ret ret = lzma_stream_decoder(
+				&strm,
+				200 * 1024 * 1024,
+				0);
+		initialized = (ret == LZMA_OK);
+		return initialized;
+	}
+
+	inline operator lzma_stream *() { return &strm; }
+	inline bool operator!() const { return !initialized; }
+
+	inline lzma_stream *get() { return &strm; }
+};
+
+class File {
+	FILE *f = nullptr;
+
+public:
+	inline ~File()
+	{
+		if (f)
+			fclose(f);
+	}
+
+	inline FILE **operator&() { return &f; }
+	inline operator FILE *() const { return f; }
+	inline bool operator!() const { return !f; }
+};
+
+/* ------------------------------------------------------------------------ */
+
+struct bspatch_stream {
+	void *opaque;
+	int (*read)(const struct bspatch_stream *stream, void *buffer,
+			int length);
+};
+
+/* ------------------------------------------------------------------------ */
+
+static int64_t offtin(const uint8_t *buf)
+{
+	int64_t y;
+
+	y = buf[7] & 0x7F;
+	y = y * 256;
+	y += buf[6];
+	y = y * 256;
+	y += buf[5];
+	y = y * 256;
+	y += buf[4];
+	y = y * 256;
+	y += buf[3];
+	y = y * 256;
+	y += buf[2];
+	y = y * 256;
+	y += buf[1];
+	y = y * 256;
+	y += buf[0];
+
+	if (buf[7] & 0x80)
+		y = -y;
+
+	return y;
+}
+
+/* ------------------------------------------------------------------------ */
+
+static int bspatch(const uint8_t *old, int64_t oldsize, uint8_t *newp,
+		int64_t newsize, struct bspatch_stream *stream)
+{
+	uint8_t buf[8];
+	int64_t oldpos, newpos;
+	int64_t ctrl[3];
+	int64_t i;
+
+	oldpos = 0;
+	newpos = 0;
+	while (newpos < newsize) {
+		/* Read control data */
+		for (i = 0; i <= 2; i++) {
+			if (stream->read(stream, buf, 8))
+				return -1;
+			ctrl[i] = offtin(buf);
+		};
+
+		/* Sanity-check */
+		if (newpos + ctrl[0] > newsize)
+			return -1;
+
+		/* Read diff string */
+		if (stream->read(stream, newp + newpos, (int)ctrl[0]))
+			return -1;
+
+		/* Add old data to diff string */
+		for (i = 0; i < ctrl[0]; i++)
+			if ((oldpos + i >= 0) && (oldpos + i < oldsize))
+				newp[newpos + i] += old[oldpos + i];
+
+		/* Adjust pointers */
+		newpos += ctrl[0];
+		oldpos += ctrl[0];
+
+		/* Sanity-check */
+		if (newpos + ctrl[1] > newsize)
+			return -1;
+
+		/* Read extra string */
+		if (stream->read(stream, newp + newpos, (int)ctrl[1]))
+			return -1;
+
+		/* Adjust pointers */
+		newpos += ctrl[1];
+		oldpos += ctrl[2];
+	};
+
+	return 0;
+}
+
+/* ------------------------------------------------------------------------ */
+
+struct patch_data {
+	HANDLE        h;
+	lzma_stream   *strm;
+	uint8_t       buf[READ_BUF_SIZE];
+};
+
+static int read_lzma(const struct bspatch_stream *stream, void *buffer, int len)
+{
+	if (!len)
+		return 0;
+
+	patch_data  *data    = (patch_data*)stream->opaque;
+	HANDLE      h        = data->h;
+	lzma_stream *strm    = data->strm;
+
+	strm->avail_out = (size_t)len;
+	strm->next_out  = (uint8_t *)buffer;
+
+	for (;;) {
+		if (strm->avail_in == 0) {
+			DWORD read_size;
+			if (!ReadFile(h, data->buf, READ_BUF_SIZE, &read_size,
+						nullptr))
+				return -1;
+			if (read_size == 0)
+				return -1;
+
+			strm->avail_in = (size_t)read_size;
+			strm->next_in  = data->buf;
+		}
+
+		lzma_ret ret = lzma_code(strm, LZMA_RUN);
+		if (ret == LZMA_STREAM_END)
+			return 0;
+		if (ret != LZMA_OK)
+			return -1;
+		if (strm->avail_out == 0)
+			break;
+	}
+
+	return 0;
+}
+
+int ApplyPatch(const wchar_t *patchFile, const wchar_t *targetFile)
+try {
+	uint8_t               header[24];
+	int64_t               newsize;
+	struct bspatch_stream stream;
+	bool                  success;
+
+	WinHandle  hPatch;
+	WinHandle  hTarget;
+	LZMAStream strm;
+
+	/* --------------------------------- *
+	 * open patch and file to patch      */
+
+	hPatch = CreateFile(patchFile, GENERIC_READ, 0, nullptr,
+			OPEN_EXISTING, 0, nullptr);
+	if (!hPatch.Valid())
+		throw int(GetLastError());
+
+	hTarget = CreateFile(targetFile, GENERIC_READ, 0, nullptr,
+			OPEN_EXISTING, 0, nullptr);
+	if (!hTarget.Valid())
+		throw int(GetLastError());
+
+	/* --------------------------------- *
+	 * read patch header                 */
+
+	DWORD read;
+	success = !!ReadFile(hPatch, header, sizeof(header), &read, nullptr);
+	if (success && read == sizeof(header)) {
+		if (memcmp(header, "JIMSLEY/BSDIFF43", 16))
+			throw int(-4);
+	} else {
+		throw int(GetLastError());
+	}
+
+	/* --------------------------------- *
+	 * allocate new file size data       */
+
+	newsize = offtin(header + 16);
+	if (newsize < 0 || newsize >= 0x7ffffffff)
+		throw int(-5);
+
+	vector<uint8_t> newData;
+	try {
+		newData.resize(newsize);
+	} catch (...) {
+		throw int(-1);
+	}
+
+	/* --------------------------------- *
+	 * read old file                     */
+
+	DWORD targetFileSize;
+
+	targetFileSize = GetFileSize(hTarget, nullptr);
+	if (targetFileSize == INVALID_FILE_SIZE)
+		throw int(GetLastError());
+
+	vector<uint8_t> oldData;
+	try {
+		oldData.resize(targetFileSize);
+	} catch (...) {
+		throw int(-1);
+	}
+
+	if (!ReadFile(hTarget, &oldData[0], targetFileSize, &read, nullptr))
+		throw int(GetLastError());
+	if (read != targetFileSize)
+		throw int(-1);
+
+	/* --------------------------------- *
+	 * patch to new file data            */
+
+	if (!strm.init_decoder())
+		throw int(-10);
+
+	patch_data data;
+	data.h    = hPatch;
+	data.strm = strm.get();
+
+	stream.read   = read_lzma;
+	stream.opaque = &data;
+
+	int ret = bspatch(oldData.data(), oldData.size(), newData.data(),
+			newData.size(), &stream);
+	if (ret != 0)
+		throw int(-9);
+
+	/* --------------------------------- *
+	 * write new file                    */
+
+	hTarget = nullptr;
+	hTarget = CreateFile(targetFile, GENERIC_WRITE, 0, nullptr,
+			CREATE_ALWAYS, 0, nullptr);
+	if (!hTarget.Valid())
+		throw int(GetLastError());
+
+	DWORD written;
+
+	success = !!WriteFile(hTarget, newData.data(), (DWORD)newsize,
+			&written, nullptr);
+	if (!success || written != newsize)
+		throw int(GetLastError());
+
+	return 0;
+
+} catch (int code) {
+	return code;
+}

+ 21 - 0
UI/win-update/updater/resource.h

@@ -0,0 +1,21 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by updater.rc
+//
+#define IDD_UPDATEDIALOG                101
+#define IDI_ICON1                       103
+#define IDC_PROGRESS                    1001
+#define IDC_STATUS                      1002
+#define IDCBUTTON                       1004
+#define IDC_BUTTON                      1004
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE        104
+#define _APS_NEXT_COMMAND_VALUE         40001
+#define _APS_NEXT_CONTROL_VALUE         1005
+#define _APS_NEXT_SYMED_VALUE           101
+#endif
+#endif

+ 1361 - 0
UI/win-update/updater/updater.cpp

@@ -0,0 +1,1361 @@
+/******************************************************************************
+ Copyright (C) 2017 Hugh Bailey <[email protected]>
+
+ 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 2 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, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+******************************************************************************/
+
+#include "updater.hpp"
+
+#include <util/windows/CoTaskMemPtr.hpp>
+
+#include <future>
+#include <vector>
+#include <string>
+#include <mutex>
+
+using namespace std;
+
+/* ----------------------------------------------------------------------- */
+
+HANDLE     cancelRequested = nullptr;
+HANDLE     updateThread    = nullptr;
+HINSTANCE  hinstMain       = nullptr;
+HWND       hwndMain        = nullptr;
+HCRYPTPROV hProvider       = 0;
+
+static bool bExiting     = false;
+static bool updateFailed = false;
+static bool is32bit      = false;
+
+static bool downloadThreadFailure = false;
+
+int totalFileSize     = 0;
+int completedFileSize = 0;
+static int completedUpdates  = 0;
+
+struct LastError {
+	DWORD code;
+	inline LastError() { code = GetLastError(); }
+};
+
+void FreeWinHttpHandle(HINTERNET handle)
+{
+	WinHttpCloseHandle(handle);
+}
+
+/* ----------------------------------------------------------------------- */
+
+// http://www.codeproject.com/Articles/320748/Haephrati-Elevating-during-runtime
+static bool IsAppRunningAsAdminMode()
+{
+	BOOL  fIsRunAsAdmin        = FALSE;
+	DWORD dwError              = ERROR_SUCCESS;
+	PSID  pAdministratorsGroup = nullptr;
+
+	/* Allocate and initialize a SID of the administrators group. */
+	SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY;
+	if (!AllocateAndInitializeSid(&NtAuthority,
+	                              2,
+	                              SECURITY_BUILTIN_DOMAIN_RID,
+	                              DOMAIN_ALIAS_RID_ADMINS,
+	                              0,
+	                              0,
+	                              0,
+	                              0,
+	                              0,
+	                              0,
+	                              &pAdministratorsGroup)) {
+		dwError = GetLastError();
+		goto Cleanup;
+	}
+
+	/* Determine whether the SID of administrators group is enabled in the
+	 * primary access token of the process. */
+	if (!CheckTokenMembership(nullptr, pAdministratorsGroup,
+				&fIsRunAsAdmin)) {
+		dwError = GetLastError();
+		goto Cleanup;
+	}
+
+Cleanup:
+	/* Centralized cleanup for all allocated resources. */
+	if (pAdministratorsGroup) {
+		FreeSid(pAdministratorsGroup);
+		pAdministratorsGroup = nullptr;
+	}
+
+	/* Throw the error if something failed in the function. */
+	if (ERROR_SUCCESS != dwError)
+		return false;
+
+	return !!fIsRunAsAdmin;
+}
+
+static void Status(const wchar_t *fmt, ...)
+{
+	wchar_t str[512];
+
+	va_list argptr;
+	va_start(argptr, fmt);
+
+	StringCbVPrintf(str, sizeof(str), fmt, argptr);
+
+	SetDlgItemText(hwndMain, IDC_STATUS, str);
+
+	va_end(argptr);
+}
+
+static void CreateFoldersForPath(const wchar_t *path)
+{
+	wchar_t *p = (wchar_t *)path;
+
+	while (*p) {
+		if (*p == '\\' || *p == '/') {
+			*p = 0;
+			CreateDirectory(path, nullptr);
+			*p = '\\';
+		}
+		p++;
+	}
+}
+
+static bool MyCopyFile(const wchar_t *src, const wchar_t *dest)
+try {
+	WinHandle hSrc;
+	WinHandle hDest;
+
+	hSrc = CreateFile(src, GENERIC_READ, 0, nullptr, OPEN_EXISTING,
+			FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
+	if (!hSrc.Valid())
+		throw LastError();
+
+	hDest = CreateFile(dest, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS,
+			0, nullptr);
+	if (!hDest.Valid())
+		throw LastError();
+
+	BYTE  buf[65536];
+	DWORD read, wrote;
+
+	for (;;) {
+		if (!ReadFile(hSrc, buf, sizeof(buf), &read, nullptr))
+			throw LastError();
+
+		if (read == 0)
+			break;
+
+		if (!WriteFile(hDest, buf, read, &wrote, nullptr))
+			throw LastError();
+
+		if (wrote != read)
+			return false;
+	}
+
+	return true;
+
+} catch (LastError error) {
+	SetLastError(error.code);
+	return false;
+}
+
+static bool IsSafeFilename(const wchar_t *path)
+{
+	const wchar_t *p = path;
+
+	if (!*p)
+		return false;
+
+	if (wcsstr(path, L".."))
+		return false;
+
+	if (*p == '/')
+		return false;
+
+	while (*p) {
+		if (!isalnum(*p) &&
+		    *p != '.' &&
+		    *p != '/' &&
+		    *p != '_' &&
+		    *p != '-')
+			return false;
+		p++;
+	}
+
+	return true;
+}
+
+static string QuickReadFile(const wchar_t *path)
+{
+	string data;
+
+	WinHandle handle = CreateFileW(path, GENERIC_READ, 0, nullptr,
+			OPEN_EXISTING, 0, nullptr);
+	if (!handle.Valid()) {
+		return string();
+	}
+
+	LARGE_INTEGER size;
+
+	if (!GetFileSizeEx(handle, &size)) {
+		return string();
+	}
+
+	data.resize((size_t)size.QuadPart);
+
+	DWORD read;
+	if (!ReadFile(handle,
+	              &data[0],
+	              (DWORD)data.size(),
+	              &read,
+	              nullptr)) {
+		return string();
+	}
+	if (read != size.QuadPart) {
+		return string();
+	}
+
+	return data;
+}
+
+/* ----------------------------------------------------------------------- */
+
+enum state_t {
+	STATE_INVALID,
+	STATE_PENDING_DOWNLOAD,
+	STATE_DOWNLOADING,
+	STATE_DOWNLOADED,
+	STATE_INSTALLED,
+};
+
+struct update_t {
+	wstring sourceURL;
+	wstring outputPath;
+	wstring tempPath;
+	wstring previousFile;
+	wstring basename;
+	string  packageName;
+
+	DWORD   fileSize = 0;
+	BYTE    hash[BLAKE2_HASH_LENGTH];
+	BYTE    downloadhash[BLAKE2_HASH_LENGTH];
+	BYTE    my_hash[BLAKE2_HASH_LENGTH];
+	state_t state     = STATE_INVALID;
+	bool    has_hash  = false;
+	bool    patchable = false;
+
+	inline update_t() {}
+	inline update_t(const update_t &from)
+	        : sourceURL(from.sourceURL),
+	          outputPath(from.outputPath),
+	          tempPath(from.tempPath),
+	          previousFile(from.previousFile),
+	          basename(from.basename),
+	          packageName(from.packageName),
+	          fileSize(from.fileSize),
+	          state(from.state),
+	          has_hash(from.has_hash),
+	          patchable(from.patchable)
+	{
+		memcpy(hash, from.hash, sizeof(hash));
+		memcpy(downloadhash, from.downloadhash, sizeof(downloadhash));
+		memcpy(my_hash, from.my_hash, sizeof(my_hash));
+	}
+
+	inline update_t(update_t &&from)
+	        : sourceURL(std::move(from.sourceURL)),
+	          outputPath(std::move(from.outputPath)),
+	          tempPath(std::move(from.tempPath)),
+	          previousFile(std::move(from.previousFile)),
+	          basename(std::move(from.basename)),
+	          packageName(std::move(from.packageName)),
+	          fileSize(from.fileSize),
+	          state(from.state),
+	          has_hash(from.has_hash),
+	          patchable(from.patchable)
+	{
+		from.state = STATE_INVALID;
+
+		memcpy(hash, from.hash, sizeof(hash));
+		memcpy(downloadhash, from.downloadhash, sizeof(downloadhash));
+		memcpy(my_hash, from.my_hash, sizeof(my_hash));
+	}
+
+	void CleanPartialUpdate()
+	{
+		if (state == STATE_INSTALLED) {
+			if (!previousFile.empty()) {
+				DeleteFile(outputPath.c_str());
+				MyCopyFile(previousFile.c_str(),
+						outputPath.c_str());
+				DeleteFile(previousFile.c_str());
+			} else {
+				DeleteFile(outputPath.c_str());
+			}
+		} else if (state == STATE_DOWNLOADED) {
+			DeleteFile(tempPath.c_str());
+		}
+	}
+
+	inline update_t &operator=(const update_t &from)
+	{
+	        sourceURL = from.sourceURL;
+	        outputPath = from.outputPath;
+	        tempPath = from.tempPath;
+	        previousFile = from.previousFile;
+	        basename = from.basename;
+	        packageName = from.packageName;
+	        fileSize = from.fileSize;
+	        state = from.state;
+	        has_hash = from.has_hash;
+	        patchable = from.patchable;
+
+		memcpy(hash, from.hash, sizeof(hash));
+		memcpy(downloadhash, from.downloadhash, sizeof(downloadhash));
+		memcpy(my_hash, from.my_hash, sizeof(my_hash));
+	}
+};
+
+static vector<update_t> updates;
+static mutex updateMutex;
+
+static inline void CleanupPartialUpdates()
+{
+	for (update_t &update : updates)
+		update.CleanPartialUpdate();
+}
+
+/* ----------------------------------------------------------------------- */
+
+bool DownloadWorkerThread()
+{
+	HttpHandle hSession = WinHttpOpen(L"OBS Studio Updater/2.1",
+	                                  WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
+	                                  WINHTTP_NO_PROXY_NAME,
+	                                  WINHTTP_NO_PROXY_BYPASS,
+	                                  0);
+	if (!hSession) {
+		downloadThreadFailure = true;
+		Status(L"Update failed: Couldn't open obsproject.com");
+		return false;
+	}
+
+	HttpHandle hConnect = WinHttpConnect(hSession, L"obsproject.com",
+			INTERNET_DEFAULT_HTTPS_PORT, 0);
+	if (!hConnect) {
+		downloadThreadFailure = true;
+		Status(L"Update failed: Couldn't connect to obsproject.com");
+		return false;
+	}
+
+	for (;;) {
+		bool foundWork = false;
+
+		unique_lock<mutex> ulock(updateMutex);
+
+		for (update_t &update : updates) {
+			int responseCode;
+
+			DWORD waitResult =
+				WaitForSingleObject(cancelRequested, 0);
+			if (waitResult == WAIT_OBJECT_0) {
+				return false;
+			}
+
+			if (update.state != STATE_PENDING_DOWNLOAD)
+				continue;
+
+			update.state = STATE_DOWNLOADING;
+
+			ulock.unlock();
+
+			foundWork = true;
+
+			if (downloadThreadFailure) {
+				return false;
+			}
+
+			Status(L"Downloading %s", update.outputPath.c_str());
+
+			if (!HTTPGetFile(hConnect,
+			                 update.sourceURL.c_str(),
+			                 update.tempPath.c_str(),
+			                 L"Accept-Encoding: gzip",
+			                 &responseCode)) {
+
+				downloadThreadFailure = true;
+				DeleteFile(update.tempPath.c_str());
+				Status(L"Update failed: Could not download "
+				       L"%s (error code %d)",
+				       update.outputPath.c_str(),
+				       responseCode);
+				return 1;
+			}
+
+			if (responseCode != 200) {
+				downloadThreadFailure = true;
+				DeleteFile(update.tempPath.c_str());
+				Status(L"Update failed: Could not download "
+				       L"%s (error code %d)",
+				       update.outputPath.c_str(),
+				       responseCode);
+				return 1;
+			}
+
+			BYTE downloadHash[BLAKE2_HASH_LENGTH];
+			if (!CalculateFileHash(update.tempPath.c_str(),
+						downloadHash)) {
+				downloadThreadFailure = true;
+				DeleteFile(update.tempPath.c_str());
+				Status(L"Update failed: Couldn't verify "
+						L"integrity of %s",
+						update.outputPath.c_str());
+				return 1;
+			}
+
+			if (memcmp(update.downloadhash, downloadHash, 20)) {
+				downloadThreadFailure = true;
+				DeleteFile(update.tempPath.c_str());
+				Status(L"Update failed: Integrity check "
+						L"failed on %s",
+						update.outputPath.c_str());
+				return 1;
+			}
+
+			ulock.lock();
+
+			update.state = STATE_DOWNLOADED;
+			completedUpdates++;
+		}
+
+		if (!foundWork) {
+			break;
+		}
+		if (downloadThreadFailure) {
+			return false;
+		}
+	}
+
+	return true;
+}
+
+static bool RunDownloadWorkers(int num)
+try {
+	vector<future<bool>> thread_success_results;
+	thread_success_results.resize(num);
+
+	for (future<bool> &result : thread_success_results) {
+		result = async(DownloadWorkerThread);
+	}
+	for (future<bool> &result : thread_success_results) {
+		if (!result.get()) {
+			return false;
+		}
+	}
+
+	return true;
+
+} catch (...) {
+	return false;
+}
+
+/* ----------------------------------------------------------------------- */
+
+static inline bool UTF8ToWide(wchar_t *wide, int wideSize, const char *utf8)
+{
+	return !!MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wide, wideSize);
+}
+
+static inline bool WideToUTF8(char *utf8, int utf8Size, const wchar_t *wide)
+{
+	return !!WideCharToMultiByte(CP_UTF8, 0, wide, -1, utf8, utf8Size,
+			nullptr, nullptr);
+}
+
+static inline bool FileExists(const wchar_t *path)
+{
+	WIN32_FIND_DATAW wfd;
+	HANDLE           hFind;
+
+	hFind = FindFirstFileW(path, &wfd);
+	if (hFind != INVALID_HANDLE_VALUE)
+		FindClose(hFind);
+
+	return hFind != INVALID_HANDLE_VALUE;
+}
+
+static bool NonCorePackageInstalled(const char *name)
+{
+	if (strcmp(name, "obs-browser") == 0) {
+		return FileExists(L"obs-plugins\\32bit\\obs-browser.dll");
+	} else if (strcmp(name, "realsense") == 0) {
+		return FileExists(L"obs-plugins\\32bit\\win-ivcam.dll");
+	}
+
+	return false;
+}
+
+#define UTF8ToWideBuf(wide, utf8) UTF8ToWide(wide, _countof(wide), utf8)
+#define WideToUTF8Buf(utf8, wide) WideToUTF8(utf8, _countof(utf8), wide)
+
+#define UPDATE_URL L"https://obsproject.com/update_studio"
+
+static bool AddPackageUpdateFiles(json_t *root, size_t idx,
+		const wchar_t *tempPath)
+{
+	json_t *package = json_array_get(root, idx);
+	json_t *name    = json_object_get(package, "name");
+	json_t *files   = json_object_get(package, "files");
+
+	if (!json_is_array(files))
+		return true;
+	if (!json_is_string(name))
+		return true;
+
+	wchar_t wPackageName[512];
+	const char *packageName = json_string_value(name);
+	size_t fileCount = json_array_size(files);
+
+	if (!UTF8ToWideBuf(wPackageName, packageName))
+		return false;
+
+	if (strcmp(packageName, "core") != 0 &&
+	    !NonCorePackageInstalled(packageName))
+		return true;
+
+	for (size_t j = 0; j < fileCount; j++) {
+		json_t *file     = json_array_get(files, j);
+		json_t *fileName = json_object_get(file, "name");
+		json_t *hash     = json_object_get(file, "hash");
+		json_t *size     = json_object_get(file, "size");
+
+		if (!json_is_string(fileName))
+			continue;
+		if (!json_is_string(hash))
+			continue;
+		if (!json_is_integer(size))
+			continue;
+
+		const char *fileUTF8 = json_string_value(fileName);
+		const char *hashUTF8 = json_string_value(hash);
+		int fileSize         = (int)json_integer_value(size);
+
+		if (strlen(hashUTF8) != BLAKE2_HASH_LENGTH * 2)
+			continue;
+
+		/* convert strings to wide */
+
+		wchar_t sourceURL[1024];
+		wchar_t updateFileName[MAX_PATH];
+		wchar_t updateHashStr[BLAKE2_HASH_STR_LENGTH];
+		wchar_t tempFilePath[MAX_PATH];
+
+		if (!UTF8ToWideBuf(updateFileName, fileUTF8))
+			continue;
+		if (!UTF8ToWideBuf(updateHashStr, hashUTF8))
+			continue;
+
+		/* make sure paths are safe */
+
+		if (!IsSafeFilename(updateFileName)) {
+			Status(L"Update failed: Unsafe path '%s' found in "
+					L"manifest", updateFileName);
+			return false;
+		}
+
+		StringCbPrintf(sourceURL, sizeof(sourceURL), L"%s/%s/%s",
+				UPDATE_URL, wPackageName, updateFileName);
+		StringCbPrintf(tempFilePath, sizeof(tempFilePath),
+				L"%s\\%s", tempPath, updateHashStr);
+
+		/* Check file hash */
+
+		BYTE    existingHash[BLAKE2_HASH_LENGTH];
+		wchar_t fileHashStr[BLAKE2_HASH_STR_LENGTH];
+		bool    has_hash;
+
+		/* We don't really care if this fails, it's just to avoid
+		 * wasting bandwidth by downloading unmodified files */
+		if (CalculateFileHash(updateFileName, existingHash)) {
+
+			HashToString(existingHash, fileHashStr);
+			if (wcscmp(fileHashStr, updateHashStr) == 0)
+				continue;
+
+			has_hash = true;
+		} else {
+			has_hash = false;
+		}
+
+		/* Add update file */
+
+		update_t update;
+		update.fileSize     = fileSize;
+		update.basename     = updateFileName;
+		update.outputPath   = updateFileName;
+		update.tempPath     = tempFilePath;
+		update.sourceURL    = sourceURL;
+		update.packageName  = packageName;
+		update.state        = STATE_PENDING_DOWNLOAD;
+		update.patchable    = false;
+
+		StringToHash(updateHashStr, update.downloadhash);
+		memcpy(update.hash, update.downloadhash, sizeof(update.hash));
+
+		update.has_hash = has_hash;
+		if (has_hash)
+			StringToHash(fileHashStr, update.my_hash);
+
+		updates.push_back(move(update));
+
+		totalFileSize += fileSize;
+	}
+
+	return true;
+}
+
+static void UpdateWithPatchIfAvailable(const char *name, const char *hash,
+		const char *source,
+		int size)
+{
+	wchar_t widePatchableFilename[MAX_PATH];
+	wchar_t widePatchHash[MAX_PATH];
+	wchar_t sourceURL[1024];
+	wchar_t patchHashStr[BLAKE2_HASH_STR_LENGTH];
+
+	if (strncmp(source, "https://obsproject.com/", 23) != 0)
+		return;
+
+	string patchPackageName = name;
+
+	const char *slash = strchr(name, '/');
+	if (!slash)
+		return;
+
+	patchPackageName.resize(slash - name);
+	name = slash + 1;
+
+	if (!UTF8ToWideBuf(widePatchableFilename, name))
+		return;
+	if (!UTF8ToWideBuf(widePatchHash, hash))
+		return;
+	if (!UTF8ToWideBuf(sourceURL, source))
+		return;
+	if (!UTF8ToWideBuf(patchHashStr, hash))
+		return;
+
+	for (update_t &update : updates) {
+		if (update.packageName != patchPackageName)
+			continue;
+		if (update.basename != widePatchableFilename)
+			continue;
+
+		StringToHash(patchHashStr, update.downloadhash);
+
+		/* Replace the source URL with the patch file, mark it as
+		 * patchable, and re-calculate download size */
+		totalFileSize -= (update.fileSize - size);
+		update.sourceURL = sourceURL;
+		update.fileSize  = size;
+		update.patchable = true;
+		break;
+	}
+}
+
+static bool UpdateFile(update_t &file)
+{
+	wchar_t oldFileRenamedPath[MAX_PATH];
+
+	if (file.patchable)
+		Status(L"Updating %s...", file.outputPath.c_str());
+	else
+		Status(L"Installing %s...", file.outputPath.c_str());
+
+	/* Check if we're replacing an existing file or just installing a new
+	 * one */
+	DWORD attribs = GetFileAttributes(file.outputPath.c_str());
+
+	if (attribs != INVALID_FILE_ATTRIBUTES) {
+		wchar_t *curFileName = nullptr;
+		wchar_t  baseName[MAX_PATH];
+
+		StringCbCopy(baseName, sizeof(baseName),
+				file.outputPath.c_str());
+
+		curFileName = wcsrchr(baseName, '/');
+		if (curFileName) {
+			curFileName[0] = '\0';
+			curFileName++;
+		} else
+			curFileName = baseName;
+
+		/* Backup the existing file in case a rollback is needed */
+		StringCbCopy(oldFileRenamedPath,
+				sizeof(oldFileRenamedPath),
+				file.outputPath.c_str());
+		StringCbCat(oldFileRenamedPath,
+				sizeof(oldFileRenamedPath),
+				L".old");
+
+		if (!MyCopyFile(file.outputPath.c_str(), oldFileRenamedPath)) {
+			int is_sharing_violation =
+				(GetLastError() == ERROR_SHARING_VIOLATION);
+
+			if (is_sharing_violation)
+				Status(L"Update failed: %s is still in use.  "
+				       L"Close all programs and try again.",
+				       curFileName);
+			else
+				Status(L"Update failed: Couldn't backup %s "
+				       L"(error %d)",
+				       curFileName, GetLastError());
+			return false;
+		}
+
+		int  error_code;
+		bool installed_ok;
+
+		if (file.patchable) {
+			error_code = ApplyPatch(
+					file.tempPath.c_str(),
+					file.outputPath.c_str());
+			installed_ok = (error_code == 0);
+
+			if (installed_ok) {
+				BYTE patchedFileHash[BLAKE2_HASH_LENGTH];
+				if (!CalculateFileHash(file.outputPath.c_str(),
+						patchedFileHash)) {
+					Status(L"Update failed: Couldn't "
+					       L"verify integrity of patched %s",
+					       curFileName);
+					return false;
+				}
+
+				if (memcmp(file.hash, patchedFileHash,
+						BLAKE2_HASH_LENGTH) != 0) {
+					Status(L"Update failed: Integrity "
+					       L"check of patched "
+					       L"%s failed",
+					       curFileName);
+					return false;
+				}
+			}
+		} else {
+			installed_ok = MyCopyFile(
+					file.tempPath.c_str(),
+					file.outputPath.c_str());
+			error_code = GetLastError();
+		}
+
+		if (!installed_ok) {
+			int is_sharing_violation =
+				(error_code == ERROR_SHARING_VIOLATION);
+
+			if (is_sharing_violation)
+				Status(L"Update failed: %s is still in use.  "
+				       L"Close all "
+				       L"programs and try again.",
+				       curFileName);
+			else
+				Status(L"Update failed: Couldn't update %s "
+				       L"(error %d)",
+				       curFileName,
+				       GetLastError());
+			return false;
+		}
+
+		file.previousFile = oldFileRenamedPath;
+		file.state        = STATE_INSTALLED;
+	} else {
+		if (file.patchable) {
+			/* Uh oh, we thought we could patch something but it's
+			 * no longer there! */
+			Status(L"Update failed: Source file %s not found",
+					file.outputPath.c_str());
+			return false;
+		}
+
+		/* We may be installing into new folders,
+		 * make sure they exist */
+		CreateFoldersForPath(file.outputPath.c_str());
+
+		bool success = !!MyCopyFile(
+				file.tempPath.c_str(),
+				file.outputPath.c_str());
+		if (!success) {
+			Status(L"Update failed: Couldn't install %s (error %d)",
+					file.outputPath.c_str(),
+					GetLastError());
+			return false;
+		}
+
+		file.previousFile = L"";
+		file.state        = STATE_INSTALLED;
+	}
+
+	return true;
+}
+
+static wchar_t tempPath[MAX_PATH] = {};
+
+#define PATCH_MANIFEST_URL \
+	L"https://obsproject.com/update_studio/getpatchmanifest"
+#define HASH_NULL \
+	L"0000000000000000000000000000000000000000"
+
+static bool Update(wchar_t *cmdLine)
+{
+	/* ------------------------------------- *
+	 * Check to make sure OBS isn't running  */
+
+	HANDLE hObsUpdateMutex = OpenMutexW(SYNCHRONIZE, false,
+			L"OBSStudioUpdateMutex");
+	if (hObsUpdateMutex) {
+		HANDLE hWait[2];
+		hWait[0] = hObsUpdateMutex;
+		hWait[1] = cancelRequested;
+
+		int i = WaitForMultipleObjects(2, hWait, false, INFINITE);
+
+		if (i == WAIT_OBJECT_0)
+			ReleaseMutex(hObsUpdateMutex);
+
+		CloseHandle(hObsUpdateMutex);
+
+		if (i == WAIT_OBJECT_0 + 1)
+			return false;
+	}
+
+	/* ------------------------------------- *
+	 * Init crypt stuff                      */
+
+	CryptProvider hProvider;
+	if (!CryptAcquireContext(&hProvider, nullptr, MS_ENH_RSA_AES_PROV,
+				PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) {
+		SetDlgItemTextW(hwndMain, IDC_STATUS,
+				L"Update failed: CryptAcquireContext failure");
+		return false;
+	}
+
+	::hProvider = hProvider;
+
+	/* ------------------------------------- */
+
+	SetDlgItemTextW(hwndMain, IDC_STATUS,
+			L"Searching for available updates...");
+
+	/* ------------------------------------- *
+	 * Check if updating portable build      */
+
+	bool bIsPortable = false;
+
+	if (cmdLine[0]) {
+		int argc;
+		LPWSTR *argv = CommandLineToArgvW(cmdLine, &argc);
+
+		if (argv) {
+			for (int i = 0; i < argc; i++) {
+				if (wcscmp(argv[i], L"Portable") == 0) {
+					bIsPortable = true;
+				}
+			}
+
+			LocalFree((HLOCAL)argv);
+		}
+	}
+
+	/* ------------------------------------- *
+	 * Get config path                       */
+
+	wchar_t lpAppDataPath[MAX_PATH];
+	lpAppDataPath[0] = 0;
+
+	if (bIsPortable) {
+		GetCurrentDirectory(_countof(lpAppDataPath), lpAppDataPath);
+		StringCbCat(lpAppDataPath, sizeof(lpAppDataPath), L"\\config");
+	} else {
+		CoTaskMemPtr<wchar_t> pOut;
+		HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData,
+				KF_FLAG_DEFAULT, nullptr, &pOut);
+		if (hr != S_OK) {
+			Status(L"Update failed: Could not determine AppData "
+					L"location");
+			return false;
+		}
+
+		StringCbCopy(lpAppDataPath, sizeof(lpAppDataPath), pOut);
+		StringCbCat(lpAppDataPath, sizeof(lpAppDataPath),
+				L"\\obs-studio");
+	}
+
+	/* ------------------------------------- *
+	 * Get download path                     */
+
+	wchar_t manifestPath[MAX_PATH];
+	wchar_t tempDirName[MAX_PATH];
+
+	manifestPath[0] = 0;
+	tempDirName[0]  = 0;
+
+	StringCbPrintf(manifestPath, sizeof(manifestPath),
+			L"%s\\updates\\manifest.json", lpAppDataPath);
+	if (!GetTempPathW(_countof(tempPath), tempPath)) {
+		Status(L"Update failed: Failed to get temp path: %ld",
+				GetLastError());
+		return false;
+	}
+	if (!GetTempFileNameW(tempDirName, L"obs-studio", 0, tempDirName)) {
+		Status(L"Update failed: Failed to create temp dir name: %ld",
+				GetLastError());
+		return false;
+	}
+
+	StringCbCat(tempPath, sizeof(tempPath), tempDirName);
+	CreateDirectory(tempPath, nullptr);
+
+	/* ------------------------------------- *
+	 * Load manifest file                    */
+
+	Json root;
+	{
+		string manifestFile = QuickReadFile(manifestPath);
+		if (manifestFile.empty()) {
+			Status(L"Update failed: Couldn't load manifest file");
+			return false;
+		}
+
+		json_error_t error;
+		root = json_loads(manifestFile.c_str(), 0, &error);
+
+		if (!root) {
+			Status(L"Update failed: Couldn't parse update "
+					L"manifest: %S", error.text);
+			return false;
+		}
+	}
+
+	if (!json_is_object(root.get())) {
+		Status(L"Update failed: Invalid update manifest");
+		return false;
+	}
+
+	/* ------------------------------------- *
+	 * Parse current manifest update files   */
+
+	json_t *packages = json_object_get(root, "packages");
+	size_t packageCount = json_array_size(packages);
+
+	for (size_t i = 0; i < packageCount; i++) {
+		if (!AddPackageUpdateFiles(packages, i, tempPath)) {
+			Status(L"Failed to process update packages");
+			return false;
+		}
+	}
+
+	/* ------------------------------------- *
+	 * Exit if updates already installed     */
+
+	if (!updates.size()) {
+		Status(L"All available updates are already installed.");
+		return true;
+	}
+
+	/* ------------------------------------- *
+	 * Generate file hash json               */
+
+	Json files(json_array());
+
+	for (update_t &update : updates) {
+		wchar_t whash_string[BLAKE2_HASH_STR_LENGTH];
+		char    hash_string[BLAKE2_HASH_STR_LENGTH];
+		char    outputPath[MAX_PATH];
+
+		if (!update.has_hash)
+			continue;
+
+		/* check hash */
+		HashToString(update.my_hash, whash_string);
+		if (wcscmp(whash_string, HASH_NULL) == 0)
+			continue;
+
+		if (!WideToUTF8Buf(hash_string, whash_string))
+			continue;
+		if (!WideToUTF8Buf(outputPath, update.basename.c_str()))
+			continue;
+
+		string package_path;
+		package_path = update.packageName;
+		package_path += "/";
+		package_path += outputPath;
+
+		json_t *obj = json_object();
+		json_object_set(obj, "name", json_string(package_path.c_str()));
+		json_object_set(obj, "hash", json_string(hash_string));
+		json_array_append_new(files, obj);
+	}
+
+	/* ------------------------------------- *
+	 * Send file hashes                      */
+
+	string newManifest;
+	{
+		char *post_body = json_dumps(files, JSON_COMPACT);
+
+		int    responseCode;
+
+		int len = (int)strlen(post_body);
+		uLong compressSize = compressBound(len);
+		string compressedJson;
+
+		compressedJson.resize(compressSize);
+		compress2((Bytef*)&compressedJson[0], &compressSize,
+				(const Bytef*)post_body, len,
+				Z_BEST_COMPRESSION);
+		compressedJson.resize(compressSize);
+
+		bool success = !!HTTPPostData(PATCH_MANIFEST_URL,
+				(BYTE *)&compressedJson[0],
+				(int)compressedJson.size(),
+				L"Accept-Encoding: gzip", &responseCode,
+				newManifest);
+		free(post_body);
+
+		if (!success)
+			return false;
+
+		if (responseCode != 200) {
+			Status(L"Update failed: HTTP/%d while trying to "
+					L"download patch manifest",
+					responseCode);
+			return false;
+		}
+	}
+
+	/* ------------------------------------- *
+	 * Parse new manifest                    */
+
+	json_error_t error;
+	root = json_loads(newManifest.c_str(), 0, &error);
+	if (!root) {
+		Status(L"Update failed: Couldn't parse patch manifest: %S",
+				error.text);
+		return false;
+	}
+
+	if (!json_is_array(root.get())) {
+		Status(L"Update failed: Invalid patch manifest");
+		return false;
+	}
+
+	packageCount = json_array_size(root);
+
+	for (size_t i = 0; i < packageCount; i++) {
+		json_t *patch = json_array_get(root, i);
+
+		if (!json_is_object(patch)) {
+			Status(L"Update failed: Invalid patch manifest");
+			return false;
+		}
+
+		json_t *name_json   = json_object_get(patch, "name");
+		json_t *hash_json   = json_object_get(patch, "hash");
+		json_t *source_json = json_object_get(patch, "source");
+		json_t *size_json   = json_object_get(patch, "size");
+
+		if (!json_is_string(name_json))
+			continue;
+		if (!json_is_string(hash_json))
+			continue;
+		if (!json_is_string(source_json))
+			continue;
+		if (!json_is_integer(size_json))
+			continue;
+
+		const char *name   = json_string_value(name_json);
+		const char *hash   = json_string_value(hash_json);
+		const char *source = json_string_value(source_json);
+		int         size   = (int)json_integer_value(size_json);
+
+		UpdateWithPatchIfAvailable(name, hash, source, size);
+	}
+
+	/* ------------------------------------- *
+	 * Download Updates                      */
+
+	if (!RunDownloadWorkers(2))
+		return false;
+
+	if (completedUpdates != updates.size()) {
+		Status(L"Update failed to download all files.");
+		return false;
+	}
+
+	/* ------------------------------------- *
+	 * Install updates                       */
+
+	for (update_t &update : updates) {
+		if (!UpdateFile(update))
+			return false;
+	}
+
+	/* If we get here, all updates installed successfully so we can purge
+	 * the old versions */
+	for (update_t &update : updates) {
+		if (!update.previousFile.empty())
+			DeleteFile(update.previousFile.c_str());
+
+		/* We delete here not above in case of duplicate hashes */
+		if (!update.tempPath.empty())
+			DeleteFile(update.tempPath.c_str());
+	}
+
+	Status(L"Update complete.");
+	SetDlgItemText(hwndMain, IDC_BUTTON, L"Launch OBS");
+	return true;
+}
+
+static DWORD WINAPI UpdateThread(void *arg)
+{
+	wchar_t *cmdLine = (wchar_t *)arg;
+
+	bool success = Update(cmdLine);
+
+	if (!success) {
+		/* This handles deleting temp files and rolling back and
+		 * partially installed updates */
+		CleanupPartialUpdates();
+
+		if (tempPath[0])
+			RemoveDirectory(tempPath);
+
+		if (WaitForSingleObject(cancelRequested, 0) == WAIT_OBJECT_0)
+			Status(L"Update aborted.");
+
+		SendDlgItemMessage(hwndMain, IDC_PROGRESS, PBM_SETSTATE,
+				PBST_ERROR, 0);
+
+		SetDlgItemText(hwndMain, IDC_BUTTON, L"Exit");
+		EnableWindow(GetDlgItem(hwndMain, IDC_BUTTON), true);
+
+		updateFailed = true;
+	} else {
+		if (tempPath[0])
+			RemoveDirectory(tempPath);
+	}
+
+	if (bExiting)
+		ExitProcess(success);
+	return 0;
+}
+
+static void CancelUpdate(bool quit)
+{
+	if (WaitForSingleObject(updateThread, 0) != WAIT_OBJECT_0) {
+		bExiting = quit;
+		SetEvent(cancelRequested);
+	} else {
+		PostQuitMessage(0);
+	}
+}
+
+static void LaunchOBS()
+{
+	wchar_t cwd[MAX_PATH];
+	wchar_t newCwd[MAX_PATH];
+	wchar_t obsPath[MAX_PATH];
+
+	GetCurrentDirectory(_countof(cwd) - 1, cwd);
+
+	StringCbCopy(obsPath, sizeof(obsPath), cwd);
+	StringCbCat(obsPath, sizeof(obsPath), is32bit
+			? L"\\bin\\32bit"
+			: L"\\bin\\64bit");
+	SetCurrentDirectory(obsPath);
+	StringCbCopy(newCwd, sizeof(newCwd), obsPath);
+
+	StringCbCat(obsPath, sizeof(obsPath), is32bit
+			? L"\\obs32.exe"
+			: L"\\obs64.exe");
+
+	if (!FileExists(obsPath)) {
+		StringCbCopy(obsPath, sizeof(obsPath), cwd);
+		StringCbCat(obsPath, sizeof(obsPath), L"\\bin\\32bit");
+		SetCurrentDirectory(obsPath);
+		StringCbCopy(newCwd, sizeof(newCwd), obsPath);
+
+		StringCbCat(obsPath, sizeof(obsPath), L"\\obs32.exe");
+
+		if (!FileExists(obsPath)) {
+			/* TODO: give user a message maybe? */
+			return;
+		}
+	}
+
+	SHELLEXECUTEINFO execInfo;
+
+	ZeroMemory(&execInfo, sizeof(execInfo));
+
+	execInfo.cbSize      = sizeof(execInfo);
+	execInfo.lpFile      = obsPath;
+	execInfo.lpDirectory = newCwd;
+	execInfo.nShow       = SW_SHOWNORMAL;
+
+	ShellExecuteEx(&execInfo);
+}
+
+static INT_PTR CALLBACK UpdateDialogProc(HWND hwnd, UINT message,
+		WPARAM wParam, LPARAM lParam)
+{
+	switch (message) {
+	case WM_INITDIALOG: {
+		static HICON hMainIcon = LoadIcon(hinstMain,
+				MAKEINTRESOURCE(IDI_ICON1));
+		SendMessage(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hMainIcon);
+		SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hMainIcon);
+		return true;
+	}
+
+	case WM_COMMAND:
+		if (LOWORD(wParam) == IDC_BUTTON) {
+			if (HIWORD(wParam) == BN_CLICKED) {
+				DWORD result = WaitForSingleObject(
+						updateThread, 0);
+				if (result == WAIT_OBJECT_0) {
+					if (updateFailed)
+						PostQuitMessage(0);
+					else
+						PostQuitMessage(1);
+				} else {
+					EnableWindow((HWND)lParam, false);
+					CancelUpdate(false);
+				}
+			}
+		}
+		return true;
+
+	case WM_CLOSE:
+		CancelUpdate(true);
+		return true;
+	}
+
+	return false;
+}
+
+static void RestartAsAdmin(LPWSTR lpCmdLine)
+{
+	wchar_t myPath[MAX_PATH];
+	if (!GetModuleFileNameW(nullptr, myPath, _countof(myPath) - 1)) {
+		return;
+	}
+
+	wchar_t cwd[MAX_PATH];
+	GetCurrentDirectoryW(_countof(cwd) - 1, cwd);
+
+	SHELLEXECUTEINFO shExInfo = {0};
+	shExInfo.cbSize           = sizeof(shExInfo);
+	shExInfo.fMask            = SEE_MASK_NOCLOSEPROCESS;
+	shExInfo.hwnd             = 0;
+	shExInfo.lpVerb           = L"runas";  /* Operation to perform */
+	shExInfo.lpFile           = myPath;    /* Application to start */
+	shExInfo.lpParameters     = lpCmdLine; /* Additional parameters */
+	shExInfo.lpDirectory      = cwd;
+	shExInfo.nShow            = SW_NORMAL;
+	shExInfo.hInstApp         = 0;
+
+	/* annoyingly the actual elevated updater will disappear behind other
+	 * windows :( */
+	AllowSetForegroundWindow(ASFW_ANY);
+
+	if (ShellExecuteEx(&shExInfo)) {
+		DWORD exitCode;
+
+		if (GetExitCodeProcess(shExInfo.hProcess, &exitCode)) {
+			if (exitCode == 1) {
+				LaunchOBS();
+			}
+		}
+		CloseHandle(shExInfo.hProcess);
+	}
+}
+
+int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR lpCmdLine, int)
+{
+	INITCOMMONCONTROLSEX icce;
+
+	if (!IsAppRunningAsAdminMode()) {
+		HANDLE hLowMutex = CreateMutexW(nullptr, true,
+				L"OBSUpdaterRunningAsNonAdminUser");
+
+		RestartAsAdmin(lpCmdLine);
+
+		if (hLowMutex) {
+			ReleaseMutex(hLowMutex);
+			CloseHandle(hLowMutex);
+		}
+
+		return 0;
+	} else {
+		{
+			wchar_t cwd[MAX_PATH];
+			wchar_t newPath[MAX_PATH];
+			GetCurrentDirectoryW(_countof(cwd) - 1, cwd);
+
+			is32bit = wcsstr(cwd, L"bin\\32bit") != nullptr;
+			StringCbCat(cwd, sizeof(cwd), L"\\..\\..");
+
+			GetFullPathName(cwd, _countof(newPath), newPath,
+					nullptr);
+			SetCurrentDirectory(newPath);
+		}
+
+		hinstMain = hInstance;
+
+		icce.dwSize = sizeof(icce);
+		icce.dwICC  = ICC_PROGRESS_CLASS;
+
+		InitCommonControlsEx(&icce);
+
+		hwndMain = CreateDialog(hInstance,
+				MAKEINTRESOURCE(IDD_UPDATEDIALOG), nullptr,
+				UpdateDialogProc);
+		if (!hwndMain) {
+			return -1;
+		}
+
+		ShowWindow(hwndMain, SW_SHOWNORMAL);
+		SetForegroundWindow(hwndMain);
+
+		cancelRequested = CreateEvent(nullptr, true, false, nullptr);
+		updateThread = CreateThread(nullptr, 0, UpdateThread,
+				lpCmdLine, 0, nullptr);
+
+		MSG msg;
+		while (GetMessage(&msg, nullptr, 0, 0)) {
+			if (!IsDialogMessage(hwndMain, &msg)) {
+				TranslateMessage(&msg);
+				DispatchMessage(&msg);
+			}
+		}
+
+		/* there is no non-elevated process waiting for us if UAC is
+		 * disabled */
+		WinHandle hMutex = OpenMutex(SYNCHRONIZE, false,
+				L"OBSUpdaterRunningAsNonAdminUser");
+		if (msg.wParam == 1 && !hMutex) {
+			LaunchOBS();
+		}
+
+		return (int)msg.wParam;
+	}
+}

+ 103 - 0
UI/win-update/updater/updater.hpp

@@ -0,0 +1,103 @@
+#pragma once
+
+#define WINVER 0x0600
+#define _WIN32_WINDOWS 0x0600
+#define _WIN32_WINNT 0x0600
+#define WIN32_LEAN_AND_MEAN
+
+#define ZLIB_CONST
+
+#include <windows.h>
+#include <winhttp.h>
+#include <commctrl.h>
+#include <Wincrypt.h>
+#include <shlobj.h>
+#include <shellapi.h>
+#include <malloc.h>
+#include <stdlib.h>
+#include <tchar.h>
+#include <strsafe.h>
+#include <zlib.h>
+#include <ctype.h>
+#include <blake2.h>
+
+#include <string>
+
+#include "../win-update-helpers.hpp"
+
+#define BLAKE2_HASH_LENGTH 20
+#define BLAKE2_HASH_STR_LENGTH ((BLAKE2_HASH_LENGTH * 2) + 1)
+
+#if defined _M_IX86
+#pragma comment(linker, \
+                "/manifestdependency:\"type='win32' " \
+                "name='Microsoft.Windows.Common-Controls' " \
+                "version='6.0.0.0' " \
+                "processorArchitecture='x86' " \
+                "publicKeyToken='6595b64144ccf1df' " \
+                "language='*'\"")
+#elif defined _M_IA64
+#pragma comment(linker, \
+                "/manifestdependency:\"type='win32' " \
+                "name='Microsoft.Windows.Common-Controls' " \
+                "version='6.0.0.0' " \
+                "processorArchitecture='ia64' " \
+                "publicKeyToken='6595b64144ccf1df' " \
+                "language='*'\"")
+#elif defined _M_X64
+#pragma comment(linker, \
+                "/manifestdependency:\"type='win32' " \
+                "name='Microsoft.Windows.Common-Controls' " \
+                "version='6.0.0.0' " \
+                "processorArchitecture='amd64' " \
+                "publicKeyToken='6595b64144ccf1df' " \
+                "language='*'\"")
+#else
+#pragma comment(linker, \
+                "/manifestdependency:\"type='win32' " \
+                "name='Microsoft.Windows.Common-Controls' " \
+                "version='6.0.0.0' processorArchitecture='*' " \
+                "publicKeyToken='6595b64144ccf1df' " \
+                "language='*'\"")
+#endif
+
+#include <util/windows/WinHandle.hpp>
+#include <jansson.h>
+#include "resource.h"
+
+bool HTTPGetFile(HINTERNET      hConnect,
+                 const wchar_t *url,
+                 const wchar_t *outputPath,
+                 const wchar_t *extraHeaders,
+                 int *          responseCode);
+bool HTTPPostData(const wchar_t *url,
+                  const BYTE *   data,
+                  int            dataLen,
+                  const wchar_t *extraHeaders,
+                  int *          responseCode,
+                  std::string &  response);
+
+void HashToString(const BYTE *in, wchar_t *out);
+void StringToHash(const wchar_t *in, BYTE *out);
+
+bool CalculateFileHash(const wchar_t *path, BYTE *hash);
+
+int ApplyPatch(LPCTSTR patchFile, LPCTSTR targetFile);
+
+extern HWND       hwndMain;
+extern HCRYPTPROV hProvider;
+extern int        totalFileSize;
+extern int        completedFileSize;
+extern HANDLE     cancelRequested;
+
+#pragma pack(push, r1, 1)
+
+typedef struct {
+	BLOBHEADER blobheader;
+	RSAPUBKEY  rsapubkey;
+} PUBLICKEYHEADER;
+
+#pragma pack(pop, r1)
+
+void FreeWinHttpHandle(HINTERNET handle);
+using HttpHandle = CustomHandle<HINTERNET, FreeWinHttpHandle>;

+ 145 - 0
UI/win-update/updater/updater.rc

@@ -0,0 +1,145 @@
+// Microsoft Visual C++ generated resource script.
+//
+#include "resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "afxres.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+#pragma code_page(1252)
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE 
+BEGIN
+    "resource.h\0"
+END
+
+2 TEXTINCLUDE 
+BEGIN
+    "#include ""afxres.h""\r\n"
+    "\0"
+END
+
+3 TEXTINCLUDE 
+BEGIN
+    "\r\n"
+    "\0"
+END
+
+#endif    // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Dialog
+//
+
+IDD_UPDATEDIALOG DIALOGEX 0, 0, 316, 56
+STYLE DS_SETFONT | DS_MODALFRAME | DS_SETFOREGROUND | DS_FIXEDSYS | DS_CENTER | WS_MINIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU
+EXSTYLE WS_EX_APPWINDOW
+CAPTION "OBS Studio Update"
+FONT 8, "MS Shell Dlg", 400, 0, 0x1
+BEGIN
+    PUSHBUTTON      "Cancel",IDC_BUTTON,259,34,50,14
+    CONTROL         "",IDC_PROGRESS,"msctls_progress32",PBS_SMOOTH,7,17,302,14
+    LTEXT           "Waiting for OBS to exit...",IDC_STATUS,7,7,302,8,SS_WORDELLIPSIS
+END
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// DESIGNINFO
+//
+
+#ifdef APSTUDIO_INVOKED
+GUIDELINES DESIGNINFO
+BEGIN
+    IDD_UPDATEDIALOG, DIALOG
+    BEGIN
+        LEFTMARGIN, 7
+        RIGHTMARGIN, 309
+        TOPMARGIN, 7
+        BOTTOMMARGIN, 48
+    END
+END
+#endif    // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION 1,0,0,1
+ PRODUCTVERSION 1,0,0,1
+ FILEFLAGSMASK 0x3fL
+#ifdef _DEBUG
+ FILEFLAGS 0x1L
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS 0x40004L
+ FILETYPE 0x1L
+ FILESUBTYPE 0x0L
+BEGIN
+    BLOCK "StringFileInfo"
+    BEGIN
+        BLOCK "040904b0"
+        BEGIN
+            VALUE "CompanyName", "obsproject.com"
+            VALUE "FileDescription", "OBS Updater"
+            VALUE "FileVersion", "1.0.0.1"
+            VALUE "InternalName", "updater.exe"
+            VALUE "LegalCopyright", "Copyright (C) 2013 Richard Stanway"
+            VALUE "OriginalFilename", "updater.exe"
+            VALUE "ProductName", "OBS Updater"
+            VALUE "ProductVersion", "1.0.0.1"
+        END
+    END
+    BLOCK "VarFileInfo"
+    BEGIN
+        VALUE "Translation", 0x409, 1200
+    END
+END
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Icon
+//
+
+// Icon with lowest ID value placed first to ensure application icon
+// remains consistent on all systems.
+IDI_ICON1               ICON                    "../../../cmake/winrc/obs-studio.ico"
+#endif    // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif    // not APSTUDIO_INVOKED
+