1 #include "stdafx.h"
2 #include "file_sharing.h"
3 #include "progress_meter.h"
4 #include "save_file_info.h"
5 #include "parse_game.h"
6 #include "options.h"
7 #include "text_serialization.h"
8 
9 #include <curl/curl.h>
10 
FileSharing(const string & url,Options & o,long long id)11 FileSharing::FileSharing(const string& url, Options& o, long long id) : uploadUrl(url), options(o),
12     uploadLoop(bindMethod(&FileSharing::uploadingLoop, this)), installId(id), wasCancelled(false) {
13   curl_global_init(CURL_GLOBAL_ALL);
14 }
15 
~FileSharing()16 FileSharing::~FileSharing() {
17   // pushing null acts as a signal for the upload loop to finish
18   uploadQueue.push(nullptr);
19 }
20 
21 struct CallbackData {
22   function<void(double)> progressCallback;
23   FileSharing* fileSharing;
24 };
25 
progressFunction(void * ptr,double totalDown,double nowDown,double totalUp,double nowUp)26 int progressFunction(void* ptr, double totalDown, double nowDown, double totalUp, double nowUp) {
27   auto callbackData = static_cast<CallbackData*>(ptr);
28   if (totalUp > 0)
29     callbackData->progressCallback(nowUp / totalUp);
30   if (totalDown > 0)
31     callbackData->progressCallback(nowDown / totalDown);
32   if (callbackData->fileSharing->consumeCancelled())
33     return 1;
34   else
35     return 0;
36 }
37 
dataFun(void * buffer,size_t size,size_t nmemb,void * userp)38 size_t dataFun(void *buffer, size_t size, size_t nmemb, void *userp) {
39   string& buf = *((string*) userp);
40   buf += string((char*) buffer, (char*) buffer + size * nmemb);
41   return size * nmemb;
42 }
43 
escapeSpaces(string s)44 static string escapeSpaces(string s) {
45   string ret;
46   for (auto c : s)
47     if (c == ' ')
48       ret += "%20";
49     else
50       ret += c;
51   return s;
52 }
53 
escapeEverything(const string & s)54 static string escapeEverything(const string& s) {
55   char* tmp = curl_easy_escape(curl_easy_init(), s.c_str(), (int) s.size());
56   string ret(tmp);
57   curl_free(tmp);
58   return ret;
59 }
60 
unescapeEverything(const string & s)61 static string unescapeEverything(const string& s) {
62   char* tmp = curl_easy_unescape(curl_easy_init(), s.c_str(), (int) s.size(), nullptr);
63   string ret(tmp);
64   curl_free(tmp);
65   return ret;
66 }
67 
curlUpload(const char * path,const char * url,const CallbackData & callback,int timeout)68 static optional<string> curlUpload(const char* path, const char* url, const CallbackData& callback, int timeout) {
69   struct curl_httppost *formpost=NULL;
70   struct curl_httppost *lastptr=NULL;
71 
72   curl_formadd(&formpost,
73       &lastptr,
74       CURLFORM_COPYNAME, "fileToUpload",
75       CURLFORM_FILE, path,
76       CURLFORM_END);
77 
78   curl_formadd(&formpost,
79       &lastptr,
80       CURLFORM_COPYNAME, "submit",
81       CURLFORM_COPYCONTENTS, "send",
82       CURLFORM_END);
83 
84   if (CURL* curl = curl_easy_init()) {
85     curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, dataFun);
86     string ret;
87     curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret);
88     /* what URL that receives this POST */
89     curl_easy_setopt(curl, CURLOPT_URL, escapeSpaces(url).c_str());
90     curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
91     if (timeout > 0)
92       curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout);
93     curl_easy_setopt(curl, CURLOPT_NOPROGRESS, false);
94     curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, &callback);
95     curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, progressFunction);
96     CURLcode res = curl_easy_perform(curl);
97     if (res != CURLE_OK)
98       ret = string("Upload failed: ") + curl_easy_strerror(res);
99     curl_easy_cleanup(curl);
100     curl_formfree(formpost);
101     if (!ret.empty())
102       return ret;
103     else
104       return none;
105   } else
106     return string("Failed to initialize libcurl");
107 }
108 
getCallbackData(FileSharing * f)109 static CallbackData getCallbackData(FileSharing* f) {
110   return { [] (double) {}, f };
111 }
112 
getCallbackData(FileSharing * f,ProgressMeter & meter)113 static CallbackData getCallbackData(FileSharing* f, ProgressMeter& meter) {
114   return { [&meter] (double p) { meter.setProgress((float) p); }, f };
115 }
116 
uploadSite(const FilePath & path,ProgressMeter & meter)117 optional<string> FileSharing::uploadSite(const FilePath& path, ProgressMeter& meter) {
118   if (!options.getBoolValue(OptionId::ONLINE))
119     return none;
120   return curlUpload(path.getPath(), (uploadUrl + "/upload_site.php").c_str(), getCallbackData(this, meter), 0);
121 }
122 
uploadHighscores(const FilePath & path)123 void FileSharing::uploadHighscores(const FilePath& path) {
124   if (options.getBoolValue(OptionId::ONLINE))
125     uploadQueue.push([this, path] {
126       curlUpload(path.getPath(), (uploadUrl + "/upload_scores.php").c_str(), getCallbackData(this), 5);
127     });
128 }
129 
uploadingLoop()130 void FileSharing::uploadingLoop() {
131   function<void()> uploadFun = uploadQueue.pop();
132   if (uploadFun)
133     uploadFun();
134   else
135     uploadLoop.setDone();
136 }
137 
uploadGameEvent(const GameEvent & data1,bool requireGameEventsPermission)138 bool FileSharing::uploadGameEvent(const GameEvent& data1, bool requireGameEventsPermission) {
139   GameEvent data(data1);
140   data.emplace("installId", toString(installId));
141   if (options.getBoolValue(OptionId::ONLINE) &&
142       (!requireGameEventsPermission || options.getBoolValue(OptionId::GAME_EVENTS))) {
143     uploadGameEventImpl(data, 5);
144     return true;
145   } else
146     return false;
147 }
148 
uploadGameEventImpl(const GameEvent & data,int tries)149 void FileSharing::uploadGameEventImpl(const GameEvent& data, int tries) {
150   if (tries >= 0)
151     uploadQueue.push([data, this, tries] {
152       string params;
153       for (auto& elem : data) {
154         if (!params.empty())
155           params += "&";
156         params += elem.first + "=" + escapeEverything(elem.second);
157       }
158       if (CURL* curl = curl_easy_init()) {
159         string ret;
160         curl_easy_setopt(curl, CURLOPT_URL, escapeSpaces(uploadUrl + "/game_event.php").c_str());
161         curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10);
162         curl_easy_setopt(curl, CURLOPT_POSTFIELDS, params.c_str());
163         CURLcode res = curl_easy_perform(curl);
164         curl_easy_cleanup(curl);
165         if (res != CURLE_OK)
166           uploadGameEventImpl(data, tries - 1);
167       }
168     });
169 }
170 
downloadHighscores(int version)171 string FileSharing::downloadHighscores(int version) {
172   string ret;
173   if (options.getBoolValue(OptionId::ONLINE))
174     if(CURL* curl = curl_easy_init()) {
175       curl_easy_setopt(curl, CURLOPT_URL,
176           escapeSpaces(uploadUrl + "/highscores.php?version=" + toString(version)).c_str());
177       curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, dataFun);
178       curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5);
179       curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret);
180       curl_easy_perform(curl);
181       curl_easy_cleanup(curl);
182     }
183   return ret;
184 }
185 
186 template<typename Elem>
parseLines(const string & s,function<optional<Elem> (const vector<string> &)> parseLine)187 static vector<Elem> parseLines(const string& s, function<optional<Elem>(const vector<string>&)> parseLine) {
188   std::stringstream iss(s);
189   vector<Elem> ret;
190   while (!!iss) {
191     char buf[10000];
192     iss.getline(buf, 10000);
193     if (!iss)
194       break;
195     INFO << "Parsing " << string(buf);
196     if (auto elem = parseLine(split(buf, {','})))
197       ret.push_back(*elem);
198   }
199   return ret;
200 
201 }
202 
downloadContent(const string & url)203 optional<string> FileSharing::downloadContent(const string& url) {
204   if (CURL* curl = curl_easy_init()) {
205     curl_easy_setopt(curl, CURLOPT_URL, escapeSpaces(url).c_str());
206     curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, dataFun);
207     curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10);
208     curl_easy_setopt(curl, CURLOPT_NOPROGRESS, false);
209     auto callback = getCallbackData(this);
210     curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, &callback);
211     curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, progressFunction);
212     string ret;
213     curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret);
214     CURLcode res = curl_easy_perform(curl);
215     curl_easy_cleanup(curl);
216     if (res == CURLE_OK)
217       return ret;
218   }
219   return none;
220 }
221 
parseSite(const vector<string> & fields)222 static optional<FileSharing::SiteInfo> parseSite(const vector<string>& fields) {
223   if (fields.size() < 6)
224     return none;
225   INFO << "Parsed " << fields;
226   FileSharing::SiteInfo elem;
227   elem.fileInfo.filename = fields[0];
228   try {
229     elem.fileInfo.date = fromString<int>(fields[1]);
230     elem.wonGames = fromString<int>(fields[2]);
231     elem.totalGames = fromString<int>(fields[3]);
232     elem.version = fromString<int>(fields[5]);
233     elem.fileInfo.download = true;
234     TextInput input(fields[4]);
235     input.getArchive() >> elem.gameInfo;
236   } catch (cereal::Exception) {
237     return none;
238   } catch (ParsingException e) {
239     return none;
240   }
241   return elem;
242 }
243 
listSites()244 optional<vector<FileSharing::SiteInfo>> FileSharing::listSites() {
245   if (!options.getBoolValue(OptionId::ONLINE))
246     return {};
247   if (auto content = downloadContent(uploadUrl + "/get_sites.php"))
248     return parseLines<FileSharing::SiteInfo>(*content, parseSite);
249   else
250     return none;
251 }
252 
parseBoardMessage(const vector<string> & fields)253 static optional<FileSharing::BoardMessage> parseBoardMessage(const vector<string>& fields) {
254   if (fields.size() >= 2)
255     return FileSharing::BoardMessage{unescapeEverything(fields[0]), unescapeEverything(fields[1])};
256   else
257     return none;
258 }
259 
getBoardMessages(int boardId)260 optional<vector<FileSharing::BoardMessage>> FileSharing::getBoardMessages(int boardId) {
261   if (options.getBoolValue(OptionId::ONLINE))
262     if (auto content = downloadContent(uploadUrl + "/get_messages.php?boardId=" + toString(boardId)))
263       return parseLines<FileSharing::BoardMessage>(*content, parseBoardMessage);
264   return none;
265 }
266 
uploadBoardMessage(const string & gameId,int hash,const string & author,const string & text)267 bool FileSharing::uploadBoardMessage(const string& gameId, int hash, const string& author, const string& text) {
268   return uploadGameEvent({
269       { "gameId", gameId },
270       { "eventType", "boardMessage"},
271       { "boardId", toString(hash) },
272       { "author", author },
273       { "text", text }
274   }, false);
275 }
276 
cancel()277 void FileSharing::cancel() {
278   wasCancelled = true;
279 }
280 
consumeCancelled()281 bool FileSharing::consumeCancelled() {
282   return wasCancelled.exchange(false);
283 }
284 
writeToFile(void * ptr,size_t size,size_t nmemb,FILE * stream)285 static size_t writeToFile(void *ptr, size_t size, size_t nmemb, FILE *stream) {
286   return fwrite(ptr, size, nmemb, stream);
287 }
288 
download(const string & filename,const DirectoryPath & dir,ProgressMeter & meter)289 optional<string> FileSharing::download(const string& filename, const DirectoryPath& dir, ProgressMeter& meter) {
290   if (!options.getBoolValue(OptionId::ONLINE))
291     return string("Downloading not enabled!");
292   //progressFun = [&] (double p) { meter.setProgress(p);};
293   if (CURL *curl = curl_easy_init()) {
294     auto path = dir.file(filename);
295     INFO << "Downloading to " << path;
296     if (FILE* fp = fopen(path.getPath(), "wb")) {
297       curl_easy_setopt(curl, CURLOPT_URL, escapeSpaces(uploadUrl + "/uploads/" + filename).c_str());
298       curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeToFile);
299       curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
300       // Internal CURL progressmeter must be disabled if we provide our own callback
301       curl_easy_setopt(curl, CURLOPT_NOPROGRESS, false);
302       // Install the callback function
303       auto callback = getCallbackData(this, meter);
304       curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, &callback);
305       curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, progressFunction);
306       curl_easy_setopt(curl, CURLOPT_FAILONERROR, true);
307       CURLcode res = curl_easy_perform(curl);
308       string ret;
309       if(res != CURLE_OK)
310         ret = string("Download failed: ") + curl_easy_strerror(res);
311       curl_easy_cleanup(curl);
312       fclose(fp);
313       if (!ret.empty()) {
314         remove(path.getPath());
315         return ret;
316       } else
317         return none;
318     } else
319       return string("Failed to open file: "_s + path.getPath());
320   } else
321     return string("Failed to initialize libcurl");
322 }
323 
324