obs/Source/LogUploader.cpp
palana 766c527ed3 Add approximate log file age to Gist description
The accuracy depends on the consistency of the local time and for older
log files the accuracy will degrade rather quickly
2014-02-12 07:57:53 +01:00

356 lines
11 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>
using namespace std;
namespace
{
struct HTTPHandleDeleter
{
void operator()(HINTERNET h) { WinHttpCloseHandle(h); }
};
struct HTTPHandle : 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;
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())
{
OutputDebugString(data.Array());
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<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();
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_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);
}