Skip to content

Commit

Permalink
Self patching Mac application
Browse files Browse the repository at this point in the history
  • Loading branch information
colincornaby committed Jan 9, 2024
1 parent 63026e7 commit fb2d2ab
Show file tree
Hide file tree
Showing 10 changed files with 264 additions and 14 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ find_package(ZLIB REQUIRED)

if(APPLE)
find_package(Security)
find_package(libarchive REQUIRED)
elseif(UNIX)
find_package(LIBSECRET)
find_package(Uuid REQUIRED)
Expand Down
1 change: 1 addition & 0 deletions Sources/Plasma/Apps/plClient/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ target_link_libraries(
CURL::libcurl
"$<$<PLATFORM_ID:Darwin>:-framework Cocoa>"
"$<$<PLATFORM_ID:Darwin>:-framework QuartzCore>"
"$<$<PLATFORM_ID:Darwin>:archive>"
)
target_include_directories(plClient PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")

Expand Down
3 changes: 2 additions & 1 deletion Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSPatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ NS_ASSUME_NONNULL_BEGIN

- (void)patcher:(PLSPatcher*)patcher beganDownloadOfFile:(NSString*)file;
- (void)patcher:(PLSPatcher*)patcher updatedProgress:(NSString*)progressMessage withBytes:(NSUInteger)bytes outOf:(uint64_t)totalBytes;
- (void)patcherCompleted:(PLSPatcher*)patcher;
- (void)patcherCompleted:(PLSPatcher*)patcher didSelfPatch:(BOOL)selfPatched;
- (void)patcherCompletedWithError:(PLSPatcher*)patcher error:(NSError*)error;

@end
Expand All @@ -60,6 +60,7 @@ NS_ASSUME_NONNULL_BEGIN
@property(weak) id<PLSPatcherDelegate> delegate;
@property(readonly) BOOL selfPatched;

- (NSURL *)completeSelfPatch;
- (void)start;

@end
Expand Down
126 changes: 125 additions & 1 deletion Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSPatcher.mm
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
#import "PLSPatcher.h"
#import "NSString+StringTheory.h"

#include <archive.h>
#include <archive_entry.h>
#include <unordered_set>
#include <string_theory/format>

Expand All @@ -61,13 +63,15 @@
void IOnPatchComplete(ENetError result, const ST::string& msg);
void IOnProgressTick(uint64_t curBytes, uint64_t totalBytes, const ST::string& status);
void IOnDownloadBegin(const plFileName& file);
void ISelfPatch(const plFileName& file);
};

@interface PLSPatcher ()
@property BOOL selfPatched;
@property pfPatcher* patcher;
@property NSTimer* networkPumpTimer;
@property Patcher cppPatcher;
@property NSURL* updatedClientURL;
@end

@implementation PLSPatcher
Expand All @@ -88,6 +92,7 @@ - (id)init
_patcher->OnCompletion(std::bind(&Patcher::IOnPatchComplete, _cppPatcher, std::placeholders::_1,
std::placeholders::_2));
_patcher->OnFileDownloadDesired(IApproveDownload);
_patcher->OnSelfPatch(std::bind(&Patcher::ISelfPatch, _cppPatcher, std::placeholders::_1));

self.networkPumpTimer = [NSTimer timerWithTimeInterval:1.0 / 1000.0
repeats:true
Expand All @@ -106,6 +111,35 @@ - (void)start
self.patcher->Start();
}

- (NSURL *)completeSelfPatch
{
NSString* destinationPath = [NSString stringWithSTString:plManifest::PatcherExecutable().AsString()];
NSURL *destinationURL = [NSURL fileURLWithPath:[NSString stringWithSTString:plManifest::PatcherExecutable().AsString()]];

if ([NSFileManager.defaultManager fileExistsAtPath:destinationPath]) {
// need to swap

char originalPath[PATH_MAX] = {0};
[self.updatedClientURL.path getFileSystemRepresentation:originalPath maxLength:sizeof(originalPath)];

char newPath[PATH_MAX] = {0};
[destinationURL.path getFileSystemRepresentation:newPath maxLength:sizeof(newPath)];

renamex_np(newPath, originalPath, RENAME_SWAP);

// delete the old version - this is very likely us
// we want to terminate after. Our bundle will no longer be valid.
[NSFileManager.defaultManager removeItemAtURL:self.updatedClientURL error:nil];
return destinationURL;
} else {
// no executable already present! Just move things into place.
[NSFileManager.defaultManager moveItemAtURL:self.updatedClientURL toURL:destinationURL error:nil];
return destinationURL;
}

return nil;
}

