├── src ├── Precompiled.cpp ├── Steam │ ├── Steam.h │ ├── Captcha.h │ ├── Misc.h │ ├── Trade.h │ ├── Guard.h │ └── Auth.h ├── Precompiled.h ├── Curl.h ├── Crypto.h ├── Main.cpp ├── Misc.h ├── Market.h └── Account.h ├── screenshot.png ├── OpenMarketClient.sln ├── Makefile ├── README.md ├── vsproject ├── OpenMarketClient.vcxproj.filters └── OpenMarketClient.vcxproj ├── .gitattributes ├── .gitignore └── LICENSE /src/Precompiled.cpp: -------------------------------------------------------------------------------- 1 | #include "Precompiled.h" -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soyware/OpenMarketClient/HEAD/screenshot.png -------------------------------------------------------------------------------- /src/Steam/Steam.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Steam 4 | { 5 | namespace Auth 6 | { 7 | const size_t jwtBufSz = 600; 8 | } 9 | 10 | void RateLimit() 11 | { 12 | static std::chrono::high_resolution_clock::time_point nextRequestTime; 13 | std::this_thread::sleep_until(nextRequestTime); 14 | 15 | const auto curTime = std::chrono::high_resolution_clock::now(); 16 | const auto requestInterval = 1s; 17 | nextRequestTime = curTime + requestInterval; 18 | } 19 | 20 | CURLcode curl_easy_perform(CURL* curl) 21 | { 22 | RateLimit(); 23 | 24 | return ::curl_easy_perform(curl); 25 | } 26 | 27 | inline uint64_t SteamID32To64(uint32_t id32) 28 | { 29 | return (id32 | 0x110000100000000); 30 | } 31 | 32 | inline uint32_t SteamID64To32(uint64_t id64) 33 | { 34 | return (id64 & 0xFFFFFFFF); 35 | } 36 | } 37 | 38 | #include "Misc.h" 39 | #include "Captcha.h" 40 | #include "Trade.h" 41 | #include "Guard.h" 42 | #include "Auth.h" -------------------------------------------------------------------------------- /OpenMarketClient.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.329 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "OpenMarketClient", "vsproject\OpenMarketClient.vcxproj", "{454F87DA-5D7C-4458-8834-BC39DBF19FE8}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Debug|x86 = Debug|x86 12 | Release|x64 = Release|x64 13 | Release|x86 = Release|x86 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {454F87DA-5D7C-4458-8834-BC39DBF19FE8}.Debug|x64.ActiveCfg = Debug|x64 17 | {454F87DA-5D7C-4458-8834-BC39DBF19FE8}.Debug|x64.Build.0 = Debug|x64 18 | {454F87DA-5D7C-4458-8834-BC39DBF19FE8}.Debug|x86.ActiveCfg = Debug|Win32 19 | {454F87DA-5D7C-4458-8834-BC39DBF19FE8}.Debug|x86.Build.0 = Debug|Win32 20 | {454F87DA-5D7C-4458-8834-BC39DBF19FE8}.Release|x64.ActiveCfg = Release|x64 21 | {454F87DA-5D7C-4458-8834-BC39DBF19FE8}.Release|x64.Build.0 = Release|x64 22 | {454F87DA-5D7C-4458-8834-BC39DBF19FE8}.Release|x86.ActiveCfg = Release|Win32 23 | {454F87DA-5D7C-4458-8834-BC39DBF19FE8}.Release|x86.Build.0 = Release|Win32 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {3BB62A65-5AA0-4130-9487-706400039F4E} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /src/Precompiled.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifdef _WIN32 4 | #define _CRT_SECURE_NO_WARNINGS 5 | #endif // _WIN32 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #ifdef _WIN32 17 | 18 | // target windows 7 19 | #define _WIN32_WINNT 0x0601 20 | #define WIN32_LEAN_AND_MEAN 21 | #define NOMINMAX 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #ifndef PATH_MAX 30 | #define PATH_MAX MAX_PATH 31 | #endif // !PATH_MAX 32 | 33 | #define WOLFSSL_LIB 34 | #include 35 | #undef WOLFSSL_LIB 36 | 37 | #else 38 | 39 | #include 40 | #include 41 | 42 | #define _isatty isatty 43 | #define _stat stat 44 | #define _fstat fstat 45 | #define _fileno fileno 46 | 47 | #include 48 | 49 | #endif // _WIN32 50 | 51 | #include "rapidjson/document.h" 52 | #include "rapidjson/writer.h" 53 | #include "rapidjson/stringbuffer.h" 54 | #include "wolfssl/wolfcrypt/error-crypt.h" 55 | #include "wolfssl/wolfcrypt/coding.h" 56 | #include "wolfssl/wolfcrypt/rsa.h" 57 | #include "wolfssl/wolfcrypt/aes.h" 58 | #include "wolfssl/wolfcrypt/pwdbased.h" 59 | #include "wolfssl/wolfcrypt/hmac.h" 60 | #include "wolfssl/version.h" 61 | #include "curl/curl.h" 62 | 63 | using namespace std::chrono_literals; 64 | 65 | #define UINT32_MAX_STR_SIZE sizeof("4294967295") 66 | #define UINT64_MAX_STR_SIZE sizeof("18446744073709551615") -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CXX=g++ 2 | CXXFLAGS=-std=c++17 -O2 -fno-unwind-tables -fno-asynchronous-unwind-tables -flto -Wall 3 | 4 | # Directories 5 | DIR_RAPIDJSON=../libs/rapidjson 6 | DIR_INSTALLED_LIBS=/usr/local/lib 7 | DIR_SRC=src 8 | DIR_OUT=build/linux 9 | DIR_OBJ=$(DIR_OUT)/obj 10 | 11 | # Target 12 | TARGET=OpenMarketClient 13 | 14 | # Precompiled Header 15 | PCH_HEADER=$(DIR_SRC)/Precompiled.h 16 | PCH_SOURCE=$(DIR_SRC)/Precompiled.cpp 17 | PCH_GCH=$(DIR_OBJ)/$(notdir $(PCH_HEADER)).gch 18 | 19 | # Compiler and Linker Flags 20 | LDFLAGS=-Wl,-s,-rpath,$(DIR_INSTALLED_LIBS) -lpthread -lstdc++fs -lwolfssl -lcurl 21 | INCLUDES=-I$(DIR_RAPIDJSON)/include 22 | 23 | # Find all .cpp files, excluding the PCH source which is not needed for compilation 24 | SOURCES=$(filter-out $(PCH_SOURCE), $(shell find $(DIR_SRC) -name '*.cpp')) 25 | # Create object file paths for regular sources 26 | OBJECTS=$(patsubst $(DIR_SRC)/%.cpp,$(DIR_OBJ)/%.o,$(SOURCES)) 27 | # Create dependency file paths for object files 28 | DEPS=$(patsubst %.o,%.d,$(OBJECTS)) 29 | 30 | .PHONY: all clean 31 | 32 | all: $(DIR_OUT)/$(TARGET) 33 | 34 | # Link the executable 35 | $(DIR_OUT)/$(TARGET): $(OBJECTS) 36 | @echo "Linking..." 37 | $(CXX) $(CXXFLAGS) $^ -o $@ $(LDFLAGS) 38 | 39 | # Compile source files into object files 40 | $(DIR_OBJ)/%.o: $(DIR_SRC)/%.cpp $(PCH_GCH) | $(DIR_OBJ) 41 | @echo "Compiling $<..." 42 | $(CXX) $(CXXFLAGS) $(INCLUDES) -MMD -MP -MF $(patsubst %.o,%.d,$@) -include $(PCH_HEADER) -c $< -o $@ 43 | 44 | # Create the precompiled header from the header file 45 | $(PCH_GCH): $(PCH_HEADER) | $(DIR_OBJ) 46 | @echo "Creating precompiled header..." 47 | $(CXX) $(CXXFLAGS) $(INCLUDES) $< -o $@ 48 | 49 | # Create build directories 50 | $(DIR_OBJ): 51 | @mkdir -p $(DIR_OBJ) 52 | 53 | clean: 54 | @echo "Cleaning build files..." 55 | rm -rf $(DIR_OBJ) $(DIR_OUT)/$(TARGET) 56 | 57 | # Include dependency files 58 | -include $(DEPS) 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenMarketClient 2 | 3 | An unofficial, cross-platform, console-based client for the following markets: 4 | * [market.csgo.com](https://market.csgo.com) 5 | * [market.dota2.net](https://market.dota2.net) 6 | * [tf2.tm](https://tf2.tm) 7 | * [rust.tm](https://rust.tm) 8 | * [gifts.tm](https://gifts.tm) 9 | 10 | ![screenshot](screenshot.png) 11 | 12 | # Features 13 | * Multi-account support 14 | * Proxy support 15 | * Sets Steam inventory public 16 | * Sets trade token and Steam web API key on the market 17 | * Keeps your market profile online 18 | * Sends sold items 19 | * Receives bought items 20 | * Accepts Steam Guard confirmations of sent offers 21 | * Cancels offers that aren't accepted within 10 minutes (required since Steam removed the `CancelTradeOffer` web API) 22 | * Ability to import Steam Desktop Authenticator's `.maFile` 23 | * Accounts are password encrypted 24 | 25 | # Usage 26 | You'll be asked to enter an encryption password which will be used to encrypt and decrypt saved accounts. 27 | 28 | ## Adding an Account Manually 29 | If you run the client without any accounts added, you will be asked to add a new one. To add another account later, launch the program with the `--new` command-line option. 30 | 31 | ## Importing Steam Desktop Authenticator's .maFile 32 | To import an account from SDA, place the unencrypted `.maFile` into the `accounts` folder (create the folder if it doesn't exist). The program will automatically import most of the required details. 33 | 34 | ## Required Details 35 | * Market API key ([you can get one here](https://market.csgo.com/docs-v2)) 36 | * Steam username 37 | * Steam password 38 | * Steam Guard Mobile Authenticator details: 39 | * Two-factor authentication code 40 | * Identity secret 41 | 42 | You can find instructions on how to extract Steam Guard Mobile Authenticator details from your phone [here](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Two-factor-authentication#android-phone). 43 | 44 | ## Command-line Options 45 | * `--new` - Add a new account by manually entering the details 46 | * `--proxy [scheme://][username:password@]host[:port]` - Sets the global proxy 47 | * `--market-use-proxy` - Tells the market to perform actions using the proxy specified in `--proxy`, presumably to avoid Steam bans 48 | 49 | # Build Requirements 50 | * C++17 supporting compiler 51 | * libcurl 52 | * wolfSSL 53 | * RapidJSON 54 | -------------------------------------------------------------------------------- /vsproject/OpenMarketClient.vcxproj.filters: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | {19a00402-8df7-4715-bf3c-ed3301bf5470} 18 | 19 | 20 | 21 | 22 | Source Files 23 | 24 | 25 | Source Files 26 | 27 | 28 | 29 | 30 | Header Files 31 | 32 | 33 | Header Files 34 | 35 | 36 | Header Files 37 | 38 | 39 | Header Files 40 | 41 | 42 | Header Files 43 | 44 | 45 | Header Files 46 | 47 | 48 | Header Files\Steam 49 | 50 | 51 | Header Files\Steam 52 | 53 | 54 | Header Files\Steam 55 | 56 | 57 | Header Files\Steam 58 | 59 | 60 | Header Files\Steam 61 | 62 | 63 | Header Files\Steam 64 | 65 | 66 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/Steam/Captcha.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Steam 4 | { 5 | namespace Captcha 6 | { 7 | const size_t gidBufSz = UINT64_MAX_STR_SIZE; 8 | const size_t answerBufSz = 6 + 1; 9 | 10 | // out buffer size must be at least gidBufSz 11 | bool GetGID(CURL* curl, char* out) 12 | { 13 | Log(LogChannel::STEAM, "Refreshing captcha..."); 14 | 15 | Curl::CResponse response; 16 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 17 | curl_easy_setopt(curl, CURLOPT_URL, "https://steamcommunity.com/login/refreshcaptcha/"); 18 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 19 | 20 | const CURLcode respCode = curl_easy_perform(curl); 21 | 22 | if (respCode != CURLE_OK) 23 | { 24 | Curl::PrintError(curl, respCode); 25 | return false; 26 | } 27 | 28 | rapidjson::Document parsed; 29 | parsed.ParseInsitu(response.data); 30 | 31 | if (parsed.HasParseError()) 32 | { 33 | putsnn("JSON parsing failed\n"); 34 | return false; 35 | } 36 | 37 | const rapidjson::Value& gid = parsed["gid"]; 38 | 39 | if (gid.IsString()) 40 | strcpy(out, gid.GetString()); 41 | else 42 | strcpy(out, "-1"); 43 | 44 | printf("ok\n"); 45 | return true; 46 | } 47 | 48 | // out buffer size must be at least answerBufSz 49 | bool GetAnswer(CURL* curl, const char* gid, char* out) 50 | { 51 | const char urlPart[] = "https://steamcommunity.com/login/rendercaptcha/?gid="; 52 | 53 | const size_t urlSz = sizeof(urlPart) - 1 + gidBufSz - 1 + 1; 54 | char url[urlSz]; 55 | 56 | char* urlEnd = url; 57 | urlEnd = stpcpy(urlEnd, urlPart); 58 | strcpy(urlEnd, gid); 59 | 60 | #ifdef _WIN32 61 | Log(LogChannel::STEAM, "Downloading captcha image..."); 62 | 63 | const char filename[] = "captcha.png"; 64 | 65 | FILE* file = fopen(filename, "wb"); 66 | if (!file) 67 | { 68 | putsnn("file creation failed\n"); 69 | return false; 70 | } 71 | 72 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, file); 73 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NULL); // set write callback to default file write 74 | curl_easy_setopt(curl, CURLOPT_URL, url); 75 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 76 | 77 | const CURLcode respCode = curl_easy_perform(curl); 78 | 79 | fclose(file); 80 | 81 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, Curl::CResponse::WriteCallback); 82 | 83 | if (respCode != CURLE_OK) 84 | { 85 | Curl::PrintError(curl, respCode); 86 | return false; 87 | } 88 | 89 | putsnn("ok\n"); 90 | 91 | Log(LogChannel::STEAM, "Opening captcha image..."); 92 | 93 | // not sure if needed MSDC says call it before calling ShellExecute 94 | const HRESULT coInitRes = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); 95 | const bool coInitSucceeded = ((coInitRes == S_OK) || (coInitRes == S_FALSE)); 96 | 97 | if (32 >= (INT_PTR)ShellExecuteA(NULL, NULL, filename, NULL, NULL, SW_SHOWNORMAL)) 98 | { 99 | if (coInitSucceeded) 100 | CoUninitialize(); 101 | 102 | putsnn("fail\n"); 103 | return false; 104 | } 105 | 106 | if (coInitSucceeded) 107 | CoUninitialize(); 108 | 109 | putsnn("ok\n"); 110 | #else 111 | 112 | Log(LogChannel::STEAM, "Captcha URL: %s\n", url); 113 | #endif // _WIN32 114 | 115 | if (!GetUserInputString("Enter captcha answer", out, answerBufSz, answerBufSz - 1)) 116 | return false; 117 | 118 | return true; 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /src/Curl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Curl 4 | { 5 | class CResponse 6 | { 7 | public: 8 | char* data = nullptr; 9 | size_t size = 0; 10 | 11 | CResponse() 12 | { 13 | 14 | } 15 | 16 | ~CResponse() 17 | { 18 | free(data); 19 | } 20 | 21 | CResponse(const CResponse&) = delete; 22 | CResponse(const CResponse&&) = delete; 23 | 24 | void Empty() 25 | { 26 | free(data); 27 | data = nullptr; 28 | size = 0; 29 | } 30 | 31 | static size_t WriteCallback(void* data, size_t size, size_t count, CResponse* out) 32 | { 33 | const size_t totalSize = count * size; 34 | char* newMem = (char*)realloc(out->data, out->size + totalSize + 1); 35 | if (!newMem) 36 | { 37 | #ifdef _DEBUG 38 | putsnn("libcurl write callback realloc failed\n"); 39 | #endif // _DEBUG 40 | return 0; 41 | } 42 | 43 | out->data = newMem; 44 | memcpy(out->data + out->size, data, totalSize); 45 | out->size += totalSize; 46 | out->data[out->size] = '\0'; 47 | 48 | return totalSize; 49 | } 50 | }; 51 | 52 | void PrintError(CURL* curl, CURLcode respCode) 53 | { 54 | if (respCode == CURLE_HTTP_RETURNED_ERROR) 55 | { 56 | long httpCode; 57 | curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); 58 | printf("request failed (HTTP response code %ld)\n", httpCode); 59 | } 60 | else 61 | printf("request failed (libcurl code %d)\n", respCode); 62 | } 63 | 64 | bool DownloadCACert(CURL* curl, const char* path) 65 | { 66 | Log(LogChannel::LIBCURL, "Downloading CA certificate..."); 67 | 68 | FILE* file = fopen(path, "wb"); 69 | if (!file) 70 | { 71 | putsnn("writing failed\n"); 72 | return false; 73 | } 74 | 75 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, file); 76 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 77 | curl_easy_setopt(curl, CURLOPT_URL, "https://curl.haxx.se/ca/cacert.pem"); 78 | curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); 79 | 80 | const CURLcode respCode = curl_easy_perform(curl); 81 | 82 | fclose(file); 83 | 84 | curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); 85 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, stdout); 86 | 87 | if (respCode != CURLE_OK) 88 | { 89 | remove(path); 90 | PrintError(curl, respCode); 91 | return false; 92 | } 93 | 94 | putsnn("ok\n"); 95 | return true; 96 | } 97 | 98 | bool SetCACert(CURL* curl, const char* path) 99 | { 100 | bool certOk = false; 101 | 102 | FILE* file = fopen(path, "rb"); 103 | // check if the certificate file exists and isn't too old 104 | if (file) 105 | { 106 | struct _stat fileStat; 107 | 108 | if (!_fstat(_fileno(file), &fileStat)) { 109 | const double modTimeDiff = difftime(time(nullptr), fileStat.st_mtime); 110 | 111 | // (90 days) 112 | certOk = modTimeDiff < (60.0 * 60.0 * 24.0 * 90.0); 113 | } 114 | 115 | fclose(file); 116 | } 117 | 118 | if (!certOk && !DownloadCACert(curl, path)) 119 | return false; 120 | 121 | if (curl_easy_setopt(curl, CURLOPT_CAINFO, path) != CURLE_OK) 122 | { 123 | Log(LogChannel::LIBCURL, "Setting CA certificate failed\n"); 124 | return false; 125 | } 126 | 127 | return true; 128 | } 129 | 130 | CURL* Init(const char* proxy) 131 | { 132 | if (curl_global_init(CURL_GLOBAL_ALL)) 133 | { 134 | Log(LogChannel::LIBCURL, "Global init failed\n"); 135 | return nullptr; 136 | } 137 | 138 | CURL* curl = curl_easy_init(); 139 | if (!curl) 140 | { 141 | curl_global_cleanup(); 142 | Log(LogChannel::LIBCURL, "Easy session init failed\n"); 143 | return nullptr; 144 | } 145 | 146 | curl_easy_setopt(curl, CURLOPT_TIMEOUT, 20L); 147 | curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); 148 | curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); 149 | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 150 | curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L); 151 | 152 | if (proxy && proxy[0]) 153 | { 154 | Log(LogChannel::LIBCURL, "Setting a proxy..."); 155 | 156 | if (curl_easy_setopt(curl, CURLOPT_PROXY, proxy) != CURLE_OK) 157 | { 158 | curl_easy_cleanup(curl); 159 | curl_global_cleanup(); 160 | putsnn("fail\n"); 161 | return nullptr; 162 | } 163 | 164 | putsnn("ok\n"); 165 | } 166 | 167 | // let libcurl use system's default on linux 168 | #ifdef _WIN32 169 | if (!SetCACert(curl, "cacert.pem")) 170 | { 171 | curl_easy_cleanup(curl); 172 | curl_global_cleanup(); 173 | return nullptr; 174 | } 175 | #endif // _WIN32 176 | 177 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CResponse::WriteCallback); 178 | 179 | return curl; 180 | } 181 | } -------------------------------------------------------------------------------- /src/Crypto.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef BASE64_LINE_SZ 4 | #define BASE64_LINE_SZ 64 5 | #endif // !BASE64_LINE_SZ 6 | 7 | constexpr word32 PlainToBase64Size(size_t inLen, Escaped escaped) 8 | { 9 | word32 outSz = (inLen + 3 - 1) / 3 * 4; 10 | word32 addSz = (outSz + BASE64_LINE_SZ - 1) / BASE64_LINE_SZ; /* new lines */ 11 | 12 | if (escaped == WC_ESC_NL_ENC) 13 | addSz *= 3; /* instead of just \n, we're doing %0A triplet */ 14 | else if (escaped == WC_NO_NL_ENC) 15 | addSz = 0; /* encode without \n */ 16 | 17 | outSz += addSz; 18 | 19 | return outSz; 20 | } 21 | 22 | constexpr word32 Base64ToPlainSize(size_t base64Len, Escaped escaped) 23 | { 24 | word32 lineBreaks = 0; 25 | 26 | if (escaped == WC_ESC_NL_ENC) 27 | { 28 | // Each line break is encoded as %0A (3 bytes) 29 | // So number of line breaks = total encoded newline bytes / 3 30 | lineBreaks = (base64Len / (BASE64_LINE_SZ + 3)) * 3; 31 | } 32 | else if (escaped == WC_NO_NL_ENC) 33 | lineBreaks = 0; 34 | else 35 | { 36 | // Default case: plain newlines ('\n' = 1 byte) 37 | lineBreaks = base64Len / (BASE64_LINE_SZ + 1); 38 | } 39 | 40 | word32 base64DataLen = base64Len - lineBreaks; 41 | 42 | // Each 4 Base64 characters represent 3 bytes of data 43 | word32 plainLen = (base64DataLen / 4) * 3; 44 | 45 | return plainLen; 46 | } 47 | 48 | constexpr size_t GetBase64PaddedLen(size_t inLen) 49 | { 50 | size_t outLen = inLen; 51 | 52 | const size_t multiple = 4; 53 | const size_t remainder = inLen % multiple; 54 | if (remainder != 0) 55 | outLen += multiple - remainder; 56 | 57 | return outLen; 58 | } 59 | 60 | void Base64URLToBase64(char* in, size_t len) 61 | { 62 | for (size_t i = 0; i < len; ++i) 63 | { 64 | if (in[i] == '-') 65 | in[i] = '+'; 66 | else if (in[i] == '_') 67 | in[i] = '/'; 68 | } 69 | } 70 | 71 | namespace Crypto 72 | { 73 | bool Encrypt(const char* password, word32 keySz, int scryptCost, int scryptBlockSz, int scryptParallel, 74 | const byte* plaintext, word32 plaintextSz, 75 | byte* outSalt, word32 outSaltSz, 76 | byte* outIV, word32 outIVSz, 77 | byte* outAuthTag, word32 outAuthTagSz, 78 | byte* outCipher) 79 | { 80 | Log(LogChannel::GENERAL, "Encrypting..."); 81 | 82 | WC_RNG rng; 83 | if (wc_InitRng(&rng)) 84 | { 85 | putsnn("RNG init failed\n"); 86 | return false; 87 | } 88 | 89 | const bool rngFailed = (wc_RNG_GenerateBlock(&rng, outSalt, outSaltSz) 90 | || wc_RNG_GenerateBlock(&rng, outIV, outIVSz)); 91 | 92 | wc_FreeRng(&rng); 93 | 94 | if (rngFailed) 95 | { 96 | putsnn("RNG generation failed\n"); 97 | return false; 98 | } 99 | 100 | byte* key = (byte*)malloc(keySz); 101 | if (!key) 102 | { 103 | putsnn("key allocation failed\n"); 104 | return false; 105 | } 106 | 107 | Aes aes; 108 | 109 | const bool stretchFailed = 110 | (wc_scrypt(key, (byte*)password, strlen(password), 111 | outSalt, outSaltSz, scryptCost, scryptBlockSz, scryptParallel, keySz) || 112 | wc_AesGcmSetKey(&aes, key, keySz)); 113 | 114 | free(key); 115 | 116 | if (stretchFailed) 117 | { 118 | putsnn("key stretching or setting AES key failed\n"); 119 | return false; 120 | } 121 | 122 | if (wc_AesGcmEncrypt(&aes, outCipher, 123 | plaintext, plaintextSz, outIV, outIVSz, outAuthTag, outAuthTagSz, nullptr, 0)) 124 | { 125 | putsnn("encryption failed\n"); 126 | return false; 127 | } 128 | 129 | putsnn("ok\n"); 130 | return true; 131 | } 132 | 133 | bool Decrypt(const char* password, word32 keySz, int scryptCost, int scryptBlockSz, int scryptParallel, 134 | const byte* cipher, size_t cipherSz, 135 | const byte* salt, word32 saltSz, 136 | const byte* iv, word32 ivSz, 137 | const byte* authTag, word32 authTagSz, 138 | byte* outPlaintext) 139 | { 140 | Log(LogChannel::GENERAL, "Decrypting..."); 141 | 142 | byte* key = (byte*)malloc(keySz); 143 | if (!key) 144 | { 145 | putsnn("key allocation failed\n"); 146 | return false; 147 | } 148 | 149 | Aes aes; 150 | 151 | const bool stretchFailed = 152 | (wc_scrypt(key, (byte*)password, 153 | strlen(password), salt, saltSz, scryptCost, scryptBlockSz, scryptParallel, keySz) || 154 | wc_AesGcmSetKey(&aes, key, keySz)); 155 | 156 | free(key); 157 | 158 | if (stretchFailed) 159 | { 160 | putsnn("key stretching or setting AES key failed\n"); 161 | return false; 162 | } 163 | 164 | if (wc_AesGcmDecrypt(&aes, outPlaintext, cipher, cipherSz, iv, ivSz, authTag, authTagSz, nullptr, 0)) 165 | { 166 | putsnn("wrong password or decryption failed\n"); 167 | return false; 168 | } 169 | 170 | putsnn("ok\n"); 171 | return true; 172 | } 173 | } -------------------------------------------------------------------------------- /src/Main.cpp: -------------------------------------------------------------------------------- 1 | #include "Precompiled.h" 2 | #include 3 | #include "Misc.h" 4 | #include "Curl.h" 5 | #include "Crypto.h" 6 | #include "Steam/Steam.h" 7 | #include "Market.h" 8 | #include "Account.h" 9 | 10 | #define OPENMARKETCLIENT_VERSION "0.4.4" 11 | 12 | void SetLocale() 13 | { 14 | // LC_ALL breaks SetConsoleOutputCP 15 | setlocale(LC_TIME, ""); 16 | #ifdef _WIN32 17 | _setmode(_fileno(stdin), _O_U16TEXT); 18 | SetConsoleOutputCP(CP_UTF8); 19 | #endif // _WIN32 20 | } 21 | 22 | void PrintVersion() 23 | { 24 | putsnn("OpenMarketClient v" OPENMARKETCLIENT_VERSION ", built with " 25 | "libcurl v" LIBCURL_VERSION ", " 26 | "wolfSSL v" LIBWOLFSSL_VERSION_STRING ", " 27 | "rapidJSON v" RAPIDJSON_VERSION_STRING "\n"); 28 | } 29 | 30 | namespace Args 31 | { 32 | bool newAcc = false; 33 | bool marketUseProxy = false; 34 | const char* proxy = nullptr; 35 | 36 | void PrintHelp() 37 | { 38 | putsnn("Options:\n" 39 | "--help\t\t\t\t\t\t\tPrint help\n" 40 | "--new\t\t\t\t\t\t\tAdd a new account by manually entering the details\n" 41 | "--proxy [scheme://][username:password@]host[:port]\tSets the global proxy\n" 42 | "--market-use-proxy\t\t\t\t\tTells the market to perform actions using " 43 | "the proxy specified in --proxy, presumably to avoid Steam bans\n"); 44 | } 45 | 46 | bool Parse(int argc, char** const argv) 47 | { 48 | for (int i = 1; i < argc; ++i) 49 | { 50 | const char* arg = argv[i]; 51 | 52 | if (!strcmp(arg, "--help")) 53 | { 54 | PrintHelp(); 55 | return false; 56 | } 57 | else if (!strcmp(arg, "--new")) 58 | newAcc = true; 59 | else if (!strcmp(arg, "--market-use-proxy")) 60 | marketUseProxy = true; 61 | else if ((i < (argc - 1)) && !strcmp(arg, "--proxy")) // check if second to last argument 62 | { 63 | proxy = argv[i + 1]; 64 | ++i; 65 | } 66 | else 67 | Log(LogChannel::GENERAL, "Unknown argument: %s\n", arg); 68 | } 69 | return true; 70 | } 71 | } 72 | 73 | bool SetWorkDirToExeDir() 74 | { 75 | const char* exeDir = GetExeDir(); 76 | if (!exeDir) 77 | return false; 78 | 79 | #ifdef _WIN32 80 | const size_t wideExeDirLen = PATH_MAX; 81 | wchar_t wideExeDir[wideExeDirLen]; 82 | 83 | if (!MultiByteToWideChar(CP_UTF8, 0, exeDir, -1, wideExeDir, wideExeDirLen)) 84 | return false; 85 | 86 | return !_wchdir(wideExeDir); 87 | #else 88 | 89 | return !chdir(exeDir); 90 | #endif // _WIN32 91 | } 92 | 93 | bool InitSavedAccounts(CURL* curl, const char* sessionId, const char* encryptPass, std::vector* accounts) 94 | { 95 | const std::filesystem::path dir(CAccount::directory); 96 | 97 | if (!std::filesystem::exists(dir)) 98 | return true; 99 | 100 | bool success = true; 101 | 102 | for (const auto& entry : std::filesystem::directory_iterator(dir)) 103 | { 104 | const auto& path = entry.path(); 105 | const auto& extension = path.extension(); 106 | 107 | const bool isMaFile = !extension.compare(".maFile"); 108 | 109 | if (extension.compare(CAccount::extension) && !isMaFile) 110 | continue; 111 | 112 | const auto& stem = path.stem(); 113 | 114 | #ifdef _WIN32 115 | const auto* widePath = path.c_str(); 116 | const auto* wideFilenameNoExt = stem.c_str(); 117 | 118 | char szPath[PATH_MAX]; 119 | char szFilenameNoExt[PATH_MAX]; 120 | 121 | if (!WideCharToMultiByte(CP_UTF8, 0, widePath, -1, szPath, sizeof(szPath), NULL, NULL) || 122 | !WideCharToMultiByte(CP_UTF8, 0, wideFilenameNoExt, -1, szFilenameNoExt, sizeof(szFilenameNoExt), NULL, NULL)) 123 | { 124 | Log(LogChannel::GENERAL, "One of the account's filename UTF-16 to UTF-8 mapping failed\n"); 125 | success = false; 126 | break; 127 | } 128 | 129 | #else 130 | const char* szPath = path.c_str(); 131 | const char* szFilenameNoExt = stem.c_str(); 132 | #endif 133 | 134 | CAccount account; 135 | 136 | if (!account.Init(curl, sessionId, encryptPass, szFilenameNoExt, szPath, isMaFile)) 137 | { 138 | success = false; 139 | break; 140 | } 141 | 142 | accounts->emplace_back(account); 143 | } 144 | 145 | return success; 146 | } 147 | 148 | int main(int argc, char** argv) 149 | { 150 | // disable stdout buffering if stdout is a terminal 151 | if (_isatty(_fileno(stdout))) 152 | setvbuf(stdout, nullptr, _IONBF, 0); 153 | 154 | SetLocale(); 155 | PrintVersion(); 156 | 157 | if (!Args::Parse(argc, argv)) 158 | { 159 | Pause(); 160 | return 0; 161 | } 162 | 163 | if (!SetWorkDirToExeDir()) 164 | { 165 | Log(LogChannel::GENERAL, "Setting working directory failed\n"); 166 | Pause(); 167 | return 1; 168 | } 169 | 170 | CURL* curl = Curl::Init(Args::proxy); 171 | if (!curl) 172 | { 173 | Pause(); 174 | return 1; 175 | } 176 | 177 | char sessionId[Steam::sessionIdBufSz]; 178 | 179 | if (!Steam::GenerateSessionId(sessionId) || !Steam::SetSessionCookie(curl, sessionId)) 180 | { 181 | curl_easy_cleanup(curl); 182 | curl_global_cleanup(); 183 | Pause(); 184 | return 1; 185 | } 186 | 187 | if (!Steam::Guard::SyncTime(curl)) 188 | { 189 | curl_easy_cleanup(curl); 190 | curl_global_cleanup(); 191 | Pause(); 192 | return 1; 193 | } 194 | 195 | char encryptPass[64]; 196 | if (!GetUserInputString("Enter encryption password", encryptPass, sizeof(encryptPass), 10, false)) 197 | { 198 | curl_easy_cleanup(curl); 199 | curl_global_cleanup(); 200 | Pause(); 201 | return 1; 202 | } 203 | 204 | std::vector accounts; 205 | 206 | if (Args::newAcc) 207 | { 208 | CAccount account; 209 | while (!account.Init(curl, sessionId, encryptPass)); 210 | 211 | accounts.emplace_back(account); 212 | } 213 | 214 | if (!InitSavedAccounts(curl, sessionId, encryptPass, &accounts)) 215 | { 216 | curl_easy_cleanup(curl); 217 | curl_global_cleanup(); 218 | Pause(); 219 | return 1; 220 | } 221 | 222 | if (accounts.empty()) 223 | { 224 | Log(LogChannel::GENERAL, "No accounts, adding a new one\n"); 225 | 226 | CAccount account; 227 | while (!account.Init(curl, sessionId, encryptPass)); 228 | 229 | accounts.emplace_back(account); 230 | } 231 | 232 | memset(encryptPass, 0, sizeof(encryptPass)); 233 | 234 | #ifdef _WIN32 235 | // prevent sleep 236 | SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED); 237 | #endif // _WIN32 238 | 239 | const char* marketProxy = Args::marketUseProxy ? Args::proxy : nullptr; 240 | 241 | const size_t accountCount = accounts.size(); 242 | 243 | while (true) 244 | { 245 | for (size_t i = 0; i < accountCount; ++i) 246 | accounts[i].RunMarkets(curl, sessionId, marketProxy); 247 | 248 | if (1 < accountCount) 249 | putchar('\n'); 250 | 251 | std::this_thread::sleep_for(1min); 252 | } 253 | 254 | curl_easy_cleanup(curl); 255 | curl_global_cleanup(); 256 | Pause(); 257 | return 0; 258 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_h.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *_wpftmp.csproj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !?*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | *- Backup*.rdl 259 | 260 | # Microsoft Fakes 261 | FakesAssemblies/ 262 | 263 | # GhostDoc plugin setting file 264 | *.GhostDoc.xml 265 | 266 | # Node.js Tools for Visual Studio 267 | .ntvs_analysis.dat 268 | node_modules/ 269 | 270 | # Visual Studio 6 build log 271 | *.plg 272 | 273 | # Visual Studio 6 workspace options file 274 | *.opt 275 | 276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 277 | *.vbw 278 | 279 | # Visual Studio LightSwitch build output 280 | **/*.HTMLClient/GeneratedArtifacts 281 | **/*.DesktopClient/GeneratedArtifacts 282 | **/*.DesktopClient/ModelManifest.xml 283 | **/*.Server/GeneratedArtifacts 284 | **/*.Server/ModelManifest.xml 285 | _Pvt_Extensions 286 | 287 | # Paket dependency manager 288 | .paket/paket.exe 289 | paket-files/ 290 | 291 | # FAKE - F# Make 292 | .fake/ 293 | 294 | # JetBrains Rider 295 | .idea/ 296 | *.sln.iml 297 | 298 | # CodeRush personal settings 299 | .cr/personal 300 | 301 | # Python Tools for Visual Studio (PTVS) 302 | __pycache__/ 303 | *.pyc 304 | 305 | # Cake - Uncomment if you are using it 306 | # tools/** 307 | # !tools/packages.config 308 | 309 | # Tabs Studio 310 | *.tss 311 | 312 | # Telerik's JustMock configuration file 313 | *.jmconfig 314 | 315 | # BizTalk build output 316 | *.btp.cs 317 | *.btm.cs 318 | *.odx.cs 319 | *.xsd.cs 320 | 321 | # OpenCover UI analysis results 322 | OpenCover/ 323 | 324 | # Azure Stream Analytics local run output 325 | ASALocalRun/ 326 | 327 | # MSBuild Binary and Structured Log 328 | *.binlog 329 | 330 | # NVidia Nsight GPU debugger configuration file 331 | *.nvuser 332 | 333 | # MFractors (Xamarin productivity tool) working folder 334 | .mfractor/ 335 | 336 | # Local History for Visual Studio 337 | .localhistory/ 338 | 339 | # BeatPulse healthcheck temp database 340 | healthchecksdb 341 | 342 | config 343 | cacert.pem 344 | build/ -------------------------------------------------------------------------------- /src/Misc.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // puts without newline 4 | inline int putsnn(const char* buf) 5 | { 6 | return fputs(buf, stdout); 7 | } 8 | 9 | thread_local const char* g_pszLogAccountName; 10 | 11 | class CLoggingContext 12 | { 13 | public: 14 | CLoggingContext(const char* name) { 15 | g_pszLogAccountName = name; 16 | } 17 | ~CLoggingContext() { 18 | g_pszLogAccountName = nullptr; 19 | } 20 | }; 21 | 22 | enum class LogChannel 23 | { 24 | GENERAL, 25 | LIBCURL, 26 | STEAM, 27 | MARKET 28 | }; 29 | 30 | 31 | void Log(LogChannel channel, const char* format, ...) 32 | { 33 | const char* logChannelNames[] = 34 | { 35 | "", 36 | "libcurl", 37 | "Steam", 38 | "Market" 39 | }; 40 | 41 | const time_t timestamp = time(nullptr); 42 | 43 | // zh_CN.utf8 locale's time on linux looks like this 2022年10月18日 15时08分28秒 44 | // so allocate some space 45 | char dateTime[64]; 46 | 47 | #ifdef _WIN32 48 | // windows didn't support utf-8 codepages until recently, so map UTF-16 to UTF-8 instead 49 | const size_t wideDatatimeLen = sizeof(dateTime); 50 | wchar_t wideDatetime[wideDatatimeLen]; 51 | wcsftime(wideDatetime, wideDatatimeLen, L"%x %X", localtime(×tamp)); 52 | 53 | if (!WideCharToMultiByte(CP_UTF8, 0, wideDatetime, -1, dateTime, sizeof(dateTime), NULL, NULL)) 54 | strcpy(dateTime, "timestamp UTF-16 to UTF-8 mapping failed"); 55 | 56 | #else 57 | strftime(dateTime, sizeof(dateTime), "%x %X", localtime(×tamp)); 58 | #endif // _WIN32 59 | 60 | printf("[%s] ", dateTime); 61 | 62 | if (g_pszLogAccountName) 63 | printf("[%s] ", g_pszLogAccountName); 64 | 65 | if (channel != LogChannel::GENERAL) 66 | printf("[%s] ", logChannelNames[(size_t)channel]); 67 | 68 | va_list args; 69 | va_start(args, format); 70 | vprintf(format, args); 71 | va_end(args); 72 | } 73 | 74 | #ifdef _WIN32 75 | void FlashCurrentWindow() 76 | { 77 | static const HWND hWnd = GetConsoleWindow(); 78 | if (!hWnd) return; 79 | FlashWindow(hWnd, TRUE); 80 | } 81 | #endif 82 | 83 | void SetStdinEcho(bool enable) 84 | { 85 | #ifdef _WIN32 86 | const HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE); 87 | DWORD mode; 88 | GetConsoleMode(hStdin, &mode); 89 | 90 | if (!enable) 91 | mode &= ~ENABLE_ECHO_INPUT; 92 | else 93 | mode |= ENABLE_ECHO_INPUT; 94 | 95 | SetConsoleMode(hStdin, mode); 96 | 97 | #else 98 | termios tty; 99 | tcgetattr(STDIN_FILENO, &tty); 100 | 101 | if (!enable) 102 | tty.c_lflag &= ~ECHO; 103 | else 104 | tty.c_lflag |= ECHO; 105 | 106 | tcsetattr(STDIN_FILENO, TCSANOW, &tty); 107 | #endif // _WIN32 108 | } 109 | 110 | bool GetUserInputString(const char* msg, char* buf, size_t bufSz, size_t minLen = 1, bool echoStdin = true) 111 | { 112 | const size_t maxLen = bufSz - 1; 113 | 114 | if (!echoStdin) 115 | SetStdinEcho(false); 116 | 117 | #ifdef _WIN32 118 | FlashCurrentWindow(); 119 | 120 | wchar_t* wideBuf = (wchar_t*)malloc(bufSz * sizeof(wchar_t)); 121 | if (!wideBuf) 122 | { 123 | Log(LogChannel::GENERAL, "Wide input buffer allocation failed\n"); 124 | return false; 125 | } 126 | #endif 127 | 128 | while (true) 129 | { 130 | if (1 < minLen) 131 | { 132 | if (minLen == maxLen) 133 | Log(LogChannel::GENERAL, "%s (%u bytes): ", msg, maxLen); 134 | else 135 | Log(LogChannel::GENERAL, "%s (%u-%u bytes): ", msg, minLen, maxLen); 136 | } 137 | else 138 | Log(LogChannel::GENERAL, "%s (%u bytes max): ", msg, maxLen); 139 | 140 | size_t len = 0; 141 | 142 | #ifdef _WIN32 143 | wint_t c; 144 | while ((c = getwchar()) != L'\n' && c != WEOF) 145 | #else 146 | // if the byte is a part of UTF-8 char it would have 8th bit set 147 | // therefore we can't accidentally find newline 148 | int c; 149 | while ((c = getchar()) != '\n' && c != EOF) 150 | #endif // _WIN32 151 | { 152 | if (len <= maxLen) 153 | #ifdef _WIN32 154 | wideBuf[len] = c; 155 | #else 156 | buf[len] = c; 157 | #endif // _WIN32 158 | 159 | ++len; 160 | } 161 | 162 | if (!echoStdin) 163 | putchar('\n'); 164 | 165 | if (minLen > len || len > maxLen) 166 | continue; 167 | 168 | #ifdef _WIN32 169 | wideBuf[len] = L'\0'; 170 | #else 171 | buf[len] = '\0'; 172 | #endif // _WIN32 173 | 174 | #ifdef _WIN32 175 | if (!WideCharToMultiByte(CP_UTF8, 0, wideBuf, len + 1, buf, bufSz, NULL, NULL)) 176 | { 177 | Log(LogChannel::GENERAL, "Input UTF-16 to UTF-8 mapping failed\n"); 178 | continue; 179 | } 180 | #endif // _WIN32 181 | 182 | break; 183 | } 184 | 185 | #ifdef _WIN32 186 | free(wideBuf); 187 | #endif // _WIN32 188 | 189 | if (!echoStdin) 190 | SetStdinEcho(true); 191 | 192 | return true; 193 | } 194 | 195 | #ifdef _WIN32 196 | 197 | inline void* mempcpy(void* dest, const void* src, size_t size) 198 | { 199 | return (char*)memcpy(dest, src, size) + size; 200 | } 201 | 202 | inline char* stpcpy(char* dest, const char* src) 203 | { 204 | const size_t len = strlen(src); 205 | return (char*)memcpy(dest, src, len + 1) + len; 206 | } 207 | 208 | inline char* stpncpy(char* dest, const char* src, size_t count) 209 | { 210 | const size_t len = strnlen(src, count); 211 | memcpy(dest, src, len); 212 | dest += len; 213 | if (len == count) 214 | return dest; 215 | return (char*)memset(dest, '\0', count - len); 216 | } 217 | 218 | // windows utf-8 fopen 219 | FILE* u8fopen(const char* path, const char* mode) 220 | { 221 | const size_t widePathLen = PATH_MAX; 222 | wchar_t widePath[widePathLen]; 223 | 224 | const size_t wideModeLen = 32; 225 | wchar_t wideMode[widePathLen]; 226 | 227 | if (!MultiByteToWideChar(CP_UTF8, 0, path, -1, widePath, widePathLen) || 228 | !MultiByteToWideChar(CP_UTF8, 0, mode, -1, wideMode, wideModeLen)) 229 | return nullptr; 230 | 231 | return _wfopen(widePath, wideMode); 232 | } 233 | 234 | #else 235 | 236 | inline FILE* u8fopen(const char* path, const char* mode) 237 | { 238 | return fopen(path, mode); 239 | } 240 | 241 | #endif // _WIN32 242 | 243 | // writes pointer to file contents heap to output 244 | bool ReadFile(const char* path, unsigned char** out, long* outSz) 245 | { 246 | FILE* file = u8fopen(path, "rb"); 247 | if (!file) 248 | return false; 249 | 250 | long fsize; 251 | 252 | if (fseek(file, 0, SEEK_END) || 253 | ((fsize = ftell(file)) == -1L) || 254 | fseek(file, 0, SEEK_SET)) 255 | { 256 | fclose(file); 257 | return false; 258 | } 259 | 260 | unsigned char* contents = (unsigned char*)malloc(fsize); 261 | if (!contents) 262 | { 263 | fclose(file); 264 | return false; 265 | } 266 | 267 | if (fread(contents, sizeof(unsigned char), fsize, file) != (size_t)fsize) 268 | { 269 | free(contents); 270 | fclose(file); 271 | return false; 272 | } 273 | 274 | fclose(file); 275 | 276 | *out = contents; 277 | *outSz = fsize; 278 | 279 | return true; 280 | } 281 | 282 | // get executable dir 283 | const char* GetExeDir() 284 | { 285 | static char dir[PATH_MAX] = { 0 }; 286 | if (!dir[0]) 287 | { 288 | #ifdef _WIN32 289 | const size_t wideDirLen = sizeof(dir); 290 | wchar_t wideDir[wideDirLen]; 291 | if (!GetModuleFileNameW(NULL, wideDir, wideDirLen)) 292 | return nullptr; 293 | 294 | if (!WideCharToMultiByte(CP_UTF8, 0, wideDir, -1, dir, sizeof(dir), NULL, NULL)) 295 | return nullptr; 296 | 297 | // if the byte is a part of UTF-8 char it would have 8th bit set 298 | // therefore we can't accidentally find backslash 299 | char* del = strrchr(dir, '\\'); 300 | #else 301 | if (readlink("/proc/self/exe", dir, sizeof(dir)) == -1) 302 | return nullptr; 303 | 304 | char* del = strrchr(dir, '/'); 305 | #endif // _WIN32 306 | 307 | if (del) 308 | *(del + 1) = '\0'; 309 | } 310 | return dir; 311 | } 312 | 313 | void Pause() 314 | { 315 | #ifdef _WIN32 316 | // check if stdout isn't a terminal 317 | if (!_isatty(_fileno(stdout))) 318 | return; 319 | 320 | FlashCurrentWindow(); 321 | 322 | // check if run by doubleclicking the executable 323 | if (getenv("PROMPT")) 324 | return; 325 | 326 | putsnn("Press Enter to continue\n"); 327 | 328 | wint_t c; 329 | while ((c = getwchar()) != L'\n' && c != WEOF); 330 | 331 | #endif // _WIN32 332 | } 333 | 334 | inline uint32_t byteswap32(uint32_t dw) 335 | { 336 | uint32_t res; 337 | 338 | res = dw >> 24; 339 | res |= ((dw & 0x00FF0000) >> 8); 340 | res |= ((dw & 0x0000FF00) << 8); 341 | res |= ((dw & 0x000000FF) << 24); 342 | 343 | return res; 344 | } 345 | 346 | inline uint64_t byteswap64(uint64_t qw) 347 | { 348 | uint64_t res; 349 | 350 | res = qw >> 56; 351 | res |= ((qw & 0x00FF000000000000ull) >> 40); 352 | res |= ((qw & 0x0000FF0000000000ull) >> 24); 353 | res |= ((qw & 0x000000FF00000000ull) >> 8); 354 | res |= ((qw & 0x00000000FF000000ull) << 8); 355 | res |= ((qw & 0x0000000000FF0000ull) << 24); 356 | res |= ((qw & 0x000000000000FF00ull) << 40); 357 | res |= ((qw & 0x00000000000000FFull) << 56); 358 | 359 | return res; 360 | } 361 | 362 | void ClearConsole() 363 | { 364 | #ifdef _WIN32 365 | // is stdout a terminal 366 | if (!_isatty(_fileno(stdout))) 367 | return; 368 | 369 | const HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE); 370 | 371 | // Get the number of character cells in the current buffer. 372 | CONSOLE_SCREEN_BUFFER_INFO csbi; 373 | if (!GetConsoleScreenBufferInfo(hStdout, &csbi)) 374 | return; 375 | 376 | // Scroll the rectangle of the entire buffer. 377 | SMALL_RECT scrollRect; 378 | scrollRect.Left = 0; 379 | scrollRect.Top = 0; 380 | scrollRect.Right = csbi.dwSize.X; 381 | scrollRect.Bottom = csbi.dwSize.Y; 382 | 383 | // Scroll it upwards off the top of the buffer with a magnitude of the entire height. 384 | COORD scrollTarget; 385 | scrollTarget.X = 0; 386 | scrollTarget.Y = (SHORT)(0 - csbi.dwSize.Y); 387 | 388 | // Fill with empty spaces with the buffer's default text attribute. 389 | CHAR_INFO fill; 390 | fill.Char.AsciiChar = ' '; 391 | fill.Attributes = csbi.wAttributes; 392 | 393 | // Do the scroll 394 | ScrollConsoleScreenBufferA(hStdout, &scrollRect, NULL, scrollTarget, &fill); 395 | 396 | // Move the cursor to the top left corner too. 397 | csbi.dwCursorPosition.X = 0; 398 | csbi.dwCursorPosition.Y = 0; 399 | 400 | SetConsoleCursorPosition(hStdout, csbi.dwCursorPosition); 401 | #else 402 | 403 | putsnn("\x1b[H\x1b[J\x1b[3J"); 404 | 405 | #endif // _WIN32 406 | } -------------------------------------------------------------------------------- /src/Steam/Misc.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Steam 4 | { 5 | const size_t sessionIdBufSz = 24 + 1; 6 | const size_t apiKeyBufSz = 32 + 1; 7 | 8 | // steam login token must be set when calling this 9 | // out buffer size must be at least apiKeyBufSz 10 | bool GetApiKey(CURL* curl, const char* sessionId, char* out) 11 | { 12 | Log(LogChannel::STEAM, "Checking if the account has an API key..."); 13 | 14 | Curl::CResponse respKey; 15 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &respKey); 16 | curl_easy_setopt(curl, CURLOPT_URL, "https://steamcommunity.com/dev/apikey?l=english"); 17 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 18 | 19 | CURLcode respCode = curl_easy_perform(curl); 20 | 21 | if (respCode != CURLE_OK) 22 | { 23 | Curl::PrintError(curl, respCode); 24 | return false; 25 | } 26 | 27 | // put it here so destructor doesn't get called before we copied 28 | Curl::CResponse respRegister; 29 | 30 | const char keySubstr[] = ">Key: "; 31 | const char* foundKeySubstr = strstr(respKey.data, keySubstr); 32 | if (!foundKeySubstr) 33 | { 34 | respKey.Empty(); // not needed anymore 35 | 36 | putsnn("no\n"); 37 | 38 | Log(LogChannel::STEAM, "Registering new API key for the account..."); 39 | 40 | const char postFieldSession[] = "domain=localhost&agreeToTerms=agreed&sessionid="; 41 | 42 | const size_t postFieldsBufSz = sizeof(postFieldSession) - 1 + sessionIdBufSz - 1 + 1; 43 | char postFields[postFieldsBufSz]; 44 | 45 | char* postFieldsEnd = postFields; 46 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldSession); 47 | strcpy(postFieldsEnd, sessionId); 48 | 49 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &respRegister); 50 | curl_easy_setopt(curl, CURLOPT_URL, "https://steamcommunity.com/dev/registerkey"); 51 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 52 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 53 | 54 | respCode = curl_easy_perform(curl); 55 | 56 | if (respCode != CURLE_OK) 57 | { 58 | Curl::PrintError(curl, respCode); 59 | return false; 60 | } 61 | 62 | foundKeySubstr = strstr(respRegister.data, keySubstr); 63 | if (!foundKeySubstr) 64 | { 65 | putsnn("registration failed\n"); 66 | return false; 67 | } 68 | 69 | putsnn("ok\n"); 70 | } 71 | else 72 | putsnn("yes\n"); 73 | 74 | const char* keyStart = foundKeySubstr + sizeof(keySubstr) - 1; 75 | stpncpy(out, keyStart, (strchr(keyStart, '<') - keyStart))[0] = '\0'; 76 | 77 | return true; 78 | } 79 | 80 | // out buffer size must be at least sessionIdBufSz 81 | bool GenerateSessionId(char* out) 82 | { 83 | byte rawSessionId[(sessionIdBufSz - 1) / 2]; 84 | 85 | WC_RNG rng; 86 | 87 | if (wc_InitRng(&rng)) 88 | { 89 | Log(LogChannel::STEAM, "Session ID generation failed: RNG init failed\n"); 90 | return false; 91 | } 92 | 93 | if (wc_RNG_GenerateBlock(&rng, rawSessionId, sizeof(rawSessionId))) 94 | { 95 | wc_FreeRng(&rng); 96 | Log(LogChannel::STEAM, "Session ID generation failed: RNG generation failed\n"); 97 | return false; 98 | } 99 | 100 | wc_FreeRng(&rng); 101 | 102 | word32 sessionIdSz = sessionIdBufSz; 103 | 104 | if (Base16_Encode(rawSessionId, sizeof(rawSessionId), (byte*)out, &sessionIdSz)) 105 | { 106 | Log(LogChannel::STEAM, "Session ID generation failed: encoding failed\n"); 107 | return false; 108 | } 109 | 110 | return true; 111 | } 112 | 113 | bool SetCookie(CURL* curl, const char* domain, const char* name, const char* value) 114 | { 115 | char cookie[1024]; 116 | 117 | char* cookieEnd = cookie; 118 | cookieEnd = stpcpy(cookieEnd, domain); /* Hostname */ 119 | cookieEnd = stpcpy(cookieEnd, "\tFALSE" /* Include subdomains */ 120 | "\t/" /* Path */ 121 | "\tTRUE" /* Secure */ 122 | "\t0" /* Expiry in epoch time format. 0 == session */ 123 | "\t"); 124 | cookieEnd = stpcpy(cookieEnd, name); /* Name */ 125 | *cookieEnd++ = '\t'; 126 | strcpy(cookieEnd, value); /* Value */ 127 | 128 | return (curl_easy_setopt(curl, CURLOPT_COOKIELIST, cookie) == CURLE_OK); 129 | } 130 | 131 | bool SetSessionCookie(CURL* curl, const char* sessionId) 132 | { 133 | if (!SetCookie(curl, "steamcommunity.com", "sessionid", sessionId)) 134 | { 135 | Log(LogChannel::STEAM, "Setting session ID cookie failed\n"); 136 | return false; 137 | } 138 | 139 | return true; 140 | } 141 | 142 | bool SetLoginCookie(CURL* curl, const char* steamId64, const char* loginToken) 143 | { 144 | const size_t cookieLoginBufSz = UINT64_MAX_STR_SIZE - 1 + Auth::jwtBufSz - 1 + 1; 145 | char cookieLogin[cookieLoginBufSz]; 146 | 147 | char* cookieLoginEnd = cookieLogin; 148 | cookieLoginEnd = stpcpy(cookieLoginEnd, steamId64); 149 | cookieLoginEnd = stpcpy(cookieLoginEnd, "%7C%7C"); 150 | strcpy(cookieLoginEnd, loginToken); 151 | 152 | if (!SetCookie(curl, "#HttpOnly_steamcommunity.com", "steamLoginSecure", cookieLogin)) 153 | { 154 | Log(LogChannel::STEAM, "Setting login cookie failed\n"); 155 | return false; 156 | } 157 | 158 | return true; 159 | } 160 | 161 | bool SetRefreshCookie(CURL* curl, const char* steamId64, const char* refreshToken) 162 | { 163 | const size_t cookieRefreshBufSz = UINT64_MAX_STR_SIZE - 1 + Auth::jwtBufSz - 1 + 1; 164 | char cookieRefresh[cookieRefreshBufSz]; 165 | 166 | char* cookieRefreshEnd = cookieRefresh; 167 | cookieRefreshEnd = stpcpy(cookieRefreshEnd, steamId64); 168 | cookieRefreshEnd = stpcpy(cookieRefreshEnd, "%7C%7C"); 169 | strcpy(cookieRefreshEnd, refreshToken); 170 | 171 | if (!SetCookie(curl, "#HttpOnly_login.steampowered.com", "steamRefresh_steam", cookieRefresh)) 172 | { 173 | Log(LogChannel::STEAM, "Setting refresh cookie failed\n"); 174 | return false; 175 | } 176 | 177 | return true; 178 | } 179 | 180 | bool SetInventoryPublic(CURL* curl, const char* sessionId, const char* steamId64) 181 | { 182 | Log(LogChannel::STEAM, "Setting inventory visibility to public..."); 183 | 184 | // get current privacy settings 185 | 186 | const char urlStart[] = "https://steamcommunity.com/profiles/"; 187 | const char urlPath[] = "/ajaxsetprivacy/"; 188 | 189 | const size_t urlBufSz = 190 | sizeof(urlStart) - 1 + UINT64_MAX_STR_SIZE - 1 + 191 | sizeof(urlPath) - 1 + 1; 192 | 193 | char url[urlBufSz]; 194 | 195 | char* urlEnd = url; 196 | urlEnd = stpcpy(urlEnd, urlStart); 197 | urlEnd = stpcpy(urlEnd, steamId64); 198 | strcpy(urlEnd, urlPath); 199 | 200 | const char postFieldSession[] = "sessionid="; 201 | const char postFieldPrivacy[] = "&Privacy="; 202 | const char postFieldCommentPerm[] = "&eCommentPermission="; 203 | 204 | const size_t postFieldsBufSz = 205 | sizeof(postFieldSession) - 1 + sessionIdBufSz - 1 + 206 | sizeof(postFieldPrivacy) - 1 + 132 + 207 | sizeof(postFieldCommentPerm) - 1 + 1 + 1; 208 | 209 | char postFields[postFieldsBufSz]; 210 | 211 | char* postFieldsEnd = postFields; 212 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldSession); 213 | postFieldsEnd = stpcpy(postFieldsEnd, sessionId); 214 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldPrivacy); 215 | postFieldsEnd = stpcpy(postFieldsEnd, "{\"PrivacyProfile\":3}"); 216 | strcpy(postFieldsEnd, postFieldCommentPerm); // empty comm perm to make steam return our current settings 217 | 218 | Curl::CResponse response; 219 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 220 | curl_easy_setopt(curl, CURLOPT_URL, url); 221 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 222 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 223 | 224 | CURLcode respCode = curl_easy_perform(curl); 225 | 226 | if (respCode != CURLE_OK) 227 | { 228 | putsnn("get "); 229 | Curl::PrintError(curl, respCode); 230 | return false; 231 | } 232 | 233 | if (!strcmp(response.data, "null")) 234 | { 235 | putsnn("get request unsucceeded\n"); 236 | return false; 237 | } 238 | 239 | rapidjson::Document parsed; 240 | parsed.ParseInsitu(response.data); 241 | 242 | if (parsed.HasParseError()) 243 | { 244 | putsnn("get JSON parsing failed\n"); 245 | return false; 246 | } 247 | 248 | if (!parsed["success"].GetInt()) 249 | { 250 | putsnn("get request unsucceeded\n"); 251 | return false; 252 | } 253 | 254 | rapidjson::Value& privacy = parsed["Privacy"]; 255 | rapidjson::Value& privacySettings = privacy["PrivacySettings"]; 256 | 257 | rapidjson::Value& privacyProfile = privacySettings["PrivacyProfile"]; 258 | rapidjson::Value& privacyInventory = privacySettings["PrivacyInventory"]; 259 | rapidjson::Value& privacyInventoryGifts = privacySettings["PrivacyInventoryGifts"]; 260 | 261 | const int privacyProfileVal = privacyProfile.GetInt(); 262 | 263 | // 3 public 264 | // 2 friends only 265 | // 1 private 266 | 267 | if ((privacyProfileVal == 3) && 268 | (privacyInventory.GetInt() == 3) && 269 | (privacyInventoryGifts.GetInt() == 3)) 270 | { 271 | putsnn("already public\n"); 272 | return true; 273 | } 274 | 275 | for (auto itr = privacySettings.MemberBegin(); itr != privacySettings.MemberEnd(); ++itr) 276 | { 277 | // if a setting privacy is higher than profile privacy 278 | // e.g. friends set public, yet profile private 279 | // make friends private after we set profile public 280 | if (privacyProfileVal < itr->value.GetInt()) 281 | itr->value.SetInt(privacyProfileVal); 282 | } 283 | 284 | privacyProfile.SetInt(3); 285 | privacyInventory.SetInt(3); 286 | privacyInventoryGifts.SetInt(3); 287 | 288 | rapidjson::StringBuffer privacySettingsStrBuf; 289 | rapidjson::Writer privacySettingsWriter(privacySettingsStrBuf); 290 | 291 | if (!privacySettings.Accept(privacySettingsWriter)) 292 | { 293 | putsnn("converting privacy settings JSON to string failed\n"); 294 | return false; 295 | } 296 | 297 | // convert comment to other settings values 298 | // 1 public 299 | // 0 friends only 300 | // 2 private 301 | int newCommentPerm = ((privacy["eCommentPermission"].GetInt() + 1) % 3) + 1; 302 | 303 | response.Empty(); // not needed anymore 304 | 305 | // same reasoning as above 306 | if (privacyProfileVal < newCommentPerm) 307 | newCommentPerm = privacyProfileVal; 308 | 309 | newCommentPerm = (newCommentPerm + 1) % 3; // convert back 310 | 311 | postFieldsEnd = postFields; 312 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldSession); 313 | postFieldsEnd = stpcpy(postFieldsEnd, sessionId); 314 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldPrivacy); 315 | postFieldsEnd = stpcpy(postFieldsEnd, privacySettingsStrBuf.GetString()); 316 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldCommentPerm); 317 | strcpy(postFieldsEnd, std::to_string(newCommentPerm).c_str()); 318 | 319 | Curl::CResponse respSet; 320 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &respSet); 321 | curl_easy_setopt(curl, CURLOPT_URL, url); 322 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 323 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 324 | 325 | respCode = curl_easy_perform(curl); 326 | 327 | if (respCode != CURLE_OK) 328 | { 329 | putsnn("set "); 330 | Curl::PrintError(curl, respCode); 331 | return false; 332 | } 333 | 334 | if (!strcmp(respSet.data, "null")) 335 | { 336 | putsnn("set request unsucceeded\n"); 337 | return false; 338 | } 339 | 340 | rapidjson::Document parsedSet; 341 | parsedSet.ParseInsitu(respSet.data); 342 | 343 | if (parsedSet.HasParseError()) 344 | { 345 | putsnn("set JSON parsing failed\n"); 346 | return false; 347 | } 348 | 349 | if (parsedSet["success"].GetInt() != 1) 350 | { 351 | putsnn("set request unsucceeded\n"); 352 | return false; 353 | } 354 | 355 | putsnn("ok\n"); 356 | return true; 357 | } 358 | 359 | // if we don't do this we can't accept trade confirmation 360 | bool AcknowledgeTradeProtection(CURL* curl, const char* sessionId) 361 | { 362 | Log(LogChannel::STEAM, "Acknowledging trade protection..."); 363 | 364 | const char postFieldSession[] = "sessionid="; 365 | const char postFieldMessage[] = "&message=1"; 366 | 367 | const size_t postFieldsBufSz = 368 | sizeof(postFieldSession) - 1 + sessionIdBufSz - 1 + 369 | sizeof(postFieldMessage) - 1 + 1; 370 | 371 | char postFields[postFieldsBufSz]; 372 | 373 | char* postFieldsEnd = postFields; 374 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldSession); 375 | postFieldsEnd = stpcpy(postFieldsEnd, sessionId); 376 | strcpy(postFieldsEnd, postFieldMessage); 377 | 378 | Curl::CResponse response; 379 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 380 | curl_easy_setopt(curl, CURLOPT_URL, "https://steamcommunity.com//trade/new/acknowledge"); 381 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 382 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 383 | 384 | CURLcode respCode = curl_easy_perform(curl); 385 | 386 | if (respCode != CURLE_OK) 387 | { 388 | Curl::PrintError(curl, respCode); 389 | return false; 390 | } 391 | 392 | putsnn("ok\n"); 393 | return true; 394 | } 395 | } -------------------------------------------------------------------------------- /src/Steam/Trade.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Steam 4 | { 5 | namespace Trade 6 | { 7 | const size_t offerIdBufSz = UINT64_MAX_STR_SIZE; 8 | const size_t tokenBufSz = 8 + 1; 9 | const size_t msgBufSz = sizeof("ABCD ... /trade/") - 1 + UINT64_MAX_STR_SIZE - 1 + sizeof("/abcde12345-/") - 1 + 1; 10 | 11 | enum class ETradeOfferState 12 | { 13 | INVALID = 1, 14 | ACTIVE = 2, 15 | ACCEPTED = 3, 16 | COUNTERED = 4, 17 | EXPIRED = 5, 18 | CANCELED = 6, 19 | DECLINED = 7, 20 | INVALID_ITEMS = 8, 21 | CREATED_NEEDS_CONFIRMATION = 9, 22 | CANCELED_BY_SECOND_FACTOR = 10, 23 | IN_ESCROW = 11, 24 | }; 25 | 26 | bool Accept(CURL* curl, const char* sessionId, const char* offerId, const char* partnerId64) 27 | { 28 | Log(LogChannel::STEAM, "Accepting trade offer..."); 29 | 30 | const char urlStart[] = "https://steamcommunity.com/tradeoffer/"; 31 | 32 | const size_t urlBufSz = sizeof(urlStart) - 1 + offerIdBufSz - 1 + sizeof("/accept") - 1 + 1; 33 | char url[urlBufSz]; 34 | 35 | char* urlEnd = url; 36 | urlEnd = stpcpy(urlEnd, urlStart); 37 | urlEnd = stpcpy(urlEnd, offerId); 38 | urlEnd = stpcpy(urlEnd, "/"); 39 | 40 | // libcurl copies the string 41 | curl_easy_setopt(curl, CURLOPT_REFERER, url); 42 | 43 | strcpy(urlEnd, "accept"); 44 | 45 | curl_easy_setopt(curl, CURLOPT_URL, url); 46 | 47 | const char postFieldSession[] = "serverid=1&sessionid="; 48 | const char postFieldPartnerId64[] = "&partner="; 49 | const char postFieldOffer[] = "&tradeofferid="; 50 | 51 | const size_t postFieldsBufSz = 52 | sizeof(postFieldSession) - 1 + sessionIdBufSz - 1 + 53 | sizeof(postFieldPartnerId64) - 1 + UINT64_MAX_STR_SIZE - 1 + 54 | sizeof(postFieldOffer) - 1 + offerIdBufSz - 1 + 1; 55 | 56 | char postFields[postFieldsBufSz]; 57 | 58 | char* postFieldsEnd = postFields; 59 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldSession); 60 | postFieldsEnd = stpcpy(postFieldsEnd, sessionId); 61 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldPartnerId64); 62 | postFieldsEnd = stpcpy(postFieldsEnd, partnerId64); 63 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldOffer); 64 | strcpy(postFieldsEnd, offerId); 65 | 66 | // Confirmation required: 67 | // {"tradeid":null,"needs_mobile_confirmation":true,"needs_email_confirmation":true,"email_domain":"gmail.com"} 68 | // No confirmation required: {"tradeid":"2251163828378018000"} 69 | Curl::CResponse response; 70 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 71 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 72 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 73 | 74 | const CURLcode respCode = curl_easy_perform(curl); 75 | 76 | curl_easy_setopt(curl, CURLOPT_REFERER, NULL); 77 | 78 | if (respCode != CURLE_OK) 79 | { 80 | Curl::PrintError(curl, respCode); 81 | return false; 82 | } 83 | 84 | putsnn("ok\n"); 85 | return true; 86 | } 87 | 88 | // outOfferId buffer size must be at least offerIdBufSz 89 | bool Send(CURL* curl, const char* sessionId, uint32_t nPartnerId32, const char* token, 90 | const char* message, const char* assets, char* outOfferId) 91 | { 92 | Log(LogChannel::STEAM, "Sending trade offer..."); 93 | 94 | const std::string partnerId32(std::to_string(nPartnerId32)); 95 | const std::string partnerId64(std::to_string(SteamID32To64(nPartnerId32))); 96 | 97 | const char refererStart[] = "https://steamcommunity.com/tradeoffer/new/?partner="; 98 | const char refererToken[] = "&token="; 99 | 100 | const size_t refererBufSz = 101 | sizeof(refererStart) - 1 + UINT32_MAX_STR_SIZE - 1 + 102 | sizeof(refererToken) - 1 + tokenBufSz - 1 + 1; 103 | 104 | char referer[refererBufSz]; 105 | 106 | char* refererEnd = referer; 107 | refererEnd = stpcpy(refererEnd, refererStart); 108 | refererEnd = stpcpy(refererEnd, partnerId32.c_str()); 109 | refererEnd = stpcpy(refererEnd, refererToken); 110 | strcpy(refererEnd, token); 111 | 112 | curl_easy_setopt(curl, CURLOPT_REFERER, referer); 113 | 114 | const char postFieldSession[] = "serverid=1&sessionid="; 115 | const char postFieldPartnerId64[] = "&partner="; 116 | 117 | const char postFieldAssets[] = "&json_tradeoffer={\"newversion\":true,\"version\":2,\"me\":{\"assets\":"; 118 | 119 | const char postFieldToken[] = 120 | ",\"currency\":[],\"ready\":false},\"them\":{\"assets\":[],\"currency\":[],\"ready\":false}}" 121 | "&trade_offer_create_params={\"trade_offer_access_token\":\""; 122 | 123 | const char postFieldMessage[] = "\"}" 124 | "&tradeoffermessage="; 125 | 126 | const size_t postFieldsBufSz = 127 | sizeof(postFieldSession) - 1 + sessionIdBufSz - 1 + 128 | sizeof(postFieldPartnerId64) - 1 + UINT64_MAX_STR_SIZE - 1 + 129 | sizeof(postFieldAssets) - 1 + strlen(assets) + 130 | sizeof(postFieldToken) - 1 + tokenBufSz - 1 + 131 | sizeof(postFieldMessage) - 1 + msgBufSz - 1 + 1; 132 | 133 | char* postFields = (char*)malloc(postFieldsBufSz); 134 | if (!postFields) 135 | { 136 | putsnn("allocation failed\n"); 137 | return false; 138 | } 139 | 140 | char* postFieldsEnd = postFields; 141 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldSession); 142 | postFieldsEnd = stpcpy(postFieldsEnd, sessionId); 143 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldPartnerId64); 144 | postFieldsEnd = stpcpy(postFieldsEnd, partnerId64.c_str()); 145 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldAssets); 146 | postFieldsEnd = stpcpy(postFieldsEnd, assets); 147 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldToken); 148 | postFieldsEnd = stpcpy(postFieldsEnd, token); 149 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldMessage); 150 | strcpy(postFieldsEnd, message); 151 | 152 | Curl::CResponse response; 153 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 154 | curl_easy_setopt(curl, CURLOPT_URL, "https://steamcommunity.com/tradeoffer/new/send"); 155 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 156 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 157 | 158 | const CURLcode respCode = curl_easy_perform(curl); 159 | 160 | free(postFields); 161 | 162 | curl_easy_setopt(curl, CURLOPT_REFERER, NULL); 163 | 164 | if (respCode != CURLE_OK) 165 | { 166 | Curl::PrintError(curl, respCode); 167 | return false; 168 | } 169 | 170 | rapidjson::Document parsed; 171 | parsed.ParseInsitu(response.data); 172 | 173 | if (parsed.HasParseError()) 174 | { 175 | putsnn("JSON parsing failed\n"); 176 | return false; 177 | } 178 | 179 | const auto iterOfferId = parsed.FindMember("tradeofferid"); 180 | if (iterOfferId == parsed.MemberEnd()) 181 | { 182 | putsnn("request unsucceeded\n"); 183 | return false; 184 | } 185 | 186 | strcpy(outOfferId, iterOfferId->value.GetString()); 187 | 188 | putsnn("ok\n"); 189 | return true; 190 | } 191 | 192 | bool Cancel(CURL* curl, const char* sessionId, const char* offerId) 193 | { 194 | Log(LogChannel::STEAM, "Cancelling trade offer..."); 195 | 196 | const char urlStart[] = "https://steamcommunity.com/tradeoffer/"; 197 | 198 | const size_t urlBufSz = sizeof(urlStart) - 1 + offerIdBufSz - 1 + sizeof("/cancel") - 1 + 1; 199 | char url[urlBufSz]; 200 | 201 | char* urlEnd = url; 202 | urlEnd = stpcpy(urlEnd, urlStart); 203 | urlEnd = stpcpy(urlEnd, offerId); 204 | strcpy(urlEnd, "/cancel"); 205 | 206 | const char postFieldSession[] = "sessionid="; 207 | 208 | const size_t postFieldsBufSz = sizeof(postFieldSession) - 1 + sessionIdBufSz - 1 + 1; 209 | char postFields[postFieldsBufSz]; 210 | 211 | char* postFieldsEnd = postFields; 212 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldSession); 213 | strcpy(postFieldsEnd, sessionId); 214 | 215 | Curl::CResponse response; 216 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 217 | curl_easy_setopt(curl, CURLOPT_URL, url); 218 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 219 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 220 | 221 | const CURLcode respCode = curl_easy_perform(curl); 222 | 223 | if (respCode != CURLE_OK) 224 | { 225 | Curl::PrintError(curl, respCode); 226 | return false; 227 | } 228 | 229 | rapidjson::Document parsed; 230 | parsed.ParseInsitu(response.data); 231 | 232 | if (parsed.HasParseError()) 233 | { 234 | putsnn("JSON parsing failed\n"); 235 | return false; 236 | } 237 | 238 | const auto iterOfferId = parsed.FindMember("tradeofferid"); 239 | if (iterOfferId == parsed.MemberEnd()) 240 | { 241 | putsnn("request unsucceeded\n"); 242 | return false; 243 | } 244 | 245 | putsnn("ok\n"); 246 | return true; 247 | } 248 | 249 | bool GetOffers(CURL* curl, const char* apiKey, bool getSent, bool getReceived, bool getDescriptions, 250 | bool activeOnly, bool historicalOnly, const char* language, uint64_t timeHistoricalCutoff, 251 | uint32_t cursor, rapidjson::Document* outDoc) 252 | { 253 | //Log(LogChannel::STEAM, "Getting trade offers..."); 254 | 255 | const char urlStart[] = "https://api.steampowered.com/IEconService/GetTradeOffers/v1/?key="; 256 | const char urlSent[] = "&get_sent_offers=true"; 257 | const char urlReceived[] = "&get_received_offers=true"; 258 | const char urlDesc[] = "&get_descriptions=true"; 259 | const char urlActive[] = "&active_only=true"; 260 | const char urlHistorical[] = "&historical_only=true"; 261 | const char urlLang[] = "&language="; 262 | const char urlTime[] = "&time_historical_cutoff="; 263 | const char urlCursor[] = "&cursor="; 264 | 265 | const size_t urlBufSz = 266 | sizeof(urlStart) - 1 + apiKeyBufSz - 1 + 267 | sizeof(urlSent) - 1 + 268 | sizeof(urlReceived) - 1 + 269 | sizeof(urlDesc) - 1 + 270 | sizeof(urlActive) - 1 + 271 | sizeof(urlHistorical) - 1 + 272 | sizeof(urlLang) - 1 + sizeof("portuguese") - 1 + // longest steam language 273 | sizeof(urlTime) - 1 + UINT32_MAX_STR_SIZE - 1 + 274 | sizeof(urlCursor) - 1 + UINT32_MAX_STR_SIZE - 1 + 1; 275 | 276 | char url[urlBufSz]; 277 | 278 | char* urlEnd = url; 279 | urlEnd = stpcpy(urlEnd, urlStart); 280 | urlEnd = stpcpy(urlEnd, apiKey); 281 | 282 | if (getSent) 283 | urlEnd = stpcpy(urlEnd, urlSent); 284 | 285 | if (getReceived) 286 | urlEnd = stpcpy(urlEnd, urlReceived); 287 | 288 | if (getDescriptions) 289 | urlEnd = stpcpy(urlEnd, urlDesc); 290 | 291 | if (activeOnly) 292 | urlEnd = stpcpy(urlEnd, urlActive); 293 | 294 | if (historicalOnly) 295 | urlEnd = stpcpy(urlEnd, urlHistorical); 296 | 297 | if (language) 298 | { 299 | urlEnd = stpcpy(urlEnd, urlLang); 300 | urlEnd = stpcpy(urlEnd, language); 301 | } 302 | 303 | if (timeHistoricalCutoff) 304 | { 305 | urlEnd = stpcpy(urlEnd, urlTime); 306 | urlEnd = stpcpy(urlEnd, std::to_string(timeHistoricalCutoff).c_str()); 307 | } 308 | 309 | if (cursor) 310 | { 311 | urlEnd = stpcpy(urlEnd, urlCursor); 312 | strcpy(urlEnd, std::to_string(cursor).c_str()); 313 | } 314 | 315 | Curl::CResponse response; 316 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 317 | curl_easy_setopt(curl, CURLOPT_URL, url); 318 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 319 | 320 | const CURLcode respCode = curl_easy_perform(curl); 321 | 322 | if (respCode != CURLE_OK) 323 | { 324 | //Curl::PrintError(curl, respCode); 325 | return false; 326 | } 327 | 328 | outDoc->Parse(response.data); 329 | 330 | if (outDoc->HasParseError()) 331 | { 332 | //putsnn("JSON parsing failed\n"); 333 | return false; 334 | } 335 | 336 | const auto iterResponse = outDoc->FindMember("response"); 337 | if (iterResponse == outDoc->MemberEnd()) 338 | { 339 | //putsnn("request unsucceeded\n"); 340 | return false; 341 | } 342 | 343 | //putsnn("ok\n"); 344 | return true; 345 | } 346 | 347 | // out buffer size must be at least tokenBufSz 348 | bool GetToken(CURL* curl, char* out) 349 | { 350 | Log(LogChannel::STEAM, "Getting trade token..."); 351 | 352 | Curl::CResponse response; 353 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 354 | curl_easy_setopt(curl, CURLOPT_URL, "https://steamcommunity.com/my/tradeoffers/privacy"); 355 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 356 | 357 | const CURLcode respCode = curl_easy_perform(curl); 358 | 359 | if (respCode != CURLE_OK) 360 | { 361 | Curl::PrintError(curl, respCode); 362 | return false; 363 | } 364 | 365 | const char searchStr[] = "https://steamcommunity.com/tradeoffer/new/?partner="; 366 | 367 | const char* tradeUrl = strstr(response.data, searchStr); 368 | if (!tradeUrl) 369 | { 370 | putsnn("trade URL not found\n"); 371 | return false; 372 | } 373 | 374 | const char* tradeToken = (char*)memchr( 375 | tradeUrl + sizeof(searchStr) - 1, 376 | '=', 377 | UINT32_MAX_STR_SIZE - 1 + sizeof("&token=") - 1); 378 | 379 | if (!tradeToken) 380 | { 381 | putsnn("trade token not found\n"); 382 | return false; 383 | } 384 | 385 | ++tradeToken; 386 | 387 | stpncpy(out, tradeToken, Steam::Trade::tokenBufSz - 1)[0] = '\0'; 388 | 389 | putsnn("ok\n"); 390 | return true; 391 | } 392 | } 393 | } -------------------------------------------------------------------------------- /vsproject/OpenMarketClient.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 15.0 23 | {454F87DA-5D7C-4458-8834-BC39DBF19FE8} 24 | Win32Proj 25 | market 26 | 10.0 27 | OpenMarketClient 28 | 29 | 30 | 31 | Application 32 | true 33 | v142 34 | MultiByte 35 | 36 | 37 | Application 38 | false 39 | v142 40 | true 41 | MultiByte 42 | 43 | 44 | Application 45 | true 46 | v142 47 | MultiByte 48 | 49 | 50 | Application 51 | false 52 | v142 53 | true 54 | MultiByte 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | false 76 | $(SolutionDir)build\$(Platform)\$(Configuration)\ 77 | $(OutDir)obj\ 78 | NativeRecommendedRules.ruleset 79 | 80 | 81 | true 82 | false 83 | $(SolutionDir)build\$(Platform)\$(Configuration)\ 84 | $(OutDir)obj\ 85 | NativeRecommendedRules.ruleset 86 | 87 | 88 | false 89 | false 90 | $(SolutionDir)build\$(Platform)\$(Configuration)\ 91 | $(OutDir)obj\ 92 | NativeRecommendedRules.ruleset 93 | 94 | 95 | false 96 | false 97 | $(SolutionDir)build\$(Platform)\$(Configuration)\ 98 | $(OutDir)obj\ 99 | NativeRecommendedRules.ruleset 100 | 101 | 102 | 103 | Use 104 | Level4 105 | Disabled 106 | true 107 | WIN32;_DEBUG;_CONSOLE;CURL_STATICLIB;%(PreprocessorDefinitions) 108 | true 109 | ProgramDatabase 110 | ..\..\libs\wolfssl;..\..\libs\curl\include;..\..\libs\rapidjson\include;%(AdditionalIncludeDirectories) 111 | Fast 112 | stdcpp17 113 | false 114 | Precompiled.h 115 | 116 | 117 | Console 118 | true 119 | false 120 | Ws2_32.lib;zlibstaticd.lib;wolfssl.lib;libcurl-d.lib;%(AdditionalDependencies) 121 | ..\..\libs\zlib\build\$(Platform)\$(Configuration);..\..\libs\wolfssl\build\$(Platform)\$(Configuration);..\..\libs\curl\build\$(Platform)\lib\$(Configuration);%(AdditionalLibraryDirectories) 122 | 123 | 124 | 125 | 126 | Use 127 | Level4 128 | Disabled 129 | true 130 | _DEBUG;_CONSOLE;CURL_STATICLIB;%(PreprocessorDefinitions) 131 | true 132 | Fast 133 | ..\..\libs\wolfssl;..\..\libs\curl\include;..\..\libs\rapidjson\include;%(AdditionalIncludeDirectories) 134 | stdcpp17 135 | false 136 | Precompiled.h 137 | 138 | 139 | Console 140 | true 141 | false 142 | Ws2_32.lib;wolfssl.lib;libcurl-d.lib;%(AdditionalDependencies) 143 | ..\..\libs\wolfssl\build\$(Platform)\$(Configuration);..\..\libs\curl\build\$(Platform)\lib\$(Configuration);%(AdditionalLibraryDirectories) 144 | 145 | 146 | 147 | 148 | Use 149 | Level4 150 | MaxSpeed 151 | true 152 | true 153 | true 154 | WIN32;NDEBUG;_CONSOLE;CURL_STATICLIB;%(PreprocessorDefinitions) 155 | true 156 | ..\..\libs\wolfssl;..\..\libs\curl\include;..\..\libs\rapidjson\include;%(AdditionalIncludeDirectories) 157 | None 158 | Fast 159 | stdcpp17 160 | AnySuitable 161 | false 162 | Speed 163 | Precompiled.h 164 | 165 | 166 | Console 167 | true 168 | true 169 | false 170 | false 171 | Ws2_32.lib;zlibstatic.lib;wolfssl.lib;libcurl.lib;%(AdditionalDependencies) 172 | ..\..\libs\zlib\build\$(Platform)\$(Configuration);..\..\libs\wolfssl\build\$(Platform)\$(Configuration);..\..\libs\curl\build\$(Platform)\lib\$(Configuration);%(AdditionalLibraryDirectories) 173 | UseLinkTimeCodeGeneration 174 | 175 | 176 | 177 | 178 | Use 179 | Level4 180 | MaxSpeed 181 | true 182 | true 183 | true 184 | NDEBUG;_CONSOLE;CURL_STATICLIB;%(PreprocessorDefinitions) 185 | true 186 | Fast 187 | None 188 | ..\..\libs\wolfssl;..\..\libs\curl\include;..\..\libs\rapidjson\include;%(AdditionalIncludeDirectories) 189 | stdcpp17 190 | AnySuitable 191 | false 192 | Speed 193 | Precompiled.h 194 | 195 | 196 | Console 197 | true 198 | true 199 | false 200 | false 201 | Ws2_32.lib;wolfssl.lib;libcurl.lib;%(AdditionalDependencies) 202 | ..\..\libs\wolfssl\build\$(Platform)\$(Configuration);..\..\libs\curl\build\$(Platform)\$(Configuration)\lib\$(Configuration);%(AdditionalLibraryDirectories) 203 | UseLinkTimeCodeGeneration 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | Create 224 | Create 225 | Create 226 | Create 227 | 228 | 229 | 230 | 231 | 232 | -------------------------------------------------------------------------------- /src/Steam/Guard.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Steam 4 | { 5 | namespace Guard 6 | { 7 | const size_t secretsSz = PlainToBase64Size(WC_SHA_DIGEST_SIZE, WC_NO_NL_ENC); 8 | 9 | const size_t deviceIdBufSz = sizeof("android:") - 1 + 36 + 1; 10 | 11 | const size_t twoFactorCodeBufSz = 5 + 1; 12 | const size_t confTagMaxLen = 32; // everyone does 32 char tag limit, no idea why 13 | const size_t confHashSz = PlainToBase64Size(WC_SHA_DIGEST_SIZE, WC_NO_NL_ENC); 14 | const size_t confIdBufSz = UINT64_MAX_STR_SIZE; 15 | const size_t confKeyBufSz = UINT64_MAX_STR_SIZE; 16 | 17 | const size_t confQueueParamsBufSz = 18 | sizeof("m=android&p=") - 1 + deviceIdBufSz - 1 + 19 | sizeof("&a=") - 1 + UINT64_MAX_STR_SIZE - 1 + 20 | sizeof("&k=") - 1 + confHashSz * 3 + // multiply by 3 due to URL encoding 21 | sizeof("&t=") - 1 + UINT64_MAX_STR_SIZE - 1 + 22 | sizeof("&tag=") - 1 + confTagMaxLen + 1; 23 | 24 | time_t timeDiff = 0; 25 | 26 | bool SyncTime(CURL* curl) 27 | { 28 | Log(LogChannel::STEAM, "Syncing time..."); 29 | 30 | curl_easy_setopt(curl, CURLOPT_URL, "https://api.steampowered.com/ITwoFactorService/QueryTime/v1/"); 31 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 32 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, ""); 33 | 34 | Curl::CResponse response; 35 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 36 | 37 | const CURLcode respCode = curl_easy_perform(curl); 38 | 39 | if (respCode != CURLE_OK) 40 | { 41 | Curl::PrintError(curl, respCode); 42 | return false; 43 | } 44 | 45 | rapidjson::Document parsed; 46 | parsed.ParseInsitu(response.data); 47 | 48 | if (parsed.HasParseError()) 49 | { 50 | putsnn("JSON parsing failed\n"); 51 | return false; 52 | } 53 | 54 | const auto iterResponse = parsed.FindMember("response"); 55 | if (iterResponse == parsed.MemberEnd() || iterResponse->value.ObjectEmpty()) 56 | { 57 | putsnn("request unsucceeded\n"); 58 | return false; 59 | } 60 | 61 | const char* serverTime = iterResponse->value["server_time"].GetString(); 62 | 63 | timeDiff = time(nullptr) - atoll(serverTime); 64 | 65 | putsnn("ok\n"); 66 | return true; 67 | } 68 | 69 | inline time_t GetSteamTime() 70 | { 71 | return time(nullptr) + timeDiff; 72 | } 73 | 74 | // out buffer size must be at least twoFactorCodeBufSz 75 | bool GenerateTwoFactorAuthCode(const char* sharedSecret, char* out) 76 | { 77 | Log(LogChannel::STEAM, "Generating two factor auth code..."); 78 | 79 | byte rawShared[WC_SHA_DIGEST_SIZE + 1]; 80 | word32 rawSharedSz = sizeof(rawShared); 81 | 82 | if (Base64_Decode((byte*)sharedSecret, secretsSz, rawShared, &rawSharedSz)) 83 | { 84 | putsnn("shared secret decoding failed\n"); 85 | return false; 86 | } 87 | 88 | // https://en.wikipedia.org/wiki/Time-based_one-time_password 89 | // https://www.rfc-editor.org/rfc/rfc4226#section-5.3 90 | const time_t totpInterval = 30; 91 | time_t totpCounter = (GetSteamTime() / totpInterval); 92 | 93 | // The Key (K), the Counter (C), and Data values are hashed high-order byte first 94 | #ifdef LITTLE_ENDIAN_ORDER 95 | totpCounter = byteswap64(totpCounter); 96 | #endif // LITTLE_ENDIAN 97 | 98 | byte hmacHash[WC_SHA_DIGEST_SIZE]; 99 | 100 | Hmac hmac; 101 | if (wc_HmacSetKey(&hmac, WC_SHA, rawShared, rawSharedSz) || 102 | wc_HmacUpdate(&hmac, (byte*)&totpCounter, sizeof(totpCounter)) || 103 | wc_HmacFinal(&hmac, hmacHash)) 104 | { 105 | putsnn("HMAC failed\n"); 106 | return false; 107 | } 108 | 109 | const byte hotpOffset = (hmacHash[sizeof(hmacHash) - 1] & 0xF); 110 | uint32_t hotpBinCode = *(uint32_t*)(hmacHash + hotpOffset); 111 | 112 | // We treat the dynamic binary code as a 31-bit, unsigned, big-endian integer 113 | #ifdef LITTLE_ENDIAN_ORDER 114 | hotpBinCode = byteswap32(hotpBinCode); 115 | #endif // LITTLE_ENDIAN 116 | 117 | hotpBinCode &= 0x7FFFFFFF; 118 | 119 | const char codeChars[] = "23456789BCDFGHJKMNPQRTVWXY"; 120 | const size_t codeCharsCount = sizeof(codeChars) - 1; 121 | 122 | for (size_t i = 0; i < (twoFactorCodeBufSz - 1); ++i) 123 | { 124 | out[i] = codeChars[hotpBinCode % codeCharsCount]; 125 | hotpBinCode /= codeCharsCount; 126 | } 127 | out[twoFactorCodeBufSz - 1] = '\0'; 128 | 129 | putsnn("ok\n"); 130 | return true; 131 | } 132 | 133 | // out buffer size must be at least confHashBufSz 134 | // outLen is the size of out buffer on input and result size on output 135 | bool GenerateConfirmationHash(const char* identitySecret, time_t timestamp, const char* tag, byte* out, word32* outSz) 136 | { 137 | size_t tagLen = strlen(tag); 138 | if (tagLen > confTagMaxLen) 139 | tagLen = confTagMaxLen; 140 | 141 | const size_t msgBufSz = sizeof(timestamp) + confTagMaxLen; 142 | byte msg[msgBufSz]; 143 | const size_t msgSz = sizeof(timestamp) + tagLen; 144 | 145 | #ifdef LITTLE_ENDIAN_ORDER 146 | timestamp = byteswap64(timestamp); 147 | #endif // LITTLE_ENDIAN 148 | 149 | *(time_t*)msg = timestamp; 150 | 151 | memcpy(msg + sizeof(timestamp), tag, tagLen); 152 | 153 | byte rawIdentity[WC_SHA_DIGEST_SIZE + 1]; 154 | word32 rawIdentitySz = sizeof(rawIdentity); 155 | 156 | Hmac hmac; 157 | byte hmacHash[WC_SHA_DIGEST_SIZE]; 158 | 159 | const bool success = 160 | (!Base64_Decode((byte*)identitySecret, secretsSz, rawIdentity, &rawIdentitySz) && 161 | !wc_HmacSetKey(&hmac, WC_SHA, rawIdentity, rawIdentitySz) && 162 | !wc_HmacUpdate(&hmac, msg, msgSz) && 163 | !wc_HmacFinal(&hmac, hmacHash) && 164 | !Base64_Encode_NoNl(hmacHash, sizeof(hmacHash), out, outSz)); 165 | 166 | return success; 167 | } 168 | 169 | // out buffer size must be at least confQueueParamsBufSz 170 | bool GenerateConfirmationQueryParams(CURL* curl, const char* steamId64, const char* identitySecret, 171 | const char* deviceId, const char* tag, char* out) 172 | { 173 | const time_t timestamp = GetSteamTime(); 174 | 175 | byte hash[confHashSz]; 176 | word32 hashSz = confHashSz; 177 | 178 | if (!GenerateConfirmationHash(identitySecret, timestamp, tag, hash, &hashSz)) 179 | return false; 180 | 181 | char* escapedHash = curl_easy_escape(curl, (char*)hash, hashSz); 182 | 183 | char* outEnd = out; 184 | outEnd = stpcpy(outEnd, "m=android&p="); 185 | outEnd = stpcpy(outEnd, deviceId); 186 | outEnd = stpcpy(outEnd, "&a="); 187 | outEnd = stpcpy(outEnd, steamId64); 188 | outEnd = stpcpy(outEnd, "&k="); 189 | outEnd = stpcpy(outEnd, escapedHash); 190 | outEnd = stpcpy(outEnd, "&t="); 191 | outEnd = stpcpy(outEnd, std::to_string(timestamp).c_str()); 192 | outEnd = stpcpy(outEnd, "&tag="); 193 | strcpy(outEnd, tag); 194 | 195 | curl_free(escapedHash); 196 | 197 | return true; 198 | } 199 | 200 | bool FetchConfirmations(CURL* curl, const char* steamId64, const char* identitySecret, 201 | const char* deviceId, rapidjson::Document* out) 202 | { 203 | Log(LogChannel::STEAM, "Fetching confirmations..."); 204 | 205 | char postFields[confQueueParamsBufSz]; 206 | if (!GenerateConfirmationQueryParams(curl, steamId64, identitySecret, deviceId, "conf", postFields)) 207 | { 208 | putsnn("query params generation failed\n"); 209 | return false; 210 | } 211 | 212 | Curl::CResponse response; 213 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 214 | curl_easy_setopt(curl, CURLOPT_URL, "https://steamcommunity.com/mobileconf/getlist"); 215 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 216 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 217 | 218 | const CURLcode respCode = curl_easy_perform(curl); 219 | 220 | if (respCode != CURLE_OK) 221 | { 222 | Curl::PrintError(curl, respCode); 223 | return false; 224 | } 225 | 226 | out->Parse(response.data); 227 | 228 | if (out->HasParseError()) 229 | { 230 | putsnn("JSON parsing failed\n"); 231 | return false; 232 | } 233 | 234 | if (!(*out)["success"].GetBool()) 235 | { 236 | putsnn("request unsucceeded\n"); 237 | return false; 238 | } 239 | 240 | putsnn("ok\n"); 241 | return true; 242 | } 243 | 244 | bool AcceptConfirmation(CURL* curl, 245 | const char* steamId64, const char* identitySecret, const char* deviceId, const char* offerId) 246 | { 247 | rapidjson::Document docConfs; 248 | if (!FetchConfirmations(curl, steamId64, identitySecret, deviceId, &docConfs)) 249 | return false; 250 | 251 | Log(LogChannel::STEAM, "Accepting confirmation..."); 252 | 253 | const char cId[] = "&op=allow&cid="; 254 | const char cK[] = "&ck="; 255 | 256 | const size_t postFieldsBufSz = 257 | confQueueParamsBufSz - 1 + 258 | sizeof(cId) - 1 + confIdBufSz - 1 + 259 | sizeof(cK) - 1 + confKeyBufSz - 1 + 1; 260 | 261 | char postFields[postFieldsBufSz]; 262 | 263 | if (!GenerateConfirmationQueryParams(curl, steamId64, identitySecret, deviceId, "allow", postFields)) 264 | { 265 | putsnn("query params generation failed\n"); 266 | return false; 267 | } 268 | 269 | const char* confId = nullptr; 270 | const char* confNonce = nullptr; 271 | 272 | const rapidjson::Value& confs = docConfs["conf"]; 273 | const rapidjson::SizeType confCount = confs.Size(); 274 | 275 | for (rapidjson::SizeType i = 0; i < confCount; ++i) 276 | { 277 | const rapidjson::Value& conf = confs[i]; 278 | 279 | if (conf["type"].GetInt() == 2 && !strcmp(conf["creator_id"].GetString(), offerId)) 280 | { 281 | confId = conf["id"].GetString(); 282 | confNonce = conf["nonce"].GetString(); 283 | } 284 | } 285 | 286 | if (!confId || !confNonce) 287 | { 288 | putsnn("finding confirmation params failed\n"); 289 | return false; 290 | } 291 | 292 | char* postFieldsEnd = postFields + strlen(postFields); 293 | postFieldsEnd = stpcpy(postFieldsEnd, cId); 294 | postFieldsEnd = stpcpy(postFieldsEnd, confId); 295 | postFieldsEnd = stpcpy(postFieldsEnd, cK); 296 | strcpy(postFieldsEnd, confNonce); 297 | 298 | Curl::CResponse respOp; 299 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &respOp); 300 | curl_easy_setopt(curl, CURLOPT_URL, "https://steamcommunity.com/mobileconf/ajaxop"); 301 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 302 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 303 | 304 | const CURLcode respCodeOp = curl_easy_perform(curl); 305 | 306 | if (respCodeOp != CURLE_OK) 307 | { 308 | Curl::PrintError(curl, respCodeOp); 309 | return false; 310 | } 311 | 312 | rapidjson::Document parsedOp; 313 | parsedOp.ParseInsitu(respOp.data); 314 | 315 | if (parsedOp.HasParseError()) 316 | { 317 | putsnn("JSON parsing failed\n"); 318 | return false; 319 | } 320 | 321 | if (!parsedOp["success"].GetBool()) 322 | { 323 | putsnn("request unsucceeded\n"); 324 | return false; 325 | } 326 | 327 | putsnn("ok\n"); 328 | return true; 329 | } 330 | 331 | // unused 332 | bool AcceptConfirmations(CURL* curl, 333 | const char* steamId64, const char* identitySecret, const char* deviceId, 334 | const char** offerIds, size_t offerIdCount) 335 | { 336 | rapidjson::Document docConfs; 337 | if (!FetchConfirmations(curl, steamId64, identitySecret, deviceId, &docConfs)) 338 | return false; 339 | 340 | Log(LogChannel::STEAM, "Accepting confirmations..."); 341 | 342 | const char opAllow[] = "&op=allow"; 343 | 344 | const size_t confParamsLen = sizeof("&cid[]=&ck[]=") - 1 + confIdBufSz - 1 + confKeyBufSz - 1; 345 | const size_t postFieldsBufSz = confQueueParamsBufSz - 1 + sizeof(opAllow) - 1 + confParamsLen * offerIdCount + 1; 346 | 347 | char* postFields = (char*)malloc(postFieldsBufSz); 348 | if (!postFields) 349 | { 350 | putsnn("allocation failed\n"); 351 | return false; 352 | } 353 | 354 | if (!GenerateConfirmationQueryParams(curl, steamId64, identitySecret, deviceId, "allow", postFields)) 355 | { 356 | free(postFields); 357 | putsnn("query params generation failed\n"); 358 | return false; 359 | } 360 | 361 | size_t confirmedCount = 0; 362 | 363 | char* postFieldsEnd = postFields + strlen(postFields); 364 | postFieldsEnd = stpcpy(postFieldsEnd, opAllow); 365 | 366 | for (size_t i = 0; i < offerIdCount; ++i) 367 | { 368 | const char* confId = nullptr; 369 | const char* confNonce = nullptr; 370 | 371 | const rapidjson::Value& confs = docConfs["conf"]; 372 | const rapidjson::SizeType confCount = confs.Size(); 373 | 374 | for (rapidjson::SizeType j = 0; j < confCount; ++j) 375 | { 376 | const rapidjson::Value& conf = confs[j]; 377 | 378 | if (conf["type"].GetInt() == 2 && !strcmp(conf["creator_id"].GetString(), offerIds[i])) 379 | { 380 | confId = conf["id"].GetString(); 381 | confNonce = conf["nonce"].GetString(); 382 | } 383 | } 384 | 385 | if (!confId || !confNonce) 386 | continue; 387 | 388 | postFieldsEnd = stpcpy(postFieldsEnd, "&cid[]="); 389 | postFieldsEnd = stpcpy(postFieldsEnd, confId); 390 | postFieldsEnd = stpcpy(postFieldsEnd, "&ck[]="); 391 | postFieldsEnd = stpcpy(postFieldsEnd, confNonce); 392 | 393 | ++confirmedCount; 394 | } 395 | 396 | Curl::CResponse respMultiOp; 397 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &respMultiOp); 398 | curl_easy_setopt(curl, CURLOPT_URL, "https://steamcommunity.com/mobileconf/multiajaxop"); 399 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 400 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 401 | 402 | const CURLcode respCodeMultiOp = curl_easy_perform(curl); 403 | 404 | free(postFields); 405 | 406 | if (respCodeMultiOp != CURLE_OK) 407 | { 408 | Curl::PrintError(curl, respCodeMultiOp); 409 | return false; 410 | } 411 | 412 | rapidjson::Document parsedMultiOp; 413 | parsedMultiOp.ParseInsitu(respMultiOp.data); 414 | 415 | if (parsedMultiOp.HasParseError()) 416 | { 417 | putsnn("JSON parsing failed\n"); 418 | return false; 419 | } 420 | 421 | if (!parsedMultiOp["success"].GetBool()) 422 | { 423 | putsnn("request unsucceeded\n"); 424 | return false; 425 | } 426 | 427 | if (confirmedCount != offerIdCount) 428 | printf("accepted %zu out of %zu\n", confirmedCount, offerIdCount); 429 | else 430 | putsnn("ok\n"); 431 | 432 | return true; 433 | } 434 | 435 | // out buffer size must be at least deviceIdBufSz 436 | bool GetDeviceId(CURL* curl, const char* steamId64, const char* accessToken, char* out) 437 | { 438 | Log(LogChannel::STEAM, "Getting Steam Guard Mobile device ID..."); 439 | 440 | const char postFieldAccessToken[] = "access_token="; 441 | const char postFieldSteamId[] = "&steamid="; 442 | 443 | const size_t postFieldsBufSz = 444 | sizeof(postFieldAccessToken) - 1 + Auth::jwtBufSz + 445 | sizeof(postFieldSteamId) - 1 + UINT64_MAX_STR_SIZE - 1 + 1; 446 | 447 | char postFields[postFieldsBufSz]; 448 | 449 | char* postFieldsEnd = postFields; 450 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldAccessToken); 451 | postFieldsEnd = stpcpy(postFieldsEnd, accessToken); 452 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldSteamId); 453 | strcpy(postFieldsEnd, steamId64); 454 | 455 | Curl::CResponse response; 456 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 457 | curl_easy_setopt(curl, CURLOPT_URL, "https://api.steampowered.com/ITwoFactorService/QueryStatus/v1/"); 458 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 459 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 460 | 461 | const CURLcode respCode = curl_easy_perform(curl); 462 | 463 | if (respCode != CURLE_OK) 464 | { 465 | Curl::PrintError(curl, respCode); 466 | return false; 467 | } 468 | 469 | rapidjson::Document parsed; 470 | parsed.ParseInsitu(response.data); 471 | 472 | if (parsed.HasParseError()) 473 | { 474 | putsnn("JSON parsing failed\n"); 475 | return false; 476 | } 477 | 478 | const auto iterResponse = parsed.FindMember("response"); 479 | if (iterResponse == parsed.MemberEnd() || iterResponse->value.ObjectEmpty()) 480 | { 481 | putsnn("request unsucceeded\n"); 482 | return false; 483 | } 484 | 485 | const char* deviceId = iterResponse->value["device_identifier"].GetString(); 486 | 487 | strcpy(out, deviceId); 488 | 489 | putsnn("ok\n"); 490 | return true; 491 | } 492 | } 493 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /src/Market.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Market 4 | { 5 | const size_t apiKeySz = 31; 6 | const size_t hashBufSz = 50; 7 | const int offerTTL = (10 * 60); 8 | 9 | enum class Market 10 | { 11 | CSGO, 12 | DOTA, 13 | TF2, 14 | RUST, 15 | GIFTS, 16 | 17 | COUNT 18 | }; 19 | 20 | const char* marketNames[] = 21 | { 22 | "CSGO", 23 | "DOTA", 24 | "TF2", 25 | "RUST", 26 | "GIFTS", 27 | }; 28 | 29 | const char* marketBaseUrls[] = 30 | { 31 | "https://market.csgo.com/api/v2/", 32 | "https://market.dota2.net/api/v2/", 33 | "https://tf2.tm/api/v2/", 34 | "https://rust.tm/api/v2/", 35 | "https://gifts.tm/api/v2/", 36 | }; 37 | 38 | const size_t marketBaseUrlMaxSz = sizeof("https://market.dota2.net/api/v2/"); 39 | 40 | //const bool isMarketP2P[] = 41 | //{ 42 | // true, 43 | // true, 44 | // true, 45 | // true, 46 | // true, 47 | //}; 48 | 49 | enum class ItemStatus 50 | { 51 | SELLING = 1, 52 | GIVE, 53 | WAITING_SELLER, 54 | TAKE, 55 | GIVEN, 56 | CANCELLED, 57 | WAITING_ACCEPT 58 | }; 59 | 60 | void RateLimit() 61 | { 62 | static std::chrono::high_resolution_clock::time_point nextRequestTime; 63 | std::this_thread::sleep_until(nextRequestTime); 64 | 65 | const auto curTime = std::chrono::high_resolution_clock::now(); 66 | const auto requestInterval = 1s; 67 | nextRequestTime = curTime + requestInterval; 68 | } 69 | 70 | CURLcode curl_easy_perform(CURL* curl) 71 | { 72 | RateLimit(); 73 | 74 | return ::curl_easy_perform(curl); 75 | } 76 | 77 | // deprecated 78 | bool Ping(CURL* curl, const char* apiKey) 79 | { 80 | const char query[] = "ping?key="; 81 | 82 | const size_t urlBufSz = marketBaseUrlMaxSz - 1 + sizeof(query) - 1 + apiKeySz + 1; 83 | char url[urlBufSz]; 84 | 85 | char* urlEnd = url; 86 | urlEnd = stpcpy(urlEnd, marketBaseUrls[(int)Market::CSGO]); 87 | urlEnd = stpcpy(urlEnd, query); 88 | strcpy(urlEnd, apiKey); 89 | 90 | curl_easy_setopt(curl, CURLOPT_URL, url); 91 | curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); 92 | 93 | return (curl_easy_perform(curl) == CURLE_OK); 94 | } 95 | 96 | bool PingNew(CURL* curl, const char* apiKey, const char* accessToken, const char* proxy) 97 | { 98 | const char query[] = "ping-new?key="; 99 | 100 | const size_t urlBufSz = marketBaseUrlMaxSz - 1 + sizeof(query) - 1 + apiKeySz + 1; 101 | char url[urlBufSz]; 102 | 103 | char* urlEnd = url; 104 | urlEnd = stpcpy(urlEnd, marketBaseUrls[(int)Market::CSGO]); 105 | urlEnd = stpcpy(urlEnd, query); 106 | strcpy(urlEnd, apiKey); 107 | 108 | rapidjson::Document jsonDoc; 109 | jsonDoc.SetObject(); 110 | jsonDoc.AddMember("access_token", 111 | rapidjson::Value(accessToken, jsonDoc.GetAllocator()).Move(), 112 | jsonDoc.GetAllocator()); 113 | 114 | if (proxy) 115 | { 116 | jsonDoc.AddMember("proxy", 117 | rapidjson::Value(proxy, jsonDoc.GetAllocator()).Move(), 118 | jsonDoc.GetAllocator()); 119 | } 120 | 121 | rapidjson::StringBuffer buffer; 122 | rapidjson::Writer writer(buffer); 123 | jsonDoc.Accept(writer); 124 | const char* jsonStr = buffer.GetString(); 125 | 126 | curl_easy_setopt(curl, CURLOPT_URL, url); 127 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonStr); 128 | 129 | curl_slist* headers = NULL; 130 | headers = curl_slist_append(headers, "Content-Type: application/json"); 131 | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); 132 | 133 | Curl::CResponse response; 134 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 135 | 136 | const CURLcode res = curl_easy_perform(curl); 137 | 138 | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, NULL); 139 | curl_slist_free_all(headers); 140 | 141 | if (res != CURLE_OK) 142 | return false; 143 | 144 | rapidjson::Document parsed; 145 | parsed.Parse(response.data); 146 | 147 | if (parsed.HasParseError()) 148 | { 149 | Log(LogChannel::MARKET, "Ping failed: JSON parsing failed\n"); 150 | return false; 151 | } 152 | 153 | if (!parsed.HasMember("success")) 154 | { 155 | Log(LogChannel::MARKET, "Ping failed: no success field\n"); 156 | return false; 157 | } 158 | 159 | if (!parsed["success"].GetBool()) 160 | { 161 | const auto iterMessage = parsed.FindMember("message"); 162 | if (iterMessage != parsed.MemberEnd()) 163 | Log(LogChannel::MARKET, "Ping failed, received message: %s\n", iterMessage->value.GetString()); 164 | else 165 | Log(LogChannel::MARKET, "Ping failed: request unsucceeded\n"); 166 | 167 | return false; 168 | } 169 | 170 | return true; 171 | } 172 | 173 | bool GetItems(CURL* curl, const char* apiKey, int market, rapidjson::Document* outDoc) 174 | { 175 | const char query[] = "items?key="; 176 | 177 | const size_t urlBufSz = marketBaseUrlMaxSz - 1 + sizeof(query) - 1 + apiKeySz + 1; 178 | char url[urlBufSz]; 179 | 180 | char* urlEnd = url; 181 | urlEnd = stpcpy(urlEnd, marketBaseUrls[market]); 182 | urlEnd = stpcpy(urlEnd, query); 183 | strcpy(urlEnd, apiKey); 184 | 185 | Curl::CResponse response; 186 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 187 | curl_easy_setopt(curl, CURLOPT_URL, url); 188 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 189 | 190 | if (curl_easy_perform(curl) != CURLE_OK) 191 | return false; 192 | 193 | outDoc->Parse(response.data); 194 | 195 | if (outDoc->HasParseError()) 196 | return false; 197 | 198 | if (!(*outDoc)["success"].GetBool()) 199 | return false; 200 | 201 | return true; 202 | } 203 | 204 | // outOfferId buffer size must be at least offerIdBufSz 205 | bool RequestTake(CURL* curl, const char* apiKey, int market, const char* partnerId32, char* outOfferId) 206 | { 207 | Log(LogChannel::GENERAL, "[%s] Requesting details to receive items...", marketNames[market]); 208 | 209 | const char query[] = "trade-request-take?key="; 210 | const char queryBot[] = "&bot="; 211 | 212 | const size_t urlBufSz = 213 | marketBaseUrlMaxSz - 1 + sizeof(query) - 1 + 214 | apiKeySz + sizeof(queryBot) - 1 + 215 | UINT32_MAX_STR_SIZE - 1 + 1; 216 | char url[urlBufSz]; 217 | 218 | char* urlEnd = url; 219 | urlEnd = stpcpy(urlEnd, marketBaseUrls[market]); 220 | urlEnd = stpcpy(urlEnd, query); 221 | urlEnd = stpcpy(urlEnd, apiKey); 222 | urlEnd = stpcpy(urlEnd, queryBot); 223 | strcpy(urlEnd, partnerId32); 224 | 225 | Curl::CResponse response; 226 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 227 | curl_easy_setopt(curl, CURLOPT_URL, url); 228 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 229 | 230 | const CURLcode respCode = curl_easy_perform(curl); 231 | 232 | if (respCode != CURLE_OK) 233 | { 234 | Curl::PrintError(curl, respCode); 235 | return false; 236 | } 237 | 238 | rapidjson::Document parsed; 239 | parsed.ParseInsitu(response.data); 240 | 241 | if (parsed.HasParseError()) 242 | { 243 | putsnn("JSON parsing failed\n"); 244 | return false; 245 | } 246 | 247 | if (!parsed["success"].GetBool()) 248 | { 249 | putsnn("request unsucceeded\n"); 250 | return false; 251 | } 252 | 253 | const char* offerId = parsed["trade"].GetString(); 254 | 255 | strcpy(outOfferId, offerId); 256 | 257 | putsnn("ok\n"); 258 | return true; 259 | } 260 | 261 | bool RequestGiveBot(CURL* curl, const char* apiKey, int market, char* outTradeOfferId, char* outPartnerId64) 262 | { 263 | Log(LogChannel::GENERAL, "[%s] Requesting details to send items...", marketNames[market]); 264 | 265 | const char query[] = "trade-request-give?key="; 266 | 267 | const size_t urlBufSz = marketBaseUrlMaxSz - 1 + sizeof(query) - 1 + apiKeySz + 1; 268 | char url[urlBufSz]; 269 | 270 | char* urlEnd = url; 271 | urlEnd = stpcpy(urlEnd, marketBaseUrls[market]); 272 | urlEnd = stpcpy(urlEnd, query); 273 | strcpy(urlEnd, apiKey); 274 | 275 | Curl::CResponse response; 276 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 277 | curl_easy_setopt(curl, CURLOPT_URL, url); 278 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 279 | 280 | const CURLcode respCode = curl_easy_perform(curl); 281 | 282 | if (respCode != CURLE_OK) 283 | { 284 | Curl::PrintError(curl, respCode); 285 | return false; 286 | } 287 | 288 | rapidjson::Document parsed; 289 | parsed.ParseInsitu(response.data); 290 | 291 | if (parsed.HasParseError()) 292 | { 293 | putsnn("JSON parsing failed\n"); 294 | return false; 295 | } 296 | 297 | if (!parsed["success"].GetBool()) 298 | { 299 | const auto iterError = parsed.FindMember("error"); 300 | if (iterError != parsed.MemberEnd() && !strcmp(iterError->value.GetString(), "nothing")) 301 | putsnn("nothing\n"); 302 | else 303 | putsnn("request unsucceeded\n"); 304 | 305 | return false; 306 | } 307 | 308 | const char* offerId = parsed["trade"].GetString(); 309 | const char* profile = parsed["profile"].GetString(); 310 | 311 | strcpy(outTradeOfferId, offerId); 312 | stpncpy(outPartnerId64, profile + 36, strlen(profile + 36) - 1)[0] = '\0'; 313 | 314 | putsnn("ok\n"); 315 | return true; 316 | } 317 | 318 | bool RequestGiveP2PAll(CURL* curl, const char* apiKey, int market, rapidjson::Document* outDoc) 319 | { 320 | Log(LogChannel::GENERAL, "[%s] Requesting details to send items...", marketNames[market]); 321 | 322 | const char query[] = "trade-request-give-p2p-all?key="; 323 | 324 | const size_t urlBufSz = marketBaseUrlMaxSz - 1 + sizeof(query) - 1 + apiKeySz + 1; 325 | char url[urlBufSz]; 326 | 327 | char* urlEnd = url; 328 | urlEnd = stpcpy(urlEnd, marketBaseUrls[market]); 329 | urlEnd = stpcpy(urlEnd, query); 330 | strcpy(urlEnd, apiKey); 331 | 332 | Curl::CResponse response; 333 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 334 | curl_easy_setopt(curl, CURLOPT_URL, url); 335 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 336 | 337 | const CURLcode respCode = curl_easy_perform(curl); 338 | 339 | if (respCode != CURLE_OK) 340 | { 341 | Curl::PrintError(curl, respCode); 342 | return false; 343 | } 344 | 345 | outDoc->Parse(response.data); 346 | 347 | if (outDoc->HasParseError()) 348 | { 349 | putsnn("JSON parsing failed\n"); 350 | return false; 351 | } 352 | 353 | if (!(*outDoc)["success"].GetBool()) 354 | { 355 | const auto iterError = outDoc->FindMember("error"); 356 | if (iterError != outDoc->MemberEnd() && !strcmp(iterError->value.GetString(), "nothing")) 357 | putsnn("nothing\n"); 358 | else 359 | putsnn("request unsucceeded\n"); 360 | 361 | return false; 362 | } 363 | 364 | putsnn("ok\n"); 365 | return true; 366 | } 367 | 368 | bool TradeReady(CURL* curl, const char* apiKey, int market, const char* tradeOfferId) 369 | { 370 | Log(LogChannel::GENERAL, "[%s] Registering sent trade offer...", marketNames[market]); 371 | 372 | const char query[] = "trade-ready?key="; 373 | const char queryTradeOffer[] = "&tradeoffer="; 374 | 375 | const size_t urlBufSz = 376 | marketBaseUrlMaxSz - 1 + sizeof(query) - 1 + 377 | apiKeySz + sizeof(queryTradeOffer) - 1 + 378 | Steam::Trade::offerIdBufSz - 1 + 1; 379 | char url[urlBufSz]; 380 | 381 | char* urlEnd = url; 382 | urlEnd = stpcpy(urlEnd, marketBaseUrls[market]); 383 | urlEnd = stpcpy(urlEnd, query); 384 | urlEnd = stpcpy(urlEnd, apiKey); 385 | urlEnd = stpcpy(urlEnd, queryTradeOffer); 386 | strcpy(urlEnd, tradeOfferId); 387 | 388 | Curl::CResponse response; 389 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 390 | curl_easy_setopt(curl, CURLOPT_URL, url); 391 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 392 | 393 | const CURLcode respCode = curl_easy_perform(curl); 394 | 395 | if (respCode != CURLE_OK) 396 | { 397 | Curl::PrintError(curl, respCode); 398 | return false; 399 | } 400 | 401 | rapidjson::Document parsed; 402 | parsed.ParseInsitu(response.data); 403 | 404 | if (parsed.HasParseError()) 405 | { 406 | putsnn("JSON parsing failed\n"); 407 | return false; 408 | } 409 | 410 | if (!parsed["success"].GetBool()) 411 | { 412 | putsnn("request unsucceeded\n"); 413 | return false; 414 | } 415 | 416 | putsnn("ok\n"); 417 | return true; 418 | } 419 | 420 | bool GetProfileStatus(CURL* curl, const char* apiKey, rapidjson::Document* outDoc) 421 | { 422 | Log(LogChannel::MARKET, "Getting profile status..."); 423 | 424 | const char query[] = "test?key="; 425 | 426 | const size_t urlBufSz = marketBaseUrlMaxSz - 1 + sizeof(query) - 1 + apiKeySz + 1; 427 | char url[urlBufSz]; 428 | 429 | char* urlEnd = url; 430 | urlEnd = stpcpy(urlEnd, marketBaseUrls[(int)Market::CSGO]); 431 | urlEnd = stpcpy(urlEnd, query); 432 | strcpy(urlEnd, apiKey); 433 | 434 | Curl::CResponse response; 435 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 436 | curl_easy_setopt(curl, CURLOPT_URL, url); 437 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 438 | 439 | const CURLcode respCode = curl_easy_perform(curl); 440 | 441 | if (respCode != CURLE_OK) 442 | { 443 | Curl::PrintError(curl, respCode); 444 | return false; 445 | } 446 | 447 | outDoc->Parse(response.data); 448 | 449 | if (outDoc->HasParseError()) 450 | { 451 | putsnn("JSON parsing failed\n"); 452 | return false; 453 | } 454 | 455 | if (!(*outDoc)["success"].GetBool()) 456 | { 457 | putsnn("request unsucceeded\n"); 458 | return false; 459 | } 460 | 461 | putsnn("ok\n"); 462 | return true; 463 | } 464 | 465 | bool CanSell(CURL* curl, const char* apiKey) 466 | { 467 | rapidjson::Document docTest; 468 | if (!GetProfileStatus(curl, apiKey, &docTest)) 469 | return false; 470 | 471 | const rapidjson::Value& status = docTest["status"]; 472 | 473 | if (!status["steam_web_api_key"].GetBool()) 474 | { 475 | Log(LogChannel::MARKET, "Can't sell on the market: Steam web API key not set\n"); 476 | return false; 477 | } 478 | 479 | if (!status["user_token"].GetBool()) 480 | { 481 | Log(LogChannel::MARKET, "Can't sell on the market: Steam trade token not set\n"); 482 | return false; 483 | } 484 | 485 | if (!status["trade_check"].GetBool()) 486 | { 487 | Log(LogChannel::MARKET, "Can't sell on the market: trade check required - https://market.csgo.com/check\n"); 488 | return false; 489 | } 490 | 491 | if (!status["site_notmpban"].GetBool()) 492 | { 493 | Log(LogChannel::MARKET, "Can't sell on the market: banned\n"); 494 | return false; 495 | } 496 | 497 | return true; 498 | } 499 | 500 | // deprecated 501 | bool SetSteamApiKey(CURL* curl, const char* marketApiKey, const char* steamApiKey) 502 | { 503 | Log(LogChannel::MARKET, "Setting Steam API key on the market..."); 504 | 505 | const char query[] = "set-steam-api-key?key="; 506 | const char querySteamApiKey[] = "&steam-api-key="; 507 | 508 | const size_t urlBufSz = 509 | marketBaseUrlMaxSz - 1 + 510 | sizeof(query) - 1 + apiKeySz + 511 | sizeof(querySteamApiKey) - 1 + Steam::apiKeyBufSz - 1 + 1; 512 | 513 | char url[urlBufSz]; 514 | 515 | char* urlEnd = url; 516 | urlEnd = stpcpy(urlEnd, marketBaseUrls[(int)Market::CSGO]); 517 | urlEnd = stpcpy(urlEnd, query); 518 | urlEnd = stpcpy(urlEnd, marketApiKey); 519 | urlEnd = stpcpy(urlEnd, querySteamApiKey); 520 | strcpy(urlEnd, steamApiKey); 521 | 522 | Curl::CResponse response; 523 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 524 | curl_easy_setopt(curl, CURLOPT_URL, url); 525 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 526 | 527 | const CURLcode respCode = curl_easy_perform(curl); 528 | 529 | if (respCode != CURLE_OK) 530 | { 531 | Curl::PrintError(curl, respCode); 532 | return false; 533 | } 534 | 535 | rapidjson::Document parsed; 536 | parsed.ParseInsitu(response.data); 537 | 538 | if (parsed.HasParseError()) 539 | { 540 | putsnn("JSON parsing failed\n"); 541 | return false; 542 | } 543 | 544 | if (!parsed["success"].GetBool()) 545 | { 546 | putsnn("request unsucceeded\n"); 547 | return false; 548 | } 549 | 550 | putsnn("ok\n"); 551 | return true; 552 | } 553 | 554 | bool SetSteamTradeToken(CURL* curl, const char* apiKey, const char* tradeToken) 555 | { 556 | Log(LogChannel::MARKET, "Setting Steam trade token on the market..."); 557 | 558 | const char query[] = "set-trade-token?key="; 559 | const char queryTradeToken[] = "&token="; 560 | 561 | const size_t urlBufSz = 562 | marketBaseUrlMaxSz - 1 + 563 | sizeof(query) - 1 + apiKeySz + 564 | sizeof(queryTradeToken) - 1 + Steam::Trade::tokenBufSz - 1 + 1; 565 | 566 | char url[urlBufSz]; 567 | 568 | char* urlEnd = url; 569 | urlEnd = stpcpy(urlEnd, marketBaseUrls[(int)Market::CSGO]); 570 | urlEnd = stpcpy(urlEnd, query); 571 | urlEnd = stpcpy(urlEnd, apiKey); 572 | urlEnd = stpcpy(urlEnd, queryTradeToken); 573 | strcpy(urlEnd, tradeToken); 574 | 575 | Curl::CResponse response; 576 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 577 | curl_easy_setopt(curl, CURLOPT_URL, url); 578 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 579 | 580 | const CURLcode respCode = curl_easy_perform(curl); 581 | 582 | if (respCode != CURLE_OK) 583 | { 584 | Curl::PrintError(curl, respCode); 585 | return false; 586 | } 587 | 588 | rapidjson::Document parsed; 589 | parsed.ParseInsitu(response.data); 590 | 591 | if (parsed.HasParseError()) 592 | { 593 | putsnn("JSON parsing failed\n"); 594 | return false; 595 | } 596 | 597 | if (!parsed["success"].GetBool()) 598 | { 599 | putsnn("request unsucceeded\n"); 600 | return false; 601 | } 602 | 603 | putsnn("ok\n"); 604 | return true; 605 | } 606 | 607 | // steam login token must be set when calling this 608 | bool SetSteamDetails(CURL* curl, const char* apiKey, const char* steamApiKey) 609 | { 610 | rapidjson::Document docTest; 611 | if (!GetProfileStatus(curl, apiKey, &docTest)) 612 | return false; 613 | 614 | const rapidjson::Value& status = docTest["status"]; 615 | 616 | if (!status["steam_web_api_key"].GetBool()) 617 | { 618 | // this fails on the first try sometimes, don't know why 619 | if (!SetSteamApiKey(curl, apiKey, steamApiKey) && !SetSteamApiKey(curl, apiKey, steamApiKey)) 620 | return false; 621 | } 622 | 623 | if (!status["user_token"].GetBool()) 624 | { 625 | char tradeToken[Steam::Trade::tokenBufSz]; 626 | if (!Steam::Trade::GetToken(curl, tradeToken) || !SetSteamTradeToken(curl, apiKey, tradeToken)) 627 | return false; 628 | } 629 | 630 | return true; 631 | } 632 | 633 | // unused 634 | bool GoOffline(CURL* curl, const char* apiKey) 635 | { 636 | Log(LogChannel::MARKET, "Going offline..."); 637 | 638 | const char query[] = "go-offline?key="; 639 | 640 | const size_t urlBufSz = 641 | marketBaseUrlMaxSz - 1 + 642 | sizeof(query) - 1 + apiKeySz + 1; 643 | 644 | char url[urlBufSz]; 645 | 646 | char* urlEnd = url; 647 | urlEnd = stpcpy(urlEnd, marketBaseUrls[(int)Market::CSGO]); 648 | urlEnd = stpcpy(urlEnd, query); 649 | strcpy(urlEnd, apiKey); 650 | 651 | Curl::CResponse response; 652 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 653 | curl_easy_setopt(curl, CURLOPT_URL, url); 654 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 655 | 656 | const CURLcode respCode = curl_easy_perform(curl); 657 | 658 | if (respCode != CURLE_OK) 659 | { 660 | Curl::PrintError(curl, respCode); 661 | return false; 662 | } 663 | 664 | rapidjson::Document parsed; 665 | parsed.ParseInsitu(response.data); 666 | 667 | if (parsed.HasParseError()) 668 | { 669 | putsnn("JSON parsing failed\n"); 670 | return false; 671 | } 672 | 673 | if (!parsed["success"].GetBool()) 674 | { 675 | putsnn("request unsucceeded\n"); 676 | return false; 677 | } 678 | 679 | putsnn("ok\n"); 680 | return true; 681 | } 682 | } -------------------------------------------------------------------------------- /src/Account.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define ACCOUNT_SAVED_FIELDS_SZ offsetof(CAccount, name) 4 | 5 | class CAccount 6 | { 7 | char marketApiKey[Market::apiKeySz + 1] = ""; 8 | char identitySecret[Steam::Guard::secretsSz + 1] = ""; 9 | char deviceId[Steam::Guard::deviceIdBufSz] = ""; 10 | char steamApiKey[Steam::apiKeyBufSz] = ""; 11 | 12 | char steamId64[UINT64_MAX_STR_SIZE] = ""; 13 | 14 | // commented out because oauth seems to be gone 15 | //char oauthToken[Steam::Auth::oauthTokenBufSz] = ""; 16 | //char loginToken[Steam::Auth::loginTokenBufSz] = ""; 17 | 18 | char refreshToken[Steam::Auth::jwtBufSz] = ""; 19 | 20 | //char sessionId[Steam::Auth::sessionIdBufSz] = ""; 21 | 22 | 23 | // everything below isn't saved 24 | 25 | char name[PATH_MAX] = ""; 26 | 27 | class COffer 28 | { 29 | public: 30 | char marketHash[Market::hashBufSz]; 31 | char tradeOfferId[Steam::Trade::offerIdBufSz]; 32 | 33 | COffer(const char* hash, const char* offerId) 34 | { 35 | strcpy(marketHash, hash); 36 | strcpy(tradeOfferId, offerId); 37 | } 38 | }; 39 | 40 | std::vector sentOffers[(int)Market::Market::COUNT]; 41 | std::vector givenItemIds[(int)Market::Market::COUNT]; 42 | std::vector takenItemIds[(int)Market::Market::COUNT]; 43 | std::vector givenOfferIds[(int)Market::Market::COUNT]; 44 | std::vector takenOfferIds[(int)Market::Market::COUNT]; 45 | 46 | 47 | public: 48 | static constexpr const char directory[] = "accounts"; 49 | static constexpr const char extension[] = ".bin"; 50 | 51 | private: 52 | static constexpr int scryptCost = 16; // (128 * (2^16) * 8) = 64 MB RAM 53 | static constexpr int scryptBlockSz = 8; 54 | static constexpr int scryptParallel = 1; 55 | 56 | static constexpr size_t keySz = AES_256_KEY_SIZE; 57 | static constexpr size_t saltSz = (128 / 8); // NIST recommends at least 128 bits 58 | static constexpr size_t ivSz = GCM_NONCE_MID_SZ; 59 | static constexpr size_t authTagSz = (128 / 8); // max allowed tag size is 128 bits 60 | 61 | 62 | bool Save(const char* encryptPass) 63 | { 64 | byte salt[saltSz]; 65 | byte iv[ivSz]; 66 | byte authTag[authTagSz]; 67 | byte cipher[ACCOUNT_SAVED_FIELDS_SZ]; 68 | 69 | const bool encryptFailed = 70 | !Crypto::Encrypt(encryptPass, keySz, scryptCost, scryptBlockSz, scryptParallel, 71 | (byte*)this, ACCOUNT_SAVED_FIELDS_SZ, 72 | salt, saltSz, 73 | iv, ivSz, 74 | authTag, authTagSz, 75 | cipher); 76 | 77 | if (encryptFailed) 78 | return false; 79 | 80 | Log(LogChannel::GENERAL, "Saving..."); 81 | 82 | const std::filesystem::path dir(directory); 83 | 84 | if (!std::filesystem::exists(dir) && !std::filesystem::create_directory(dir)) 85 | { 86 | putsnn("accounts directory creation failed\n"); 87 | return false; 88 | } 89 | 90 | char path[PATH_MAX]; 91 | 92 | char* pathEnd = path; 93 | pathEnd = stpcpy(pathEnd, directory); 94 | pathEnd = stpcpy(pathEnd, "/"); 95 | pathEnd = stpcpy(pathEnd, name); 96 | strcpy(pathEnd, extension); 97 | 98 | FILE* file = u8fopen(path, "wb"); 99 | if (!file) 100 | { 101 | putsnn("file creation failed\n"); 102 | return false; 103 | } 104 | 105 | const bool writeFailed = 106 | ((fwrite(salt, sizeof(byte), sizeof(salt), file) != sizeof(salt)) || 107 | (fwrite(iv, sizeof(byte), sizeof(iv), file) != sizeof(iv)) || 108 | (fwrite(authTag, sizeof(byte), sizeof(authTag), file) != sizeof(authTag)) || 109 | (fwrite(cipher, sizeof(byte), sizeof(cipher), file) != sizeof(cipher))); 110 | 111 | fclose(file); 112 | 113 | if (writeFailed) 114 | { 115 | putsnn("writing failed\n"); 116 | return false; 117 | } 118 | 119 | putsnn("ok\n"); 120 | return true; 121 | } 122 | 123 | bool Load(const char* path, const char* decryptPass) 124 | { 125 | Log(LogChannel::GENERAL, "Reading..."); 126 | 127 | unsigned char* contents = nullptr; 128 | long contentsSz = 0; 129 | if (!ReadFile(path, &contents, &contentsSz)) 130 | { 131 | putsnn("fail\n"); 132 | return false; 133 | } 134 | 135 | putsnn("ok\n"); 136 | 137 | const byte* salt = contents; 138 | const byte* iv = salt + saltSz; 139 | const byte* authTag = iv + ivSz; 140 | const byte* cipher = authTag + authTagSz; 141 | 142 | const bool decryptFailed = 143 | !Crypto::Decrypt(decryptPass, keySz, scryptCost, scryptBlockSz, scryptParallel, 144 | cipher, ACCOUNT_SAVED_FIELDS_SZ, 145 | salt, saltSz, 146 | iv, ivSz, 147 | authTag, authTagSz, 148 | (byte*)this); 149 | 150 | free(contents); 151 | 152 | if (decryptFailed) 153 | return false; 154 | 155 | return true; 156 | } 157 | 158 | // outUsername buffer size must be at least Steam::Auth::usernameBufSz 159 | // outSharedSecret buffer size must be at least Steam::Guard::secretsSz + 1 160 | bool ImportMaFile(const char* path, char* outUsername, char* outSharedSecret) 161 | { 162 | Log(LogChannel::GENERAL, "Importing maFile..."); 163 | 164 | unsigned char* contents = nullptr; 165 | long contentsSz = 0; 166 | if (!ReadFile(path, &contents, &contentsSz)) 167 | { 168 | putsnn("reading failed\n"); 169 | return false; 170 | } 171 | 172 | if (contents[0] != '{') 173 | { 174 | free(contents); 175 | putsnn("invalid maFile, disable encryption in SDA before importing\n"); 176 | return false; 177 | } 178 | 179 | rapidjson::Document parsed; 180 | parsed.Parse((char*)contents, contentsSz); 181 | 182 | free(contents); 183 | 184 | if (parsed.HasParseError()) 185 | { 186 | putsnn("JSON parsing failed\n"); 187 | return false; 188 | } 189 | 190 | const auto iterSession = parsed.FindMember("Session"); 191 | if (iterSession == parsed.MemberEnd() || iterSession->value.ObjectEmpty()) 192 | { 193 | putsnn("session info not found or empty\n"); 194 | return false; 195 | } 196 | 197 | //const char* steamLoginSecure = iterSession->value["SteamLoginSecure"].GetString(); 198 | //const char* steamLoginSecureDelim = strchr(steamLoginSecure, '%'); 199 | 200 | strcpy(identitySecret, parsed["identity_secret"].GetString()); 201 | strcpy(deviceId, parsed["device_id"].GetString()); 202 | //strcpy(sessionId, iterSession->value["SessionID"].GetString()); 203 | //strcpy(loginToken, steamLoginSecureDelim + 6); 204 | //strcpy(oauthToken, iterSession->value["OAuthToken"].GetString()); // commented out because oauth seems to be gone 205 | //stpncpy(steamId64, steamLoginSecure, (steamLoginSecureDelim - steamLoginSecure))[0] = '\0'; 206 | strcpy(steamId64, std::to_string(iterSession->value["SteamID"].GetUint64()).c_str()); 207 | 208 | strcpy(outUsername, parsed["account_name"].GetString()); 209 | strcpy(outSharedSecret, parsed["shared_secret"].GetString()); 210 | 211 | putsnn("ok\n"); 212 | return true; 213 | } 214 | 215 | bool EnterMarketApiKey(CURL* curl) 216 | { 217 | while (true) 218 | { 219 | if (!GetUserInputString("Enter market API key", marketApiKey, Market::apiKeySz + 1, Market::apiKeySz)) 220 | return false; 221 | 222 | rapidjson::Document parsed; 223 | if (Market::GetProfileStatus(curl, marketApiKey, &parsed)) 224 | break; 225 | } 226 | return true; 227 | } 228 | 229 | bool EnterIdentitySecret(CURL* curl) 230 | { 231 | while (true) 232 | { 233 | if (!GetUserInputString("Enter Steam Guard identity_secret", 234 | identitySecret, Steam::Guard::secretsSz + 1, Steam::Guard::secretsSz)) 235 | return false; 236 | 237 | rapidjson::Document doc; 238 | if (Steam::Guard::FetchConfirmations(curl, steamId64, identitySecret, deviceId, &doc)) 239 | break; 240 | } 241 | return true; 242 | } 243 | 244 | bool DidJWTExpire(const char* jwt) 245 | { 246 | const char* pszJwtHeaderEnd = strchr(jwt, '.'); 247 | if (!pszJwtHeaderEnd) 248 | { 249 | Log(LogChannel::GENERAL, "Invalid refresh token\n"); 250 | return false; 251 | } 252 | 253 | const char* pszJwtPayloadEnd = strchr(pszJwtHeaderEnd + 1, '.'); 254 | if (!pszJwtPayloadEnd) 255 | { 256 | Log(LogChannel::GENERAL, "Invalid refresh token\n"); 257 | return false; 258 | } 259 | 260 | const size_t encJwtPayloadSz = pszJwtPayloadEnd - pszJwtHeaderEnd - 1; 261 | 262 | const size_t encJwtPayloadPaddedSz = GetBase64PaddedLen(encJwtPayloadSz); 263 | byte* encJwtPayloadPadded = (byte*)malloc(encJwtPayloadPaddedSz); 264 | if (!encJwtPayloadPadded) 265 | { 266 | Log(LogChannel::GENERAL, "Padded JWT payload allocation failed\n"); 267 | return false; 268 | } 269 | 270 | memcpy(encJwtPayloadPadded, pszJwtHeaderEnd + 1, encJwtPayloadSz); 271 | 272 | // add padding at the end 273 | for (size_t i = encJwtPayloadSz; i < encJwtPayloadPaddedSz; ++i) 274 | encJwtPayloadPadded[i] = '='; 275 | 276 | word32 jwtPayloadSz = Base64ToPlainSize(encJwtPayloadPaddedSz, WC_NO_NL_ENC); 277 | // add null for rapidjson 278 | char* jwtPayload = (char*)malloc(jwtPayloadSz + 1); 279 | if (!jwtPayload) 280 | { 281 | free(encJwtPayloadPadded); 282 | Log(LogChannel::GENERAL, "Plaintext JWT payload allocation failed\n"); 283 | return false; 284 | } 285 | 286 | Base64URLToBase64(jwtPayload, jwtPayloadSz); 287 | 288 | if (Base64_Decode(encJwtPayloadPadded, encJwtPayloadPaddedSz, (byte*)jwtPayload, &jwtPayloadSz)) 289 | { 290 | free(encJwtPayloadPadded); 291 | free(jwtPayload); 292 | 293 | Log(LogChannel::GENERAL, "JWT payload decoding failed\n"); 294 | return false; 295 | } 296 | 297 | jwtPayload[jwtPayloadSz] = '\0'; 298 | 299 | free(encJwtPayloadPadded); 300 | 301 | rapidjson::Document docJwtPayload; 302 | docJwtPayload.ParseInsitu(jwtPayload); 303 | 304 | const time_t exp = docJwtPayload["exp"].GetUint64(); 305 | 306 | free(jwtPayload); 307 | 308 | const time_t curTime = time(nullptr); 309 | 310 | return (curTime >= exp); 311 | } 312 | 313 | public: 314 | bool Init(CURL* curl, const char* sessionId, const char* encryptPass, 315 | const char* argName = nullptr, const char* path = nullptr, bool isMaFile = false) 316 | { 317 | char username[Steam::Auth::usernameBufSz] = ""; 318 | char sharedSecret[Steam::Guard::secretsSz + 1] = ""; 319 | 320 | if (!name[0]) 321 | { 322 | if (argName) 323 | strcpy(name, argName); 324 | else if (!GetUserInputString("Enter new account alias", name, sizeof(name))) 325 | return false; 326 | } 327 | 328 | CLoggingContext loggingContext(name); 329 | 330 | if (path) 331 | { 332 | if (isMaFile) 333 | { 334 | if (!ImportMaFile(path, username, sharedSecret)) 335 | return false; 336 | } 337 | else 338 | { 339 | if (!Load(path, encryptPass)) 340 | return false; 341 | } 342 | } 343 | 344 | char accessToken[Steam::Auth::jwtBufSz] = ""; 345 | 346 | bool loginRequired = true; 347 | 348 | // commented out because oauth seems to be gone 349 | //if (oauthToken[0]) 350 | //{ 351 | // const int refreshRes = Steam::Auth::RefreshOAuthSession(curl, oauthToken, loginToken); 352 | 353 | // if (refreshRes < 0) 354 | // return false; 355 | // else if (refreshRes == 0) 356 | // Log(LogChannel::GENERAL, "[%s] Steam OAuth token is invalid or has expired, login required\n", name); 357 | // else 358 | // loginRequired = false; 359 | //} 360 | 361 | if (refreshToken[0]) 362 | { 363 | if (!DidJWTExpire(refreshToken) && 364 | Steam::SetRefreshCookie(curl, steamId64, refreshToken) && 365 | Steam::Auth::RefreshJWTSession(curl, accessToken) && 366 | Steam::SetLoginCookie(curl, steamId64, accessToken)) 367 | loginRequired = false; 368 | else 369 | Log(LogChannel::GENERAL, "Steam refresh token is invalid or has expired, login required\n"); 370 | } 371 | 372 | bool loggedIn = !loginRequired; 373 | 374 | if (loginRequired) 375 | { 376 | char password[Steam::Auth::passwordBufSz]; 377 | 378 | if ((username[0] || GetUserInputString("Enter Steam username", username, sizeof(username))) && 379 | GetUserInputString("Enter Steam password", password, sizeof(password), 8, false)) 380 | { 381 | char clientId[Steam::Auth::clientIdBufSz]; 382 | char requestId[Steam::Auth::requestIdBufSz]; 383 | 384 | if (Steam::Auth::BeginAuthSessionViaCredentials(curl, username, password, steamId64, clientId, requestId)) 385 | { 386 | for (size_t i = 0; i < 3; ++i) 387 | { 388 | char twoFactorCode[Steam::Guard::twoFactorCodeBufSz] = ""; 389 | 390 | if (sharedSecret[0]) 391 | Steam::Guard::GenerateTwoFactorAuthCode(sharedSecret, twoFactorCode); 392 | 393 | if (!twoFactorCode[0]) 394 | { 395 | if (!GetUserInputString("Enter Steam Guard Mobile Authenticator code", 396 | twoFactorCode, sizeof(twoFactorCode), Steam::Guard::twoFactorCodeBufSz - 1)) 397 | break; 398 | } 399 | 400 | if (!Steam::Auth::UpdateAuthSessionWithSteamGuardCode(curl, steamId64, clientId, twoFactorCode)) 401 | break; 402 | 403 | if (Steam::Auth::PollAuthSessionStatus(curl, clientId, requestId, refreshToken, accessToken)) 404 | { 405 | loggedIn = true; 406 | break; 407 | } 408 | 409 | std::this_thread::sleep_for(5s); 410 | } 411 | 412 | if (loggedIn) 413 | { 414 | if (!Steam::SetRefreshCookie(curl, steamId64, refreshToken) || 415 | !Steam::SetLoginCookie(curl, steamId64, accessToken)) 416 | { 417 | memset(refreshToken, 0, sizeof(refreshToken)); 418 | memset(accessToken, 0, sizeof(accessToken)); 419 | 420 | loggedIn = false; 421 | } 422 | } 423 | } 424 | } 425 | 426 | memset(password, 0, sizeof(password)); 427 | } 428 | 429 | memset(username, 0, sizeof(username)); 430 | memset(sharedSecret, 0, sizeof(sharedSecret)); 431 | 432 | if (!loggedIn) 433 | return false; 434 | 435 | if (!deviceId[0] && !Steam::Guard::GetDeviceId(curl, steamId64, accessToken, deviceId)) 436 | return false; 437 | 438 | memset(accessToken, 0, sizeof(accessToken)); 439 | 440 | if (!steamApiKey[0] && !Steam::GetApiKey(curl, sessionId, steamApiKey)) 441 | return false; 442 | 443 | if (!identitySecret[0] && !EnterIdentitySecret(curl)) 444 | return false; 445 | 446 | if (!marketApiKey[0] && !EnterMarketApiKey(curl)) 447 | return false; 448 | 449 | if (loginRequired || isMaFile) 450 | { 451 | if (Save(encryptPass)) 452 | { 453 | if (isMaFile) 454 | std::filesystem::remove(path); 455 | } 456 | } 457 | 458 | memset(refreshToken, 0, sizeof(refreshToken)); 459 | 460 | if (!Steam::SetInventoryPublic(curl, sessionId, steamId64)) 461 | return false; 462 | 463 | if (!Steam::AcknowledgeTradeProtection(curl, sessionId)) 464 | return false; 465 | 466 | if (!Market::SetSteamDetails(curl, marketApiKey, steamApiKey)) 467 | return false; 468 | 469 | if (!Market::CanSell(curl, marketApiKey)) 470 | return false; 471 | 472 | return true; 473 | } 474 | 475 | private: 476 | // remove inactive and cancel expired 477 | bool CancelExpiredSentOffers(CURL* curl, const char* sessionId) 478 | { 479 | bool allEmpty = true; 480 | 481 | for (const auto& marketSentOffers : sentOffers) 482 | { 483 | if (!marketSentOffers.empty()) 484 | { 485 | allEmpty = false; 486 | break; 487 | } 488 | } 489 | 490 | if (allEmpty) 491 | return true; 492 | 493 | const time_t timestamp = time(nullptr); 494 | 495 | rapidjson::Document docOffers; 496 | // include inactive offers accepted within 5 mins ago so they are kept in sentOffers 497 | if (!Steam::Trade::GetOffers(curl, steamApiKey, 498 | true, false, false, true, false, nullptr, timestamp - (5 * 60), 0, &docOffers)) 499 | return false; 500 | 501 | const rapidjson::Value& offersResp = docOffers["response"]; 502 | 503 | const auto iterSentOffers = offersResp.FindMember("trade_offers_sent"); 504 | 505 | if (iterSentOffers == offersResp.MemberEnd()) 506 | { 507 | for (auto& marketSentOffers : sentOffers) 508 | marketSentOffers.clear(); 509 | 510 | return true; 511 | } 512 | 513 | const rapidjson::Value& steamSentOffers = iterSentOffers->value; 514 | const rapidjson::SizeType steamSentOffersCount = steamSentOffers.Size(); 515 | 516 | if (!steamSentOffersCount) 517 | { 518 | for (auto& marketSentOffers : sentOffers) 519 | marketSentOffers.clear(); 520 | 521 | return true; 522 | } 523 | 524 | bool allOk = true; 525 | 526 | for (auto& marketSentOffers : sentOffers) 527 | { 528 | for (auto iterSentOffer = marketSentOffers.begin(); iterSentOffer != marketSentOffers.end(); ) 529 | { 530 | bool erase = true; 531 | 532 | const char* sentOfferId = iterSentOffer->tradeOfferId; 533 | 534 | for (rapidjson::SizeType i = 0; i < steamSentOffersCount; ++i) 535 | { 536 | const rapidjson::Value& offer = steamSentOffers[i]; 537 | 538 | const char* offerId = offer["tradeofferid"].GetString(); 539 | 540 | if (strcmp(sentOfferId, offerId)) 541 | continue; 542 | 543 | const time_t timeUpdated = offer["time_updated"].GetInt64(); 544 | const time_t timeSinceUpdate = timestamp - timeUpdated; 545 | 546 | if (Market::offerTTL < timeSinceUpdate) 547 | { 548 | if (!Steam::Trade::Cancel(curl, sessionId, sentOfferId)) 549 | { 550 | erase = false; 551 | allOk = false; 552 | } 553 | } 554 | else 555 | erase = false; 556 | 557 | break; 558 | } 559 | 560 | if (erase) 561 | iterSentOffer = marketSentOffers.erase(iterSentOffer); 562 | else 563 | ++iterSentOffer; 564 | } 565 | } 566 | 567 | return allOk; 568 | } 569 | 570 | enum class MarketStatus 571 | { 572 | SOLD = (1 << 0), 573 | BOUGHT = (1 << 1) 574 | }; 575 | 576 | int GetMarketStatus(CURL* curl, int market, rapidjson::Document* outDocItems) 577 | { 578 | if (!Market::GetItems(curl, marketApiKey, market, outDocItems)) 579 | { 580 | Log(LogChannel::GENERAL, "[%s] Getting items status failed\n", Market::marketNames[market]); 581 | return -1; 582 | } 583 | 584 | auto& marketGivenItemIds = givenItemIds[market]; 585 | auto& marketTakenItemIds = takenItemIds[market]; 586 | 587 | int marketStatus = 0; 588 | 589 | const rapidjson::Value& items = (*outDocItems)["items"]; 590 | const rapidjson::SizeType itemCount = (items.IsArray() ? items.Size() : 0); 591 | 592 | for (rapidjson::SizeType i = 0; i < itemCount; ++i) 593 | { 594 | const rapidjson::Value& item = items[i]; 595 | 596 | // status is a char, convert it to int 597 | const int itemStatus = (item["status"].GetString()[0] - '0'); 598 | 599 | if (itemStatus == (int)Market::ItemStatus::GIVE) 600 | { 601 | // poor mans 'trading protection' check 602 | const int left = item["left"].GetInt(); 603 | if (left < 1) 604 | continue; 605 | 606 | const char* itemId = item["item_id"].GetString(); 607 | 608 | bool given = false; 609 | 610 | for (const auto& givenItemId : marketGivenItemIds) 611 | { 612 | if (!strcmp(itemId, givenItemId.c_str())) 613 | { 614 | given = true; 615 | break; 616 | } 617 | } 618 | 619 | if (!given) 620 | { 621 | marketGivenItemIds.emplace_back(itemId); 622 | 623 | const char* itemName = item["market_hash_name"].GetString(); 624 | Log(LogChannel::GENERAL, "[%s] Sold \"%s\"\n", Market::marketNames[market], itemName); 625 | } 626 | 627 | marketStatus |= (int)MarketStatus::SOLD; 628 | } 629 | else if (itemStatus == (int)Market::ItemStatus::TAKE) 630 | { 631 | // poor mans 'trading protection' check 632 | const int left = item["left"].GetInt(); 633 | if (left < 1) 634 | continue; 635 | 636 | const char* itemId = item["item_id"].GetString(); 637 | 638 | bool taken = false; 639 | 640 | for (const auto& takenItemId : marketTakenItemIds) 641 | { 642 | if (!strcmp(itemId, takenItemId.c_str())) 643 | { 644 | taken = true; 645 | break; 646 | } 647 | } 648 | 649 | if (!taken) 650 | { 651 | marketTakenItemIds.emplace_back(itemId); 652 | 653 | const char* itemName = item["market_hash_name"].GetString(); 654 | Log(LogChannel::GENERAL, "[%s] Bought \"%s\"\n", Market::marketNames[market], itemName); 655 | } 656 | 657 | marketStatus |= (int)MarketStatus::BOUGHT; 658 | } 659 | } 660 | 661 | if (!(marketStatus & (int)MarketStatus::SOLD)) 662 | marketGivenItemIds.clear(); 663 | 664 | if (!(marketStatus & (int)MarketStatus::BOUGHT)) 665 | marketTakenItemIds.clear(); 666 | 667 | return marketStatus; 668 | } 669 | 670 | bool GiveItemBot(CURL* curl, const char* sessionId, int market) 671 | { 672 | char offerId[Steam::Trade::offerIdBufSz]; 673 | char partnerId64[UINT64_MAX_STR_SIZE]; 674 | 675 | if (!Market::RequestGiveBot(curl, marketApiKey, market, offerId, partnerId64)) 676 | return false; 677 | 678 | for (const auto& givenOfferId : givenOfferIds[market]) 679 | { 680 | if (!strcmp(offerId, givenOfferId.c_str())) 681 | return true; 682 | } 683 | 684 | if (!Steam::Trade::Accept(curl, sessionId, offerId, partnerId64)) 685 | return false; 686 | 687 | if (!Steam::Guard::AcceptConfirmation(curl, steamId64, identitySecret, deviceId, offerId)) 688 | return false; 689 | 690 | givenOfferIds[market].emplace_back(offerId); 691 | 692 | return true; 693 | } 694 | 695 | bool GiveItemsP2P(CURL* curl, const char* sessionId, int market) 696 | { 697 | rapidjson::Document docGiveDetails; 698 | 699 | if (!Market::RequestGiveP2PAll(curl, marketApiKey, market, &docGiveDetails)) 700 | return false; 701 | 702 | const rapidjson::Value& offers = docGiveDetails["offers"]; 703 | const rapidjson::SizeType offerCount = offers.Size(); 704 | 705 | bool allOk = true; 706 | 707 | for (rapidjson::SizeType i = 0; i < offerCount; ++i) 708 | { 709 | const rapidjson::Value& offer = offers[i]; 710 | 711 | const char* offerHash = offer["hash"].GetString(); 712 | 713 | // check if we haven't sent this offer yet 714 | bool found = false; 715 | 716 | for (const auto& sentOffer : sentOffers[market]) 717 | { 718 | if (!strcmp(offerHash, sentOffer.marketHash)) 719 | { 720 | found = true; 721 | break; 722 | } 723 | } 724 | 725 | if (found) 726 | continue; 727 | 728 | rapidjson::StringBuffer itemsStrBuf; 729 | rapidjson::Writer itemsWriter(itemsStrBuf); 730 | 731 | if (!offer["items"].Accept(itemsWriter)) 732 | { 733 | allOk = false; 734 | Log(LogChannel::GENERAL, 735 | "[%s] Converting offer items JSON to string failed\n", Market::marketNames[market]); 736 | continue; 737 | } 738 | 739 | char sentOfferId[Steam::Trade::offerIdBufSz]; 740 | 741 | if (!Steam::Trade::Send(curl, 742 | sessionId, 743 | offer["partner"].GetUint(), 744 | offer["token"].GetString(), 745 | offer["tradeoffermessage"].GetString(), 746 | itemsStrBuf.GetString(), 747 | sentOfferId)) 748 | { 749 | allOk = false; 750 | continue; 751 | } 752 | 753 | if (!Steam::Guard::AcceptConfirmation(curl, steamId64, identitySecret, deviceId, sentOfferId)) 754 | { 755 | allOk = false; 756 | continue; 757 | } 758 | 759 | sentOffers[market].emplace_back(offerHash, sentOfferId); 760 | 761 | if (!Market::TradeReady(curl, marketApiKey, market, sentOfferId)) 762 | { 763 | allOk = false; 764 | continue; 765 | } 766 | } 767 | 768 | return allOk; 769 | } 770 | 771 | bool TakeItem(CURL* curl, const char* sessionId, int market, const char* partnerId32 = nullptr) 772 | { 773 | char offerId[Steam::Trade::offerIdBufSz]; 774 | 775 | if (!Market::RequestTake(curl, marketApiKey, market, partnerId32, offerId)) 776 | return false; 777 | 778 | for (const auto& takenOfferId : takenOfferIds[market]) 779 | { 780 | if (!strcmp(offerId, takenOfferId.c_str())) 781 | return true; 782 | } 783 | 784 | const uint32_t nPartnerId32 = atol(partnerId32); 785 | 786 | const std::string partnerId64(std::to_string(Steam::SteamID32To64(nPartnerId32))); 787 | 788 | if (!Steam::Trade::Accept(curl, sessionId, offerId, partnerId64.c_str())) 789 | return false; 790 | 791 | takenOfferIds[market].emplace_back(offerId); 792 | 793 | return true; 794 | } 795 | 796 | bool TakeItems(CURL* curl, const char* sessionId, int market, rapidjson::Document* docItems) 797 | { 798 | const rapidjson::Value& items = (*docItems)["items"]; 799 | if (!items.IsArray()) 800 | return true; 801 | 802 | std::unordered_set partnerIds32; 803 | 804 | for (const auto& item : items.GetArray()) 805 | { 806 | const int itemStatus = (item["status"].GetString()[0] - '0'); 807 | 808 | if (itemStatus == (int)Market::ItemStatus::TAKE) 809 | { 810 | // poor mans 'trading protection' check 811 | const int left = item["left"].GetInt(); 812 | if (left < 1) 813 | continue; 814 | 815 | const auto iterBotId = item.FindMember("botid"); 816 | if (iterBotId != item.MemberEnd()) 817 | partnerIds32.insert(iterBotId->value.GetString()); 818 | } 819 | } 820 | 821 | bool allOk = true; 822 | 823 | for (const auto& partnerId32 : partnerIds32) 824 | { 825 | if (!TakeItem(curl, sessionId, market, partnerId32.c_str())) 826 | allOk = false; 827 | } 828 | 829 | return allOk; 830 | } 831 | 832 | void PrintListings(const rapidjson::SizeType* itemCounts) 833 | { 834 | Log(LogChannel::GENERAL, "Listings: "); 835 | 836 | for (int i = 0; i < (int)Market::Market::COUNT; ++i) 837 | { 838 | printf("%s: %u", Market::marketNames[i], itemCounts[i]); 839 | 840 | if (i < ((int)Market::Market::COUNT - 1)) 841 | putsnn(" | "); 842 | } 843 | 844 | putchar('\n'); 845 | } 846 | 847 | public: 848 | bool RunMarkets(CURL* curl, const char* sessionId, const char* proxy) 849 | { 850 | CLoggingContext loggingContext(name); 851 | 852 | // commented out because oauth seems to be gone 853 | //const int refreshRes = Steam::Auth::RefreshOAuthSession(curl, oauthToken, loginToken); 854 | //if (refreshRes < 0) 855 | //{ 856 | // Log(LogChannel::GENERAL, "[%s] Steam session refresh failed\n", name); 857 | // return false; 858 | //} 859 | 860 | //if (refreshRes == 0) 861 | //{ 862 | // Log(LogChannel::GENERAL, "[%s] Steam OAuth token has expired, restart required\n", name); 863 | // return false; 864 | //} 865 | 866 | char accessToken[Steam::Auth::jwtBufSz]; 867 | 868 | if (!Steam::Auth::RefreshJWTSession(curl, accessToken)) 869 | { 870 | Log(LogChannel::GENERAL, "Steam session refresh failed\n"); 871 | return false; 872 | } 873 | 874 | if (!Steam::SetLoginCookie(curl, steamId64, accessToken)) 875 | { 876 | Log(LogChannel::GENERAL, "Setting Steam login cookie failed\n"); 877 | return false; 878 | } 879 | 880 | bool allOk = true; 881 | 882 | if (!Market::PingNew(curl, marketApiKey, accessToken, proxy)) 883 | allOk = false; 884 | 885 | memset(accessToken, 0, sizeof(accessToken)); 886 | 887 | if (!CancelExpiredSentOffers(curl, sessionId)) 888 | { 889 | allOk = false; 890 | Log(LogChannel::GENERAL, "Cancelling some of the expired sent offers failed, " 891 | "manually cancel the sent offers older than 15 mins if the error persists\n"); 892 | } 893 | 894 | rapidjson::SizeType itemCounts[(int)Market::Market::COUNT] = { 0 }; 895 | 896 | for (int marketIter = 0; marketIter < (int)Market::Market::COUNT; ++marketIter) 897 | { 898 | rapidjson::Document docItems; 899 | const int marketStatus = GetMarketStatus(curl, marketIter, &docItems); 900 | 901 | if (marketStatus < 0) 902 | { 903 | allOk = false; 904 | continue; 905 | } 906 | 907 | const rapidjson::Value& items = docItems["items"]; 908 | itemCounts[marketIter] = (items.IsArray() ? items.Size() : 0); 909 | 910 | if (!marketStatus) 911 | continue; 912 | 913 | #ifdef _WIN32 914 | FlashCurrentWindow(); 915 | #endif // _WIN32 916 | 917 | if (marketStatus & (int)MarketStatus::SOLD) 918 | { 919 | // commented out because all markets are p2p now 920 | //if (Market::isMarketP2P[i]) 921 | //{ 922 | if (!GiveItemsP2P(curl, sessionId, marketIter)) 923 | allOk = false; 924 | //} 925 | //else 926 | //{ 927 | //if (!GiveItemBot(curl, sessionId, i)) 928 | // allOk = false; 929 | //} 930 | } 931 | else 932 | { 933 | //if (!Market::isMarketP2P[i]) 934 | // givenOfferIds[i].clear(); 935 | } 936 | 937 | if (marketStatus & (int)MarketStatus::BOUGHT) 938 | { 939 | if (!TakeItems(curl, sessionId, marketIter, &docItems)) 940 | allOk = false; 941 | } 942 | else 943 | takenOfferIds[marketIter].clear(); 944 | } 945 | 946 | PrintListings(itemCounts); 947 | 948 | return allOk; 949 | } 950 | }; -------------------------------------------------------------------------------- /src/Steam/Auth.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Steam 4 | { 5 | namespace Auth 6 | { 7 | const size_t usernameBufSz = 63 + 1; 8 | const size_t passwordBufSz = 63 + 1; 9 | 10 | const size_t modulusSz = 256; // 2048 bit RSA 11 | const size_t exponentSz = 3; 12 | const size_t timestampBufSz = UINT64_MAX_STR_SIZE; 13 | 14 | const size_t oauthTokenBufSz = 32 + 1; 15 | const size_t loginTokenBufSz = 40 + 1; 16 | 17 | const size_t clientIdBufSz = UINT64_MAX_STR_SIZE; 18 | const size_t requestIdBufSz = 24 + 1; 19 | 20 | enum class LoginResult 21 | { 22 | SUCCESS, 23 | GET_PASS_ENCRYPT_KEY_FAILED, 24 | PASS_ENCRYPT_FAILED, 25 | CAPTCHA_FAILED, 26 | REQUEST_FAILED, 27 | WRONG_CAPTCHA, 28 | WRONG_TWO_FACTOR, 29 | UNSUCCEDED, 30 | OAUTH_FAILED, 31 | }; 32 | 33 | // unused outdated oauth login start 34 | 35 | // outHexModulus buffer size must be at least modulusSz * 2 36 | // outHexExponent buffer size must be at least exponentSz * 2 37 | // outTimestamp buffer size must be at least timestampBufSz 38 | bool GetPasswordRSAPublicKey(CURL* curl, const char* escUsername, 39 | byte* outHexModulus, byte* outHexExponent, char* outTimestamp) 40 | { 41 | Log(LogChannel::STEAM, "Getting password RSA public key..."); 42 | 43 | const char postFieldUsername[] = "username="; 44 | 45 | // multiple by 3 due to URL encoding 46 | const size_t postFieldsBufSz = sizeof(postFieldUsername) - 1 + (usernameBufSz - 1) * 3 + 1; 47 | char postFields[postFieldsBufSz]; 48 | 49 | char* postFieldsEnd = postFields; 50 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldUsername); 51 | strcpy(postFieldsEnd, escUsername); 52 | 53 | Curl::CResponse response; 54 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 55 | curl_easy_setopt(curl, CURLOPT_URL, "https://steamcommunity.com/login/getrsakey/"); 56 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 57 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 58 | 59 | const CURLcode respCode = curl_easy_perform(curl); 60 | 61 | if (respCode != CURLE_OK) 62 | { 63 | Curl::PrintError(curl, respCode); 64 | return false; 65 | } 66 | 67 | rapidjson::Document parsed; 68 | parsed.ParseInsitu(response.data); 69 | 70 | if (parsed.HasParseError()) 71 | { 72 | putsnn("JSON parsing failed\n"); 73 | return false; 74 | } 75 | 76 | if (!parsed["success"].GetBool()) 77 | { 78 | putsnn("request unsucceeded\n"); 79 | return false; 80 | } 81 | 82 | memcpy(outHexModulus, parsed["publickey_mod"].GetString(), modulusSz * 2); 83 | memcpy(outHexExponent, parsed["publickey_exp"].GetString(), exponentSz * 2); 84 | strcpy(outTimestamp, parsed["timestamp"].GetString()); 85 | 86 | putsnn("ok\n"); 87 | return true; 88 | } 89 | 90 | // out buffer size must be at least PlainToBase64Size(sizeof(modulus), WC_NO_NL_ENC) 91 | bool EncryptPassword(const char* password, const byte* modulus, const byte* exponent, byte* out, word32* outSz) 92 | { 93 | Log(LogChannel::STEAM, "Encrypting password..."); 94 | 95 | RsaKey pubKey; 96 | if (wc_InitRsaKey(&pubKey, nullptr)) 97 | { 98 | putsnn("RSA init failed\n"); 99 | return false; 100 | } 101 | 102 | if (wc_RsaPublicKeyDecodeRaw(modulus, modulusSz, exponent, exponentSz, &pubKey)) 103 | { 104 | wc_FreeRsaKey(&pubKey); 105 | putsnn("RSA public key decoding failed\n"); 106 | return false; 107 | } 108 | 109 | WC_RNG rng; 110 | 111 | if (wc_InitRng(&rng)) 112 | { 113 | wc_FreeRsaKey(&pubKey); 114 | putsnn("RNG init failed\n"); 115 | return false; 116 | } 117 | 118 | byte encrypted[modulusSz]; 119 | 120 | const int encryptedSz = wc_RsaPublicEncrypt((byte*)password, strlen(password), 121 | encrypted, sizeof(encrypted), &pubKey, &rng); 122 | 123 | const bool success = ((0 <= encryptedSz) && !Base64_Encode_NoNl(encrypted, encryptedSz, out, outSz)); 124 | 125 | wc_FreeRsaKey(&pubKey); 126 | wc_FreeRng(&rng); 127 | 128 | putsnn(success ? "ok\n" : "fail\n"); 129 | return success; 130 | } 131 | 132 | // outSteamId64 buffer size must be at least UINT64_MAX_STR_SIZE 133 | // outOAuthToken buffer size must be at least oauthTokenBufSz 134 | // outLoginToken buffer size must be at least loginTokenBufSz 135 | LoginResult DoLogin(CURL* curl, const char* username, const char* password, 136 | const char* twoFactorCode, char* outSteamId64, char* outOAuthToken, char* outLoginToken) 137 | { 138 | char* escUsername = curl_easy_escape(curl, username, 0); 139 | 140 | byte rsaHexModulus[modulusSz * 2]; 141 | byte rsaHexExponent[exponentSz * 2]; 142 | char rsaTimestamp[timestampBufSz]; 143 | 144 | if (!GetPasswordRSAPublicKey(curl, escUsername, rsaHexModulus, rsaHexExponent, rsaTimestamp)) 145 | { 146 | curl_free(escUsername); 147 | return LoginResult::GET_PASS_ENCRYPT_KEY_FAILED; 148 | } 149 | 150 | Log(LogChannel::STEAM, "Decoding password RSA public key..."); 151 | 152 | byte rsaModulus[modulusSz]; 153 | word32 rsaModSz = sizeof(rsaModulus); 154 | 155 | byte rsaExponent[exponentSz]; 156 | word32 rsaExpSz = sizeof(rsaExponent); 157 | 158 | if (Base16_Decode(rsaHexModulus, sizeof(rsaHexModulus), rsaModulus, &rsaModSz) || 159 | Base16_Decode(rsaHexExponent, sizeof(rsaHexExponent), rsaExponent, &rsaExpSz)) 160 | { 161 | curl_free(escUsername); 162 | putsnn("fail\n"); 163 | return LoginResult::GET_PASS_ENCRYPT_KEY_FAILED; 164 | } 165 | 166 | putsnn("ok\n"); 167 | 168 | constexpr size_t encryptedPassBufSz = PlainToBase64Size(sizeof(rsaModulus), WC_NO_NL_ENC); 169 | byte encryptedPass[encryptedPassBufSz]; 170 | word32 encryptedPassSz = sizeof(encryptedPass); 171 | 172 | if (!EncryptPassword(password, rsaModulus, rsaExponent, encryptedPass, &encryptedPassSz)) 173 | { 174 | curl_free(escUsername); 175 | return LoginResult::PASS_ENCRYPT_FAILED; 176 | } 177 | 178 | char captchaAnswer[Captcha::answerBufSz] = ""; 179 | char captchaGid[Captcha::gidBufSz]; 180 | 181 | if (!Captcha::GetGID(curl, captchaGid) || 182 | (strcmp(captchaGid, "-1") && !Captcha::GetAnswer(curl, captchaGid, captchaAnswer))) 183 | { 184 | curl_free(escUsername); 185 | return LoginResult::CAPTCHA_FAILED; 186 | } 187 | 188 | Log(LogChannel::STEAM, "Logging in..."); 189 | 190 | char* escEncryptedPass = curl_easy_escape(curl, (char*)encryptedPass, encryptedPassSz); 191 | char* escCaptchaAnswer = curl_easy_escape(curl, captchaAnswer, 0); 192 | 193 | const char postFieldUsername[] = 194 | "oauth_client_id=DE45CD61" 195 | "&oauth_scope=read_profile%20write_profile%20read_client%20write_client" 196 | "&remember_login=true" 197 | "&username="; 198 | 199 | const char postFieldPassword[] = "&password="; 200 | const char postFieldRsaTimestamp[] = "&rsatimestamp="; 201 | const char postField2FACode[] = "&twofactorcode="; 202 | const char postFieldCaptchaGid[] = "&captchagid="; 203 | const char postFieldCaptchaAnswer[] = "&captcha_text="; 204 | 205 | const size_t postFieldsBufSz = 206 | sizeof(postFieldUsername) - 1 + (usernameBufSz - 1) * 3 + // multiple by 3 due to URL encoding 207 | sizeof(postFieldPassword) - 1 + encryptedPassBufSz * 3 + 208 | sizeof(postFieldRsaTimestamp) - 1 + timestampBufSz - 1 + 209 | sizeof(postField2FACode) - 1 + Guard::twoFactorCodeBufSz - 1 + 210 | sizeof(postFieldCaptchaGid) - 1 + Captcha::gidBufSz - 1 + 211 | sizeof(postFieldCaptchaAnswer) - 1 + (Captcha::answerBufSz - 1) * 3 + 1; 212 | 213 | char postFields[postFieldsBufSz]; 214 | 215 | char* postFieldsEnd = postFields; 216 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldUsername); 217 | postFieldsEnd = stpcpy(postFieldsEnd, escUsername); 218 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldPassword); 219 | postFieldsEnd = stpcpy(postFieldsEnd, escEncryptedPass); 220 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldRsaTimestamp); 221 | postFieldsEnd = stpcpy(postFieldsEnd, rsaTimestamp); 222 | postFieldsEnd = stpcpy(postFieldsEnd, postField2FACode); 223 | postFieldsEnd = stpcpy(postFieldsEnd, twoFactorCode); 224 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldCaptchaGid); 225 | postFieldsEnd = stpcpy(postFieldsEnd, captchaGid); 226 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldCaptchaAnswer); 227 | strcpy(postFieldsEnd, escCaptchaAnswer); 228 | 229 | curl_free(escUsername); 230 | curl_free(escEncryptedPass); 231 | curl_free(escCaptchaAnswer); 232 | 233 | Curl::CResponse response; 234 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 235 | curl_easy_setopt(curl, CURLOPT_URL, "https://steamcommunity.com/login/dologin/"); 236 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 237 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 238 | curl_easy_setopt(curl, CURLOPT_COOKIE, "mobileClient=android"); 239 | 240 | const CURLcode respCode = curl_easy_perform(curl); 241 | 242 | curl_easy_setopt(curl, CURLOPT_COOKIE, NULL); 243 | 244 | if (respCode != CURLE_OK) 245 | { 246 | Curl::PrintError(curl, respCode); 247 | return LoginResult::REQUEST_FAILED; 248 | } 249 | 250 | rapidjson::Document parsed; 251 | parsed.ParseInsitu(response.data); 252 | 253 | if (parsed.HasParseError()) 254 | { 255 | putsnn("JSON parsing failed\n"); 256 | return LoginResult::REQUEST_FAILED; 257 | } 258 | 259 | if (!parsed["success"].GetBool()) 260 | { 261 | const auto iterRequires2FA = parsed.FindMember("requires_twofactor"); 262 | if (iterRequires2FA != parsed.MemberEnd() && iterRequires2FA->value.GetBool()) 263 | { 264 | putsnn("wrong two factor code\n"); 265 | return LoginResult::WRONG_TWO_FACTOR; 266 | } 267 | 268 | const auto iterCaptchaNeeded = parsed.FindMember("captcha_needed"); 269 | if (iterCaptchaNeeded != parsed.MemberEnd() && iterCaptchaNeeded->value.GetBool()) 270 | { 271 | putsnn("wrong captcha answer\n"); 272 | return LoginResult::WRONG_CAPTCHA; 273 | } 274 | 275 | const auto iterMessage = parsed.FindMember("message"); 276 | if (iterMessage != parsed.MemberEnd()) 277 | { 278 | const char* msg = iterMessage->value.GetString(); 279 | if (msg[0]) 280 | { 281 | puts(msg); // we need newline 282 | return LoginResult::UNSUCCEDED; 283 | } 284 | } 285 | 286 | putsnn("request unsucceeded\n"); 287 | return LoginResult::UNSUCCEDED; 288 | } 289 | 290 | const auto iterOAuth = parsed.FindMember("oauth"); 291 | if (iterOAuth == parsed.MemberEnd()) 292 | { 293 | putsnn("OAuth not found\n"); 294 | return LoginResult::OAUTH_FAILED; 295 | } 296 | 297 | rapidjson::Document parsedOAuth; 298 | parsedOAuth.Parse(iterOAuth->value.GetString()); 299 | 300 | if (parsedOAuth.HasParseError()) 301 | { 302 | putsnn("JSON parsing failed\n"); 303 | return LoginResult::OAUTH_FAILED; 304 | } 305 | 306 | const char* steamId64 = parsedOAuth["steamid"].GetString(); 307 | const char* oauthToken = parsedOAuth["oauth_token"].GetString(); 308 | const char* loginToken = parsedOAuth["wgtoken_secure"].GetString(); 309 | 310 | strcpy(outSteamId64, steamId64); 311 | strcpy(outOAuthToken, oauthToken); 312 | strcpy(outLoginToken, loginToken); 313 | 314 | putsnn("ok\n"); 315 | return LoginResult::SUCCESS; 316 | } 317 | 318 | // outLoginToken buffer size must be at least loginTokenBufSz 319 | int RefreshOAuthSession(CURL* curl, const char* oauthToken, char* outLoginToken) 320 | { 321 | const char postFieldAccessToken[] = "access_token="; 322 | 323 | const size_t postFieldsBufSz = sizeof(postFieldAccessToken) - 1 + oauthTokenBufSz - 1 + 1; 324 | char postFields[postFieldsBufSz]; 325 | 326 | char* postFieldsEnd = postFields; 327 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldAccessToken); 328 | strcpy(postFieldsEnd, oauthToken); 329 | 330 | Curl::CResponse response; 331 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 332 | curl_easy_setopt(curl, CURLOPT_URL, "https://api.steampowered.com/IMobileAuthService/GetWGToken/v1/"); 333 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 334 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 335 | 336 | const CURLcode respCode = curl_easy_perform(curl); 337 | 338 | if (respCode != CURLE_OK) 339 | { 340 | if (respCode == CURLE_HTTP_RETURNED_ERROR) 341 | { 342 | long httpCode; 343 | curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); 344 | if (httpCode == 401) // unauthorized 345 | { 346 | Log(LogChannel::STEAM, "Refreshing OAuth session failed: OAuth token is invalid or has expired\n"); 347 | return 0; 348 | } 349 | } 350 | 351 | Log(LogChannel::STEAM, "Refreshing OAuth session failed: "); 352 | Curl::PrintError(curl, respCode); 353 | return -1; 354 | } 355 | 356 | rapidjson::Document parsed; 357 | parsed.ParseInsitu(response.data); 358 | 359 | if (parsed.HasParseError()) 360 | { 361 | Log(LogChannel::STEAM, "Refreshing OAuth session failed: JSON parsing failed\n"); 362 | return -1; 363 | } 364 | 365 | const auto iterResponse = parsed.FindMember("response"); 366 | if (iterResponse == parsed.MemberEnd() || iterResponse->value.ObjectEmpty()) 367 | { 368 | Log(LogChannel::STEAM, "Refreshing OAuth session failed: request unsucceeded\n"); 369 | return -1; 370 | } 371 | 372 | const char* loginToken = iterResponse->value["token_secure"].GetString(); 373 | 374 | strcpy(outLoginToken, loginToken); 375 | 376 | return 1; 377 | } 378 | 379 | 380 | 381 | // outHexModulus buffer size must be at least modulusSz * 2 382 | // outHexExponent buffer size must be at least exponentSz * 2 383 | // outTimestamp buffer size must be at least timestampBufSz 384 | bool GetPasswordRSAPublicKeyJWT(CURL* curl, const char* escUsername, 385 | byte* outHexModulus, byte* outHexExponent, char* outTimestamp) 386 | { 387 | Log(LogChannel::STEAM, "Getting password RSA public key..."); 388 | 389 | const char urlStart[] = 390 | "https://api.steampowered.com/IAuthenticationService/GetPasswordRSAPublicKey/v1/?account_name="; 391 | 392 | // multiple by 3 due to URL encoding 393 | const size_t urlBufSz = sizeof(urlStart) - 1 + (usernameBufSz - 1) * 3 + 1; 394 | char url[urlBufSz]; 395 | 396 | char* urlEnd = url; 397 | urlEnd = stpcpy(urlEnd, urlStart); 398 | strcpy(urlEnd, escUsername); 399 | 400 | Curl::CResponse response; 401 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 402 | curl_easy_setopt(curl, CURLOPT_URL, url); 403 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 404 | 405 | const CURLcode respCode = curl_easy_perform(curl); 406 | 407 | if (respCode != CURLE_OK) 408 | { 409 | Curl::PrintError(curl, respCode); 410 | return false; 411 | } 412 | 413 | rapidjson::Document parsed; 414 | parsed.ParseInsitu(response.data); 415 | 416 | if (parsed.HasParseError()) 417 | { 418 | putsnn("JSON parsing failed\n"); 419 | return false; 420 | } 421 | 422 | const auto iterResponse = parsed.FindMember("response"); 423 | if (iterResponse == parsed.MemberEnd() || iterResponse->value.ObjectEmpty()) 424 | { 425 | putsnn("request unsucceeded\n"); 426 | return false; 427 | } 428 | 429 | const char* mod = iterResponse->value["publickey_mod"].GetString(); 430 | const char* exp = iterResponse->value["publickey_exp"].GetString(); 431 | const char* timestamp = iterResponse->value["timestamp"].GetString(); 432 | 433 | memcpy(outHexModulus, mod, modulusSz * 2); 434 | memcpy(outHexExponent, exp, exponentSz * 2); 435 | strcpy(outTimestamp, timestamp); 436 | 437 | putsnn("ok\n"); 438 | return true; 439 | } 440 | 441 | // outSteamId64 buffer size must be at least UINT64_MAX_STR_SIZE 442 | // outClientId buffer size must be at least clientIdBufSz 443 | // outRequestId buffer size must be at least requestIdBufSz 444 | bool BeginAuthSessionViaCredentials(CURL* curl, const char* username, const char* password, 445 | char* outSteamId64, char* outClientId, char* outRequestId) 446 | { 447 | byte rsaHexModulus[modulusSz * 2]; 448 | byte rsaHexExponent[exponentSz * 2]; 449 | char rsaTimestamp[timestampBufSz]; 450 | 451 | char* escUsername = curl_easy_escape(curl, username, 0); 452 | 453 | if (!GetPasswordRSAPublicKeyJWT(curl, escUsername, rsaHexModulus, rsaHexExponent, rsaTimestamp)) 454 | { 455 | curl_free(escUsername); 456 | return false; 457 | } 458 | 459 | Log(LogChannel::STEAM, "Decoding password RSA public key..."); 460 | 461 | byte rsaModulus[modulusSz]; 462 | word32 rsaModSz = sizeof(rsaModulus); 463 | 464 | byte rsaExponent[exponentSz]; 465 | word32 rsaExpSz = sizeof(rsaExponent); 466 | 467 | if (Base16_Decode(rsaHexModulus, sizeof(rsaHexModulus), rsaModulus, &rsaModSz) || 468 | Base16_Decode(rsaHexExponent, sizeof(rsaHexExponent), rsaExponent, &rsaExpSz)) 469 | { 470 | curl_free(escUsername); 471 | putsnn("fail\n"); 472 | return false; 473 | } 474 | 475 | putsnn("ok\n"); 476 | 477 | constexpr size_t encryptedPassBufSz = PlainToBase64Size(sizeof(rsaModulus), WC_NO_NL_ENC); 478 | byte encryptedPass[encryptedPassBufSz]; 479 | word32 encryptedPassSz = sizeof(encryptedPass); 480 | 481 | if (!EncryptPassword(password, rsaModulus, rsaExponent, encryptedPass, &encryptedPassSz)) 482 | { 483 | curl_free(escUsername); 484 | return false; 485 | } 486 | 487 | Log(LogChannel::STEAM, "Beginning auth session..."); 488 | 489 | char* escEncryptedPass = curl_easy_escape(curl, (char*)encryptedPass, encryptedPassSz); 490 | 491 | const char postFieldAccountName[] = "persistence=1&account_name="; 492 | const char postFieldEncryptedPass[] = "&encrypted_password="; 493 | const char postFieldEncryptionTime[] = "&encryption_timestamp="; 494 | 495 | const size_t postFieldsBufSz = 496 | sizeof(postFieldAccountName) - 1 + (usernameBufSz - 1) * 3 + // multiple by 3 due to URL encoding 497 | sizeof(postFieldEncryptedPass) - 1 + encryptedPassBufSz * 3 + 498 | sizeof(postFieldEncryptionTime) - 1 + Guard::twoFactorCodeBufSz - 1 + 1; 499 | 500 | char postFields[postFieldsBufSz]; 501 | 502 | char* postFieldsEnd = postFields; 503 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldAccountName); 504 | postFieldsEnd = stpcpy(postFieldsEnd, escUsername); 505 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldEncryptedPass); 506 | postFieldsEnd = stpcpy(postFieldsEnd, escEncryptedPass); 507 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldEncryptionTime); 508 | strcpy(postFieldsEnd, rsaTimestamp); 509 | 510 | curl_free(escUsername); 511 | curl_free(escEncryptedPass); 512 | 513 | Curl::CResponse response; 514 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 515 | curl_easy_setopt(curl, CURLOPT_URL, 516 | "https://api.steampowered.com/IAuthenticationService/BeginAuthSessionViaCredentials/v1/"); 517 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 518 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 519 | 520 | const CURLcode respCode = curl_easy_perform(curl); 521 | 522 | if (respCode != CURLE_OK) 523 | { 524 | Curl::PrintError(curl, respCode); 525 | return false; 526 | } 527 | 528 | rapidjson::Document parsed; 529 | parsed.ParseInsitu(response.data); 530 | 531 | if (parsed.HasParseError()) 532 | { 533 | putsnn("JSON parsing failed\n"); 534 | return false; 535 | } 536 | 537 | const auto iterResponse = parsed.FindMember("response"); 538 | if (iterResponse == parsed.MemberEnd() || iterResponse->value.ObjectEmpty()) 539 | { 540 | putsnn("request unsucceeded\n"); 541 | return false; 542 | } 543 | 544 | const auto iterSteamId = iterResponse->value.FindMember("steamid"); 545 | if (iterSteamId == iterResponse->value.MemberEnd()) 546 | { 547 | putsnn("wrong credentials\n"); 548 | return false; 549 | } 550 | 551 | const char* clientId = iterResponse->value["client_id"].GetString(); 552 | const char* requestId = iterResponse->value["request_id"].GetString(); 553 | const char* steamId64 = iterSteamId->value.GetString(); 554 | 555 | strcpy(outClientId, clientId); 556 | strcpy(outRequestId, requestId); 557 | strcpy(outSteamId64, steamId64); 558 | 559 | putsnn("ok\n"); 560 | return true; 561 | } 562 | 563 | bool UpdateAuthSessionWithSteamGuardCode(CURL* curl, 564 | const char* steamId64, const char* clientId, const char* twoFactorCode) 565 | { 566 | Log(LogChannel::STEAM, "Updating auth session with a Steam Guard code..."); 567 | 568 | const char postFieldClientId[] = "client_id="; 569 | const char postFieldSteamId[] = "&steamid="; 570 | const char postFieldCode[] = "&code_type=3&code="; // code_type 3 is k_EAuthSessionGuardType_DeviceCode 571 | 572 | const size_t postFieldsBufSz = 573 | sizeof(postFieldClientId) - 1 + clientIdBufSz - 1 + // multiple by 3 due to URL encoding 574 | sizeof(postFieldSteamId) - 1 + UINT64_MAX_STR_SIZE - 1 + 575 | sizeof(postFieldCode) - 1 + Guard::twoFactorCodeBufSz - 1 + 1; 576 | 577 | char postFields[postFieldsBufSz]; 578 | 579 | char* postFieldsEnd = postFields; 580 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldClientId); 581 | postFieldsEnd = stpcpy(postFieldsEnd, clientId); 582 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldSteamId); 583 | postFieldsEnd = stpcpy(postFieldsEnd, steamId64); 584 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldCode); 585 | strcpy(postFieldsEnd, twoFactorCode); 586 | 587 | Curl::CResponse response; 588 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 589 | curl_easy_setopt(curl, CURLOPT_URL, 590 | "https://api.steampowered.com/IAuthenticationService/UpdateAuthSessionWithSteamGuardCode/v1/"); 591 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 592 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 593 | 594 | const CURLcode respCode = curl_easy_perform(curl); 595 | 596 | if (respCode != CURLE_OK) 597 | { 598 | Curl::PrintError(curl, respCode); 599 | return false; 600 | } 601 | 602 | putsnn("ok\n"); 603 | return true; 604 | } 605 | 606 | // outRefreshToken and outAccessToken buffer size must be at least jwtBufSz 607 | bool PollAuthSessionStatus(CURL* curl, const char* clientId, const char* requestId, 608 | char* outRefreshToken, char* outAccessToken) 609 | { 610 | Log(LogChannel::STEAM, "Polling auth session status..."); 611 | 612 | char* escRequestId = curl_easy_escape(curl, requestId, 0); 613 | 614 | const char postFieldClientId[] = "client_id="; 615 | const char postFieldRequestId[] = "&request_id="; 616 | 617 | const size_t postFieldsBufSz = 618 | sizeof(postFieldClientId) - 1 + clientIdBufSz - 1 + // multiple by 3 due to URL encoding 619 | sizeof(postFieldRequestId) - 1 + (requestIdBufSz - 1) * 3 + 1; 620 | 621 | char postFields[postFieldsBufSz]; 622 | 623 | char* postFieldsEnd = postFields; 624 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldClientId); 625 | postFieldsEnd = stpcpy(postFieldsEnd, clientId); 626 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldRequestId); 627 | strcpy(postFieldsEnd, escRequestId); 628 | 629 | curl_free(escRequestId); 630 | 631 | Curl::CResponse response; 632 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 633 | curl_easy_setopt(curl, CURLOPT_URL,"https://api.steampowered.com/IAuthenticationService/PollAuthSessionStatus/v1/"); 634 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 635 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 636 | 637 | const CURLcode respCode = curl_easy_perform(curl); 638 | 639 | if (respCode != CURLE_OK) 640 | { 641 | Curl::PrintError(curl, respCode); 642 | return false; 643 | } 644 | 645 | rapidjson::Document parsed; 646 | parsed.ParseInsitu(response.data); 647 | 648 | if (parsed.HasParseError()) 649 | { 650 | putsnn("JSON parsing failed\n"); 651 | return false; 652 | } 653 | 654 | const auto iterResponse = parsed.FindMember("response"); 655 | if (iterResponse == parsed.MemberEnd() || iterResponse->value.ObjectEmpty()) 656 | { 657 | putsnn("request unsucceeded\n"); 658 | return false; 659 | } 660 | 661 | const auto iterRefreshToken = iterResponse->value.FindMember("refresh_token"); 662 | const auto iterAccessToken = iterResponse->value.FindMember("access_token"); 663 | if (iterRefreshToken == iterResponse->value.MemberEnd() || iterAccessToken == iterResponse->value.MemberEnd()) 664 | { 665 | putsnn("not logged in\n"); 666 | return false; 667 | } 668 | 669 | strcpy(outRefreshToken, iterRefreshToken->value.GetString()); 670 | strcpy(outAccessToken, iterAccessToken->value.GetString()); 671 | 672 | putsnn("logged in\n"); 673 | return true; 674 | } 675 | 676 | // unused 677 | // only works for "client" jwt audience i think 678 | bool GenerateAccessTokenForApp(CURL* curl, const char* steamId64, const char* refreshToken, char* outAccessToken) 679 | { 680 | Log(LogChannel::STEAM, "Generating access token..."); 681 | 682 | const char postFieldRefreshToken[] = "refresh_token="; 683 | const char postFieldSteamId[] = "&steamid="; 684 | 685 | const size_t postFieldsBufSz = 686 | sizeof(postFieldRefreshToken) - 1 + jwtBufSz - 1 + 687 | sizeof(postFieldSteamId) - 1 + UINT64_MAX_STR_SIZE - 1 + 1; 688 | 689 | char postFields[postFieldsBufSz]; 690 | 691 | char* postFieldsEnd = postFields; 692 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldRefreshToken); 693 | postFieldsEnd = stpcpy(postFieldsEnd, refreshToken); 694 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldSteamId); 695 | strcpy(postFieldsEnd, steamId64); 696 | 697 | Curl::CResponse response; 698 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 699 | curl_easy_setopt(curl, CURLOPT_URL, 700 | "https://api.steampowered.com/IAuthenticationService/GenerateAccessTokenForApp/v1/"); 701 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 702 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 703 | 704 | const CURLcode respCode = curl_easy_perform(curl); 705 | 706 | if (respCode != CURLE_OK) 707 | { 708 | Curl::PrintError(curl, respCode); 709 | return false; 710 | } 711 | 712 | rapidjson::Document parsed; 713 | parsed.ParseInsitu(response.data); 714 | 715 | if (parsed.HasParseError()) 716 | { 717 | putsnn("JSON parsing failed\n"); 718 | return false; 719 | } 720 | 721 | const auto iterResponse = parsed.FindMember("response"); 722 | if (iterResponse == parsed.MemberEnd() || iterResponse->value.ObjectEmpty()) 723 | { 724 | putsnn("request unsucceeded\n"); 725 | return false; 726 | } 727 | 728 | const char* accessToken = iterResponse->value["access_token"].GetString(); 729 | 730 | strcpy(outAccessToken, accessToken); 731 | 732 | putsnn("ok\n"); 733 | return true; 734 | } 735 | 736 | bool RefreshJWTSession(CURL* curl, char* outAccessToken) 737 | { 738 | Curl::CResponse response; 739 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 740 | curl_easy_setopt(curl, CURLOPT_URL, 741 | "https://login.steampowered.com/jwt/refresh?redir=https://steamcommunity.com/"); 742 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 743 | 744 | // steam returns expiry time "1" for some reason, which makes cookie expire instantly 745 | // so we must parse the cookie manually 746 | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); 747 | 748 | const CURLcode respCodeRefresh = curl_easy_perform(curl); 749 | 750 | if (respCodeRefresh != CURLE_OK) 751 | { 752 | Log(LogChannel::STEAM, "Refreshing session failed: "); 753 | Curl::PrintError(curl, respCodeRefresh); 754 | return false; 755 | } 756 | 757 | char* followUrl; 758 | if ((curl_easy_getinfo(curl, CURLINFO_REDIRECT_URL, &followUrl) != CURLE_OK) || !followUrl) 759 | { 760 | Log(LogChannel::STEAM, "Refreshing session failed: getting redirect URL failed\n"); 761 | return false; 762 | } 763 | 764 | if (!strcmp(followUrl, "https://steamcommunity.com/")) 765 | { 766 | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 767 | 768 | Log(LogChannel::STEAM, "Refreshing session failed: refresh token is invalid or has expired\n"); 769 | return false; 770 | } 771 | 772 | curl_easy_setopt(curl, CURLOPT_URL, followUrl); 773 | 774 | const CURLcode respCodeFollow = curl_easy_perform(curl); 775 | 776 | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 777 | 778 | if (respCodeFollow != CURLE_OK) 779 | { 780 | Log(LogChannel::STEAM, "Refreshing session failed: "); 781 | Curl::PrintError(curl, respCodeFollow); 782 | return false; 783 | } 784 | 785 | curl_slist* cookies; 786 | if ((curl_easy_getinfo(curl, CURLINFO_COOKIELIST, &cookies) != CURLE_OK) || !cookies) 787 | { 788 | Log(LogChannel::STEAM, "Refreshing session failed: getting cookies failed\n"); 789 | return false; 790 | } 791 | 792 | curl_slist* cookiesIter = cookies; 793 | while (cookiesIter && !strstr(cookiesIter->data, "\tsteamLoginSecure\t")) 794 | cookiesIter = cookiesIter->next; 795 | 796 | if (!cookiesIter) 797 | { 798 | curl_slist_free_all(cookies); 799 | 800 | Log(LogChannel::STEAM, "Refreshing session failed: steamLoginSecure not found\n"); 801 | return false; 802 | } 803 | 804 | strcpy(outAccessToken, strchr(cookiesIter->data, '%') + 6); 805 | 806 | curl_slist_free_all(cookies); 807 | 808 | return true; 809 | } 810 | } 811 | } --------------------------------------------------------------------------------