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