void Patcher::IOnDownloadBegin(const plFileName& file)
{
NSString* fileName = [NSString stringWithSTString:file.AsString()];
Expand Down Expand Up @@ -139,13 +173,103 @@ bool IApproveDownload(const plFileName& file)
return extExcludeList.find(file.GetFileExt()) == extExcludeList.end();
}

static la_ssize_t copy_data(struct archive *ar, struct archive *aw)
{
la_ssize_t r;
const void *buff;
size_t size;
la_int64_t offset;

for (;;) {
r = archive_read_data_block(ar, &buff, &size, &offset);
if (r == ARCHIVE_EOF)
return (ARCHIVE_OK);
if (r < ARCHIVE_OK)
return (r);
r = archive_write_data_block(aw, buff, size, offset);
if (r < ARCHIVE_OK) {
fprintf(stderr, "%s\n", archive_error_string(aw));
return (r);
}
}
}

void Patcher::ISelfPatch(const plFileName& file)
{
struct archive *a;
struct archive *ext;
struct archive_entry *entry;
int flags;
la_ssize_t r;

/* Select which attributes we want to restore. */
flags = ARCHIVE_EXTRACT_TIME;
flags |= ARCHIVE_EXTRACT_PERM;
flags |= ARCHIVE_EXTRACT_ACL;
flags |= ARCHIVE_EXTRACT_FFLAGS;

a = archive_read_new();
archive_read_support_format_all(a);
archive_read_support_filter_all(a);
ext = archive_write_disk_new();
archive_write_disk_set_options(ext, flags);
archive_write_disk_set_standard_lookup(ext);
if ((r = archive_read_open_filename(a, file.GetFileName().c_str(), 10240)))
exit(1);

plFileSystem::Unlink(plManifest::PatcherExecutable());

NSURL *tempDirectory = [NSFileManager.defaultManager URLForDirectory:NSItemReplacementDirectory inDomain:NSUserDomainMask appropriateForURL:[NSURL fileURLWithPath:NSFileManager.defaultManager.currentDirectoryPath] create:YES error:nil];
NSURL *outputURL = [tempDirectory URLByAppendingPathComponent:[NSString stringWithSTString:plManifest::PatcherExecutable().GetFileName()]];
[NSFileManager.defaultManager createDirectoryAtURL:outputURL withIntermediateDirectories:false attributes:nil error:nil];
ST::string outputPath = [outputURL.path STString];

for (;;) {
r = archive_read_next_header(a, &entry);
if (r == ARCHIVE_EOF)
break;
if (r < ARCHIVE_OK)
fprintf(stderr, "%s\n", archive_error_string(a));
if (r < ARCHIVE_WARN)
exit(1);
const char* currentFile = archive_entry_pathname(entry);
auto fullOutputPath = outputPath + "/" + currentFile;
archive_entry_set_pathname(entry, fullOutputPath.c_str());
r = archive_write_header(ext, entry);
if (r < ARCHIVE_OK)
fprintf(stderr, "%s\n", archive_error_string(ext));
else if (archive_entry_size(entry) > 0) {
r = copy_data(a, ext);
if (r < ARCHIVE_OK)
fprintf(stderr, "%s\n", archive_error_string(ext));
if (r < ARCHIVE_WARN)
exit(1);
}
r = archive_write_finish_entry(ext);
if (r < ARCHIVE_OK)
fprintf(stderr, "%s\n", archive_error_string(ext));
if (r < ARCHIVE_WARN)
exit(1);
}
archive_read_close(a);
archive_read_free(a);
archive_write_close(ext);
archive_write_free(ext);

plFileSystem::Unlink(file);

PLSPatcher* patcher = parent;
parent.updatedClientURL = outputURL;
}

