/******************************************************************************** Copyright (C) 2014 Ruwen Hahn This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. ********************************************************************************/ #include "Main.h" #include "LogUploader.h" #include "HTTPClient.h" #include #include #include using namespace std; namespace { struct HTTPHandleDeleter { void operator()(HINTERNET h) { WinHttpCloseHandle(h); } }; struct HTTPHandle : unique_ptr { explicit HTTPHandle(HINTERNET h=nullptr) : unique_ptr(h) {} operator HINTERNET() { return get(); } bool operator!() { return get() == nullptr; } }; bool HTTPProlog(String url, String &path, HTTPHandle &session, HTTPHandle &connect, bool &secure) { URL_COMPONENTS urlComponents; String hostName; hostName.SetLength(256); path.SetLength(1024); zero(&urlComponents, sizeof(urlComponents)); urlComponents.dwStructSize = sizeof(urlComponents); urlComponents.lpszHostName = hostName; urlComponents.dwHostNameLength = hostName.Length(); urlComponents.lpszUrlPath = path; urlComponents.dwUrlPathLength = path.Length(); WinHttpCrackUrl(url, 0, 0, &urlComponents); if (urlComponents.nPort == INTERNET_DEFAULT_HTTPS_PORT) secure = true; session.reset(WinHttpOpen(OBS_VERSION_STRING, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0)); if (!session) return false; connect.reset(WinHttpConnect(session, hostName, secure ? INTERNET_DEFAULT_HTTPS_PORT : INTERNET_DEFAULT_HTTP_PORT, 0)); return !!connect; } bool HTTPReceiveStatus(HTTPHandle &request, int &status) { if (!WinHttpReceiveResponse(request, NULL)) return false; TCHAR statusCode[8]; DWORD statusCodeLen; statusCodeLen = sizeof(statusCode); if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE, WINHTTP_HEADER_NAME_BY_INDEX, &statusCode, &statusCodeLen, WINHTTP_NO_HEADER_INDEX)) return false; status = wcstoul(statusCode, NULL, 10); return true; } bool HTTPPostData(String url, void *data, size_t length, int &response, List *resultBody, String const &headers=String()) { HTTPHandle session, connect; bool secure; String path; if (!HTTPProlog(url, path, session, connect, secure)) return false; HTTPHandle request(WinHttpOpenRequest(connect, L"POST", path, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, secure ? WINHTTP_FLAG_SECURE : 0)); if (!request) return false; // End the request. if (!WinHttpSendRequest(request, headers.Array(), headers.IsEmpty() ? 0 : -1, data, (DWORD)length, (DWORD)length, 0)) return false; if (!HTTPReceiveStatus(request, response)) return false; if (!resultBody) return true; DWORD size = 0, read = 0, offset = 0; do { if (!WinHttpQueryDataAvailable(request, &size) || size > 16386) return false; resultBody->SetSize(size + resultBody->Num()); if (!WinHttpReadData(request, (LPVOID)(resultBody->Array() + offset), size, &read)) return false; offset += read; } while (size && read); return true; } bool HTTPPostData(String url, String data, int &response, List *resultBody=nullptr, String const &headers=String()) { LPSTR str = data.CreateUTF8String(); bool result = HTTPPostData(url, str, strlen(str), response, resultBody, headers); Free(str); return result; } bool HTTPFindRedirect(String url, String &location) { HTTPHandle session, connect; bool secure; String path; if (!HTTPProlog(url, path, session, connect, secure)) return false; HTTPHandle request(WinHttpOpenRequest(connect, L"HEAD", path, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, secure ? WINHTTP_FLAG_SECURE : 0)); if (!request) return false; // End the request. if (!WinHttpSendRequest(request, nullptr, 0, nullptr, 0, 0, 0)) return false; int status; if (!HTTPReceiveStatus(request, status)) return false; location.SetLength(MAX_PATH); DWORD length = MAX_PATH; if (WinHttpQueryHeaders(request, WINHTTP_QUERY_LOCATION, WINHTTP_HEADER_NAME_BY_INDEX, location.Array(), &length, WINHTTP_NO_HEADER_INDEX)) { location.SetLength(length); return true; } length = MAX_PATH; if (WinHttpQueryHeaders(request, WINHTTP_QUERY_CUSTOM, L"X-Gist-Url", location.Array(), &length, WINHTTP_NO_HEADER_INDEX)) { location.SetLength(length); location = FormattedString(L"https://gist.github.com%s", location.Array()); return true; } return false; } String LogFileAge(String &name) { FILETIME now_ft, log_ft; SYSTEMTIME now_st, log_st; zero(&log_st, sizeof log_st); if (swscanf_s(name.Array(), L"%u-%02u-%02u-%02u%02u-%02u", &log_st.wYear, &log_st.wMonth, &log_st.wDay, &log_st.wHour, &log_st.wMinute, &log_st.wSecond) != 6) return String(); GetLocalTime(&now_st); SystemTimeToFileTime(&now_st, &now_ft); SystemTimeToFileTime(&log_st, &log_ft); ULARGE_INTEGER now_, log_, diff; now_.LowPart = now_ft.dwLowDateTime; now_.HighPart = now_ft.dwHighDateTime; log_.LowPart = log_ft.dwLowDateTime; log_.HighPart = log_ft.dwHighDateTime; if (now_.QuadPart <= log_.QuadPart) return String(); diff.QuadPart = now_.QuadPart - log_.QuadPart; diff.QuadPart /= 10000000; diff.QuadPart /= 60; if (diff.QuadPart < 1) return "less than a minute"; unsigned minutes = diff.QuadPart % 60; diff.QuadPart /= 60; unsigned hours = diff.QuadPart % 24; diff.QuadPart /= 24; unsigned days = diff.QuadPart % 7; unsigned weeks = (diff.QuadPart/7) % 52; diff.QuadPart /= 365; //losing accuracy ... unsigned years = (unsigned)diff.QuadPart; StringList ages; #define N_FORMAT(name, num) if (num > 0) ages << FormattedString(L"%u " name L"%s", num, num == 1 ? L"" : L"s") N_FORMAT(L"year", years); N_FORMAT(L"week", weeks); N_FORMAT(L"day", days); N_FORMAT(L"hour", hours); N_FORMAT(L"minute", minutes); #undef N_FORMAT if (ages.Num() == 1) return ages.Last(); return ages[0] << L" and " << ages[1]; } void StringEscapeJson(String &str) { str.FindReplace(L"\\", L"\\\\"); str.FindReplace(L"\"", L"\\\""); str.FindReplace(L"\n", L"\\n"); str.FindReplace(L"\r", L"\\r"); str.FindReplace(L"\f", L"\\f"); str.FindReplace(L"\b", L"\\b"); str.FindReplace(L"\t", L"\\t"); str.FindReplace(L"/", L"\\/"); } } bool UploadLogGitHub(String filename, String logData, LogUploadResult &result) { String description = FormattedString(OBS_VERSION_STRING L" log file uploaded at %s (local time).", CurrentDateTimeString().Array()); String age = LogFileAge(filename); if (age.IsValid()) description << FormattedString(L" The log file was approximately %s old at the time it was uploaded.", age.Array()); StringEscapeJson(description); StringEscapeJson(filename); StringEscapeJson(logData); String json = FormattedString(L"{ \"public\": false, \"description\": \"%s\", \"files\": { \"%s\": { \"content\": \"%s\" } } }", description.Array(), filename.Array(), logData.Array()); int response = 0; List body; if (!HTTPPostData(String(L"https://api.github.com/gists"), json, response, &body)) { result.errors << Str("LogUpload.CommunicationError"); return false; } if (response != 201) { result.errors << FormattedString(Str("LogUpload.ServiceReturnedError"), response) << FormattedString(Str("LogUpload.ServiceExpectedResponse"), 201); return false; } auto invalid_response = [&]() -> bool { result.errors << Str("LogUpload.ServiceReturnedInvalidResponse"); return false; }; if (body.Num() < 1) return invalid_response(); //make sure it's null terminated since we run string ops on it below body.Add (0); TSTR wideBody = utf8_createTstr((char const*)body.Array()); String bodyStr(wideBody); Free(wideBody); TSTR pos = sstr(bodyStr.Array(), L"\"html_url\""); if (!pos) return invalid_response(); pos = schr(pos + slen(L"\"html_url\""), '"'); if (!pos) return invalid_response(); pos += 1; TSTR end = schr(pos, '"'); if (!end) return invalid_response(); if ((end - pos) < 4) return invalid_response(); result.url = bodyStr.Mid((UINT)(pos - bodyStr.Array()), (UINT)(end - bodyStr.Array())); if (!HTTPFindRedirect(result.url, result.analyzerURL)) //the basic url doesn't work with the analyzer, so query the fully redirected url result.analyzerURL = result.url; return true; } // Game Capture log is always appended, as requested by Jim (yes, this can result in two game capture logs in one upload) static void AppendGameCaptureLog(String &data) { String path = FormattedString(L"%s\\captureHookLog.txt", OBSGetPluginDataPath().Array()); XFile f(path.Array(), XFILE_READ | XFILE_SHARED, XFILE_OPENEXISTING); if (!f.IsOpen()) return; String append; f.ReadFileToString(append); data << L"\r\n\r\nLast Game Capture Log:\r\n" << append; } bool UploadCurrentLog(LogUploadResult &result) { String data; ReadLog(data); if (data.IsEmpty()) { result.errors << Str("LogUpload.EmptyLog"); return false; } AppendGameCaptureLog(data); String filename = CurrentLogFilename(); return UploadLogGitHub(GetPathFileName(filename.FindReplace(L"\\", L"/").Array(), true), data, result); } bool UploadLog(String filename, LogUploadResult &result) { String path = FormattedString(L"%s\\logs\\%s", OBSGetAppDataPath(), filename.Array()); XFile f(path.Array(), XFILE_READ, XFILE_OPENEXISTING); if (!f.IsOpen()) { result.errors << FormattedString(Str("LogUpload.CannotOpenFile"), path.Array()); return false; } String data; f.ReadFileToString(data); if (data.IsEmpty()) { result.errors << Str("LogUpload.EmptyLog"); return false; } AppendGameCaptureLog(data); return UploadLogGitHub(filename.Array(), data, result); }