Files
pokemon_pinball_wiki/conagent-pseudocode.c
2026-07-06 20:06:06 -05:00

470 lines
15 KiB
C

/*
* Non-compilable C-like reconstruction of /games/pokemon_pro/conagent.
*
* This is not a Ghidra C export. Ghidra imported the binary but did not create
* normal functions for most stripped text during this pass. The code below is
* manually reconstructed from Ghidra imports, objdump ranges, and strings.
*/
typedef struct AgentConfig AgentConfig;
typedef struct Log Log;
typedef struct GameClient GameClient;
typedef struct HttpClient HttpClient;
typedef struct GameMessage GameMessage;
typedef struct HttpRequest HttpRequest;
typedef struct TransferRequest TransferRequest;
static volatile bool g_shutdown;
int conagent_main(int argc, char **argv)
{
AgentConfig cfg;
Log *log;
GameClient *game;
cfg = default_agent_config();
parse_agent_args(argc, argv, &cfg);
install_signal_handler(SIGTERM, request_shutdown);
install_signal_handler(SIGINT, request_shutdown);
log = create_log_channel(&cfg);
if (log == NULL) {
if (!cfg.silent) {
fwrite("WARNING: Failed to create agent log file.\n", 1, 42, stderr);
fwrite("WARNING: Failed to create agent log channel.\n", 1, 45, stderr);
}
}
log_info(log, "Stern Pinball Connectivity Agent %s", "1.0.16 (Linux,ARM64,GNU)");
log_info(log, "Using libagentcmn %s", "libagent 1.0.16 (Linux,ARM64,GNU)");
log_agent_configuration(log, &cfg);
game = game_client_create(&cfg, log);
if (game == NULL) {
log_fatal(log, "Failed to create Game process interface.");
destroy_log_channel(log);
return 0;
}
game_client_start(game, log);
while (!g_shutdown) {
game_client_update(game, log);
usleep(16000);
}
game_client_stop(game, log);
destroy_log_channel(log);
return 0;
}
GameClient *game_client_create(const AgentConfig *cfg, Log *log)
{
GameClient *gc;
gc = allocate_game_client_state();
if (gc == NULL) {
log_error(log, "Failed to allocate memory for game_client_state_t.");
return NULL;
}
gc->socket_path = cfg->game_process_host ? cfg->game_process_host
: "/usr/local/spike/agent.uds";
gc->state = NOT_CONNECTED;
gc->next_rx_id = 0;
gc->next_tx_id = 0;
gc->http = http_client_create(cfg, log);
if (gc->http == NULL) {
log_error(log, "Failed to create HTTP client to access back-end server.");
free(gc);
return NULL;
}
init_arena(&gc->json_arena, "game_client_json");
init_arena(&gc->action_arena, "game_client_action");
init_arena(&gc->config_arena, "game_client_config");
return gc;
}
void game_client_update(GameClient *gc, Log *log)
{
GameMessage msg;
if (gc->state == NOT_CONNECTED) {
if (agent_client_connect(gc->socket_path)) {
log_info(log, "Successfully connected to Game process. Resetting state.");
gc->state = NEGOTIATE_PROTOCOL;
send_protocol_request(gc);
}
return;
}
while (receive_game_message(gc, &msg)) {
GameAction action = decode_game_message(gc, &msg);
process_game_action(gc, log, action);
}
if (gc->state == COMMS_ENABLED) {
maybe_send_heartbeat(gc, log);
http_client_update(gc->http, log);
}
}
void process_game_action(GameClient *gc, Log *log, GameAction action)
{
switch (action.type) {
case AGREE_PROTOCOL_VERSION:
if (action.protocol_version == EXPECTED_PROTOCOL_VERSION) {
log_info(log, "Negotiated protocol version %d.", action.protocol_version);
gc->state = AWAIT_CONFIG;
} else {
log_error(log, "Expected protocol version %d, got unsupported version %d.",
EXPECTED_PROTOCOL_VERSION, action.protocol_version);
disconnect_game(gc);
}
break;
case CONFIG_SECURITY:
require(action.has_device_credentials,
"CONFIG_SECURITY message missing device credentials.");
store_security_config(gc, action.security);
break;
case CONFIG_NETWORK:
validate_network_config(action.network);
http_client_configure(gc->http, action.network);
break;
case CONFIG_TITLE:
require(action.serial_number, "CONFIG_TITLE message missing machine serial number.");
require(action.machine_configuration_number,
"CONFIG_TITLE message missing machine configuration number.");
require(action.code_version, "CONFIG_TITLE message missing code version identifier.");
require(action.game_title, "CONFIG_TITLE message missing game title identifier.");
require(action.game_model, "CONFIG_TITLE message missing game model identifier.");
store_title_config(gc, action.title);
break;
case COMMS_ENABLE:
if (gc->state == NEGOTIATE_PROTOCOL) {
log_error(log, "Cannot enable communications during protocol version negotiation");
break;
}
if (!have_required_config(gc)) {
log_error(log, "Cannot enable communications while waiting for required configuration data");
break;
}
if (!http_sync_api_token(gc->http, log)) {
log_error(log, "Failed to retrieve the API token from the backend server");
break;
}
gc->state = COMMS_ENABLED;
break;
case COMMS_DISABLE:
gc->state = COMMS_DISABLED;
break;
case MACHINE_REGISTER:
api_game_register(gc, log, &action.register_machine);
break;
case SET_MACHINE_UUID:
gc->machine_uuid = action.machine_uuid;
log_info(log, "Machine UUID updated to %s.", gc->machine_uuid);
break;
case PLAYER_AUTHENTICATE:
api_player_auth(gc, log, action.user_uuid);
break;
case PING_REQUEST:
api_ping(gc, log);
break;
case GAME_TO_SERVER_POST:
if (!valid_game_api_endpoint(action.endpoint)) {
nack(action.id, "The API endpoint supplied with GAME_TO_SERVER_POST request is invalid");
break;
}
api_game_to_server_post(gc, log, action.endpoint, action.json_payload);
break;
case GAME_TO_SERVER_GET:
if (!valid_game_api_endpoint(action.endpoint)) {
nack(action.id, "The API endpoint supplied with GAME_TO_SERVER_GET request is invalid");
break;
}
api_game_to_server_get(gc, log, action.endpoint, action.args);
break;
case FILE_TRANSFER_REQUEST:
http_client_queue_file_transfer(gc->http, &action.transfer);
break;
case DISABLE_INTERFACE_REQUEST:
disable_network_interface(log, action.interface_name);
break;
case CONFIGURE_INTERFACE_REQUEST:
reconfigure_network_interface(log, &action.network_interface);
break;
case QUERY_SERVER_TIME:
http_client_query_server_time(gc->http, log, action.server_host);
break;
case CHECK_FOR_UPDATES:
api_version_upgrades_available(gc, log);
break;
}
}
bool http_sync_api_token(HttpClient *http, Log *log)
{
char url[MAX_URL];
char body[MAX_JSON];
char response[MAX_RESPONSE];
json_t *json;
if (!http->server_host) {
log_error(log, "http_sync_api_token missing back-end server hostname or IP address.");
return false;
}
if (!http->api_username) {
log_error(log, "http_sync_api_token missing API token username.");
return false;
}
if (!http->api_password) {
log_error(log, "http_sync_api_token missing API token password.");
return false;
}
snprintf(url, sizeof(url), "https://%s/api-token-auth/", http->server_host);
snprintf(body, sizeof(body), "{\"username\":\"%s\",\"password\":\"%s\"}",
http->api_username, http->api_password);
CURL *curl = curl_easy_init();
if (curl == NULL) {
log_error(log, "Failed to allocate cURL request object.");
return false;
}
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, strlen(body));
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_USERAGENT, "Spike-Connectivity-Agent/1.0");
if (curl_easy_perform(curl) != CURLE_OK) {
log_error(log, "http_sync_api_token: Failed to retrieve API token");
curl_easy_cleanup(curl);
return false;
}
json = parse_json(response);
if (!json_is_object(json)) {
log_error(log, "http_sync_api_token: Expected json_type_object");
curl_easy_cleanup(curl);
return false;
}
if (!json_get_string(json, "token", http->api_token, sizeof(http->api_token))) {
log_error(log, "http_sync_api_token: Failed to find the token field.");
curl_easy_cleanup(curl);
return false;
}
curl_easy_cleanup(curl);
return true;
}
HttpResult http_execute_api(HttpClient *http, HttpRequest *req, Log *log)
{
char url[MAX_URL];
char auth[MAX_HEADER];
char device_token[MAX_HEADER];
char device_refresh[MAX_HEADER];
if (req->query_len > 0) {
snprintf(url, sizeof(url), "https://%s%s/?%.*s",
http->server_host, req->endpoint, req->query_len, req->query);
} else {
snprintf(url, sizeof(url), "https://%s%s/", http->server_host, req->endpoint);
}
snprintf(auth, sizeof(auth), "Authorization: Token %s", http->api_token);
snprintf(device_token, sizeof(device_token), "Device-Token: %s", http->device_token);
snprintf(device_refresh, sizeof(device_refresh), "Device-Refresh: %s", http->device_refresh);
curl_easy_reset(req->curl);
curl_easy_setopt(req->curl, CURLOPT_URL, url);
curl_easy_setopt(req->curl, CURLOPT_HTTPHEADER, auth);
curl_easy_setopt(req->curl, CURLOPT_HTTPHEADER, device_token);
curl_easy_setopt(req->curl, CURLOPT_HTTPHEADER, device_refresh);
curl_easy_setopt(req->curl, CURLOPT_USERAGENT, "Spike-Connectivity-Agent/1.0");
if (req->method == HTTP_PUT) {
curl_easy_setopt(req->curl, CURLOPT_CUSTOMREQUEST, "PUT");
} else if (req->method == HTTP_DELETE) {
curl_easy_setopt(req->curl, CURLOPT_CUSTOMREQUEST, "DELETE");
} else if (req->method == HTTP_POST) {
curl_easy_setopt(req->curl, CURLOPT_POSTFIELDS, req->body);
curl_easy_setopt(req->curl, CURLOPT_HTTPHEADER, "Content-Type: application/json");
}
CURLcode rc = curl_easy_perform(req->curl);
if (rc != CURLE_OK) {
log_error(log, "curl_easy_perform returned failure code %d.", rc);
return HTTP_RESULT_CURL_ERROR;
}
read_response_headers(req, "Message-Number: ", "App-Status-Code: ");
return classify_http_response(req);
}
void disable_network_interface(Log *log, const char *ifname)
{
char path[128];
char cmd[256];
if (!valid_interface_name(ifname)) {
log_error(log, "Cannot reconfigure network interface; no interface name supplied.");
return;
}
snprintf(path, sizeof(path), "/tmp/network_interface.%s", ifname);
if (!file_exists(path)) {
log_info(log, "Skipping disablement of network interface %s since interface configuration file %s does not exist.",
ifname, path);
return;
}
snprintf(cmd, sizeof(cmd), "ifdown -i %s %s", path, ifname);
log_info(log, "Executing command \"%s\".", cmd);
int rc = system(cmd);
if (rc != 0) {
log_error(log, "The command \"%s\" failed (exit code %d).", cmd, rc);
}
}
bool reconfigure_network_interface(Log *log, const NetworkInterfaceConfig *cfg)
{
char path[128];
char cmd[256];
FILE *fp;
validate_network_interface_config(cfg);
snprintf(path, sizeof(path), "/tmp/network_interface.%s", cfg->ifname);
fp = fopen(path, "w");
if (fp == NULL) {
log_error(log, "Failed to open network interface configuration file \"%s\".", path);
return false;
}
if (cfg->dhcp) {
fprintf(fp, "iface %s inet dhcp\n", cfg->ifname);
} else {
fprintf(fp, "iface %s inet static\n", cfg->ifname);
fprintf(fp, "address %s\n", cfg->address);
fprintf(fp, "netmask %s\n", cfg->netmask);
fprintf(fp, "gateway %s\n", cfg->gateway);
if (cfg->secondary_dns) {
fprintf(fp, "dns-nameservers %s %s\n", cfg->primary_dns, cfg->secondary_dns);
} else {
fprintf(fp, "dns-nameservers %s\n", cfg->primary_dns);
}
}
fflush(fp);
fclose(fp);
if (cfg->store_only) {
log_info(log, "Not actually reconfiguring network interface %s; store-only flag is set.",
cfg->ifname);
return true;
}
snprintf(cmd, sizeof(cmd), "ifup -i %s %s", path, cfg->ifname);
log_info(log, "Executing command \"%s\".", cmd);
return system(cmd) == 0;
}
void *agent_client_rx_thread(void *arg)
{
GameClient *gc = arg;
pthread_setname_np(pthread_self(), "agent_client_rx");
log_info(gc->log, "Client receive thread started normally.");
while (!gc->rx_shutdown) {
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(gc->socket_fd, &readfds);
int rc = select(gc->socket_fd + 1, &readfds, NULL, NULL, NULL);
if (rc < 0) {
if (errno == EAGAIN || errno == EINTR) {
continue;
}
gc->last_error = errno;
break;
}
if (FD_ISSET(gc->socket_fd, &readfds)) {
ssize_t n = recv(gc->socket_fd, gc->rxbuf, sizeof(gc->rxbuf), 0);
if (n == 0) {
log_info(gc->log, "Client receive thread saw client disconnect.");
break;
}
if (n < 0) {
gc->last_error = errno;
break;
}
ParserStatus st = agent_parse_frame(&gc->parser, gc->rxbuf, n);
if (st == PARSER_COMPLETE) {
push_received_message(gc, &gc->parser.message);
} else if (st == PARSER_ERROR) {
log_error(gc->log, "Encountered error decoding message frame");
reset_stream(gc);
}
}
}
log_info(gc->log, "Client receive thread terminated.");
return NULL;
}
void cloudwatch_put_log_events(CloudWatchLogger *cw, const char *message)
{
/*
* Builds an AWS Signature Version 4 request:
* service: logs
* region: us-west-1
* target: Logs_20140328.PutLogEvents
* URL: https://logs.us-west-1.amazonaws.com
*
* The binary imports SHA256_* and HMAC and contains strings for the
* canonical request, signed headers, x-amz-date, and nextSequenceToken.
*/
char payload[MAX_CLOUDWATCH_PAYLOAD];
char authorization[MAX_HEADER];
build_put_log_events_json(payload, cw->log_group, cw->log_stream,
cw->next_sequence_token, message);
aws4_sign_request(authorization, cw->access_key, cw->secret_key,
"us-west-1", "logs", payload);
curl_easy_setopt(cw->curl, CURLOPT_URL, "https://logs.us-west-1.amazonaws.com");
curl_easy_setopt(cw->curl, CURLOPT_HTTPHEADER, "x-amz-target: Logs_20140328.PutLogEvents");
curl_easy_setopt(cw->curl, CURLOPT_HTTPHEADER, authorization);
curl_easy_setopt(cw->curl, CURLOPT_POSTFIELDS, payload);
if (curl_easy_perform(cw->curl) != CURLE_OK) {
log_error(cw->log, "CLOUD LOG POST: curl_easy_perform() failed");
}
}