void Patcher::IOnPatchComplete(ENetError result, const ST::string& msg)
{
[parent.networkPumpTimer invalidate];
if (IS_NET_SUCCESS(result)) {
PLSPatcher* patcher = parent;
dispatch_async(dispatch_get_main_queue(), ^{
[patcher.delegate patcherCompleted:patcher];
[patcher.delegate patcherCompleted:patcher
didSelfPatch:(patcher.updatedClientURL != nil)];
});
} else {
NSString* msgString = [NSString stringWithSTString:msg];
Expand Down
24 changes: 20 additions & 4 deletions Sources/Plasma/Apps/plClient/Mac-Cocoa/main.mm
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ @interface AppDelegate : NSWindowController <NSApplicationDelegate,
kArgStartUpAgeName,
kArgPvdFile,
kArgSkipIntroMovies,
kArgRenderer
kArgRenderer,
kArgNoSelfPatch
};

static const plCmdArgDef s_cmdLineArgs[] = {
Expand All @@ -166,6 +167,7 @@ @interface AppDelegate : NSWindowController <NSApplicationDelegate,
{ kCmdArgFlagged | kCmdTypeString, "PvdFile", kArgPvdFile },
{ kCmdArgFlagged | kCmdTypeBool, "SkipIntroMovies", kArgSkipIntroMovies },
{ kCmdArgFlagged | kCmdTypeString, "Renderer", kArgRenderer },
{ kCmdArgFlagged | kCmdTypeBool, "NoSelfPatch", kArgNoSelfPatch }
};

plCmdParser cmdParser(s_cmdLineArgs, std::size(s_cmdLineArgs));
Expand Down Expand Up @@ -353,7 +355,8 @@ - (void)applicationDidFinishLaunching:(NSNotification*)notification
NetCommConnect();
[[PLSServerStatus sharedStatus] loadServerStatus];

if (gDataServerLocal) {
BOOL didPatch = cmdParser.IsSpecified(kArgNoSelfPatch);
if (gDataServerLocal || didPatch) {
[self initializeClient];
} else {
[self prepatch];
Expand Down Expand Up @@ -418,12 +421,25 @@ - (void)patcher:(PLSPatcher*)patcher beganDownloadOfFile:(NSString*)file
[self.patcherWindow patcher:patcher beganDownloadOfFile:file];
}

- (void)patcherCompleted:(PLSPatcher*)patcher
- (void)patcherCompleted:(PLSPatcher *)patcher didSelfPatch:(BOOL)selfPatched
{
self.patcher = nil;
[NSApp endModalSession:self.currentModalSession];
[self.patcherWindow.window close];
[self initializeClient];
if (selfPatched) {
NSURL* finalURL = [patcher completeSelfPatch];

// Pass the "we've already patched" argument
NSArray* applicationArguments = [[[NSProcessInfo processInfo] arguments] arrayByAddingObject:@"-NoSelfPatch"];

// no longer current, bye bye
[[NSWorkspace sharedWorkspace] launchApplicationAtURL:finalURL options:NSWorkspaceLaunchNewInstance configuration:@{NSWorkspaceLaunchConfigurationArguments: applicationArguments} error:nil];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[NSApp terminate:self];
});
} else {
[self initializeClient];
}
}

- (void)patcherCompletedWithError:(PLSPatcher*)patcher error:(NSError*)error
Expand Down
4 changes: 4 additions & 0 deletions Sources/Plasma/CoreLib/plFileSystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com

