470 lines
15 KiB
C
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");
|
|
}
|
|
}
|