373 lines
12 KiB
C++
373 lines
12 KiB
C++
/********************************************************************************
|
|
Copyright (C) 2014 Ruwen Hahn <palana@stunned.de>
|
|
|
|
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 <winhttp.h>
|
|
|
|
#include <map>
|
|
|
|
#include <memory>
|
|
|
|
namespace
|
|
{
|
|
struct HTTPHandleDeleter
|
|
{
|
|
void operator()(HINTERNET h) { WinHttpCloseHandle(h); }
|
|
};
|
|
|
|
struct HTTPHandle : std::unique_ptr<void, HTTPHandleDeleter>
|
|
{
|
|
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;
|
|
else
|
|
secure = false;
|
|
|
|
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<BYTE> *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<BYTE> *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"\\/");
|
|
|
|
// github api chokes when we submit unescaped %, so encode it and
|
|
// any other lower and higher characters just to be safe
|
|
String newStr;
|
|
int len = slen(str);
|
|
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
if (str[i] < ' ' || str[i] > 255 || str[i] == '%')
|
|
newStr << FormattedString(L"\\u%04X", (int)str[i]);
|
|
else
|
|
newStr << str[i];
|
|
}
|
|
|
|
str = newStr;
|
|
}
|
|
}
|
|
|
|
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<BYTE> 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);
|
|
} |