ST::string plFileName::GetFileName() const
{
if(fName.ends_with("/"))
{
return plFileName(fName.before_last('/')).GetFileName();
}
ST_ssize_t end = fName.find_last('/');
if (end < 0)
end = fName.find_last('\\');
Expand Down
56 changes: 48 additions & 8 deletions Sources/Plasma/FeatureLib/pfPatcher/pfPatcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com

#include "HeadSpin.h"
#include "plFileSystem.h"
#include "hsDarwin.h"
#include "hsStream.h"
#include "hsThread.h"
#include "hsTimer.h"
Expand Down Expand Up @@ -99,10 +100,11 @@ enum FileFlags

// Executable flags
kRedistUpdate = 1<<4,
kBundle = 1<<5,

// Begin internal flags
kLastManifestFlag = 1<<5,
kSelfPatch = 1<<6,
kLastManifestFlag = 1<<6,
kSelfPatch = 1<<7,
};

// ===================================================
Expand Down Expand Up @@ -570,12 +572,50 @@ void pfPatcherWorker::IHashFile(pfPatcherQueuedFile& file)
}

// Check to see if ours matches
plFileInfo mine(file.fClientPath);
if (mine.FileSize() == file.fFileSize) {
plMD5Checksum cliMD5(file.fClientPath);
if (cliMD5 == file.fChecksum) {
WhitelistFile(file.fClientPath, false);
return;
if (file.fFlags & kBundle) {
#if HS_BUILD_FOR_MACOS
// If this is a Mac app bundle, MD5 the executable. The executable will hold the
// code signing hash - and thus unique the entire bundle.

CFURLRef bundleURL = CFURLCreateWithFileSystemPath(nullptr, CFStringCreateWithSTString(file.fClientPath.AsString()), kCFURLPOSIXPathStyle, true);
CFBundleRef bundle = CFBundleCreate(nullptr, bundleURL);
CFAutorelease(bundleURL);

if (bundle) {
CFAutorelease(bundle);

CFURLRef executableURL = CFBundleCopyExecutableURL(bundle);

if (executableURL) {
// Ugh, CFBundleCopyExecutableURL returns a relative path from inside the app bundle.
// Sanitize.
CFURLRef fullExecutableURL = CFURLCreateFilePathURL(nullptr, executableURL, nullptr);
CFStringRef executablePath = CFURLCopyFileSystemPath(fullExecutableURL, kCFURLPOSIXPathStyle);

plFileName path(STStringFromCFString(executablePath));
plMD5Checksum cliMD5(path);

CFRelease(executablePath);
CFRelease(executableURL);

if (cliMD5 == file.fChecksum) {
WhitelistFile(file.fClientPath, false);
return;
}
}
}
#else
WhitelistFile(file.fClientPath, false);
return;
#endif
} else {
plFileInfo mine(file.fClientPath);
if (mine.FileSize() == file.fFileSize) {
plMD5Checksum cliMD5(file.fClientPath);
if (cliMD5 == file.fChecksum) {
WhitelistFile(file.fClientPath, false);
return;
}
}
}

Expand Down
16 changes: 16 additions & 0 deletions Sources/Plasma/FeatureLib/pfPatcher/plManifests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,22 +54,38 @@ Mead, WA 99021

plFileName plManifest::ClientExecutable()
{
#if HS_BUILD_FOR_MACOS
return MANIFEST("plClient.app", "UruExplorer.app");
#else
return MANIFEST("plClient.exe", "UruExplorer.exe");
#endif
}

plFileName plManifest::PatcherExecutable()
{
#if HS_BUILD_FOR_MACOS
return MANIFEST("plClient.app", "UruExplorer.app");
#else
return MANIFEST("plUruLauncher.exe", "UruLauncher.exe");
#endif
}

ST::string plManifest::ClientManifest()
{
#if HS_BUILD_FOR_MACOS
return MANIFEST("MacThinInternal", "MacThinExternal");
#else
return MANIFEST("ThinInternal", "ThinExternal");
#endif
}

ST::string plManifest::ClientImageManifest()
{
#if HS_BUILD_FOR_MACOS
return MANIFEST("MacInternal", "MacExternal");
#else
return MANIFEST("Internal", "External");
#endif
}

ST::string plManifest::PatcherManifest()
Expand Down
Loading

0 comments on commit fb2d2ab

Please sign in to comment.