hostile-takeover/server/statsposter.cpp
Nathan Fulton d6b301c3e7 Track client platform version
Client platform version can be retrieved live with /ids and is also
recorded in the playerdetail module.
2016-08-31 23:54:48 -04:00

242 lines
8.2 KiB
C++

#include "server/statsposter.h"
#include "base/base64.h"
#include "base/md5.h"
#include "server/secrets.h"
#include <algorithm>
// Stats get posted to a remote server via http post. The http
// posts are asynchronous, but the dns lookups are not, so it is handled
// in a thread to insulate the callers of this class.
// If the server is down or not responding, the posts are kept in memory, and
// retried. The kcPostsWaitingMax is the max # that will be kept, before old
// ones are thrown away. kcPostsPostingMax is the number that is allowed to be
// posted simultaneously, so the load is kept even.
#define kcPostsWaitingMax 1000
#define kcPostsPostingMax 10
#define kcMaxAttempts 10
namespace wi {
StatsPoster::StatsPoster(const base::SocketAddress &addr,
const std::string& path) : base::MessageHandler(worker_), addr_(addr),
path_(path) {
worker_.Start(this, &StatsPoster::ThreadStart);
}
StatsPoster::~StatsPoster() {
// Thread::Stop gets called in the Thread destructor, which does a join
// with the actual thread, which synchronizes exiting
}
void StatsPoster::ThreadStart(void *pv) {
// Run and process messages. RunLoop will return when this thread
// needs to exit.
worker_.RunLoop();
// Cleanup that needs to happen on this thread.
while (waiting_.size() != 0) {
delete waiting_[0];
waiting_.pop_front();
}
while (posting_.size() != 0) {
delete posting_[0];
posting_.pop_front();
}
}
void StatsPoster::Submit(const GameStats& stats) {
worker_.Post(1, this, new GameStatsData(stats));
}
void StatsPoster::OnMessage(base::Message *pmsg) {
GameStatsData *stats_data = (GameStatsData *)pmsg->data;
OnPostRequest(stats_data->stats);
delete stats_data;
}
void StatsPoster::OnPostRequest(const GameStats& stats) {
// Delete old ones, keep new ones if we have too many uncompleted
HttpPost *post = new HttpPost(addr_, path_, MakeBody(stats));
waiting_.push_back(post);
if (waiting_.size() > kcPostsWaitingMax) {
delete waiting_[0];
waiting_.pop_front();
}
SchedulePosts();
}
void StatsPoster::OnPostComplete(HttpPost *post, int status_code, int error,
const base::ByteBuffer& response) {
#if 0
RLOG() << "STATSPOSTER: PostComplete " << (dword)post << " status_code "
<< status_code;
#endif
// Find it in posting_
std::deque<HttpPost *>::iterator it = std::find(posting_.begin(),
posting_.end(), post);
// It should always be in posting_.
if (it != posting_.end()) {
posting_.erase(it);
post->SignalOnComplete.disconnect(this);
// Success? If so delete it and schedule the next one. If not success,
// push it to the front of waiting_ for later.
if (status_code == 200) {
delete post;
} else {
RLOG() << "ERROR " << status_code
<< " posting game stats. attempt:" << post->attempts()
<< " waiting:" << waiting_.size()
<< " posting:" << posting_.size();
if (post->attempts() >= kcMaxAttempts) {
RLOG() << "ERROR attemped gamestat post " << kcMaxAttempts
<< " times. Deleting.";
delete post;
} else {
waiting_.push_front(post);
}
}
} else {
RLOG() << "ERROR: Post " << (pword)post << " not found in posting_!";
}
// Schedule more posts
SchedulePosts();
}
void StatsPoster::SchedulePosts() {
while (waiting_.size() != 0 && posting_.size() < kcPostsPostingMax) {
HttpPost *post = waiting_[0];
waiting_.pop_front();
posting_.push_back(post);
post->SignalOnComplete.connect(this, &StatsPoster::OnPostComplete);
post->Submit();
#if 0
RLOG() << "STATSPOSTER: Submiting post: " << (dword)post
<< " waiting:" << waiting_.size()
<< " posting:" << posting_.size();
#endif
}
}
base::ByteBuffer *StatsPoster::MakeBody(const GameStats& stats) {
std::string json = ToJson(stats);
// Create HMAC like signature of hash(json + secret) so the receiver
// can ensure it is valid.
MD5_CTX md5;
MD5Init(&md5);
MD5Update(&md5, (const byte *)json.c_str(), json.size());
MD5Update(&md5, (const byte *)kszStatSecret, strlen(kszStatSecret));
byte hash[16];
MD5Final(hash, &md5);
char hash_str[33];
strncpyz(hash_str, base::Format::ToHex(hash, sizeof(hash)),
sizeof(hash_str));
base::ByteBuffer *bb = new base::ByteBuffer(32 + json.size());
bb->WriteBytes((const byte *)hash_str, 32);
bb->WriteBytes((const byte *)json.c_str(), json.size());
return bb;
}
// See stats/addgamestats.py for json description
std::string StatsPoster::ToJson(const GameStats& s) {
// Use yajl to get the string encoding right
#ifdef DEBUG
yajl_gen_config yajl_config = { 1, " " };
#else
yajl_gen_config yajl_config = { 0, NULL };
#endif
yajl_gen g = yajl_gen_alloc(&yajl_config);
yajl_gen_map_open(g);
GenNum(g, "server_id", s.server_id);
GenNum(g, "server_start", s.server_start);
GenNum(g, "gameid", s.gameid);
GenNum(g, "packid_id", s.params.packid.id);
GenString(g, "packid_hash", base::Format::ToHex(s.params.packid.hash,
sizeof(s.params.packid.hash)));
GenString(g, "title", s.info.title());
GenString(g, "filename", s.info.filename());
GenNum(g, "game_speed", s.params.tGameSpeed);
GenNum(g, "min_players", s.info.minplayers());
GenNum(g, "max_players", s.info.maxplayers());
GenNum(g, "start_utc", s.start_utc);
GenNum(g, "end_utc", s.end_utc);
const char *str = "player_stats";
yajl_gen_string(g, (const unsigned char *)str, strlen(str));
yajl_gen_array_open(g);
for (int i = 0; i < s.player_count; i++) {
yajl_gen_map_open(g);
GenString(g, "name", s.player_stats[i].name);
GenNum(g, "pid", s.player_stats[i].pid);
GenString(g, "ip", s.player_stats[i].ip);
GenString(g, "did", s.player_stats[i].did);
GenString(g, "platform", s.player_stats[i].platform);
str = "winstats";
yajl_gen_string(g, (const unsigned char *)str, strlen(str));
yajl_gen_map_open(g);
GenNum(g, "side_mask", s.player_stats[i].ws.sidm);
GenNum(g, "side_mask_allies", s.player_stats[i].ws.sidmAllies);
GenNum(g, "credits_acquired", s.player_stats[i].ws.cCreditsAcquired);
GenNum(g, "credits_consumed", s.player_stats[i].ws.cCreditsConsumed);
GenNum(g, "enemy_munts_killed",
s.player_stats[i].ws.cEnemyMobileUnitsKilled);
GenNum(g, "enemy_structs_killed",
s.player_stats[i].ws.cEnemyStructuresKilled);
GenNum(g, "munts_lost", s.player_stats[i].ws.cMobileUnitsLost);
GenNum(g, "structs_lost", s.player_stats[i].ws.cStructuresLost);
GenNum(g, "ff", s.player_stats[i].ws.ff);
str = "unit_counts";
yajl_gen_string(g, (const unsigned char *)str, strlen(str));
yajl_gen_array_open(g);
for (int ut = 0; ut < ARRAYSIZE(s.player_stats[i].ws.acut); ut++) {
yajl_gen_integer(g, s.player_stats[i].ws.acut[ut]);
}
yajl_gen_array_close(g);
str = "built_counts";
yajl_gen_string(g, (const unsigned char *)str, strlen(str));
yajl_gen_array_open(g);
for (int ut = 0; ut < ARRAYSIZE(s.player_stats[i].ws.acutBuilt); ut++) {
yajl_gen_integer(g, s.player_stats[i].ws.acutBuilt[ut]);
}
yajl_gen_array_close(g);
yajl_gen_map_close(g);
yajl_gen_map_close(g);
}
yajl_gen_array_close(g);
yajl_gen_map_close(g);
const char *buf;
unsigned int len;
yajl_gen_get_buf(g, (const unsigned char **)&buf, &len);
std::string result(buf);
yajl_gen_free(g);
return result;
}
void StatsPoster::GenNum(yajl_gen g, const char *key, dword value) {
yajl_gen_string(g, (const unsigned char *)key, strlen(key));
const char *s = base::Format::ToString("%lu", value);
yajl_gen_number(g, s, strlen(s));
}
void StatsPoster::GenString(yajl_gen g, const char *key, const char *value) {
yajl_gen_string(g, (const unsigned char *)key, strlen(key));
yajl_gen_string(g, (const unsigned char *)value, strlen(value));
}
} // namespace wi