├── .gitattributes ├── .gitignore ├── LICENSE ├── Makefile ├── OpenMarketClient.sln ├── README.md ├── screenshot.png ├── src ├── Account.h ├── Crypto.h ├── Curl.h ├── Main.cpp ├── Market.h ├── Misc.h ├── Precompiled.cpp ├── Precompiled.h └── Steam │ ├── Auth.h │ ├── Captcha.h │ ├── Guard.h │ ├── Misc.h │ ├── Steam.h │ └── Trade.h └── vsproject ├── OpenMarketClient.vcxproj └── OpenMarketClient.vcxproj.filters /.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 | -------------------------------------------------------------------------------- /.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/ -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CXX=g++ 2 | CXXFLAGS=-std=c++17 -O2 -Wall 3 | LDLIBS=-Wl,-rpath -Wl,/usr/local/lib/ -lpthread -lcurl -lwolfssl -lstdc++fs 4 | INCLUDES=-I../libs/wolfssl -I../libs/curl/include -I../libs/rapidjson/include 5 | SRCFOLDER=src 6 | PCH=Precompiled 7 | OUTDIR=build/linux 8 | TARGET=OpenMarketClient 9 | 10 | all: $(OUTDIR)/$(TARGET) 11 | 12 | $(OUTDIR)/$(TARGET): $(OUTDIR)/$(PCH).h.gch \ 13 | $(SRCFOLDER)/Misc.h \ 14 | $(SRCFOLDER)/Curl.h \ 15 | $(SRCFOLDER)/Crypto.h \ 16 | $(SRCFOLDER)/Steam/Misc.h \ 17 | $(SRCFOLDER)/Steam/Captcha.h \ 18 | $(SRCFOLDER)/Steam/Trade.h \ 19 | $(SRCFOLDER)/Steam/Guard.h \ 20 | $(SRCFOLDER)/Steam/Auth.h \ 21 | $(SRCFOLDER)/Market.h \ 22 | $(SRCFOLDER)/Account.h \ 23 | $(SRCFOLDER)/Main.cpp 24 | mkdir -p $(OUTDIR) 25 | $(CXX) $(CXXFLAGS) -include $(OUTDIR)/$(PCH).h $(INCLUDES) $(SRCFOLDER)/Main.cpp -o $@ $(LDLIBS) 26 | 27 | $(OUTDIR)/$(PCH).h.gch: $(SRCFOLDER)/$(PCH).h 28 | mkdir -p $(OUTDIR) 29 | $(CXX) $(CXXFLAGS) $(INCLUDES) $(SRCFOLDER)/$(PCH).h -o $@ 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenMarketClient 2 | Unofficial cross-platform console client for: 3 | * [market.csgo.com](https://market.csgo.com) 4 | * [market.dota2.net](https://market.dota2.net) 5 | * [tf2.tm](https://tf2.tm) 6 | * [rust.tm](https://rust.tm) 7 | * [gifts.tm](https://gifts.tm) 8 | 9 | ![screenshot](screenshot.png) 10 | 11 | # Features 12 | * Multi-account support 13 | * Proxy support 14 | * Sets Steam inventory public 15 | * Sets trade token and Steam web API key on the market 16 | * Keeps your market profile online 17 | * Sends sold items 18 | * Receives bought items 19 | * Accepts Steam Guard confirmations of sent offers 20 | * Cancels unaccepted offers after 15 minutes (required since Steam removed CancelTradeOffer web API) 21 | * Ability to import SteamDesktopAuthenticator's maFile 22 | * Accounts are password encrypted 23 | 24 | # Usage 25 | You'll be asked to enter the encryption password which will be used to encrypt and decrypt saved accounts. 26 | 27 | ## Entering details manually 28 | If you don't have any accounts added you'll be asked to enter new account automatically, otherwise launch the program with "--new" command line option. 29 | 30 | ## Importing SDA's maFile 31 | Put SDA's unencrypted maFile into "accounts" folder (create, if there's none) and the program will import most details automatically. 32 | 33 | ## Required details 34 | * Market API key ([you can get one here](https://market.csgo.com/docs-v2)) 35 | * Steam username 36 | * Steam password 37 | 38 | *and those Steam Guard Mobile Authenticator details* 39 | * Two factor code 40 | * Identity secret 41 | 42 | You can read how to extract them from your phone [here](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Two-factor-authentication#android-phone). 43 | 44 | ## Command line options 45 | * --new - Enter new account manually 46 | * --proxy [scheme://][username:password@]host[:port] - Set global proxy 47 | 48 | # Build requirements 49 | * C++17 supporting compiler 50 | * libcurl 51 | * wolfSSL 52 | * RapidJSON 53 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soyware/OpenMarketClient/cb5042cbc14c5c7ed40bbc39f2509fbce64c4b74/screenshot.png -------------------------------------------------------------------------------- /src/Account.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define ACCOUNT_SAVED_FIELDS_SZ offsetof(CAccount, name) 4 | 5 | class CAccount 6 | { 7 | public: 8 | char marketApiKey[Market::apiKeySz + 1] = ""; 9 | char identitySecret[Steam::Guard::secretsSz + 1] = ""; 10 | char deviceId[Steam::Guard::deviceIdBufSz] = ""; 11 | char steamApiKey[Steam::apiKeyBufSz] = ""; 12 | 13 | char steamId64[UINT64_MAX_STR_SIZE] = ""; 14 | 15 | // commented out because oauth seems to be gone 16 | //char oauthToken[Steam::Auth::oauthTokenBufSz] = ""; 17 | //char loginToken[Steam::Auth::loginTokenBufSz] = ""; 18 | 19 | char refreshToken[Steam::Auth::jwtBufSz] = ""; 20 | 21 | //char sessionId[Steam::Auth::sessionIdBufSz] = ""; 22 | 23 | 24 | // everything below isn't saved 25 | 26 | char name[128] = ""; 27 | 28 | class COffer 29 | { 30 | public: 31 | char marketHash[Market::hashBufSz]; 32 | char tradeOfferId[Steam::Trade::offerIdBufSz]; 33 | 34 | COffer(const char* hash, const char* offerId) 35 | { 36 | strcpy(marketHash, hash); 37 | strcpy(tradeOfferId, offerId); 38 | } 39 | }; 40 | 41 | std::vector sentOffers[(int)Market::Market::COUNT]; 42 | std::vector givenItemIds[(int)Market::Market::COUNT]; 43 | std::vector takenItemIds[(int)Market::Market::COUNT]; 44 | std::vector givenOfferIds[(int)Market::Market::COUNT]; 45 | std::vector takenOfferIds[(int)Market::Market::COUNT]; 46 | 47 | 48 | static constexpr const char directory[] = "accounts"; 49 | static constexpr const char extension[] = ".bin"; 50 | 51 | static constexpr int scryptCost = 16; // (128 * (2^16) * 8) = 64 MB RAM 52 | static constexpr int scryptBlockSz = 8; 53 | static constexpr int scryptParallel = 1; 54 | 55 | static constexpr size_t keySz = AES_256_KEY_SIZE; 56 | static constexpr size_t saltSz = (128 / 8); // NIST recommends at least 128 bits 57 | static constexpr size_t ivSz = GCM_NONCE_MID_SZ; 58 | static constexpr size_t authTagSz = (128 / 8); // max allowed tag size is 128 bits 59 | 60 | 61 | bool Save(const char* encryptPass) 62 | { 63 | byte salt[saltSz]; 64 | byte iv[ivSz]; 65 | byte authTag[authTagSz]; 66 | byte cipher[ACCOUNT_SAVED_FIELDS_SZ]; 67 | 68 | const bool encryptFailed = 69 | !Crypto::Encrypt(encryptPass, keySz, scryptCost, scryptBlockSz, scryptParallel, 70 | (byte*)this, ACCOUNT_SAVED_FIELDS_SZ, 71 | salt, saltSz, 72 | iv, ivSz, 73 | authTag, authTagSz, 74 | cipher); 75 | 76 | if (encryptFailed) 77 | return false; 78 | 79 | Log(LogChannel::GENERAL, "[%s] Saving...", name); 80 | 81 | const std::filesystem::path dir(directory); 82 | 83 | if (!std::filesystem::exists(dir) && !std::filesystem::create_directory(dir)) 84 | { 85 | putsnn("accounts directory creation failed\n"); 86 | return false; 87 | } 88 | 89 | char path[PATH_MAX]; 90 | 91 | char* pathEnd = path; 92 | pathEnd = stpcpy(pathEnd, directory); 93 | pathEnd = stpcpy(pathEnd, "/"); 94 | pathEnd = stpcpy(pathEnd, name); 95 | strcpy(pathEnd, extension); 96 | 97 | FILE* file = u8fopen(path, "wb"); 98 | if (!file) 99 | { 100 | putsnn("file creation failed\n"); 101 | return false; 102 | } 103 | 104 | const bool writeFailed = 105 | ((fwrite(salt, sizeof(byte), sizeof(salt), file) != sizeof(salt)) || 106 | (fwrite(iv, sizeof(byte), sizeof(iv), file) != sizeof(iv)) || 107 | (fwrite(authTag, sizeof(byte), sizeof(authTag), file) != sizeof(authTag)) || 108 | (fwrite(cipher, sizeof(byte), sizeof(cipher), file) != sizeof(cipher))); 109 | 110 | fclose(file); 111 | 112 | if (writeFailed) 113 | { 114 | putsnn("writing failed\n"); 115 | return false; 116 | } 117 | 118 | putsnn("ok\n"); 119 | return true; 120 | } 121 | 122 | bool Load(const char* path, const char* decryptPass) 123 | { 124 | Log(LogChannel::GENERAL, "[%s] Reading...", name); 125 | 126 | unsigned char* contents = nullptr; 127 | long contentsSz = 0; 128 | if (!ReadFile(path, &contents, &contentsSz)) 129 | { 130 | putsnn("fail\n"); 131 | return false; 132 | } 133 | 134 | putsnn("ok\n"); 135 | 136 | const byte* salt = contents; 137 | const byte* iv = salt + saltSz; 138 | const byte* authTag = iv + ivSz; 139 | const byte* cipher = authTag + authTagSz; 140 | 141 | const bool decryptFailed = 142 | !Crypto::Decrypt(decryptPass, keySz, scryptCost, scryptBlockSz, scryptParallel, 143 | cipher, ACCOUNT_SAVED_FIELDS_SZ, 144 | salt, saltSz, 145 | iv, ivSz, 146 | authTag, authTagSz, 147 | (byte*)this); 148 | 149 | free(contents); 150 | 151 | if (decryptFailed) 152 | return false; 153 | 154 | return true; 155 | } 156 | 157 | // outUsername buffer size must be at least Steam::Auth::usernameBufSz 158 | // outSharedSecret buffer size must be at least Steam::Guard::secretsSz + 1 159 | bool ImportMaFile(const char* path, char* outUsername, char* outSharedSecret) 160 | { 161 | Log(LogChannel::GENERAL, "[%s] Importing maFile...", name); 162 | 163 | unsigned char* contents = nullptr; 164 | long contentsSz = 0; 165 | if (!ReadFile(path, &contents, &contentsSz)) 166 | { 167 | putsnn("reading failed\n"); 168 | return false; 169 | } 170 | 171 | if (contents[0] != '{') 172 | { 173 | free(contents); 174 | putsnn("invalid maFile, disable encryption in SDA before importing\n"); 175 | return false; 176 | } 177 | 178 | rapidjson::Document parsed; 179 | parsed.Parse((char*)contents, contentsSz); 180 | 181 | free(contents); 182 | 183 | if (parsed.HasParseError()) 184 | { 185 | putsnn("JSON parsing failed\n"); 186 | return false; 187 | } 188 | 189 | const auto iterSession = parsed.FindMember("Session"); 190 | if (iterSession == parsed.MemberEnd() || iterSession->value.ObjectEmpty()) 191 | { 192 | putsnn("session info not found or empty\n"); 193 | return false; 194 | } 195 | 196 | //const char* steamLoginSecure = iterSession->value["SteamLoginSecure"].GetString(); 197 | //const char* steamLoginSecureDelim = strchr(steamLoginSecure, '%'); 198 | 199 | strcpy(identitySecret, parsed["identity_secret"].GetString()); 200 | strcpy(deviceId, parsed["device_id"].GetString()); 201 | //strcpy(sessionId, iterSession->value["SessionID"].GetString()); 202 | //strcpy(loginToken, steamLoginSecureDelim + 6); 203 | //strcpy(oauthToken, iterSession->value["OAuthToken"].GetString()); // commented out because oauth seems to be gone 204 | //stpncpy(steamId64, steamLoginSecure, (steamLoginSecureDelim - steamLoginSecure))[0] = '\0'; 205 | strcpy(steamId64, std::to_string(iterSession->value["SteamID"].GetUint64()).c_str()); 206 | 207 | strcpy(outUsername, parsed["account_name"].GetString()); 208 | strcpy(outSharedSecret, parsed["shared_secret"].GetString()); 209 | 210 | putsnn("ok\n"); 211 | return true; 212 | } 213 | 214 | bool EnterMarketApiKey(CURL* curl) 215 | { 216 | while (true) 217 | { 218 | if (!GetUserInputString("Enter market API key", marketApiKey, Market::apiKeySz + 1, Market::apiKeySz)) 219 | return false; 220 | 221 | rapidjson::Document parsed; 222 | if (Market::GetProfileStatus(curl, marketApiKey, &parsed)) 223 | break; 224 | } 225 | return true; 226 | } 227 | 228 | bool EnterIdentitySecret(CURL* curl) 229 | { 230 | while (true) 231 | { 232 | if (!GetUserInputString("Enter Steam Guard identity_secret", 233 | identitySecret, Steam::Guard::secretsSz + 1, Steam::Guard::secretsSz)) 234 | return false; 235 | 236 | rapidjson::Document doc; 237 | if (Steam::Guard::FetchConfirmations(curl, steamId64, identitySecret, deviceId, &doc)) 238 | break; 239 | } 240 | return true; 241 | } 242 | 243 | bool IsRefreshTokenValid() 244 | { 245 | const char* pszJwtHeaderEnd = strchr(refreshToken, '.'); 246 | if (!pszJwtHeaderEnd) 247 | { 248 | Log(LogChannel::GENERAL, "Invalid refresh token\n"); 249 | return false; 250 | } 251 | 252 | const char* pszJwtPayloadEnd = strchr(pszJwtHeaderEnd + 1, '.'); 253 | if (!pszJwtPayloadEnd) 254 | { 255 | Log(LogChannel::GENERAL, "Invalid refresh token\n"); 256 | return false; 257 | } 258 | 259 | const size_t encJwtPayloadSz = pszJwtPayloadEnd - pszJwtHeaderEnd - 1; 260 | 261 | const size_t encJwtPayloadPaddedSz = GetBase64PaddedLen(encJwtPayloadSz); 262 | 263 | char encJwtPayload[sizeof(refreshToken)]; 264 | memcpy(encJwtPayload, pszJwtHeaderEnd + 1, encJwtPayloadSz); 265 | 266 | // add padding at the end 267 | for (size_t i = encJwtPayloadSz; i < encJwtPayloadPaddedSz; ++i) 268 | encJwtPayload[i] = '='; 269 | 270 | char jwtPayload[Base64ToPlainSize(sizeof(encJwtPayload))]; 271 | size_t jwtPayloadSz = sizeof(jwtPayload); 272 | 273 | if (Base64_Decode((byte*)encJwtPayload, encJwtPayloadPaddedSz, (byte*)jwtPayload, &jwtPayloadSz)) 274 | { 275 | Log(LogChannel::GENERAL, "JWT payload decoding failed\n"); 276 | return false; 277 | } 278 | 279 | Base64ToBase64URL(jwtPayload, jwtPayloadSz); 280 | 281 | rapidjson::Document docJwtPayload; 282 | docJwtPayload.ParseInsitu(jwtPayload); 283 | 284 | const time_t exp = docJwtPayload["exp"].GetUint64(); 285 | 286 | const time_t curTime = time(nullptr); 287 | 288 | return (curTime < exp); 289 | } 290 | 291 | bool Init(CURL* curl, const char* sessionId, const char* encryptPass, 292 | const char* name_ = nullptr, const char* path = nullptr, bool isMaFile = false) 293 | { 294 | char username[Steam::Auth::usernameBufSz] = ""; 295 | char sharedSecret[Steam::Guard::secretsSz + 1] = ""; 296 | 297 | if (!name[0]) 298 | { 299 | if (name_) 300 | strcpy(name, name_); 301 | else if (!GetUserInputString("Enter new account alias", name, sizeof(name))) 302 | return false; 303 | } 304 | 305 | if (path) 306 | { 307 | if (isMaFile) 308 | { 309 | if (!ImportMaFile(path, username, sharedSecret)) 310 | return false; 311 | } 312 | else 313 | { 314 | if (!Load(path, encryptPass)) 315 | return false; 316 | } 317 | } 318 | 319 | char accessToken[Steam::Auth::jwtBufSz] = ""; 320 | 321 | bool loginRequired = true; 322 | 323 | // commented out because oauth seems to be gone 324 | //if (oauthToken[0]) 325 | //{ 326 | // const int refreshRes = Steam::Auth::RefreshOAuthSession(curl, oauthToken, loginToken); 327 | 328 | // if (refreshRes < 0) 329 | // return false; 330 | // else if (refreshRes == 0) 331 | // Log(LogChannel::GENERAL, "[%s] Steam OAuth token is invalid or has expired, login required\n", name); 332 | // else 333 | // loginRequired = false; 334 | //} 335 | 336 | if (refreshToken[0]) 337 | { 338 | if (IsRefreshTokenValid() && 339 | Steam::SetRefreshCookie(curl, steamId64, refreshToken) && 340 | Steam::Auth::RefreshJWTSession(curl, accessToken) && 341 | Steam::SetLoginCookie(curl, steamId64, accessToken)) 342 | loginRequired = false; 343 | else 344 | Log(LogChannel::GENERAL, "[%s] Steam refresh token is invalid or has expired, login required\n", name); 345 | } 346 | 347 | bool loggedIn = !loginRequired; 348 | 349 | if (loginRequired) 350 | { 351 | char password[Steam::Auth::passwordBufSz]; 352 | 353 | if ((username[0] || GetUserInputString("Enter Steam username", username, sizeof(username))) && 354 | GetUserInputString("Enter Steam password", password, sizeof(password), 8, false)) 355 | { 356 | char clientId[Steam::Auth::clientIdBufSz]; 357 | char requestId[Steam::Auth::requestIdBufSz]; 358 | 359 | if (Steam::Auth::BeginAuthSessionViaCredentials(curl, username, password, steamId64, clientId, requestId)) 360 | { 361 | for (size_t i = 0; i < 3; ++i) 362 | { 363 | char twoFactorCode[Steam::Guard::twoFactorCodeBufSz] = ""; 364 | 365 | if (sharedSecret[0]) 366 | Steam::Guard::GenerateTwoFactorAuthCode(sharedSecret, twoFactorCode); 367 | 368 | if (!twoFactorCode[0]) 369 | { 370 | if (!GetUserInputString("Enter Steam Guard Mobile Authenticator code", 371 | twoFactorCode, sizeof(twoFactorCode), Steam::Guard::twoFactorCodeBufSz - 1)) 372 | break; 373 | } 374 | 375 | if (!Steam::Auth::UpdateAuthSessionWithSteamGuardCode(curl, steamId64, clientId, twoFactorCode)) 376 | break; 377 | 378 | if (Steam::Auth::PollAuthSessionStatus(curl, clientId, requestId, refreshToken, accessToken)) 379 | { 380 | loggedIn = true; 381 | break; 382 | } 383 | 384 | std::this_thread::sleep_for(5s); 385 | } 386 | 387 | if (loggedIn) 388 | { 389 | if (!Steam::SetRefreshCookie(curl, steamId64, refreshToken) || 390 | !Steam::SetLoginCookie(curl, steamId64, accessToken)) 391 | { 392 | memset(refreshToken, 0, sizeof(refreshToken)); 393 | memset(accessToken, 0, sizeof(accessToken)); 394 | 395 | loggedIn = false; 396 | } 397 | } 398 | } 399 | } 400 | 401 | memset(password, 0, sizeof(password)); 402 | } 403 | 404 | memset(username, 0, sizeof(username)); 405 | memset(sharedSecret, 0, sizeof(sharedSecret)); 406 | 407 | if (!loggedIn) 408 | return false; 409 | 410 | if (!deviceId[0] && !Steam::Guard::GetDeviceId(curl, steamId64, accessToken, deviceId)) 411 | return false; 412 | 413 | memset(accessToken, 0, sizeof(accessToken)); 414 | 415 | if (!steamApiKey[0] && !Steam::GetApiKey(curl, sessionId, steamApiKey)) 416 | return false; 417 | 418 | if (!identitySecret[0] && !EnterIdentitySecret(curl)) 419 | return false; 420 | 421 | if (!marketApiKey[0] && !EnterMarketApiKey(curl)) 422 | return false; 423 | 424 | if (loginRequired || isMaFile) 425 | { 426 | if (Save(encryptPass)) 427 | { 428 | if (isMaFile) 429 | std::filesystem::remove(path); 430 | } 431 | } 432 | 433 | memset(refreshToken, 0, sizeof(refreshToken)); 434 | 435 | if (!Steam::SetInventoryPublic(curl, sessionId, steamId64)) 436 | return false; 437 | 438 | if (!Market::SetSteamDetails(curl, marketApiKey, steamApiKey)) 439 | return false; 440 | 441 | if (!Market::CanSell(curl, marketApiKey)) 442 | return false; 443 | 444 | return true; 445 | } 446 | 447 | // remove inactive and cancel expired 448 | bool CancelExpiredSentOffers(CURL* curl, const char* sessionId) 449 | { 450 | bool allEmpty = true; 451 | 452 | for (const auto& marketSentOffers : sentOffers) 453 | { 454 | if (!marketSentOffers.empty()) 455 | { 456 | allEmpty = false; 457 | break; 458 | } 459 | } 460 | 461 | if (allEmpty) 462 | return true; 463 | 464 | const time_t timestamp = time(nullptr); 465 | 466 | rapidjson::Document docOffers; 467 | // include inactive offers accepted within 5 mins ago so they are kept in sentOffers 468 | if (!Steam::Trade::GetOffers(curl, steamApiKey, 469 | true, false, false, true, false, nullptr, timestamp - (5 * 60), 0, &docOffers)) 470 | return false; 471 | 472 | const rapidjson::Value& offersResp = docOffers["response"]; 473 | 474 | const auto iterSentOffers = offersResp.FindMember("trade_offers_sent"); 475 | 476 | if (iterSentOffers == offersResp.MemberEnd()) 477 | { 478 | for (auto& marketSentOffers : sentOffers) 479 | marketSentOffers.clear(); 480 | 481 | return true; 482 | } 483 | 484 | const rapidjson::Value& steamSentOffers = iterSentOffers->value; 485 | const rapidjson::SizeType steamSentOffersCount = steamSentOffers.Size(); 486 | 487 | if (!steamSentOffersCount) 488 | { 489 | for (auto& marketSentOffers : sentOffers) 490 | marketSentOffers.clear(); 491 | 492 | return true; 493 | } 494 | 495 | bool allOk = true; 496 | 497 | for (auto& marketSentOffers : sentOffers) 498 | { 499 | for (auto iterSentOffer = marketSentOffers.begin(); iterSentOffer != marketSentOffers.end(); ) 500 | { 501 | bool erase = true; 502 | 503 | const char* sentOfferId = iterSentOffer->tradeOfferId; 504 | 505 | for (rapidjson::SizeType i = 0; i < steamSentOffersCount; ++i) 506 | { 507 | const rapidjson::Value& offer = steamSentOffers[i]; 508 | 509 | const char* offerId = offer["tradeofferid"].GetString(); 510 | 511 | if (strcmp(sentOfferId, offerId)) 512 | continue; 513 | 514 | const time_t timeUpdated = offer["time_updated"].GetInt64(); 515 | const time_t timeSinceUpdate = timestamp - timeUpdated; 516 | 517 | if (Market::offerTTL < timeSinceUpdate) 518 | { 519 | if (!Steam::Trade::Cancel(curl, sessionId, sentOfferId)) 520 | { 521 | erase = false; 522 | allOk = false; 523 | } 524 | } 525 | else 526 | erase = false; 527 | 528 | break; 529 | } 530 | 531 | if (erase) 532 | iterSentOffer = marketSentOffers.erase(iterSentOffer); 533 | else 534 | ++iterSentOffer; 535 | } 536 | } 537 | 538 | return allOk; 539 | } 540 | 541 | enum class MarketStatus 542 | { 543 | SOLD = (1 << 0), 544 | BOUGHT = (1 << 1) 545 | }; 546 | 547 | int GetMarketStatus(CURL* curl, int market, rapidjson::SizeType* outItemCount) 548 | { 549 | rapidjson::Document docItems; 550 | if (!Market::GetItems(curl, marketApiKey, market, &docItems)) 551 | { 552 | Log(LogChannel::GENERAL, "[%s] [%s] Getting items status failed\n", name, Market::marketNames[market]); 553 | return -1; 554 | } 555 | 556 | auto& marketGivenItemIds = givenItemIds[market]; 557 | auto& marketTakenItemIds = takenItemIds[market]; 558 | 559 | int marketStatus = 0; 560 | 561 | const rapidjson::Value& items = docItems["items"]; 562 | const rapidjson::SizeType itemCount = (items.IsArray() ? items.Size() : 0); 563 | 564 | for (rapidjson::SizeType i = 0; i < itemCount; ++i) 565 | { 566 | const rapidjson::Value& item = items[i]; 567 | 568 | // status is a string, convert it to int 569 | const int itemStatus = (item["status"].GetString()[0] - '0'); 570 | 571 | if (itemStatus == (int)Market::ItemStatus::GIVE) 572 | { 573 | const char* itemId = item["item_id"].GetString(); 574 | 575 | bool given = false; 576 | 577 | for (const auto& givenItemId : marketGivenItemIds) 578 | { 579 | if (!strcmp(itemId, givenItemId.c_str())) 580 | { 581 | given = true; 582 | break; 583 | } 584 | } 585 | 586 | if (!given) 587 | { 588 | marketGivenItemIds.emplace_back(itemId); 589 | 590 | const char* itemName = item["market_hash_name"].GetString(); 591 | Log(LogChannel::GENERAL, "[%s] [%s] Sold \"%s\"\n", name, Market::marketNames[market], itemName); 592 | } 593 | 594 | marketStatus |= (int)MarketStatus::SOLD; 595 | } 596 | else if (itemStatus == (int)Market::ItemStatus::TAKE) 597 | { 598 | const char* itemId = item["item_id"].GetString(); 599 | 600 | bool taken = false; 601 | 602 | for (const auto& takenItemId : marketTakenItemIds) 603 | { 604 | if (!strcmp(itemId, takenItemId.c_str())) 605 | { 606 | taken = true; 607 | break; 608 | } 609 | } 610 | 611 | if (!taken) 612 | { 613 | marketTakenItemIds.emplace_back(itemId); 614 | 615 | const char* itemName = item["market_hash_name"].GetString(); 616 | Log(LogChannel::GENERAL, "[%s] [%s] Bought \"%s\"\n", name, Market::marketNames[market], itemName); 617 | } 618 | 619 | marketStatus |= (int)MarketStatus::BOUGHT; 620 | } 621 | } 622 | 623 | if (!(marketStatus & (int)MarketStatus::SOLD)) 624 | marketGivenItemIds.clear(); 625 | 626 | if (!(marketStatus & (int)MarketStatus::BOUGHT)) 627 | marketTakenItemIds.clear(); 628 | 629 | *outItemCount = itemCount; 630 | 631 | return marketStatus; 632 | } 633 | 634 | bool GiveItemBot(CURL* curl, const char* sessionId, int market) 635 | { 636 | char offerId[Steam::Trade::offerIdBufSz]; 637 | char partnerId64[UINT64_MAX_STR_SIZE]; 638 | 639 | if (!Market::RequestGiveBot(curl, marketApiKey, market, offerId, partnerId64)) 640 | return false; 641 | 642 | for (const auto& givenOfferId : givenOfferIds[market]) 643 | { 644 | if (!strcmp(offerId, givenOfferId.c_str())) 645 | return true; 646 | } 647 | 648 | if (!Steam::Trade::Accept(curl, sessionId, offerId, partnerId64)) 649 | return false; 650 | 651 | if (!Steam::Guard::AcceptConfirmation(curl, steamId64, identitySecret, deviceId, offerId)) 652 | return false; 653 | 654 | givenOfferIds[market].emplace_back(offerId); 655 | 656 | return true; 657 | } 658 | 659 | bool GiveItemsP2P(CURL* curl, const char* sessionId, int market) 660 | { 661 | rapidjson::Document docGiveDetails; 662 | 663 | if (!Market::RequestGiveP2PAll(curl, marketApiKey, market, &docGiveDetails)) 664 | return false; 665 | 666 | const rapidjson::Value& offers = docGiveDetails["offers"]; 667 | const rapidjson::SizeType offerCount = offers.Size(); 668 | 669 | bool allOk = true; 670 | 671 | for (rapidjson::SizeType i = 0; i < offerCount; ++i) 672 | { 673 | const rapidjson::Value& offer = offers[i]; 674 | 675 | const char* offerHash = offer["hash"].GetString(); 676 | 677 | // check if we haven't sent this offer yet 678 | bool found = false; 679 | 680 | for (const auto& sentOffer : sentOffers[market]) 681 | { 682 | if (!strcmp(offerHash, sentOffer.marketHash)) 683 | { 684 | found = true; 685 | break; 686 | } 687 | } 688 | 689 | if (found) 690 | continue; 691 | 692 | rapidjson::StringBuffer itemsStrBuf; 693 | rapidjson::Writer itemsWriter(itemsStrBuf); 694 | 695 | if (!offer["items"].Accept(itemsWriter)) 696 | { 697 | allOk = false; 698 | Log(LogChannel::GENERAL, "[%s] [%s] Converting offer items JSON to string failed\n", 699 | name, Market::marketNames[market]); 700 | continue; 701 | } 702 | 703 | char sentOfferId[Steam::Trade::offerIdBufSz]; 704 | 705 | if (!Steam::Trade::Send(curl, 706 | sessionId, 707 | offer["partner"].GetUint(), 708 | offer["token"].GetString(), 709 | offer["tradeoffermessage"].GetString(), 710 | itemsStrBuf.GetString(), 711 | sentOfferId)) 712 | { 713 | allOk = false; 714 | continue; 715 | } 716 | 717 | if (!Steam::Guard::AcceptConfirmation(curl, steamId64, identitySecret, deviceId, sentOfferId)) 718 | { 719 | allOk = false; 720 | continue; 721 | } 722 | 723 | sentOffers[market].emplace_back(offerHash, sentOfferId); 724 | } 725 | 726 | return allOk; 727 | } 728 | 729 | bool TakeItems(CURL* curl, const char* sessionId, int market) 730 | { 731 | char offerId[Steam::Trade::offerIdBufSz]; 732 | char partnerId64[UINT64_MAX_STR_SIZE]; 733 | 734 | if (!Market::RequestTake(curl, marketApiKey, market, offerId, partnerId64)) 735 | return false; 736 | 737 | for (const auto& takenOfferId : takenOfferIds[market]) 738 | { 739 | if (!strcmp(offerId, takenOfferId.c_str())) 740 | return true; 741 | } 742 | 743 | if (!Steam::Trade::Accept(curl, sessionId, offerId, partnerId64)) 744 | return false; 745 | 746 | takenOfferIds[market].emplace_back(offerId); 747 | 748 | return true; 749 | } 750 | 751 | void PrintListings(const rapidjson::SizeType* itemCounts) 752 | { 753 | Log(LogChannel::GENERAL, "[%s] Listings: ", name); 754 | 755 | for (int i = 0; i < (int)Market::Market::COUNT; ++i) 756 | { 757 | printf("%s: %u", Market::marketNames[i], itemCounts[i]); 758 | 759 | if (i < ((int)Market::Market::COUNT - 1)) 760 | putsnn(" | "); 761 | } 762 | 763 | putchar('\n'); 764 | } 765 | 766 | bool RunMarkets(CURL* curl, const char* sessionId) 767 | { 768 | // commented out because oauth seems to be gone 769 | //const int refreshRes = Steam::Auth::RefreshOAuthSession(curl, oauthToken, loginToken); 770 | //if (refreshRes < 0) 771 | //{ 772 | // Log(LogChannel::GENERAL, "[%s] Steam session refresh failed\n", name); 773 | // return false; 774 | //} 775 | 776 | //if (refreshRes == 0) 777 | //{ 778 | // Log(LogChannel::GENERAL, "[%s] Steam OAuth token has expired, restart required\n", name); 779 | // return false; 780 | //} 781 | 782 | char accessToken[Steam::Auth::jwtBufSz]; 783 | 784 | if (!Steam::Auth::RefreshJWTSession(curl, accessToken)) 785 | { 786 | Log(LogChannel::GENERAL, "[%s] Steam session refresh failed\n", name); 787 | return false; 788 | } 789 | 790 | if (!Steam::SetLoginCookie(curl, steamId64, accessToken)) 791 | { 792 | Log(LogChannel::GENERAL, "[%s] Setting Steam login cookie failed\n", name); 793 | return false; 794 | } 795 | 796 | memset(accessToken, 0, sizeof(accessToken)); 797 | 798 | bool allOk = true; 799 | 800 | if (!CancelExpiredSentOffers(curl, sessionId)) 801 | { 802 | allOk = false; 803 | Log(LogChannel::GENERAL, "[%s] Cancelling some of the expired sent offers failed, " 804 | "manually cancel the sent offers older than 15 mins if the error continues\n", name); 805 | } 806 | 807 | if (!Market::Ping(curl, marketApiKey)) 808 | allOk = false; 809 | 810 | rapidjson::SizeType itemCounts[(int)Market::Market::COUNT] = { 0 }; 811 | 812 | for (int i = 0; i < (int)Market::Market::COUNT; ++i) 813 | { 814 | const int marketStatus = GetMarketStatus(curl, i, &itemCounts[i]); 815 | 816 | if (marketStatus < 0) 817 | { 818 | allOk = false; 819 | continue; 820 | } 821 | 822 | if (!marketStatus) 823 | continue; 824 | #ifdef _WIN32 825 | FlashCurrentWindow(); 826 | #endif // _WIN32 827 | 828 | if (marketStatus & (int)MarketStatus::SOLD) 829 | { 830 | //if (Market::isMarketP2P[i]) 831 | //{ 832 | if (!GiveItemsP2P(curl, sessionId, i)) 833 | allOk = false; 834 | //} 835 | //else 836 | //{ 837 | //if (!GiveItemBot(curl, sessionId, i)) 838 | // allOk = false; 839 | //} 840 | } 841 | //else 842 | //{ 843 | // if (!Market::isMarketP2P[i]) 844 | // givenOfferIds[i].clear(); 845 | //} 846 | 847 | if (marketStatus & (int)MarketStatus::BOUGHT) 848 | { 849 | if (!TakeItems(curl, sessionId, i)) 850 | allOk = false; 851 | } 852 | else 853 | takenOfferIds[i].clear(); 854 | } 855 | 856 | PrintListings(itemCounts); 857 | 858 | return allOk; 859 | } 860 | }; -------------------------------------------------------------------------------- /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 | // Source: https://github.com/wolfSSL/wolfssl/blob/master/wolfcrypt/src/coding.c 8 | constexpr word32 Base64ToPlainSize(size_t inLen) 9 | { 10 | word32 plainSz = inLen - ((inLen + (BASE64_LINE_SZ - 1)) / BASE64_LINE_SZ); 11 | plainSz = (plainSz * 3 + 3) / 4; 12 | return plainSz; 13 | } 14 | 15 | constexpr word32 PlainToBase64Size(size_t inLen, Escaped escaped) 16 | { 17 | word32 outSz = (inLen + 3 - 1) / 3 * 4; 18 | word32 addSz = (outSz + BASE64_LINE_SZ - 1) / BASE64_LINE_SZ; /* new lines */ 19 | 20 | if (escaped == WC_ESC_NL_ENC) 21 | addSz *= 3; /* instead of just \n, we're doing %0A triplet */ 22 | else if (escaped == WC_NO_NL_ENC) 23 | addSz = 0; /* encode without \n */ 24 | 25 | outSz += addSz; 26 | 27 | return outSz; 28 | } 29 | 30 | constexpr size_t GetBase64PaddedLen(size_t inLen) 31 | { 32 | size_t outLen = inLen; 33 | 34 | const size_t multiple = 4; 35 | const size_t remainder = inLen % multiple; 36 | if (remainder != 0) 37 | outLen += multiple - remainder; 38 | 39 | return outLen; 40 | } 41 | 42 | void Base64ToBase64URL(char* in, size_t len) 43 | { 44 | for (size_t i = 0; i < len; ++i) 45 | { 46 | if (in[i] == '-') 47 | in[i] = '+'; 48 | else if (in[i] == '_') 49 | in[i] = '/'; 50 | } 51 | } 52 | 53 | namespace Crypto 54 | { 55 | bool Encrypt(const char* password, word32 keySz, int scryptCost, int scryptBlockSz, int scryptParallel, 56 | const byte* plaintext, word32 plaintextSz, 57 | byte* outSalt, word32 outSaltSz, 58 | byte* outIV, word32 outIVSz, 59 | byte* outAuthTag, word32 outAuthTagSz, 60 | byte* outCipher) 61 | { 62 | Log(LogChannel::GENERAL, "Encrypting..."); 63 | 64 | WC_RNG rng; 65 | if (wc_InitRng(&rng)) 66 | { 67 | putsnn("RNG init failed\n"); 68 | return false; 69 | } 70 | 71 | const bool rngFailed = (wc_RNG_GenerateBlock(&rng, outSalt, outSaltSz) || wc_RNG_GenerateBlock(&rng, outIV, outIVSz)); 72 | 73 | wc_FreeRng(&rng); 74 | 75 | if (rngFailed) 76 | { 77 | putsnn("RNG generation failed\n"); 78 | return false; 79 | } 80 | 81 | byte* key = (byte*)malloc(keySz); 82 | if (!key) 83 | { 84 | putsnn("key allocation failed\n"); 85 | return false; 86 | } 87 | 88 | Aes aes; 89 | 90 | const bool stretchFailed = 91 | (wc_scrypt(key, (byte*)password, strlen(password), outSalt, outSaltSz, scryptCost, scryptBlockSz, scryptParallel, keySz) || 92 | wc_AesGcmSetKey(&aes, key, keySz)); 93 | 94 | free(key); 95 | 96 | if (stretchFailed) 97 | { 98 | putsnn("key stretching or setting AES key failed\n"); 99 | return false; 100 | } 101 | 102 | if (wc_AesGcmEncrypt(&aes, outCipher, plaintext, plaintextSz, outIV, outIVSz, outAuthTag, outAuthTagSz, nullptr, 0)) 103 | { 104 | putsnn("encryption failed\n"); 105 | return false; 106 | } 107 | 108 | putsnn("ok\n"); 109 | return true; 110 | } 111 | 112 | bool Decrypt(const char* password, word32 keySz, int scryptCost, int scryptBlockSz, int scryptParallel, 113 | const byte* cipher, size_t cipherSz, 114 | const byte* salt, word32 saltSz, 115 | const byte* iv, word32 ivSz, 116 | const byte* authTag, word32 authTagSz, 117 | byte* outPlaintext) 118 | { 119 | Log(LogChannel::GENERAL, "Decrypting..."); 120 | 121 | byte* key = (byte*)malloc(keySz); 122 | if (!key) 123 | { 124 | putsnn("key allocation failed\n"); 125 | return false; 126 | } 127 | 128 | Aes aes; 129 | 130 | const bool stretchFailed = 131 | (wc_scrypt(key, (byte*)password, strlen(password), salt, saltSz, scryptCost, scryptBlockSz, scryptParallel, keySz) || 132 | wc_AesGcmSetKey(&aes, key, keySz)); 133 | 134 | free(key); 135 | 136 | if (stretchFailed) 137 | { 138 | putsnn("key stretching or setting AES key failed\n"); 139 | return false; 140 | } 141 | 142 | if (wc_AesGcmDecrypt(&aes, outPlaintext, cipher, cipherSz, iv, ivSz, authTag, authTagSz, nullptr, 0)) 143 | { 144 | putsnn("wrong password or decryption failed\n"); 145 | return false; 146 | } 147 | 148 | putsnn("ok\n"); 149 | return true; 150 | } 151 | } -------------------------------------------------------------------------------- /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 | PrintError(curl, respCode); 90 | return false; 91 | } 92 | 93 | putsnn("ok\n"); 94 | return true; 95 | } 96 | 97 | bool SetCACert(CURL* curl, const char* path) 98 | { 99 | FILE* file = fopen(path, "rb"); 100 | // check if the certificate file exists, if not, download 101 | if (file) 102 | fclose(file); 103 | else if (!DownloadCACert(curl, path)) 104 | return false; 105 | 106 | if (curl_easy_setopt(curl, CURLOPT_CAINFO, path) != CURLE_OK) 107 | { 108 | Log(LogChannel::LIBCURL, "Setting CA certificate failed\n"); 109 | return false; 110 | } 111 | 112 | return true; 113 | } 114 | 115 | CURL* Init(const char* proxy) 116 | { 117 | if (curl_global_init(CURL_GLOBAL_ALL)) 118 | { 119 | Log(LogChannel::LIBCURL, "Global init failed\n"); 120 | return nullptr; 121 | } 122 | 123 | CURL* curl = curl_easy_init(); 124 | if (!curl) 125 | { 126 | curl_global_cleanup(); 127 | Log(LogChannel::LIBCURL, "Easy session init failed\n"); 128 | return nullptr; 129 | } 130 | 131 | curl_easy_setopt(curl, CURLOPT_TIMEOUT, 20L); 132 | curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); 133 | curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); 134 | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 135 | curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L); 136 | 137 | if (proxy && proxy[0]) 138 | { 139 | Log(LogChannel::LIBCURL, "Setting a proxy..."); 140 | 141 | if (curl_easy_setopt(curl, CURLOPT_PROXY, proxy) != CURLE_OK) 142 | { 143 | curl_easy_cleanup(curl); 144 | curl_global_cleanup(); 145 | putsnn("fail\n"); 146 | return nullptr; 147 | } 148 | 149 | putsnn("ok\n"); 150 | } 151 | 152 | // let libcurl use system's default on linux 153 | #ifdef _WIN32 154 | if (!SetCACert(curl, "cacert.pem")) 155 | { 156 | curl_easy_cleanup(curl); 157 | curl_global_cleanup(); 158 | return nullptr; 159 | } 160 | #endif // _WIN32 161 | 162 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CResponse::WriteCallback); 163 | 164 | return curl; 165 | } 166 | } -------------------------------------------------------------------------------- /src/Main.cpp: -------------------------------------------------------------------------------- 1 | #include "Precompiled.h" 2 | #include "Misc.h" 3 | #include "Curl.h" 4 | #include "Crypto.h" 5 | #include "Steam/Steam.h" 6 | #include "Market.h" 7 | #include "Account.h" 8 | 9 | #define OPENMARKETCLIENT_VERSION "0.4.3" 10 | 11 | /* 12 | * TODO: 13 | * better logging 14 | */ 15 | 16 | void SetLocale() 17 | { 18 | // LC_ALL breaks SetConsoleOutputCP 19 | setlocale(LC_TIME, ""); 20 | #ifdef _WIN32 21 | _setmode(_fileno(stdin), _O_U16TEXT); 22 | SetConsoleOutputCP(CP_UTF8); 23 | #endif // _WIN32 24 | } 25 | 26 | void PrintVersion() 27 | { 28 | putsnn("OpenMarketClient v" OPENMARKETCLIENT_VERSION ", built with " 29 | "libcurl v" LIBCURL_VERSION ", " 30 | "wolfSSL v" LIBWOLFSSL_VERSION_STRING ", " 31 | "rapidJSON v" RAPIDJSON_VERSION_STRING "\n"); 32 | } 33 | 34 | namespace Args 35 | { 36 | bool newAcc = false; 37 | const char* proxy = nullptr; 38 | 39 | void PrintHelp() 40 | { 41 | putsnn("Options:\n" 42 | "--help\t\t\t\t\t\t\tPrint help\n" 43 | "--new\t\t\t\t\t\t\tEnter new account manually\n" 44 | "--proxy [scheme://][username:password@]host[:port]\tSet global proxy\n"); 45 | } 46 | 47 | bool Parse(int argc, char** const argv) 48 | { 49 | for (int i = 1; i < argc; ++i) 50 | { 51 | if (!strcmp(argv[i], "--help")) 52 | { 53 | PrintHelp(); 54 | return false; 55 | } 56 | else if (!strcmp(argv[i], "--new")) 57 | newAcc = true; 58 | else if ((i < (argc - 1)) && !strcmp(argv[i], "--proxy")) // check if second to last argument 59 | { 60 | proxy = argv[i + 1]; 61 | ++i; 62 | } 63 | } 64 | return true; 65 | } 66 | } 67 | 68 | bool SetWorkDirToExeDir() 69 | { 70 | const char* exeDir = GetExeDir(); 71 | if (!exeDir) 72 | return false; 73 | 74 | #ifdef _WIN32 75 | const size_t wideExeDirLen = PATH_MAX; 76 | wchar_t wideExeDir[wideExeDirLen]; 77 | 78 | if (!MultiByteToWideChar(CP_UTF8, 0, exeDir, -1, wideExeDir, wideExeDirLen)) 79 | return false; 80 | 81 | return !_wchdir(wideExeDir); 82 | #else 83 | 84 | return !chdir(exeDir); 85 | #endif // _WIN32 86 | } 87 | 88 | bool InitSavedAccounts(CURL* curl, const char* sessionId, const char* encryptPass, std::vector* accounts) 89 | { 90 | const std::filesystem::path dir(CAccount::directory); 91 | 92 | if (!std::filesystem::exists(dir)) 93 | return true; 94 | 95 | bool success = true; 96 | 97 | for (const auto& entry : std::filesystem::directory_iterator(dir)) 98 | { 99 | const auto& path = entry.path(); 100 | const auto& extension = path.extension(); 101 | 102 | const bool isMaFile = !extension.compare(".maFile"); 103 | 104 | if (extension.compare(CAccount::extension) && !isMaFile) 105 | continue; 106 | 107 | const auto& stem = path.stem(); 108 | 109 | #ifdef _WIN32 110 | const auto* widePath = path.c_str(); 111 | const auto* wideFilenameNoExt = stem.c_str(); 112 | 113 | char szPath[PATH_MAX]; 114 | char szFilenameNoExt[sizeof(CAccount::name)]; 115 | 116 | if (!WideCharToMultiByte(CP_UTF8, 0, widePath, -1, szPath, sizeof(szPath), NULL, NULL) || 117 | !WideCharToMultiByte(CP_UTF8, 0, wideFilenameNoExt, -1, szFilenameNoExt, sizeof(szFilenameNoExt), NULL, NULL)) 118 | { 119 | Log(LogChannel::GENERAL, "One of the account's filename UTF-16 to UTF-8 mapping failed\n"); 120 | success = false; 121 | break; 122 | } 123 | 124 | #else 125 | const char* szPath = path.c_str(); 126 | const char* szFilenameNoExt = stem.c_str(); 127 | #endif 128 | 129 | CAccount account; 130 | 131 | if (!account.Init(curl, sessionId, encryptPass, szFilenameNoExt, szPath, isMaFile)) 132 | { 133 | success = false; 134 | break; 135 | } 136 | 137 | accounts->emplace_back(account); 138 | } 139 | 140 | return success; 141 | } 142 | 143 | int main(int argc, char** argv) 144 | { 145 | // disable stdout buffering if stdout is a terminal 146 | #pragma warning (suppress:4996) 147 | if (isatty(fileno(stdout))) 148 | setvbuf(stdout, nullptr, _IONBF, 0); 149 | 150 | SetLocale(); 151 | PrintVersion(); 152 | 153 | if (!Args::Parse(argc, argv)) 154 | { 155 | Pause(); 156 | return 0; 157 | } 158 | 159 | if (!SetWorkDirToExeDir()) 160 | { 161 | Log(LogChannel::GENERAL, "Setting working directory failed\n"); 162 | Pause(); 163 | return 1; 164 | } 165 | 166 | CURL* curl = Curl::Init(Args::proxy); 167 | if (!curl) 168 | { 169 | Pause(); 170 | return 1; 171 | } 172 | 173 | char sessionId[Steam::sessionIdBufSz]; 174 | 175 | if (!Steam::GenerateSessionId(sessionId) || !Steam::SetSessionCookie(curl, sessionId)) 176 | { 177 | curl_easy_cleanup(curl); 178 | curl_global_cleanup(); 179 | Pause(); 180 | return 1; 181 | } 182 | 183 | if (!Steam::Guard::SyncTime(curl)) 184 | { 185 | curl_easy_cleanup(curl); 186 | curl_global_cleanup(); 187 | Pause(); 188 | return 1; 189 | } 190 | 191 | char encryptPass[64]; 192 | if (!GetUserInputString("Enter encryption password", encryptPass, sizeof(encryptPass), 10, false)) 193 | { 194 | curl_easy_cleanup(curl); 195 | curl_global_cleanup(); 196 | Pause(); 197 | return 1; 198 | } 199 | 200 | std::vector accounts; 201 | 202 | if (Args::newAcc) 203 | { 204 | CAccount account; 205 | while (!account.Init(curl, sessionId, encryptPass)); 206 | 207 | accounts.emplace_back(account); 208 | } 209 | 210 | if (!InitSavedAccounts(curl, sessionId, encryptPass, &accounts)) 211 | { 212 | curl_easy_cleanup(curl); 213 | curl_global_cleanup(); 214 | Pause(); 215 | return 1; 216 | } 217 | 218 | if (accounts.empty()) 219 | { 220 | Log(LogChannel::GENERAL, "No accounts, adding a new one\n"); 221 | 222 | CAccount account; 223 | while (!account.Init(curl, sessionId, encryptPass)); 224 | 225 | accounts.emplace_back(account); 226 | } 227 | 228 | memset(encryptPass, 0, sizeof(encryptPass)); 229 | 230 | #ifdef _WIN32 231 | // prevent sleep 232 | SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED); 233 | #endif // _WIN32 234 | 235 | const size_t accountCount = accounts.size(); 236 | 237 | while (true) 238 | { 239 | for (size_t i = 0; i < accountCount; ++i) 240 | accounts[i].RunMarkets(curl, sessionId); 241 | 242 | if (1 < accountCount) 243 | putchar('\n'); 244 | 245 | std::this_thread::sleep_for(1min); 246 | } 247 | 248 | curl_easy_cleanup(curl); 249 | curl_global_cleanup(); 250 | Pause(); 251 | return 0; 252 | } -------------------------------------------------------------------------------- /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 = (15 * 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 | bool Ping(CURL* curl, const char* apiKey) 78 | { 79 | const char query[] = "ping?key="; 80 | 81 | const size_t urlBufSz = marketBaseUrlMaxSz - 1 + sizeof(query) - 1 + apiKeySz + 1; 82 | char url[urlBufSz]; 83 | 84 | char* urlEnd = url; 85 | urlEnd = stpcpy(urlEnd, marketBaseUrls[(int)Market::CSGO]); 86 | urlEnd = stpcpy(urlEnd, query); 87 | strcpy(urlEnd, apiKey); 88 | 89 | curl_easy_setopt(curl, CURLOPT_URL, url); 90 | curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); 91 | 92 | return (curl_easy_perform(curl) == CURLE_OK); 93 | } 94 | 95 | bool GetItems(CURL* curl, const char* apiKey, int market, rapidjson::Document* outDoc) 96 | { 97 | const char query[] = "items?key="; 98 | 99 | const size_t urlBufSz = marketBaseUrlMaxSz - 1 + sizeof(query) - 1 + apiKeySz + 1; 100 | char url[urlBufSz]; 101 | 102 | char* urlEnd = url; 103 | urlEnd = stpcpy(urlEnd, marketBaseUrls[market]); 104 | urlEnd = stpcpy(urlEnd, query); 105 | strcpy(urlEnd, apiKey); 106 | 107 | Curl::CResponse response; 108 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 109 | curl_easy_setopt(curl, CURLOPT_URL, url); 110 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 111 | 112 | if (curl_easy_perform(curl) != CURLE_OK) 113 | return false; 114 | 115 | outDoc->Parse(response.data); 116 | 117 | if (outDoc->HasParseError()) 118 | return false; 119 | 120 | if (!(*outDoc)["success"].GetBool()) 121 | return false; 122 | 123 | return true; 124 | } 125 | 126 | // outOfferId buffer size must be at least offerIdBufSz 127 | // outPartnerId64 buffer size must be at least UINT64_MAX_STR_SIZE 128 | bool RequestTake(CURL* curl, const char* apiKey, int market, char* outOfferId, char* outPartnerId64) 129 | { 130 | Log(LogChannel::MARKET, "[%s] Requesting details to receive items...", marketNames[market]); 131 | 132 | const char query[] = "trade-request-take?key="; 133 | 134 | const size_t urlBufSz = marketBaseUrlMaxSz - 1 + sizeof(query) - 1 + apiKeySz + 1; 135 | char url[urlBufSz]; 136 | 137 | char* urlEnd = url; 138 | urlEnd = stpcpy(urlEnd, marketBaseUrls[market]); 139 | urlEnd = stpcpy(urlEnd, query); 140 | strcpy(urlEnd, apiKey); 141 | 142 | Curl::CResponse response; 143 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 144 | curl_easy_setopt(curl, CURLOPT_URL, url); 145 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 146 | 147 | const CURLcode respCode = curl_easy_perform(curl); 148 | 149 | if (respCode != CURLE_OK) 150 | { 151 | Curl::PrintError(curl, respCode); 152 | return false; 153 | } 154 | 155 | rapidjson::Document parsed; 156 | parsed.ParseInsitu(response.data); 157 | 158 | if (parsed.HasParseError()) 159 | { 160 | putsnn("JSON parsing failed\n"); 161 | return false; 162 | } 163 | 164 | if (!parsed["success"].GetBool()) 165 | { 166 | putsnn("request unsucceeded\n"); 167 | return false; 168 | } 169 | 170 | const char* offerId = parsed["trade"].GetString(); 171 | const char* partnerUrl = parsed["profile"].GetString(); 172 | 173 | strcpy(outOfferId, offerId); 174 | stpncpy(outPartnerId64, partnerUrl + 36, strlen(partnerUrl + 36) - 1)[0] = '\0'; 175 | 176 | putsnn("ok\n"); 177 | return true; 178 | } 179 | 180 | bool RequestGiveBot(CURL* curl, const char* apiKey, int market, char* outTradeOfferId, char* outPartnerId64) 181 | { 182 | Log(LogChannel::MARKET, "[%s] Requesting details to send items...", marketNames[market]); 183 | 184 | const char query[] = "trade-request-give?key="; 185 | 186 | const size_t urlBufSz = marketBaseUrlMaxSz - 1 + sizeof(query) - 1 + apiKeySz + 1; 187 | char url[urlBufSz]; 188 | 189 | char* urlEnd = url; 190 | urlEnd = stpcpy(urlEnd, marketBaseUrls[market]); 191 | urlEnd = stpcpy(urlEnd, query); 192 | strcpy(urlEnd, apiKey); 193 | 194 | Curl::CResponse response; 195 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 196 | curl_easy_setopt(curl, CURLOPT_URL, url); 197 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 198 | 199 | const CURLcode respCode = curl_easy_perform(curl); 200 | 201 | if (respCode != CURLE_OK) 202 | { 203 | Curl::PrintError(curl, respCode); 204 | return false; 205 | } 206 | 207 | rapidjson::Document parsed; 208 | parsed.ParseInsitu(response.data); 209 | 210 | if (parsed.HasParseError()) 211 | { 212 | putsnn("JSON parsing failed\n"); 213 | return false; 214 | } 215 | 216 | if (!parsed["success"].GetBool()) 217 | { 218 | const auto iterError = parsed.FindMember("error"); 219 | if (iterError != parsed.MemberEnd() && !strcmp(iterError->value.GetString(), "nothing")) 220 | putsnn("nothing\n"); 221 | else 222 | putsnn("request unsucceeded\n"); 223 | 224 | return false; 225 | } 226 | 227 | const char* offerId = parsed["trade"].GetString(); 228 | const char* profile = parsed["profile"].GetString(); 229 | 230 | strcpy(outTradeOfferId, offerId); 231 | stpncpy(outPartnerId64, profile + 36, strlen(profile + 36) - 1)[0] = '\0'; 232 | 233 | putsnn("ok\n"); 234 | return true; 235 | } 236 | 237 | bool RequestGiveP2PAll(CURL* curl, const char* apiKey, int market, rapidjson::Document* outDoc) 238 | { 239 | Log(LogChannel::MARKET, "[%s] Requesting details to send items...", marketNames[market]); 240 | 241 | const char query[] = "trade-request-give-p2p-all?key="; 242 | 243 | const size_t urlBufSz = marketBaseUrlMaxSz - 1 + sizeof(query) - 1 + apiKeySz + 1; 244 | char url[urlBufSz]; 245 | 246 | char* urlEnd = url; 247 | urlEnd = stpcpy(urlEnd, marketBaseUrls[market]); 248 | urlEnd = stpcpy(urlEnd, query); 249 | strcpy(urlEnd, apiKey); 250 | 251 | Curl::CResponse response; 252 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 253 | curl_easy_setopt(curl, CURLOPT_URL, url); 254 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 255 | 256 | const CURLcode respCode = curl_easy_perform(curl); 257 | 258 | if (respCode != CURLE_OK) 259 | { 260 | Curl::PrintError(curl, respCode); 261 | return false; 262 | } 263 | 264 | outDoc->Parse(response.data); 265 | 266 | if (outDoc->HasParseError()) 267 | { 268 | putsnn("JSON parsing failed\n"); 269 | return false; 270 | } 271 | 272 | if (!(*outDoc)["success"].GetBool()) 273 | { 274 | const auto iterError = outDoc->FindMember("error"); 275 | if (iterError != outDoc->MemberEnd() && !strcmp(iterError->value.GetString(), "nothing")) 276 | putsnn("nothing\n"); 277 | else 278 | putsnn("request unsucceeded\n"); 279 | 280 | return false; 281 | } 282 | 283 | putsnn("ok\n"); 284 | return true; 285 | } 286 | 287 | bool GetProfileStatus(CURL* curl, const char* apiKey, rapidjson::Document* outDoc) 288 | { 289 | Log(LogChannel::MARKET, "Getting profile status..."); 290 | 291 | const char query[] = "test?key="; 292 | 293 | const size_t urlBufSz = marketBaseUrlMaxSz - 1 + sizeof(query) - 1 + apiKeySz + 1; 294 | char url[urlBufSz]; 295 | 296 | char* urlEnd = url; 297 | urlEnd = stpcpy(urlEnd, marketBaseUrls[(int)Market::CSGO]); 298 | urlEnd = stpcpy(urlEnd, query); 299 | strcpy(urlEnd, apiKey); 300 | 301 | Curl::CResponse response; 302 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 303 | curl_easy_setopt(curl, CURLOPT_URL, url); 304 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 305 | 306 | const CURLcode respCode = curl_easy_perform(curl); 307 | 308 | if (respCode != CURLE_OK) 309 | { 310 | Curl::PrintError(curl, respCode); 311 | return false; 312 | } 313 | 314 | outDoc->Parse(response.data); 315 | 316 | if (outDoc->HasParseError()) 317 | { 318 | putsnn("JSON parsing failed\n"); 319 | return false; 320 | } 321 | 322 | if (!(*outDoc)["success"].GetBool()) 323 | { 324 | putsnn("request unsucceeded\n"); 325 | return false; 326 | } 327 | 328 | putsnn("ok\n"); 329 | return true; 330 | } 331 | 332 | bool CanSell(CURL* curl, const char* apiKey) 333 | { 334 | rapidjson::Document docTest; 335 | if (!GetProfileStatus(curl, apiKey, &docTest)) 336 | return false; 337 | 338 | const rapidjson::Value& status = docTest["status"]; 339 | 340 | if (!status["steam_web_api_key"].GetBool()) 341 | { 342 | Log(LogChannel::MARKET, "Can't sell on the market: Steam web API key not set\n"); 343 | return false; 344 | } 345 | 346 | if (!status["user_token"].GetBool()) 347 | { 348 | Log(LogChannel::MARKET, "Can't sell on the market: Steam trade token not set\n"); 349 | return false; 350 | } 351 | 352 | if (!status["trade_check"].GetBool()) 353 | { 354 | Log(LogChannel::MARKET, "Can't sell on the market: trade check required - https://market.csgo.com/check\n"); 355 | return false; 356 | } 357 | 358 | if (!status["site_notmpban"].GetBool()) 359 | { 360 | Log(LogChannel::MARKET, "Can't sell on the market: banned\n"); 361 | return false; 362 | } 363 | 364 | return true; 365 | } 366 | 367 | bool SetSteamApiKey(CURL* curl, const char* marketApiKey, const char* steamApiKey) 368 | { 369 | Log(LogChannel::MARKET, "Setting Steam API key on the market..."); 370 | 371 | const char query[] = "set-steam-api-key?key="; 372 | const char querySteamApiKey[] = "&steam-api-key="; 373 | 374 | const size_t urlBufSz = 375 | marketBaseUrlMaxSz - 1 + 376 | sizeof(query) - 1 + apiKeySz + 377 | sizeof(querySteamApiKey) - 1 + Steam::apiKeyBufSz - 1 + 1; 378 | 379 | char url[urlBufSz]; 380 | 381 | char* urlEnd = url; 382 | urlEnd = stpcpy(urlEnd, marketBaseUrls[(int)Market::CSGO]); 383 | urlEnd = stpcpy(urlEnd, query); 384 | urlEnd = stpcpy(urlEnd, marketApiKey); 385 | urlEnd = stpcpy(urlEnd, querySteamApiKey); 386 | strcpy(urlEnd, steamApiKey); 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 SetSteamTradeToken(CURL* curl, const char* apiKey, const char* tradeToken) 421 | { 422 | Log(LogChannel::MARKET, "Setting Steam trade token on the market..."); 423 | 424 | const char query[] = "set-trade-token?key="; 425 | const char queryTradeToken[] = "&token="; 426 | 427 | const size_t urlBufSz = 428 | marketBaseUrlMaxSz - 1 + 429 | sizeof(query) - 1 + apiKeySz + 430 | sizeof(queryTradeToken) - 1 + Steam::Trade::tokenBufSz - 1 + 1; 431 | 432 | char url[urlBufSz]; 433 | 434 | char* urlEnd = url; 435 | urlEnd = stpcpy(urlEnd, marketBaseUrls[(int)Market::CSGO]); 436 | urlEnd = stpcpy(urlEnd, query); 437 | urlEnd = stpcpy(urlEnd, apiKey); 438 | urlEnd = stpcpy(urlEnd, queryTradeToken); 439 | strcpy(urlEnd, tradeToken); 440 | 441 | Curl::CResponse response; 442 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 443 | curl_easy_setopt(curl, CURLOPT_URL, url); 444 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 445 | 446 | const CURLcode respCode = curl_easy_perform(curl); 447 | 448 | if (respCode != CURLE_OK) 449 | { 450 | Curl::PrintError(curl, respCode); 451 | return false; 452 | } 453 | 454 | rapidjson::Document parsed; 455 | parsed.ParseInsitu(response.data); 456 | 457 | if (parsed.HasParseError()) 458 | { 459 | putsnn("JSON parsing failed\n"); 460 | return false; 461 | } 462 | 463 | if (!parsed["success"].GetBool()) 464 | { 465 | putsnn("request unsucceeded\n"); 466 | return false; 467 | } 468 | 469 | putsnn("ok\n"); 470 | return true; 471 | } 472 | 473 | // steam login token must be set when calling this 474 | bool SetSteamDetails(CURL* curl, const char* apiKey, const char* steamApiKey) 475 | { 476 | rapidjson::Document docTest; 477 | if (!GetProfileStatus(curl, apiKey, &docTest)) 478 | return false; 479 | 480 | const rapidjson::Value& status = docTest["status"]; 481 | 482 | if (!status["steam_web_api_key"].GetBool()) 483 | { 484 | // this fails on the first try sometimes, don't know why 485 | if (!SetSteamApiKey(curl, apiKey, steamApiKey) && !SetSteamApiKey(curl, apiKey, steamApiKey)) 486 | return false; 487 | } 488 | 489 | if (!status["user_token"].GetBool()) 490 | { 491 | char tradeToken[Steam::Trade::tokenBufSz]; 492 | if (!Steam::Trade::GetToken(curl, tradeToken) || !SetSteamTradeToken(curl, apiKey, tradeToken)) 493 | return false; 494 | } 495 | 496 | return true; 497 | } 498 | 499 | // unused 500 | bool GoOffline(CURL* curl, const char* apiKey) 501 | { 502 | Log(LogChannel::MARKET, "Going offline..."); 503 | 504 | const char query[] = "go-offline?key="; 505 | 506 | const size_t urlBufSz = 507 | marketBaseUrlMaxSz - 1 + 508 | sizeof(query) - 1 + apiKeySz + 1; 509 | 510 | char url[urlBufSz]; 511 | 512 | char* urlEnd = url; 513 | urlEnd = stpcpy(urlEnd, marketBaseUrls[(int)Market::CSGO]); 514 | urlEnd = stpcpy(urlEnd, query); 515 | strcpy(urlEnd, apiKey); 516 | 517 | Curl::CResponse response; 518 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 519 | curl_easy_setopt(curl, CURLOPT_URL, url); 520 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 521 | 522 | const CURLcode respCode = curl_easy_perform(curl); 523 | 524 | if (respCode != CURLE_OK) 525 | { 526 | Curl::PrintError(curl, respCode); 527 | return false; 528 | } 529 | 530 | rapidjson::Document parsed; 531 | parsed.ParseInsitu(response.data); 532 | 533 | if (parsed.HasParseError()) 534 | { 535 | putsnn("JSON parsing failed\n"); 536 | return false; 537 | } 538 | 539 | if (!parsed["success"].GetBool()) 540 | { 541 | putsnn("request unsucceeded\n"); 542 | return false; 543 | } 544 | 545 | putsnn("ok\n"); 546 | return true; 547 | } 548 | } -------------------------------------------------------------------------------- /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 | enum class LogChannel 10 | { 11 | GENERAL, 12 | LIBCURL, 13 | STEAM, 14 | MARKET 15 | }; 16 | 17 | const char* logChannelNames[] = 18 | { 19 | "", 20 | "libcurl", 21 | "Steam", 22 | "Market" 23 | }; 24 | 25 | void Log(LogChannel channel, const char* format, ...) 26 | { 27 | const time_t timestamp = time(nullptr); 28 | 29 | // zh_CN.utf8 locale's time on linux looks like this 2022年10月18日 15时08分28秒 30 | // so allocate some space 31 | char dateTime[64]; 32 | 33 | #ifdef _WIN32 34 | // windows didn't support utf-8 codepages until recently, so map UTF-16 to UTF-8 instead 35 | const size_t wideDatatimeLen = sizeof(dateTime); 36 | wchar_t wideDatetime[wideDatatimeLen]; 37 | wcsftime(wideDatetime, wideDatatimeLen, L"%x %X", localtime(×tamp)); 38 | 39 | if (!WideCharToMultiByte(CP_UTF8, 0, wideDatetime, -1, dateTime, sizeof(dateTime), NULL, NULL)) 40 | strcpy(dateTime, "timestamp UTF-16 to UTF-8 mapping failed"); 41 | 42 | #else 43 | strftime(dateTime, sizeof(dateTime), "%x %X", localtime(×tamp)); 44 | #endif // _WIN32 45 | 46 | printf("[%s] ", dateTime); 47 | 48 | if (channel != LogChannel::GENERAL) 49 | printf("[%s] ", logChannelNames[(size_t)channel]); 50 | 51 | va_list args; 52 | va_start(args, format); 53 | vprintf(format, args); 54 | va_end(args); 55 | } 56 | 57 | #ifdef _WIN32 58 | void FlashCurrentWindow() 59 | { 60 | static const HWND hWnd = GetConsoleWindow(); 61 | if (!hWnd) return; 62 | FlashWindow(hWnd, TRUE); 63 | } 64 | #endif 65 | 66 | void SetStdinEcho(bool enable) 67 | { 68 | #ifdef _WIN32 69 | const HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE); 70 | DWORD mode; 71 | GetConsoleMode(hStdin, &mode); 72 | 73 | if (!enable) 74 | mode &= ~ENABLE_ECHO_INPUT; 75 | else 76 | mode |= ENABLE_ECHO_INPUT; 77 | 78 | SetConsoleMode(hStdin, mode); 79 | 80 | #else 81 | termios tty; 82 | tcgetattr(STDIN_FILENO, &tty); 83 | 84 | if (!enable) 85 | tty.c_lflag &= ~ECHO; 86 | else 87 | tty.c_lflag |= ECHO; 88 | 89 | tcsetattr(STDIN_FILENO, TCSANOW, &tty); 90 | #endif // _WIN32 91 | } 92 | 93 | bool GetUserInputString(const char* msg, char* buf, size_t bufSz, size_t minLen = 1, bool echoStdin = true) 94 | { 95 | const size_t maxLen = bufSz - 1; 96 | 97 | if (!echoStdin) 98 | SetStdinEcho(false); 99 | 100 | #ifdef _WIN32 101 | FlashCurrentWindow(); 102 | 103 | wchar_t* wideBuf = (wchar_t*)malloc(bufSz * sizeof(wchar_t)); 104 | if (!wideBuf) 105 | { 106 | Log(LogChannel::GENERAL, "Wide input buffer allocation failed\n"); 107 | return false; 108 | } 109 | #endif 110 | 111 | while (true) 112 | { 113 | if (1 < minLen) 114 | { 115 | if (minLen == maxLen) 116 | Log(LogChannel::GENERAL, "%s (%u bytes): ", msg, maxLen); 117 | else 118 | Log(LogChannel::GENERAL, "%s (%u-%u bytes): ", msg, minLen, maxLen); 119 | } 120 | else 121 | Log(LogChannel::GENERAL, "%s (%u bytes max): ", msg, maxLen); 122 | 123 | size_t len = 0; 124 | 125 | #ifdef _WIN32 126 | wint_t c; 127 | while ((c = getwchar()) != L'\n' && c != WEOF) 128 | #else 129 | // if the byte is a part of UTF-8 char it would have 8th bit set 130 | // therefore we can't accidentally find newline 131 | int c; 132 | while ((c = getchar()) != '\n' && c != EOF) 133 | #endif // _WIN32 134 | { 135 | if (len <= maxLen) 136 | #ifdef _WIN32 137 | wideBuf[len] = c; 138 | #else 139 | buf[len] = c; 140 | #endif // _WIN32 141 | 142 | ++len; 143 | } 144 | 145 | if (!echoStdin) 146 | putchar('\n'); 147 | 148 | if (minLen > len || len > maxLen) 149 | continue; 150 | 151 | #ifdef _WIN32 152 | wideBuf[len] = L'\0'; 153 | #else 154 | buf[len] = '\0'; 155 | #endif // _WIN32 156 | 157 | #ifdef _WIN32 158 | if (!WideCharToMultiByte(CP_UTF8, 0, wideBuf, len + 1, buf, bufSz, NULL, NULL)) 159 | { 160 | Log(LogChannel::GENERAL, "Input UTF-16 to UTF-8 mapping failed\n"); 161 | continue; 162 | } 163 | #endif // _WIN32 164 | 165 | break; 166 | } 167 | 168 | #ifdef _WIN32 169 | free(wideBuf); 170 | #endif // _WIN32 171 | 172 | if (!echoStdin) 173 | SetStdinEcho(true); 174 | 175 | return true; 176 | } 177 | 178 | #ifdef _WIN32 179 | 180 | inline void* mempcpy(void* dest, const void* src, size_t size) 181 | { 182 | return (char*)memcpy(dest, src, size) + size; 183 | } 184 | 185 | inline char* stpcpy(char* dest, const char* src) 186 | { 187 | const size_t len = strlen(src); 188 | return (char*)memcpy(dest, src, len + 1) + len; 189 | } 190 | 191 | inline char* stpncpy(char* dest, const char* src, size_t count) 192 | { 193 | const size_t len = strnlen(src, count); 194 | memcpy(dest, src, len); 195 | dest += len; 196 | if (len == count) 197 | return dest; 198 | return (char*)memset(dest, '\0', count - len); 199 | } 200 | 201 | // windows utf-8 fopen 202 | FILE* u8fopen(const char* path, const char* mode) 203 | { 204 | const size_t widePathLen = PATH_MAX; 205 | wchar_t widePath[widePathLen]; 206 | 207 | const size_t wideModeLen = 32; 208 | wchar_t wideMode[widePathLen]; 209 | 210 | if (!MultiByteToWideChar(CP_UTF8, 0, path, -1, widePath, widePathLen) || 211 | !MultiByteToWideChar(CP_UTF8, 0, mode, -1, wideMode, wideModeLen)) 212 | return nullptr; 213 | 214 | return _wfopen(widePath, wideMode); 215 | } 216 | 217 | #else 218 | 219 | inline FILE* u8fopen(const char* path, const char* mode) 220 | { 221 | return fopen(path, mode); 222 | } 223 | 224 | #endif // _WIN32 225 | 226 | // writes pointer to file contents heap to output 227 | bool ReadFile(const char* path, unsigned char** out, long* outSz) 228 | { 229 | FILE* file = u8fopen(path, "rb"); 230 | if (!file) 231 | return false; 232 | 233 | long fsize; 234 | 235 | if (fseek(file, 0, SEEK_END) || 236 | ((fsize = ftell(file)) == -1L) || 237 | fseek(file, 0, SEEK_SET)) 238 | { 239 | fclose(file); 240 | return false; 241 | } 242 | 243 | unsigned char* contents = (unsigned char*)malloc(fsize); 244 | if (!contents) 245 | { 246 | fclose(file); 247 | return false; 248 | } 249 | 250 | if (fread(contents, sizeof(unsigned char), fsize, file) != (size_t)fsize) 251 | { 252 | fclose(file); 253 | free(contents); 254 | return false; 255 | } 256 | 257 | fclose(file); 258 | 259 | *out = contents; 260 | *outSz = fsize; 261 | 262 | return true; 263 | } 264 | 265 | // get executable dir 266 | const char* GetExeDir() 267 | { 268 | static char dir[PATH_MAX] = { 0 }; 269 | if (!dir[0]) 270 | { 271 | #ifdef _WIN32 272 | const size_t wideDirLen = sizeof(dir); 273 | wchar_t wideDir[wideDirLen]; 274 | if (!GetModuleFileNameW(NULL, wideDir, wideDirLen)) 275 | return nullptr; 276 | 277 | if (!WideCharToMultiByte(CP_UTF8, 0, wideDir, -1, dir, sizeof(dir), NULL, NULL)) 278 | return nullptr; 279 | 280 | // if the byte is a part of UTF-8 char it would have 8th bit set 281 | // therefore we can't accidentally find backslash 282 | char* del = strrchr(dir, '\\'); 283 | #else 284 | if (readlink("/proc/self/exe", dir, sizeof(dir)) == -1) 285 | return nullptr; 286 | 287 | char* del = strrchr(dir, '/'); 288 | #endif // _WIN32 289 | 290 | if (del) 291 | *(del + 1) = '\0'; 292 | } 293 | return dir; 294 | } 295 | 296 | void Pause() 297 | { 298 | #ifdef _WIN32 299 | // check if stdout isn't a terminal 300 | if (!_isatty(_fileno(stdout))) 301 | return; 302 | 303 | FlashCurrentWindow(); 304 | 305 | // check if run by doubleclicking the executable 306 | if (getenv("PROMPT")) 307 | return; 308 | 309 | putsnn("Press Enter to continue\n"); 310 | 311 | wint_t c; 312 | while ((c = getwchar()) != L'\n' && c != WEOF); 313 | 314 | #endif // _WIN32 315 | } 316 | 317 | inline uint64_t SteamID32To64(uint32_t id32) 318 | { 319 | return (id32 | 0x110000100000000); 320 | } 321 | 322 | inline uint32_t SteamID64To32(uint64_t id64) 323 | { 324 | return (id64 & 0xFFFFFFFF); 325 | } 326 | 327 | inline uint32_t byteswap32(uint32_t dw) 328 | { 329 | uint32_t res; 330 | 331 | res = dw >> 24; 332 | res |= ((dw & 0x00FF0000) >> 8); 333 | res |= ((dw & 0x0000FF00) << 8); 334 | res |= ((dw & 0x000000FF) << 24); 335 | 336 | return res; 337 | } 338 | 339 | inline uint64_t byteswap64(uint64_t qw) 340 | { 341 | uint64_t res; 342 | 343 | res = qw >> 56; 344 | res |= ((qw & 0x00FF000000000000ull) >> 40); 345 | res |= ((qw & 0x0000FF0000000000ull) >> 24); 346 | res |= ((qw & 0x000000FF00000000ull) >> 8); 347 | res |= ((qw & 0x00000000FF000000ull) << 8); 348 | res |= ((qw & 0x0000000000FF0000ull) << 24); 349 | res |= ((qw & 0x000000000000FF00ull) << 40); 350 | res |= ((qw & 0x00000000000000FFull) << 56); 351 | 352 | return res; 353 | } 354 | 355 | void ClearConsole() 356 | { 357 | #ifdef _WIN32 358 | // is stdout a terminal 359 | if (!_isatty(_fileno(stdout))) 360 | return; 361 | 362 | const HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE); 363 | 364 | // Get the number of character cells in the current buffer. 365 | CONSOLE_SCREEN_BUFFER_INFO csbi; 366 | if (!GetConsoleScreenBufferInfo(hStdout, &csbi)) 367 | return; 368 | 369 | // Scroll the rectangle of the entire buffer. 370 | SMALL_RECT scrollRect; 371 | scrollRect.Left = 0; 372 | scrollRect.Top = 0; 373 | scrollRect.Right = csbi.dwSize.X; 374 | scrollRect.Bottom = csbi.dwSize.Y; 375 | 376 | // Scroll it upwards off the top of the buffer with a magnitude of the entire height. 377 | COORD scrollTarget; 378 | scrollTarget.X = 0; 379 | scrollTarget.Y = (SHORT)(0 - csbi.dwSize.Y); 380 | 381 | // Fill with empty spaces with the buffer's default text attribute. 382 | CHAR_INFO fill; 383 | fill.Char.AsciiChar = ' '; 384 | fill.Attributes = csbi.wAttributes; 385 | 386 | // Do the scroll 387 | ScrollConsoleScreenBufferA(hStdout, &scrollRect, NULL, scrollTarget, &fill); 388 | 389 | // Move the cursor to the top left corner too. 390 | csbi.dwCursorPosition.X = 0; 391 | csbi.dwCursorPosition.Y = 0; 392 | 393 | SetConsoleCursorPosition(hStdout, csbi.dwCursorPosition); 394 | #else 395 | 396 | putsnn("\x1b[H\x1b[J\x1b[3J"); 397 | 398 | #endif // _WIN32 399 | } -------------------------------------------------------------------------------- /src/Precompiled.cpp: -------------------------------------------------------------------------------- 1 | #include "Precompiled.h" -------------------------------------------------------------------------------- /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 | 15 | #ifdef _WIN32 16 | 17 | // target windows 7 18 | #define _WIN32_WINNT 0x0601 19 | #define WIN32_LEAN_AND_MEAN 20 | #define NOMINMAX 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #ifndef PATH_MAX 29 | #define PATH_MAX MAX_PATH 30 | #endif // !PATH_MAX 31 | 32 | #define WOLFSSL_LIB 33 | #include 34 | #undef WOLFSSL_LIB 35 | 36 | #else 37 | 38 | #include 39 | #include 40 | 41 | #include 42 | 43 | #endif // _WIN32 44 | 45 | #include "rapidjson/document.h" 46 | #include "rapidjson/writer.h" 47 | #include "rapidjson/stringbuffer.h" 48 | #include "wolfssl/wolfcrypt/error-crypt.h" 49 | #include "wolfssl/wolfcrypt/coding.h" 50 | #include "wolfssl/wolfcrypt/rsa.h" 51 | #include "wolfssl/wolfcrypt/aes.h" 52 | #include "wolfssl/wolfcrypt/pwdbased.h" 53 | #include "wolfssl/wolfcrypt/hmac.h" 54 | #include "wolfssl/version.h" 55 | #include "curl/curl.h" 56 | 57 | using namespace std::chrono_literals; 58 | 59 | #define UINT32_MAX_STR_SIZE sizeof("4294967295") 60 | #define UINT64_MAX_STR_SIZE sizeof("18446744073709551615") -------------------------------------------------------------------------------- /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, byte* outHexModulus, byte* outHexExponent, char* outTimestamp) 39 | { 40 | Log(LogChannel::STEAM, "Getting password RSA public key..."); 41 | 42 | const char postFieldUsername[] = "username="; 43 | 44 | // multiple by 3 due to URL encoding 45 | const size_t postFieldsBufSz = sizeof(postFieldUsername) - 1 + (usernameBufSz - 1) * 3 + 1; 46 | char postFields[postFieldsBufSz]; 47 | 48 | char* postFieldsEnd = postFields; 49 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldUsername); 50 | strcpy(postFieldsEnd, escUsername); 51 | 52 | Curl::CResponse response; 53 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 54 | curl_easy_setopt(curl, CURLOPT_URL, "https://steamcommunity.com/login/getrsakey/"); 55 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 56 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 57 | 58 | const CURLcode respCode = curl_easy_perform(curl); 59 | 60 | if (respCode != CURLE_OK) 61 | { 62 | Curl::PrintError(curl, respCode); 63 | return false; 64 | } 65 | 66 | rapidjson::Document parsed; 67 | parsed.ParseInsitu(response.data); 68 | 69 | if (parsed.HasParseError()) 70 | { 71 | putsnn("JSON parsing failed\n"); 72 | return false; 73 | } 74 | 75 | if (!parsed["success"].GetBool()) 76 | { 77 | putsnn("request unsucceeded\n"); 78 | return false; 79 | } 80 | 81 | memcpy(outHexModulus, parsed["publickey_mod"].GetString(), modulusSz * 2); 82 | memcpy(outHexExponent, parsed["publickey_exp"].GetString(), exponentSz * 2); 83 | strcpy(outTimestamp, parsed["timestamp"].GetString()); 84 | 85 | putsnn("ok\n"); 86 | return true; 87 | } 88 | 89 | // out buffer size must be at least PlainToBase64Size(sizeof(modulus), WC_NO_NL_ENC) 90 | bool EncryptPassword(const char* password, const byte* modulus, const byte* exponent, byte* out, word32* outSz) 91 | { 92 | Log(LogChannel::STEAM, "Encrypting password..."); 93 | 94 | RsaKey pubKey; 95 | if (wc_InitRsaKey(&pubKey, nullptr)) 96 | { 97 | putsnn("RSA init failed\n"); 98 | return false; 99 | } 100 | 101 | if (wc_RsaPublicKeyDecodeRaw(modulus, modulusSz, exponent, exponentSz, &pubKey)) 102 | { 103 | wc_FreeRsaKey(&pubKey); 104 | putsnn("RSA public key decoding failed\n"); 105 | return false; 106 | } 107 | 108 | WC_RNG rng; 109 | 110 | if (wc_InitRng(&rng)) 111 | { 112 | wc_FreeRsaKey(&pubKey); 113 | putsnn("RNG init failed\n"); 114 | return false; 115 | } 116 | 117 | byte encrypted[modulusSz]; 118 | 119 | const int encryptedSz = wc_RsaPublicEncrypt((byte*)password, strlen(password), encrypted, sizeof(encrypted), &pubKey, &rng); 120 | 121 | const bool success = ((0 <= encryptedSz) && !Base64_Encode_NoNl(encrypted, encryptedSz, out, outSz)); 122 | 123 | wc_FreeRsaKey(&pubKey); 124 | wc_FreeRng(&rng); 125 | 126 | putsnn(success ? "ok\n" : "fail\n"); 127 | return success; 128 | } 129 | 130 | // outSteamId64 buffer size must be at least UINT64_MAX_STR_SIZE 131 | // outOAuthToken buffer size must be at least oauthTokenBufSz 132 | // outLoginToken buffer size must be at least loginTokenBufSz 133 | LoginResult DoLogin(CURL* curl, const char* username, const char* password, const char* twoFactorCode, char* outSteamId64, char* outOAuthToken, char* outLoginToken) 134 | { 135 | char* escUsername = curl_easy_escape(curl, username, 0); 136 | 137 | byte rsaHexModulus[modulusSz * 2]; 138 | byte rsaHexExponent[exponentSz * 2]; 139 | char rsaTimestamp[timestampBufSz]; 140 | 141 | if (!GetPasswordRSAPublicKey(curl, escUsername, rsaHexModulus, rsaHexExponent, rsaTimestamp)) 142 | { 143 | curl_free(escUsername); 144 | return LoginResult::GET_PASS_ENCRYPT_KEY_FAILED; 145 | } 146 | 147 | Log(LogChannel::STEAM, "Decoding password RSA public key..."); 148 | 149 | byte rsaModulus[modulusSz]; 150 | word32 rsaModSz = sizeof(rsaModulus); 151 | 152 | byte rsaExponent[exponentSz]; 153 | word32 rsaExpSz = sizeof(rsaExponent); 154 | 155 | if (Base16_Decode(rsaHexModulus, sizeof(rsaHexModulus), rsaModulus, &rsaModSz) || 156 | Base16_Decode(rsaHexExponent, sizeof(rsaHexExponent), rsaExponent, &rsaExpSz)) 157 | { 158 | curl_free(escUsername); 159 | putsnn("fail\n"); 160 | return LoginResult::GET_PASS_ENCRYPT_KEY_FAILED; 161 | } 162 | 163 | putsnn("ok\n"); 164 | 165 | constexpr size_t encryptedPassBufSz = PlainToBase64Size(sizeof(rsaModulus), WC_NO_NL_ENC); 166 | byte encryptedPass[encryptedPassBufSz]; 167 | word32 encryptedPassSz = sizeof(encryptedPass); 168 | 169 | if (!EncryptPassword(password, rsaModulus, rsaExponent, encryptedPass, &encryptedPassSz)) 170 | { 171 | curl_free(escUsername); 172 | return LoginResult::PASS_ENCRYPT_FAILED; 173 | } 174 | 175 | char captchaAnswer[Captcha::answerBufSz] = ""; 176 | char captchaGid[Captcha::gidBufSz]; 177 | 178 | if (!Captcha::GetGID(curl, captchaGid) || 179 | (strcmp(captchaGid, "-1") && !Captcha::GetAnswer(curl, captchaGid, captchaAnswer))) 180 | { 181 | curl_free(escUsername); 182 | return LoginResult::CAPTCHA_FAILED; 183 | } 184 | 185 | Log(LogChannel::STEAM, "Logging in..."); 186 | 187 | char* escEncryptedPass = curl_easy_escape(curl, (char*)encryptedPass, encryptedPassSz); 188 | char* escCaptchaAnswer = curl_easy_escape(curl, captchaAnswer, 0); 189 | 190 | const char postFieldUsername[] = 191 | "oauth_client_id=DE45CD61" 192 | "&oauth_scope=read_profile%20write_profile%20read_client%20write_client" 193 | "&remember_login=true" 194 | "&username="; 195 | 196 | const char postFieldPassword[] = "&password="; 197 | const char postFieldRsaTimestamp[] = "&rsatimestamp="; 198 | const char postField2FACode[] = "&twofactorcode="; 199 | const char postFieldCaptchaGid[] = "&captchagid="; 200 | const char postFieldCaptchaAnswer[] = "&captcha_text="; 201 | 202 | const size_t postFieldsBufSz = 203 | sizeof(postFieldUsername) - 1 + (usernameBufSz - 1) * 3 + // multiple by 3 due to URL encoding 204 | sizeof(postFieldPassword) - 1 + encryptedPassBufSz * 3 + 205 | sizeof(postFieldRsaTimestamp) - 1 + timestampBufSz - 1 + 206 | sizeof(postField2FACode) - 1 + Guard::twoFactorCodeBufSz - 1 + 207 | sizeof(postFieldCaptchaGid) - 1 + Captcha::gidBufSz - 1 + 208 | sizeof(postFieldCaptchaAnswer) - 1 + (Captcha::answerBufSz - 1) * 3 + 1; 209 | 210 | char postFields[postFieldsBufSz]; 211 | 212 | char* postFieldsEnd = postFields; 213 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldUsername); 214 | postFieldsEnd = stpcpy(postFieldsEnd, escUsername); 215 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldPassword); 216 | postFieldsEnd = stpcpy(postFieldsEnd, escEncryptedPass); 217 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldRsaTimestamp); 218 | postFieldsEnd = stpcpy(postFieldsEnd, rsaTimestamp); 219 | postFieldsEnd = stpcpy(postFieldsEnd, postField2FACode); 220 | postFieldsEnd = stpcpy(postFieldsEnd, twoFactorCode); 221 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldCaptchaGid); 222 | postFieldsEnd = stpcpy(postFieldsEnd, captchaGid); 223 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldCaptchaAnswer); 224 | strcpy(postFieldsEnd, escCaptchaAnswer); 225 | 226 | curl_free(escUsername); 227 | curl_free(escEncryptedPass); 228 | curl_free(escCaptchaAnswer); 229 | 230 | Curl::CResponse response; 231 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 232 | curl_easy_setopt(curl, CURLOPT_URL, "https://steamcommunity.com/login/dologin/"); 233 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 234 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 235 | curl_easy_setopt(curl, CURLOPT_COOKIE, "mobileClient=android"); 236 | 237 | const CURLcode respCode = curl_easy_perform(curl); 238 | 239 | curl_easy_setopt(curl, CURLOPT_COOKIE, NULL); 240 | 241 | if (respCode != CURLE_OK) 242 | { 243 | Curl::PrintError(curl, respCode); 244 | return LoginResult::REQUEST_FAILED; 245 | } 246 | 247 | rapidjson::Document parsed; 248 | parsed.ParseInsitu(response.data); 249 | 250 | if (parsed.HasParseError()) 251 | { 252 | putsnn("JSON parsing failed\n"); 253 | return LoginResult::REQUEST_FAILED; 254 | } 255 | 256 | if (!parsed["success"].GetBool()) 257 | { 258 | const auto iterRequires2FA = parsed.FindMember("requires_twofactor"); 259 | if (iterRequires2FA != parsed.MemberEnd() && iterRequires2FA->value.GetBool()) 260 | { 261 | putsnn("wrong two factor code\n"); 262 | return LoginResult::WRONG_TWO_FACTOR; 263 | } 264 | 265 | const auto iterCaptchaNeeded = parsed.FindMember("captcha_needed"); 266 | if (iterCaptchaNeeded != parsed.MemberEnd() && iterCaptchaNeeded->value.GetBool()) 267 | { 268 | putsnn("wrong captcha answer\n"); 269 | return LoginResult::WRONG_CAPTCHA; 270 | } 271 | 272 | const auto iterMessage = parsed.FindMember("message"); 273 | if (iterMessage != parsed.MemberEnd()) 274 | { 275 | const char* msg = iterMessage->value.GetString(); 276 | if (msg[0]) 277 | { 278 | puts(msg); // we need newline 279 | return LoginResult::UNSUCCEDED; 280 | } 281 | } 282 | 283 | putsnn("request unsucceeded\n"); 284 | return LoginResult::UNSUCCEDED; 285 | } 286 | 287 | const auto iterOAuth = parsed.FindMember("oauth"); 288 | if (iterOAuth == parsed.MemberEnd()) 289 | { 290 | putsnn("OAuth not found\n"); 291 | return LoginResult::OAUTH_FAILED; 292 | } 293 | 294 | rapidjson::Document parsedOAuth; 295 | parsedOAuth.Parse(iterOAuth->value.GetString()); 296 | 297 | if (parsedOAuth.HasParseError()) 298 | { 299 | putsnn("JSON parsing failed\n"); 300 | return LoginResult::OAUTH_FAILED; 301 | } 302 | 303 | const char* steamId64 = parsedOAuth["steamid"].GetString(); 304 | const char* oauthToken = parsedOAuth["oauth_token"].GetString(); 305 | const char* loginToken = parsedOAuth["wgtoken_secure"].GetString(); 306 | 307 | strcpy(outSteamId64, steamId64); 308 | strcpy(outOAuthToken, oauthToken); 309 | strcpy(outLoginToken, loginToken); 310 | 311 | putsnn("ok\n"); 312 | return LoginResult::SUCCESS; 313 | } 314 | 315 | // outLoginToken buffer size must be at least loginTokenBufSz 316 | int RefreshOAuthSession(CURL* curl, const char* oauthToken, char* outLoginToken) 317 | { 318 | const char postFieldAccessToken[] = "access_token="; 319 | 320 | const size_t postFieldsBufSz = sizeof(postFieldAccessToken) - 1 + oauthTokenBufSz - 1 + 1; 321 | char postFields[postFieldsBufSz]; 322 | 323 | char* postFieldsEnd = postFields; 324 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldAccessToken); 325 | strcpy(postFieldsEnd, oauthToken); 326 | 327 | Curl::CResponse response; 328 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 329 | curl_easy_setopt(curl, CURLOPT_URL, "https://api.steampowered.com/IMobileAuthService/GetWGToken/v1/"); 330 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 331 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 332 | 333 | const CURLcode respCode = curl_easy_perform(curl); 334 | 335 | if (respCode != CURLE_OK) 336 | { 337 | if (respCode == CURLE_HTTP_RETURNED_ERROR) 338 | { 339 | long httpCode; 340 | curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); 341 | if (httpCode == 401) // unauthorized 342 | { 343 | Log(LogChannel::STEAM, "Refreshing OAuth session failed: OAuth token is invalid or has expired\n"); 344 | return 0; 345 | } 346 | } 347 | 348 | Log(LogChannel::STEAM, "Refreshing OAuth session failed: "); 349 | Curl::PrintError(curl, respCode); 350 | return -1; 351 | } 352 | 353 | rapidjson::Document parsed; 354 | parsed.ParseInsitu(response.data); 355 | 356 | if (parsed.HasParseError()) 357 | { 358 | Log(LogChannel::STEAM, "Refreshing OAuth session failed: JSON parsing failed\n"); 359 | return -1; 360 | } 361 | 362 | const auto iterResponse = parsed.FindMember("response"); 363 | if (iterResponse == parsed.MemberEnd() || iterResponse->value.ObjectEmpty()) 364 | { 365 | Log(LogChannel::STEAM, "Refreshing OAuth session failed: request unsucceeded\n"); 366 | return -1; 367 | } 368 | 369 | const char* loginToken = iterResponse->value["token_secure"].GetString(); 370 | 371 | strcpy(outLoginToken, loginToken); 372 | 373 | return 1; 374 | } 375 | 376 | 377 | 378 | // outHexModulus buffer size must be at least modulusSz * 2 379 | // outHexExponent buffer size must be at least exponentSz * 2 380 | // outTimestamp buffer size must be at least timestampBufSz 381 | bool GetPasswordRSAPublicKeyJWT(CURL* curl, const char* escUsername, byte* outHexModulus, byte* outHexExponent, char* outTimestamp) 382 | { 383 | Log(LogChannel::STEAM, "Getting password RSA public key..."); 384 | 385 | const char urlStart[] = "https://api.steampowered.com/IAuthenticationService/GetPasswordRSAPublicKey/v1/?account_name="; 386 | 387 | // multiple by 3 due to URL encoding 388 | const size_t urlBufSz = sizeof(urlStart) - 1 + (usernameBufSz - 1) * 3 + 1; 389 | char url[urlBufSz]; 390 | 391 | char* urlEnd = url; 392 | urlEnd = stpcpy(urlEnd, urlStart); 393 | strcpy(urlEnd, escUsername); 394 | 395 | Curl::CResponse response; 396 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 397 | curl_easy_setopt(curl, CURLOPT_URL, url); 398 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 399 | 400 | const CURLcode respCode = curl_easy_perform(curl); 401 | 402 | if (respCode != CURLE_OK) 403 | { 404 | Curl::PrintError(curl, respCode); 405 | return false; 406 | } 407 | 408 | rapidjson::Document parsed; 409 | parsed.ParseInsitu(response.data); 410 | 411 | if (parsed.HasParseError()) 412 | { 413 | putsnn("JSON parsing failed\n"); 414 | return false; 415 | } 416 | 417 | const auto iterResponse = parsed.FindMember("response"); 418 | if (iterResponse == parsed.MemberEnd() || iterResponse->value.ObjectEmpty()) 419 | { 420 | putsnn("request unsucceeded\n"); 421 | return false; 422 | } 423 | 424 | const char* mod = iterResponse->value["publickey_mod"].GetString(); 425 | const char* exp = iterResponse->value["publickey_exp"].GetString(); 426 | const char* timestamp = iterResponse->value["timestamp"].GetString(); 427 | 428 | memcpy(outHexModulus, mod, modulusSz * 2); 429 | memcpy(outHexExponent, exp, exponentSz * 2); 430 | strcpy(outTimestamp, timestamp); 431 | 432 | putsnn("ok\n"); 433 | return true; 434 | } 435 | 436 | // outSteamId64 buffer size must be at least UINT64_MAX_STR_SIZE 437 | // outClientId buffer size must be at least clientIdBufSz 438 | // outRequestId buffer size must be at least requestIdBufSz 439 | bool BeginAuthSessionViaCredentials(CURL* curl, const char* username, const char* password, char* outSteamId64, char* outClientId, char* outRequestId) 440 | { 441 | byte rsaHexModulus[modulusSz * 2]; 442 | byte rsaHexExponent[exponentSz * 2]; 443 | char rsaTimestamp[timestampBufSz]; 444 | 445 | char* escUsername = curl_easy_escape(curl, username, 0); 446 | 447 | if (!GetPasswordRSAPublicKeyJWT(curl, escUsername, rsaHexModulus, rsaHexExponent, rsaTimestamp)) 448 | { 449 | curl_free(escUsername); 450 | return false; 451 | } 452 | 453 | Log(LogChannel::STEAM, "Decoding password RSA public key..."); 454 | 455 | byte rsaModulus[modulusSz]; 456 | word32 rsaModSz = sizeof(rsaModulus); 457 | 458 | byte rsaExponent[exponentSz]; 459 | word32 rsaExpSz = sizeof(rsaExponent); 460 | 461 | if (Base16_Decode(rsaHexModulus, sizeof(rsaHexModulus), rsaModulus, &rsaModSz) || 462 | Base16_Decode(rsaHexExponent, sizeof(rsaHexExponent), rsaExponent, &rsaExpSz)) 463 | { 464 | curl_free(escUsername); 465 | putsnn("fail\n"); 466 | return false; 467 | } 468 | 469 | putsnn("ok\n"); 470 | 471 | constexpr size_t encryptedPassBufSz = PlainToBase64Size(sizeof(rsaModulus), WC_NO_NL_ENC); 472 | byte encryptedPass[encryptedPassBufSz]; 473 | word32 encryptedPassSz = sizeof(encryptedPass); 474 | 475 | if (!EncryptPassword(password, rsaModulus, rsaExponent, encryptedPass, &encryptedPassSz)) 476 | { 477 | curl_free(escUsername); 478 | return false; 479 | } 480 | 481 | Log(LogChannel::STEAM, "Beginning auth session..."); 482 | 483 | char* escEncryptedPass = curl_easy_escape(curl, (char*)encryptedPass, encryptedPassSz); 484 | 485 | const char postFieldAccountName[] = "persistence=1&account_name="; 486 | const char postFieldEncryptedPass[] = "&encrypted_password="; 487 | const char postFieldEncryptionTime[] = "&encryption_timestamp="; 488 | 489 | const size_t postFieldsBufSz = 490 | sizeof(postFieldAccountName) - 1 + (usernameBufSz - 1) * 3 + // multiple by 3 due to URL encoding 491 | sizeof(postFieldEncryptedPass) - 1 + encryptedPassBufSz * 3 + 492 | sizeof(postFieldEncryptionTime) - 1 + Guard::twoFactorCodeBufSz - 1 + 1; 493 | 494 | char postFields[postFieldsBufSz]; 495 | 496 | char* postFieldsEnd = postFields; 497 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldAccountName); 498 | postFieldsEnd = stpcpy(postFieldsEnd, escUsername); 499 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldEncryptedPass); 500 | postFieldsEnd = stpcpy(postFieldsEnd, escEncryptedPass); 501 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldEncryptionTime); 502 | strcpy(postFieldsEnd, rsaTimestamp); 503 | 504 | curl_free(escUsername); 505 | curl_free(escEncryptedPass); 506 | 507 | Curl::CResponse response; 508 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 509 | curl_easy_setopt(curl, CURLOPT_URL, "https://api.steampowered.com/IAuthenticationService/BeginAuthSessionViaCredentials/v1/"); 510 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 511 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 512 | 513 | const CURLcode respCode = curl_easy_perform(curl); 514 | 515 | if (respCode != CURLE_OK) 516 | { 517 | Curl::PrintError(curl, respCode); 518 | return false; 519 | } 520 | 521 | rapidjson::Document parsed; 522 | parsed.ParseInsitu(response.data); 523 | 524 | if (parsed.HasParseError()) 525 | { 526 | putsnn("JSON parsing failed\n"); 527 | return false; 528 | } 529 | 530 | const auto iterResponse = parsed.FindMember("response"); 531 | if (iterResponse == parsed.MemberEnd() || iterResponse->value.ObjectEmpty()) 532 | { 533 | putsnn("request unsucceeded\n"); 534 | return false; 535 | } 536 | 537 | const auto iterSteamId = iterResponse->value.FindMember("steamid"); 538 | if (iterSteamId == iterResponse->value.MemberEnd()) 539 | { 540 | putsnn("wrong credentials\n"); 541 | return false; 542 | } 543 | 544 | const char* clientId = iterResponse->value["client_id"].GetString(); 545 | const char* requestId = iterResponse->value["request_id"].GetString(); 546 | const char* steamId64 = iterSteamId->value.GetString(); 547 | 548 | strcpy(outClientId, clientId); 549 | strcpy(outRequestId, requestId); 550 | strcpy(outSteamId64, steamId64); 551 | 552 | putsnn("ok\n"); 553 | return true; 554 | } 555 | 556 | bool UpdateAuthSessionWithSteamGuardCode(CURL* curl, const char* steamId64, const char* clientId, const char* twoFactorCode) 557 | { 558 | Log(LogChannel::STEAM, "Updating auth session with a Steam Guard code..."); 559 | 560 | const char postFieldClientId[] = "client_id="; 561 | const char postFieldSteamId[] = "&steamid="; 562 | const char postFieldCode[] = "&code_type=3&code="; // code_type 3 is k_EAuthSessionGuardType_DeviceCode 563 | 564 | const size_t postFieldsBufSz = 565 | sizeof(postFieldClientId) - 1 + clientIdBufSz - 1 + // multiple by 3 due to URL encoding 566 | sizeof(postFieldSteamId) - 1 + UINT64_MAX_STR_SIZE - 1 + 567 | sizeof(postFieldCode) - 1 + Guard::twoFactorCodeBufSz - 1 + 1; 568 | 569 | char postFields[postFieldsBufSz]; 570 | 571 | char* postFieldsEnd = postFields; 572 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldClientId); 573 | postFieldsEnd = stpcpy(postFieldsEnd, clientId); 574 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldSteamId); 575 | postFieldsEnd = stpcpy(postFieldsEnd, steamId64); 576 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldCode); 577 | strcpy(postFieldsEnd, twoFactorCode); 578 | 579 | Curl::CResponse response; 580 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 581 | curl_easy_setopt(curl, CURLOPT_URL, "https://api.steampowered.com/IAuthenticationService/UpdateAuthSessionWithSteamGuardCode/v1/"); 582 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 583 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 584 | 585 | const CURLcode respCode = curl_easy_perform(curl); 586 | 587 | if (respCode != CURLE_OK) 588 | { 589 | Curl::PrintError(curl, respCode); 590 | return false; 591 | } 592 | 593 | putsnn("ok\n"); 594 | return true; 595 | } 596 | 597 | // outRefreshToken and outAccessToken buffer size must be at least jwtBufSz 598 | bool PollAuthSessionStatus(CURL* curl, const char* clientId, const char* requestId, char* outRefreshToken, char* outAccessToken) 599 | { 600 | Log(LogChannel::STEAM, "Polling auth session status..."); 601 | 602 | char* escRequestId = curl_easy_escape(curl, requestId, 0); 603 | 604 | const char postFieldClientId[] = "client_id="; 605 | const char postFieldRequestId[] = "&request_id="; 606 | 607 | const size_t postFieldsBufSz = 608 | sizeof(postFieldClientId) - 1 + clientIdBufSz - 1 + // multiple by 3 due to URL encoding 609 | sizeof(postFieldRequestId) - 1 + (requestIdBufSz - 1) * 3 + 1; 610 | 611 | char postFields[postFieldsBufSz]; 612 | 613 | char* postFieldsEnd = postFields; 614 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldClientId); 615 | postFieldsEnd = stpcpy(postFieldsEnd, clientId); 616 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldRequestId); 617 | strcpy(postFieldsEnd, escRequestId); 618 | 619 | curl_free(escRequestId); 620 | 621 | Curl::CResponse response; 622 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 623 | curl_easy_setopt(curl, CURLOPT_URL, "https://api.steampowered.com/IAuthenticationService/PollAuthSessionStatus/v1/"); 624 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 625 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 626 | 627 | const CURLcode respCode = curl_easy_perform(curl); 628 | 629 | if (respCode != CURLE_OK) 630 | { 631 | Curl::PrintError(curl, respCode); 632 | return false; 633 | } 634 | 635 | rapidjson::Document parsed; 636 | parsed.ParseInsitu(response.data); 637 | 638 | if (parsed.HasParseError()) 639 | { 640 | putsnn("JSON parsing failed\n"); 641 | return false; 642 | } 643 | 644 | const auto iterResponse = parsed.FindMember("response"); 645 | if (iterResponse == parsed.MemberEnd() || iterResponse->value.ObjectEmpty()) 646 | { 647 | putsnn("request unsucceeded\n"); 648 | return false; 649 | } 650 | 651 | const auto iterRefreshToken = iterResponse->value.FindMember("refresh_token"); 652 | const auto iterAccessToken = iterResponse->value.FindMember("access_token"); 653 | if (iterRefreshToken == iterResponse->value.MemberEnd() || iterAccessToken == iterResponse->value.MemberEnd()) 654 | { 655 | putsnn("not logged in\n"); 656 | return false; 657 | } 658 | 659 | strcpy(outRefreshToken, iterRefreshToken->value.GetString()); 660 | strcpy(outAccessToken, iterAccessToken->value.GetString()); 661 | 662 | putsnn("logged in\n"); 663 | return true; 664 | } 665 | 666 | // unused 667 | // only works for "client" jwt audience i think 668 | bool GenerateAccessTokenForApp(CURL* curl, const char* steamId64, const char* refreshToken, char* outAccessToken) 669 | { 670 | Log(LogChannel::STEAM, "Generating access token..."); 671 | 672 | const char postFieldRefreshToken[] = "refresh_token="; 673 | const char postFieldSteamId[] = "&steamid="; 674 | 675 | const size_t postFieldsBufSz = 676 | sizeof(postFieldRefreshToken) - 1 + jwtBufSz - 1 + 677 | sizeof(postFieldSteamId) - 1 + UINT64_MAX_STR_SIZE - 1 + 1; 678 | 679 | char postFields[postFieldsBufSz]; 680 | 681 | char* postFieldsEnd = postFields; 682 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldRefreshToken); 683 | postFieldsEnd = stpcpy(postFieldsEnd, refreshToken); 684 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldSteamId); 685 | strcpy(postFieldsEnd, steamId64); 686 | 687 | Curl::CResponse response; 688 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 689 | curl_easy_setopt(curl, CURLOPT_URL, "https://api.steampowered.com/IAuthenticationService/GenerateAccessTokenForApp/v1/"); 690 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 691 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 692 | 693 | const CURLcode respCode = curl_easy_perform(curl); 694 | 695 | if (respCode != CURLE_OK) 696 | { 697 | Curl::PrintError(curl, respCode); 698 | return false; 699 | } 700 | 701 | rapidjson::Document parsed; 702 | parsed.ParseInsitu(response.data); 703 | 704 | if (parsed.HasParseError()) 705 | { 706 | putsnn("JSON parsing failed\n"); 707 | return false; 708 | } 709 | 710 | const auto iterResponse = parsed.FindMember("response"); 711 | if (iterResponse == parsed.MemberEnd() || iterResponse->value.ObjectEmpty()) 712 | { 713 | putsnn("request unsucceeded\n"); 714 | return false; 715 | } 716 | 717 | const char* accessToken = iterResponse->value["access_token"].GetString(); 718 | 719 | strcpy(outAccessToken, accessToken); 720 | 721 | putsnn("ok\n"); 722 | return true; 723 | } 724 | 725 | bool RefreshJWTSession(CURL* curl, char* outAccessToken) 726 | { 727 | Curl::CResponse response; 728 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 729 | curl_easy_setopt(curl, CURLOPT_URL, "https://login.steampowered.com/jwt/refresh?redir=https://steamcommunity.com/"); 730 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 731 | 732 | // steam returns expiry time "1" for some reason, which makes cookie expire instantly, so we must parse cookie manually 733 | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); 734 | 735 | const CURLcode respCodeRefresh = curl_easy_perform(curl); 736 | 737 | if (respCodeRefresh != CURLE_OK) 738 | { 739 | Log(LogChannel::STEAM, "Refreshing session failed: "); 740 | Curl::PrintError(curl, respCodeRefresh); 741 | return false; 742 | } 743 | 744 | char* followUrl; 745 | if ((curl_easy_getinfo(curl, CURLINFO_REDIRECT_URL, &followUrl) != CURLE_OK) || !followUrl) 746 | { 747 | Log(LogChannel::STEAM, "Refreshing session failed: getting redirect URL failed\n"); 748 | return false; 749 | } 750 | 751 | if (!strcmp(followUrl, "https://steamcommunity.com/")) 752 | { 753 | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 754 | 755 | Log(LogChannel::STEAM, "Refreshing session failed: refresh token is invalid or has expired\n"); 756 | return false; 757 | } 758 | 759 | curl_easy_setopt(curl, CURLOPT_URL, followUrl); 760 | 761 | const CURLcode respCodeFollow = curl_easy_perform(curl); 762 | 763 | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 764 | 765 | if (respCodeFollow != CURLE_OK) 766 | { 767 | Log(LogChannel::STEAM, "Refreshing session failed: "); 768 | Curl::PrintError(curl, respCodeFollow); 769 | return false; 770 | } 771 | 772 | curl_slist* cookies; 773 | if ((curl_easy_getinfo(curl, CURLINFO_COOKIELIST, &cookies) != CURLE_OK) || !cookies) 774 | { 775 | Log(LogChannel::STEAM, "Refreshing session failed: getting cookies failed\n"); 776 | return false; 777 | } 778 | 779 | curl_slist* cookiesIter = cookies; 780 | while (cookiesIter && !strstr(cookiesIter->data, "\tsteamLoginSecure\t")) 781 | cookiesIter = cookiesIter->next; 782 | 783 | if (!cookiesIter) 784 | { 785 | curl_slist_free_all(cookies); 786 | 787 | Log(LogChannel::STEAM, "Refreshing session failed: steamLoginSecure not found\n"); 788 | return false; 789 | } 790 | 791 | strcpy(outAccessToken, strchr(cookiesIter->data, '%') + 6); 792 | 793 | curl_slist_free_all(cookies); 794 | 795 | return true; 796 | } 797 | } 798 | } -------------------------------------------------------------------------------- /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/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, const char* deviceId, const char* tag, char* out) 171 | { 172 | const time_t timestamp = GetSteamTime(); 173 | 174 | byte hash[confHashSz]; 175 | word32 hashSz = confHashSz; 176 | 177 | if (!GenerateConfirmationHash(identitySecret, timestamp, tag, hash, &hashSz)) 178 | return false; 179 | 180 | char* escapedHash = curl_easy_escape(curl, (char*)hash, hashSz); 181 | 182 | char* outEnd = out; 183 | outEnd = stpcpy(outEnd, "m=android&p="); 184 | outEnd = stpcpy(outEnd, deviceId); 185 | outEnd = stpcpy(outEnd, "&a="); 186 | outEnd = stpcpy(outEnd, steamId64); 187 | outEnd = stpcpy(outEnd, "&k="); 188 | outEnd = stpcpy(outEnd, escapedHash); 189 | outEnd = stpcpy(outEnd, "&t="); 190 | outEnd = stpcpy(outEnd, std::to_string(timestamp).c_str()); 191 | outEnd = stpcpy(outEnd, "&tag="); 192 | strcpy(outEnd, tag); 193 | 194 | curl_free(escapedHash); 195 | 196 | return true; 197 | } 198 | 199 | bool FetchConfirmations(CURL* curl, const char* steamId64, const char* identitySecret, const char* deviceId, rapidjson::Document* out) 200 | { 201 | Log(LogChannel::STEAM, "Fetching confirmations..."); 202 | 203 | char postFields[confQueueParamsBufSz]; 204 | if (!GenerateConfirmationQueryParams(curl, steamId64, identitySecret, deviceId, "conf", postFields)) 205 | { 206 | putsnn("query params generation failed\n"); 207 | return false; 208 | } 209 | 210 | Curl::CResponse response; 211 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 212 | curl_easy_setopt(curl, CURLOPT_URL, "https://steamcommunity.com/mobileconf/getlist"); 213 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 214 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 215 | 216 | const CURLcode respCode = curl_easy_perform(curl); 217 | 218 | if (respCode != CURLE_OK) 219 | { 220 | Curl::PrintError(curl, respCode); 221 | return false; 222 | } 223 | 224 | out->Parse(response.data); 225 | 226 | if (out->HasParseError()) 227 | { 228 | putsnn("JSON parsing failed\n"); 229 | return false; 230 | } 231 | 232 | if (!(*out)["success"].GetBool()) 233 | { 234 | putsnn("request unsucceeded\n"); 235 | return false; 236 | } 237 | 238 | putsnn("ok\n"); 239 | return true; 240 | } 241 | 242 | bool AcceptConfirmation(CURL* curl, 243 | const char* steamId64, const char* identitySecret, const char* deviceId, const char* offerId) 244 | { 245 | rapidjson::Document docConfs; 246 | if (!FetchConfirmations(curl, steamId64, identitySecret, deviceId, &docConfs)) 247 | return false; 248 | 249 | Log(LogChannel::STEAM, "Accepting confirmation..."); 250 | 251 | const char cId[] = "&op=allow&cid="; 252 | const char cK[] = "&ck="; 253 | 254 | const size_t postFieldsBufSz = 255 | confQueueParamsBufSz - 1 + 256 | sizeof(cId) - 1 + confIdBufSz - 1 + 257 | sizeof(cK) - 1 + confKeyBufSz - 1 + 1; 258 | 259 | char postFields[postFieldsBufSz]; 260 | 261 | if (!GenerateConfirmationQueryParams(curl, steamId64, identitySecret, deviceId, "allow", postFields)) 262 | { 263 | putsnn("query params generation failed\n"); 264 | return false; 265 | } 266 | 267 | const char* confId = nullptr; 268 | const char* confNonce = nullptr; 269 | 270 | const rapidjson::Value& confs = docConfs["conf"]; 271 | const rapidjson::SizeType confCount = confs.Size(); 272 | 273 | for (rapidjson::SizeType i = 0; i < confCount; ++i) 274 | { 275 | const rapidjson::Value& conf = confs[i]; 276 | 277 | if (conf["type"].GetInt() == 2 && !strcmp(conf["creator_id"].GetString(), offerId)) 278 | { 279 | confId = conf["id"].GetString(); 280 | confNonce = conf["nonce"].GetString(); 281 | } 282 | } 283 | 284 | if (!confId || !confNonce) 285 | { 286 | putsnn("finding confirmation params failed\n"); 287 | return false; 288 | } 289 | 290 | char* postFieldsEnd = postFields + strlen(postFields); 291 | postFieldsEnd = stpcpy(postFieldsEnd, cId); 292 | postFieldsEnd = stpcpy(postFieldsEnd, confId); 293 | postFieldsEnd = stpcpy(postFieldsEnd, cK); 294 | strcpy(postFieldsEnd, confNonce); 295 | 296 | Curl::CResponse respOp; 297 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &respOp); 298 | curl_easy_setopt(curl, CURLOPT_URL, "https://steamcommunity.com/mobileconf/ajaxop"); 299 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 300 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 301 | 302 | const CURLcode respCodeOp = curl_easy_perform(curl); 303 | 304 | if (respCodeOp != CURLE_OK) 305 | { 306 | Curl::PrintError(curl, respCodeOp); 307 | return false; 308 | } 309 | 310 | rapidjson::Document parsedOp; 311 | parsedOp.ParseInsitu(respOp.data); 312 | 313 | if (parsedOp.HasParseError()) 314 | { 315 | putsnn("JSON parsing failed\n"); 316 | return false; 317 | } 318 | 319 | if (!parsedOp["success"].GetBool()) 320 | { 321 | putsnn("request unsucceeded\n"); 322 | return false; 323 | } 324 | 325 | putsnn("ok\n"); 326 | return true; 327 | } 328 | 329 | // unused 330 | bool AcceptConfirmations(CURL* curl, 331 | const char* steamId64, const char* identitySecret, const char* deviceId, 332 | const char** offerIds, size_t offerIdCount) 333 | { 334 | rapidjson::Document docConfs; 335 | if (!FetchConfirmations(curl, steamId64, identitySecret, deviceId, &docConfs)) 336 | return false; 337 | 338 | Log(LogChannel::STEAM, "Accepting confirmations..."); 339 | 340 | const char opAllow[] = "&op=allow"; 341 | 342 | const size_t confParamsLen = sizeof("&cid[]=&ck[]=") - 1 + confIdBufSz - 1 + confKeyBufSz - 1; 343 | const size_t postFieldsBufSz = confQueueParamsBufSz - 1 + sizeof(opAllow) - 1 + confParamsLen * offerIdCount + 1; 344 | 345 | char* postFields = (char*)malloc(postFieldsBufSz); 346 | if (!postFields) 347 | { 348 | putsnn("allocation failed\n"); 349 | return false; 350 | } 351 | 352 | if (!GenerateConfirmationQueryParams(curl, steamId64, identitySecret, deviceId, "allow", postFields)) 353 | { 354 | free(postFields); 355 | putsnn("query params generation failed\n"); 356 | return false; 357 | } 358 | 359 | size_t confirmedCount = 0; 360 | 361 | char* postFieldsEnd = postFields + strlen(postFields); 362 | postFieldsEnd = stpcpy(postFieldsEnd, opAllow); 363 | 364 | for (size_t i = 0; i < offerIdCount; ++i) 365 | { 366 | const char* confId = nullptr; 367 | const char* confNonce = nullptr; 368 | 369 | const rapidjson::Value& confs = docConfs["conf"]; 370 | const rapidjson::SizeType confCount = confs.Size(); 371 | 372 | for (rapidjson::SizeType j = 0; j < confCount; ++j) 373 | { 374 | const rapidjson::Value& conf = confs[j]; 375 | 376 | if (conf["type"].GetInt() == 2 && !strcmp(conf["creator_id"].GetString(), offerIds[i])) 377 | { 378 | confId = conf["id"].GetString(); 379 | confNonce = conf["nonce"].GetString(); 380 | } 381 | } 382 | 383 | if (!confId || !confNonce) 384 | continue; 385 | 386 | postFieldsEnd = stpcpy(postFieldsEnd, "&cid[]="); 387 | postFieldsEnd = stpcpy(postFieldsEnd, confId); 388 | postFieldsEnd = stpcpy(postFieldsEnd, "&ck[]="); 389 | postFieldsEnd = stpcpy(postFieldsEnd, confNonce); 390 | 391 | ++confirmedCount; 392 | } 393 | 394 | Curl::CResponse respMultiOp; 395 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &respMultiOp); 396 | curl_easy_setopt(curl, CURLOPT_URL, "https://steamcommunity.com/mobileconf/multiajaxop"); 397 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 398 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 399 | 400 | const CURLcode respCodeMultiOp = curl_easy_perform(curl); 401 | 402 | free(postFields); 403 | 404 | if (respCodeMultiOp != CURLE_OK) 405 | { 406 | Curl::PrintError(curl, respCodeMultiOp); 407 | return false; 408 | } 409 | 410 | rapidjson::Document parsedMultiOp; 411 | parsedMultiOp.ParseInsitu(respMultiOp.data); 412 | 413 | if (parsedMultiOp.HasParseError()) 414 | { 415 | putsnn("JSON parsing failed\n"); 416 | return false; 417 | } 418 | 419 | if (!parsedMultiOp["success"].GetBool()) 420 | { 421 | putsnn("request unsucceeded\n"); 422 | return false; 423 | } 424 | 425 | if (confirmedCount != offerIdCount) 426 | printf("accepted %u out of %u\n", confirmedCount, offerIdCount); 427 | else 428 | putsnn("ok\n"); 429 | 430 | return true; 431 | } 432 | 433 | // out buffer size must be at least deviceIdBufSz 434 | bool GetDeviceId(CURL* curl, const char* steamId64, const char* accessToken, char* out) 435 | { 436 | Log(LogChannel::STEAM, "Getting Steam Guard Mobile device ID..."); 437 | 438 | const char postFieldAccessToken[] = "access_token="; 439 | const char postFieldSteamId[] = "&steamid="; 440 | 441 | const size_t postFieldsBufSz = 442 | sizeof(postFieldAccessToken) - 1 + Auth::jwtBufSz + 443 | sizeof(postFieldSteamId) - 1 + UINT64_MAX_STR_SIZE - 1 + 1; 444 | 445 | char postFields[postFieldsBufSz]; 446 | 447 | char* postFieldsEnd = postFields; 448 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldAccessToken); 449 | postFieldsEnd = stpcpy(postFieldsEnd, accessToken); 450 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldSteamId); 451 | strcpy(postFieldsEnd, steamId64); 452 | 453 | Curl::CResponse response; 454 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 455 | curl_easy_setopt(curl, CURLOPT_URL, "https://api.steampowered.com/ITwoFactorService/QueryStatus/v1/"); 456 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 457 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 458 | 459 | const CURLcode respCode = curl_easy_perform(curl); 460 | 461 | if (respCode != CURLE_OK) 462 | { 463 | Curl::PrintError(curl, respCode); 464 | return false; 465 | } 466 | 467 | rapidjson::Document parsed; 468 | parsed.ParseInsitu(response.data); 469 | 470 | if (parsed.HasParseError()) 471 | { 472 | putsnn("JSON parsing failed\n"); 473 | return false; 474 | } 475 | 476 | const auto iterResponse = parsed.FindMember("response"); 477 | if (iterResponse == parsed.MemberEnd() || iterResponse->value.ObjectEmpty()) 478 | { 479 | putsnn("request unsucceeded\n"); 480 | return false; 481 | } 482 | 483 | const char* deviceId = iterResponse->value["device_identifier"].GetString(); 484 | 485 | strcpy(out, deviceId); 486 | 487 | putsnn("ok\n"); 488 | return true; 489 | } 490 | } 491 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | 28 | #include "Misc.h" 29 | #include "Captcha.h" 30 | #include "Trade.h" 31 | #include "Guard.h" 32 | #include "Auth.h" -------------------------------------------------------------------------------- /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: {"tradeid":null,"needs_mobile_confirmation":true,"needs_email_confirmation":true,"email_domain":"gmail.com"} 67 | // No confirmation required: {"tradeid":"2251163828378018000"} 68 | Curl::CResponse response; 69 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 70 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 71 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 72 | 73 | const CURLcode respCode = curl_easy_perform(curl); 74 | 75 | curl_easy_setopt(curl, CURLOPT_REFERER, NULL); 76 | 77 | if (respCode != CURLE_OK) 78 | { 79 | Curl::PrintError(curl, respCode); 80 | return false; 81 | } 82 | 83 | putsnn("ok\n"); 84 | return true; 85 | } 86 | 87 | // outOfferId buffer size must be at least offerIdBufSz 88 | bool Send(CURL* curl, const char* sessionId, uint32_t nPartnerId32, const char* token, const char* message, const char* assets, char* outOfferId) 89 | { 90 | Log(LogChannel::STEAM, "Sending trade offer..."); 91 | 92 | const std::string partnerId32(std::to_string(nPartnerId32)); 93 | const std::string partnerId64(std::to_string(SteamID32To64(nPartnerId32))); 94 | 95 | const char refererStart[] = "https://steamcommunity.com/tradeoffer/new/?partner="; 96 | const char refererToken[] = "&token="; 97 | 98 | const size_t refererBufSz = 99 | sizeof(refererStart) - 1 + UINT32_MAX_STR_SIZE - 1 + 100 | sizeof(refererToken) - 1 + tokenBufSz - 1 + 1; 101 | 102 | char referer[refererBufSz]; 103 | 104 | char* refererEnd = referer; 105 | refererEnd = stpcpy(refererEnd, refererStart); 106 | refererEnd = stpcpy(refererEnd, partnerId32.c_str()); 107 | refererEnd = stpcpy(refererEnd, refererToken); 108 | strcpy(refererEnd, token); 109 | 110 | curl_easy_setopt(curl, CURLOPT_REFERER, referer); 111 | 112 | const char postFieldSession[] = "serverid=1&sessionid="; 113 | const char postFieldPartnerId64[] = "&partner="; 114 | 115 | const char postFieldAssets[] = "&json_tradeoffer={\"newversion\":true,\"version\":2,\"me\":{\"assets\":"; 116 | 117 | const char postFieldToken[] = ",\"currency\":[],\"ready\":false},\"them\":{\"assets\":[],\"currency\":[],\"ready\":false}}" 118 | "&trade_offer_create_params={\"trade_offer_access_token\":\""; 119 | 120 | const char postFieldMessage[] = "\"}" 121 | "&tradeoffermessage="; 122 | 123 | const size_t postFieldsBufSz = 124 | sizeof(postFieldSession) - 1 + sessionIdBufSz - 1 + 125 | sizeof(postFieldPartnerId64) - 1 + UINT64_MAX_STR_SIZE - 1 + 126 | sizeof(postFieldAssets) - 1 + strlen(assets) + 127 | sizeof(postFieldToken) - 1 + tokenBufSz - 1 + 128 | sizeof(postFieldMessage) - 1 + msgBufSz - 1 + 1; 129 | 130 | char* postFields = (char*)malloc(postFieldsBufSz); 131 | if (!postFields) 132 | { 133 | putsnn("allocation failed\n"); 134 | return false; 135 | } 136 | 137 | char* postFieldsEnd = postFields; 138 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldSession); 139 | postFieldsEnd = stpcpy(postFieldsEnd, sessionId); 140 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldPartnerId64); 141 | postFieldsEnd = stpcpy(postFieldsEnd, partnerId64.c_str()); 142 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldAssets); 143 | postFieldsEnd = stpcpy(postFieldsEnd, assets); 144 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldToken); 145 | postFieldsEnd = stpcpy(postFieldsEnd, token); 146 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldMessage); 147 | strcpy(postFieldsEnd, message); 148 | 149 | Curl::CResponse response; 150 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 151 | curl_easy_setopt(curl, CURLOPT_URL, "https://steamcommunity.com/tradeoffer/new/send"); 152 | curl_easy_setopt(curl, CURLOPT_POST, 1L); 153 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFields); 154 | 155 | const CURLcode respCode = curl_easy_perform(curl); 156 | 157 | free(postFields); 158 | 159 | curl_easy_setopt(curl, CURLOPT_REFERER, NULL); 160 | 161 | if (respCode != CURLE_OK) 162 | { 163 | Curl::PrintError(curl, respCode); 164 | return false; 165 | } 166 | 167 | rapidjson::Document parsed; 168 | parsed.ParseInsitu(response.data); 169 | 170 | if (parsed.HasParseError()) 171 | { 172 | putsnn("JSON parsing failed\n"); 173 | return false; 174 | } 175 | 176 | const auto iterOfferId = parsed.FindMember("tradeofferid"); 177 | if (iterOfferId == parsed.MemberEnd()) 178 | { 179 | putsnn("request unsucceeded\n"); 180 | return false; 181 | } 182 | 183 | strcpy(outOfferId, iterOfferId->value.GetString()); 184 | 185 | putsnn("ok\n"); 186 | return true; 187 | } 188 | 189 | bool Cancel(CURL* curl, const char* sessionId, const char* offerId) 190 | { 191 | Log(LogChannel::STEAM, "Cancelling trade offer..."); 192 | 193 | const char urlStart[] = "https://steamcommunity.com/tradeoffer/"; 194 | 195 | const size_t urlBufSz = sizeof(urlStart) - 1 + offerIdBufSz - 1 + sizeof("/cancel") - 1 + 1; 196 | char url[urlBufSz]; 197 | 198 | char* urlEnd = url; 199 | urlEnd = stpcpy(urlEnd, urlStart); 200 | urlEnd = stpcpy(urlEnd, offerId); 201 | strcpy(urlEnd, "/cancel"); 202 | 203 | const char postFieldSession[] = "sessionid="; 204 | 205 | const size_t postFieldsBufSz = sizeof(postFieldSession) - 1 + sessionIdBufSz - 1 + 1; 206 | char postFields[postFieldsBufSz]; 207 | 208 | char* postFieldsEnd = postFields; 209 | postFieldsEnd = stpcpy(postFieldsEnd, postFieldSession); 210 | strcpy(postFieldsEnd, sessionId); 211 | 212 | Curl::CResponse response; 213 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 214 | curl_easy_setopt(curl, CURLOPT_URL, url); 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 | rapidjson::Document parsed; 227 | parsed.ParseInsitu(response.data); 228 | 229 | if (parsed.HasParseError()) 230 | { 231 | putsnn("JSON parsing failed\n"); 232 | return false; 233 | } 234 | 235 | const auto iterOfferId = parsed.FindMember("tradeofferid"); 236 | if (iterOfferId == parsed.MemberEnd()) 237 | { 238 | putsnn("request unsucceeded\n"); 239 | return false; 240 | } 241 | 242 | putsnn("ok\n"); 243 | return true; 244 | } 245 | 246 | bool GetOffers(CURL* curl, const char* apiKey, bool getSent, bool getReceived, bool getDescriptions, 247 | bool activeOnly, bool historicalOnly, const char* language, uint64_t timeHistoricalCutoff, 248 | uint32_t cursor, rapidjson::Document* outDoc) 249 | { 250 | //Log(LogChannel::STEAM, "Getting trade offers..."); 251 | 252 | const char urlStart[] = "https://api.steampowered.com/IEconService/GetTradeOffers/v1/?key="; 253 | const char urlSent[] = "&get_sent_offers=true"; 254 | const char urlReceived[] = "&get_received_offers=true"; 255 | const char urlDesc[] = "&get_descriptions=true"; 256 | const char urlActive[] = "&active_only=true"; 257 | const char urlHistorical[] = "&historical_only=true"; 258 | const char urlLang[] = "&language="; 259 | const char urlTime[] = "&time_historical_cutoff="; 260 | const char urlCursor[] = "&cursor="; 261 | 262 | const size_t urlBufSz = 263 | sizeof(urlStart) - 1 + apiKeyBufSz - 1 + 264 | sizeof(urlSent) - 1 + 265 | sizeof(urlReceived) - 1 + 266 | sizeof(urlDesc) - 1 + 267 | sizeof(urlActive) - 1 + 268 | sizeof(urlHistorical) - 1 + 269 | sizeof(urlLang) - 1 + sizeof("portuguese") - 1 + // longest steam language 270 | sizeof(urlTime) - 1 + UINT32_MAX_STR_SIZE - 1 + 271 | sizeof(urlCursor) - 1 + UINT32_MAX_STR_SIZE - 1 + 1; 272 | 273 | char url[urlBufSz]; 274 | 275 | char* urlEnd = url; 276 | urlEnd = stpcpy(urlEnd, urlStart); 277 | urlEnd = stpcpy(urlEnd, apiKey); 278 | 279 | if (getSent) 280 | urlEnd = stpcpy(urlEnd, urlSent); 281 | 282 | if (getReceived) 283 | urlEnd = stpcpy(urlEnd, urlReceived); 284 | 285 | if (getDescriptions) 286 | urlEnd = stpcpy(urlEnd, urlDesc); 287 | 288 | if (activeOnly) 289 | urlEnd = stpcpy(urlEnd, urlActive); 290 | 291 | if (historicalOnly) 292 | urlEnd = stpcpy(urlEnd, urlHistorical); 293 | 294 | if (language) 295 | { 296 | urlEnd = stpcpy(urlEnd, urlLang); 297 | urlEnd = stpcpy(urlEnd, language); 298 | } 299 | 300 | if (timeHistoricalCutoff) 301 | { 302 | urlEnd = stpcpy(urlEnd, urlTime); 303 | urlEnd = stpcpy(urlEnd, std::to_string(timeHistoricalCutoff).c_str()); 304 | } 305 | 306 | if (cursor) 307 | { 308 | urlEnd = stpcpy(urlEnd, urlCursor); 309 | strcpy(urlEnd, std::to_string(cursor).c_str()); 310 | } 311 | 312 | Curl::CResponse response; 313 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 314 | curl_easy_setopt(curl, CURLOPT_URL, url); 315 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 316 | 317 | const CURLcode respCode = curl_easy_perform(curl); 318 | 319 | if (respCode != CURLE_OK) 320 | { 321 | //Curl::PrintError(curl, respCode); 322 | return false; 323 | } 324 | 325 | outDoc->Parse(response.data); 326 | 327 | if (outDoc->HasParseError()) 328 | { 329 | //putsnn("JSON parsing failed\n"); 330 | return false; 331 | } 332 | 333 | const auto iterResponse = outDoc->FindMember("response"); 334 | if (iterResponse == outDoc->MemberEnd()) 335 | { 336 | //putsnn("request unsucceeded\n"); 337 | return false; 338 | } 339 | 340 | //putsnn("ok\n"); 341 | return true; 342 | } 343 | 344 | // out buffer size must be at least tokenBufSz 345 | bool GetToken(CURL* curl, char* out) 346 | { 347 | Log(LogChannel::STEAM, "Getting trade token..."); 348 | 349 | Curl::CResponse response; 350 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 351 | curl_easy_setopt(curl, CURLOPT_URL, "https://steamcommunity.com/my/tradeoffers/privacy"); 352 | curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); 353 | 354 | const CURLcode respCode = curl_easy_perform(curl); 355 | 356 | if (respCode != CURLE_OK) 357 | { 358 | Curl::PrintError(curl, respCode); 359 | return false; 360 | } 361 | 362 | const char searchStr[] = "https://steamcommunity.com/tradeoffer/new/?partner="; 363 | 364 | const char* tradeUrl = strstr(response.data, searchStr); 365 | if (!tradeUrl) 366 | { 367 | putsnn("trade URL not found\n"); 368 | return false; 369 | } 370 | 371 | const char* tradeToken = (char*)memchr( 372 | tradeUrl + sizeof(searchStr) - 1, 373 | '=', 374 | UINT32_MAX_STR_SIZE - 1 + sizeof("&token=") - 1); 375 | 376 | if (!tradeToken) 377 | { 378 | putsnn("trade token not found\n"); 379 | return false; 380 | } 381 | 382 | ++tradeToken; 383 | 384 | stpncpy(out, tradeToken, Steam::Trade::tokenBufSz - 1)[0] = '\0'; 385 | 386 | putsnn("ok\n"); 387 | return true; 388 | } 389 | } 390 | } -------------------------------------------------------------------------------- /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;wolfssl.lib;libcurld.lib;%(AdditionalDependencies) 121 | ..\..\libs\wolfssl\build\$(Platform)\$(Configuration);..\..\libs\curl\build\$(Platform)\LIB $(Configuration) - LIB wolfSSL;%(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;libcurld.lib;%(AdditionalDependencies) 143 | ..\..\libs\wolfssl\build\$(Platform)\$(Configuration);..\..\libs\curl\build\$(Platform)\LIB $(Configuration) - LIB wolfSSL;%(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;wolfssl.lib;libcurl.lib;%(AdditionalDependencies) 172 | ..\..\libs\wolfssl\build\$(Platform)\$(Configuration);..\..\libs\curl\build\$(Platform)\LIB $(Configuration) - LIB wolfSSL;%(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)\LIB $(Configuration) - LIB wolfSSL;%(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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------