#define LOG_MODULE "patch-net-profile" #include #include #include "asset/nx2/lib/usb-rank.h" #include "asset/nx2/lib/usb-save.h" #include "capnhook/hook/filehook.h" #include "pumpnet/lib/profile-token.h" #include "pumpnet/lib/pumpnet.h" #include "util/fs.h" #include "util/log.h" #include "util/time.h" #define PUMPNET_MAX_NUM_PLAYERS 2 #define PUMPNET_PROFILE_FILE_NAME "pumpnet.bin" #define PUMPNET_TRY_CONNECT_TIMEOUT_MS 2000 struct profile_virtual_mnt_point_file_info { int player; enum pumpnet_lib_file_type file_type; const char* file_path; size_t file_size; }; struct profile_virtual_mnt_point_info { struct { struct profile_virtual_mnt_point_file_info file_info[PUMPNET_LIB_FILE_TYPE_COUNT]; } player[PUMPNET_MAX_NUM_PLAYERS]; }; struct profile_virtual_file { uint64_t player_ref_id; FILE* handle; const struct profile_virtual_mnt_point_file_info* file_info; size_t buffer_pos; uint8_t* buffer; bool is_written; }; struct profile_virtual_mnt_point { struct profile_virtual_file files[PUMPNET_LIB_FILE_TYPE_COUNT]; }; static enum cnh_result _patch_net_profile_filehook(struct cnh_filehook_irp* irp); static enum cnh_result _patch_net_profile_fopen(struct cnh_filehook_irp* irp); static enum cnh_result _patch_net_profile_fclose(struct cnh_filehook_irp* irp, struct profile_virtual_file* virtual_file); static enum cnh_result _patch_net_profile_fread(struct cnh_filehook_irp* irp, struct profile_virtual_file* virtual_file); static enum cnh_result _patch_net_profile_fwrite(struct cnh_filehook_irp* irp, struct profile_virtual_file* virtual_file); static enum cnh_result _patch_net_profile_fseek(struct cnh_filehook_irp* irp, struct profile_virtual_file* virtual_file); static enum cnh_result _patch_net_profile_ftell(struct cnh_filehook_irp* irp, struct profile_virtual_file* virtual_file); static enum cnh_result _patch_net_profile_feof(struct cnh_filehook_irp* irp, struct profile_virtual_file* virtual_file); static struct profile_virtual_file* _patch_net_profile_get_virtual_mnt_point(FILE* handle); static bool _patch_net_profile_determine_is_profile_and_player_and_file_type( struct cnh_filehook_irp* irp, uint8_t* player, enum pumpnet_lib_file_type* file_type); static enum cnh_result _patch_net_profile_open_pumpnet_profile_file( struct cnh_filehook_irp* irp, uint8_t player, enum pumpnet_lib_file_type file_type); static bool _patch_net_profile_close_pumpnet_profile_file(struct profile_virtual_file* virtual_file); static const struct profile_virtual_mnt_point_info _patch_net_profile_virtual_mnt_point_infos[] = { { .player = { { .file_info = { { .player = 0, .file_type = PUMPNET_LIB_FILE_TYPE_SAVE, .file_path = "/mnt/0/nx2save.bin", .file_size = ASSET_NX2_USB_SAVE_SIZE }, { .player = 0, .file_type = PUMPNET_LIB_FILE_TYPE_RANK, .file_path = "/mnt/0/nx2rank.bin", .file_size = ASSET_NX2_USB_RANK_SIZE } } }, { .file_info = { { .player = 1, .file_type = PUMPNET_LIB_FILE_TYPE_SAVE, .file_path = "/mnt/1/nx2save.bin", .file_size = ASSET_NX2_USB_SAVE_SIZE }, { .player = 1, .file_type = PUMPNET_LIB_FILE_TYPE_RANK, .file_path = "/mnt/1/nx2rank.bin", .file_size = ASSET_NX2_USB_RANK_SIZE } } }, } } }; static const struct profile_virtual_mnt_point_info* _patch_net_profile_file_info_ref; static struct profile_virtual_mnt_point _patch_net_profile_virtual_mnt_points[PUMPNET_MAX_NUM_PLAYERS]; static pthread_mutex_t _patch_net_profile_mutex; static enum cnh_result _patch_net_profile_filehook(struct cnh_filehook_irp* irp) { if (irp->op == CNH_FILEHOOK_IRP_OP_OPEN) { return _patch_net_profile_fopen(irp); } else { struct profile_virtual_file* virt_file = _patch_net_profile_get_virtual_mnt_point(irp->file); if (virt_file != NULL) { switch (irp->op) { case CNH_FILEHOOK_IRP_OP_CLOSE: return _patch_net_profile_fclose(irp, virt_file); case CNH_FILEHOOK_IRP_OP_READ: return _patch_net_profile_fread(irp, virt_file); case CNH_FILEHOOK_IRP_OP_WRITE: return _patch_net_profile_fwrite(irp, virt_file); case CNH_FILEHOOK_IRP_OP_FGETS: log_die("Unsupported operation"); return CNH_RESULT_OTHER_ERROR; case CNH_FILEHOOK_IRP_OP_SEEK: return _patch_net_profile_fseek(irp, virt_file); case CNH_FILEHOOK_IRP_OP_TELL: return _patch_net_profile_ftell(irp, virt_file); case CNH_FILEHOOK_IRP_OP_EOF: return _patch_net_profile_feof(irp, virt_file); default: log_die("Unhandled switch case"); return CNH_RESULT_OTHER_ERROR; } } else { return cnh_filehook_invoke_next(irp); } } } static enum cnh_result _patch_net_profile_fopen(struct cnh_filehook_irp* irp) { uint8_t player; enum pumpnet_lib_file_type file_type; player = -1; file_type = PUMPNET_LIB_FILE_TYPE_COUNT; // not a profile file, pass if (!_patch_net_profile_determine_is_profile_and_player_and_file_type(irp, &player, &file_type)) { return cnh_filehook_invoke_next(irp); } return _patch_net_profile_open_pumpnet_profile_file(irp, player, file_type); } static enum cnh_result _patch_net_profile_fclose(struct cnh_filehook_irp* irp, struct profile_virtual_file* virtual_file) { log_debug("fclose %s", virtual_file->file_info->file_path); return _patch_net_profile_close_pumpnet_profile_file(virtual_file) ? CNH_RESULT_SUCCESS : CNH_RESULT_OTHER_ERROR; } static enum cnh_result _patch_net_profile_fread(struct cnh_filehook_irp* irp, struct profile_virtual_file* virtual_file) { uint32_t len; pthread_mutex_lock(&_patch_net_profile_mutex); log_debug("fread %s, pos 0x%X, bytes %d", virtual_file->file_info->file_path, virtual_file->buffer_pos, irp->read.nbytes); /* read up to eof, only */ if (virtual_file->buffer_pos + irp->read.nbytes < virtual_file->file_info->file_size) { len = irp->read.nbytes; } else { len = virtual_file->file_info->file_size - virtual_file->buffer_pos; } memcpy(&irp->read.bytes[irp->read.pos], virtual_file->buffer + virtual_file->buffer_pos, len); virtual_file->buffer_pos += len; irp->read.pos += len; pthread_mutex_unlock(&_patch_net_profile_mutex); return CNH_RESULT_SUCCESS; } static enum cnh_result _patch_net_profile_fwrite(struct cnh_filehook_irp* irp, struct profile_virtual_file* virtual_file) { uint32_t len; pthread_mutex_lock(&_patch_net_profile_mutex); log_debug("fwrite %s, pos 0x%X, bytes %d", virtual_file->file_info->file_path, virtual_file->buffer_pos, irp->read.nbytes); /* write up to eof, only */ if (virtual_file->buffer_pos + irp->write.nbytes < virtual_file->file_info->file_size) { len = irp->write.nbytes; } else { len = virtual_file->file_info->file_size - virtual_file->buffer_pos; } memcpy(virtual_file->buffer + virtual_file->buffer_pos, irp->write.bytes, len); virtual_file->buffer_pos += len; virtual_file->is_written = true; irp->write.pos += len; pthread_mutex_unlock(&_patch_net_profile_mutex); return CNH_RESULT_SUCCESS; } static enum cnh_result _patch_net_profile_fseek(struct cnh_filehook_irp* irp, struct profile_virtual_file* virtual_file) { pthread_mutex_lock(&_patch_net_profile_mutex); switch (irp->seek_origin) { case SEEK_SET: virtual_file->buffer_pos = (size_t) irp->seek_offset; break; case SEEK_CUR: virtual_file->buffer_pos += (size_t) irp->seek_offset; break; case SEEK_END: virtual_file->buffer_pos = virtual_file->file_info->file_size; virtual_file->buffer_pos += (size_t) irp->seek_offset; break; default: log_die("Unhandled origin %d", irp->seek_origin); break; } log_debug("fseek %s, offset 0x%X, origin %d", virtual_file->file_info->file_path, irp->seek_offset, irp->seek_origin); pthread_mutex_unlock(&_patch_net_profile_mutex); return CNH_RESULT_SUCCESS; } static enum cnh_result _patch_net_profile_ftell(struct cnh_filehook_irp* irp, struct profile_virtual_file* virtual_file) { pthread_mutex_lock(&_patch_net_profile_mutex); irp->tell_offset = virtual_file->buffer_pos; pthread_mutex_unlock(&_patch_net_profile_mutex); return CNH_RESULT_SUCCESS; } static enum cnh_result _patch_net_profile_feof(struct cnh_filehook_irp* irp, struct profile_virtual_file* virtual_file) { pthread_mutex_lock(&_patch_net_profile_mutex); irp->eof = virtual_file->buffer_pos == virtual_file->file_info->file_size; pthread_mutex_unlock(&_patch_net_profile_mutex); return CNH_RESULT_SUCCESS; } // ===================================================================================================================== static struct profile_virtual_file* _patch_net_profile_get_virtual_mnt_point(FILE* handle) { for (int i = 0; i < PUMPNET_MAX_NUM_PLAYERS; i++) { for (int j = 0; j < PUMPNET_LIB_FILE_TYPE_COUNT; j++) { if (_patch_net_profile_virtual_mnt_points->files[j].handle == handle) { return &_patch_net_profile_virtual_mnt_points->files[j]; } } } return NULL; } static bool _patch_net_profile_determine_is_profile_and_player_and_file_type( struct cnh_filehook_irp* irp, uint8_t* player, enum pumpnet_lib_file_type* file_type) { log_assert(irp); log_assert(player); log_assert(file_type); // compare to static string to avoid slowing everything down for (int i = 0; i < PUMPNET_MAX_NUM_PLAYERS; i++) { for (int j = 0; j < PUMPNET_LIB_FILE_TYPE_COUNT; j++) { if (!strcmp(irp->open_filename, _patch_net_profile_virtual_mnt_point_infos->player[i].file_info[j].file_path)) { *player = i; *file_type = (enum pumpnet_lib_file_type) j; return true; } } } return false; } static struct profile_virtual_file* _patch_net_profile_setup_virtual_file( uint8_t player, enum pumpnet_lib_file_type file_type, uint64_t player_ref_id) { struct profile_virtual_file* virtual_file; log_debug("Setup virtual file, player %d, file_type %d", player, file_type); virtual_file = &_patch_net_profile_virtual_mnt_points[player].files[file_type]; // sanity checks log_assert(!virtual_file->handle); log_assert(!virtual_file->file_info); log_assert(!virtual_file->buffer); virtual_file->player_ref_id = player_ref_id; virtual_file->handle = cnh_filehook_open_dummy_file_handle(); virtual_file->file_info = &_patch_net_profile_virtual_mnt_point_infos->player[player].file_info[file_type]; virtual_file->buffer_pos = 0; virtual_file->is_written = false; virtual_file->buffer = (uint8_t*) malloc(virtual_file->file_info->file_size); log_debug("Setup virtual file done, size %d", virtual_file->file_info->file_size); return virtual_file; } static void _patch_net_profile_destroy_virtual_file(uint8_t player, enum pumpnet_lib_file_type file_type) { struct profile_virtual_file* virtual_file; log_debug("Destroying virtual file, player %d, file_type %d", player, file_type); virtual_file = &_patch_net_profile_virtual_mnt_points[player].files[file_type]; // sanity checks log_assert(virtual_file->handle); log_assert(virtual_file->file_info); log_assert(virtual_file->buffer); cnh_filehook_close_dummy_file_handle(virtual_file->handle); virtual_file->handle = NULL; virtual_file->file_info = NULL; free(virtual_file->buffer); virtual_file->buffer = NULL; } static bool _patch_net_profile_get_token(int player, uint64_t* player_ref_id) { log_assert(player_ref_id); log_assert(player >= 0); log_assert(player <= 1); char path_profile_token[128]; *player_ref_id = 0; sprintf(path_profile_token, "/mnt/%d/%s", player, PUMPNET_PROFILE_FILE_NAME); if (util_fs_path_exists(path_profile_token)) { log_info("Found pumpnet profile file %s", path_profile_token); if (pumpnet_lib_profile_token_load(path_profile_token, player_ref_id)) { return true; } else { log_error("Loading pumpnet profile token %s failed, fallback to local profiles", path_profile_token); } } else { log_info("No pumpnet profile found, fallback to local profile"); } return false; } static bool _patch_net_profile_download_profile_file(struct profile_virtual_file* virtual_file) { int player; enum pumpnet_lib_file_type file_type; uint64_t player_ref_id; player = virtual_file->file_info->player; file_type = virtual_file->file_info->file_type; player_ref_id = virtual_file->player_ref_id; log_info("Profile file player %d, file_type %d, refId %llX downloading from server...", player, file_type, player_ref_id); if (!pumpnet_lib_get(file_type, player_ref_id, virtual_file->buffer, virtual_file->file_info->file_size)) { log_error("Downloading file player %d, file_type %d, refId %llX failed", player, file_type, player_ref_id); return false; } log_info("Downloading file player %d, file_type %d, refId %llX successful", player, file_type, player_ref_id); return true; } static enum cnh_result _patch_net_profile_open_pumpnet_profile_file( struct cnh_filehook_irp* irp, uint8_t player, enum pumpnet_lib_file_type file_type) { uint64_t player_ref_id; log_debug("open %s %d %d", irp->open_filename, player, file_type); // check if pumpnet.bin token is available and server reachable // download all profile files on the first file request and hook all following files to virtual endpoints of // the already downloaded and buffered data if (_patch_net_profile_get_token(player, &player_ref_id)) { struct profile_virtual_file* virtual_file = _patch_net_profile_setup_virtual_file(player, file_type, player_ref_id); if (_patch_net_profile_download_profile_file(virtual_file)) { irp->file = virtual_file->handle; pthread_mutex_unlock(&_patch_net_profile_mutex); return CNH_RESULT_SUCCESS; } else { _patch_net_profile_destroy_virtual_file(player, file_type); pthread_mutex_unlock(&_patch_net_profile_mutex); return CNH_RESULT_NO_SUCH_FILE_OR_DIR; } } return cnh_filehook_invoke_next(irp); } static bool _patch_net_profile_upload_profile_file(struct profile_virtual_file* virtual_file) { int player; enum pumpnet_lib_file_type file_type; uint64_t player_ref_id; player = virtual_file->file_info->player; file_type = virtual_file->file_info->file_type; player_ref_id = virtual_file->player_ref_id; log_info("Profile file player %d, file_type %d, refId %llX uploading to server...", player, file_type, player_ref_id); if (!pumpnet_lib_put(file_type, player_ref_id, virtual_file->buffer, virtual_file->file_info->file_size)) { log_error("Uploading file player %d, file_type %d, refId %llX failed", player, file_type, player_ref_id); return false; } log_info("Uploading file player %d, file_type %d, refId %llX successful", player, file_type, player_ref_id); return true; } static bool _patch_net_profile_close_pumpnet_profile_file(struct profile_virtual_file* virtual_file) { bool res = true; pthread_mutex_lock(&_patch_net_profile_mutex); if (virtual_file->is_written) { res = _patch_net_profile_upload_profile_file(virtual_file); } _patch_net_profile_destroy_virtual_file(virtual_file->file_info->player, virtual_file->file_info->file_type); pthread_mutex_unlock(&_patch_net_profile_mutex); return res; } // ===================================================================================================================== void patch_net_profile_init( enum asset_game_version game, const char* pumpnet_server_addr, uint64_t machine_id, const char* cert_dir_path, bool verbose_debug_log) { uint8_t idx; /* use a switch instead of offset'ing the enum for additional safety and block unsupported games */ switch (game) { case ASSET_GAME_VERSION_NX2: idx = 0; break; // case PUMPNET_LIB_GAME_NXA: // idx = 1; // break; // // case PUMPNET_LIB_GAME_FST: // idx = 2; // break; // // case PUMPNET_LIB_GAME_FEX: // idx = 3; // break; // // case PUMPNET_LIB_GAME_F2: // idx = 4; // break; case ASSET_GAME_VERSION_NXA: case ASSET_GAME_VERSION_FIESTA: case ASSET_GAME_VERSION_FIESTA_EX: case ASSET_GAME_VERSION_FIESTA_2: default: log_error("Unsupported game version %d specified, disabled network service", game); return; } _patch_net_profile_file_info_ref = &_patch_net_profile_virtual_mnt_point_infos[idx]; size_t max_data_size = 0; // determine max file size for network buffers, file sizes identical on for all players. for (int i = 0; i < PUMPNET_LIB_FILE_TYPE_COUNT; i++) { max_data_size += _patch_net_profile_file_info_ref->player[0].file_info[i].file_size; } // set buffers to NULL to detect illegal states for (int i = 0; i < PUMPNET_LIB_FILE_TYPE_COUNT; i++) { for (int j = 0; j < PUMPNET_MAX_NUM_PLAYERS; j++) { _patch_net_profile_virtual_mnt_points[j].files[i].player_ref_id = 0; _patch_net_profile_virtual_mnt_points[j].files[i].handle = NULL; _patch_net_profile_virtual_mnt_points[j].files[i].file_info = NULL; _patch_net_profile_virtual_mnt_points[j].files[i].buffer = NULL; } } pumpnet_lib_init(game, pumpnet_server_addr, machine_id, cert_dir_path, verbose_debug_log); cnh_filehook_push_handler(_patch_net_profile_filehook); pthread_mutex_init(&_patch_net_profile_mutex, NULL); log_info("Initialized: game %d, server %s, machine id %llX", game, pumpnet_server_addr, machine_id); } void patch_net_profile_shutdown() { pumpnet_lib_shutdown(); pthread_mutex_destroy(&_patch_net_profile_mutex); log_info("Shut down"); }