Compare commits

..

No commits in common. 'master' and 'DepotDownloader_2.4.5' have entirely different histories.

@ -6,16 +6,11 @@ indent_style = space
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
# Code files
[*.{cs, csx, vb, vbx}]
indent_size = 4
# Github yaml files
[*.yml]
indent_size = 2
# XML project files
[*.{csproj, vbproj, vcxproj, vcxproj.filters, proj, projitems, shproj}]
indent_size = 2
@ -100,6 +95,7 @@ dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_s
dotnet_naming_symbols.instance_fields.applicable_kinds = field
dotnet_naming_style.instance_field_style.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _
# Locals and parameters are camelCase
dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion
@ -128,9 +124,26 @@ dotnet_naming_symbols.all_members.applicable_kinds = *
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# Async methods should have "Async" suffix
dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods
dotnet_naming_rule.async_methods_end_in_async.style = end_in_async
dotnet_naming_rule.async_methods_end_in_async.severity = warning
dotnet_naming_symbols.any_async_methods.applicable_kinds = method
dotnet_naming_symbols.any_async_methods.applicable_accessibilities = *
dotnet_naming_symbols.any_async_methods.required_modifiers = async
dotnet_naming_style.end_in_async.required_prefix =
dotnet_naming_style.end_in_async.required_suffix = Async
dotnet_naming_style.end_in_async.capitalization = pascal_case
dotnet_naming_style.end_in_async.word_separator =
# error RS2008: Enable analyzer release tracking for the analyzer project containing rule '{0}'
dotnet_diagnostic.RS2008.severity = none
# IDE0005: Remove unnecessary import
dotnet_diagnostic.IDE0005.severity = warning
# IDE0007: Use `var` instead of explicit type
dotnet_diagnostic.IDE0007.severity = warning
@ -148,11 +161,6 @@ dotnet_diagnostic.IDE0044.severity = warning
# CSharp code style settings:
[*.cs]
# Require file header OR A source file contains a header that does not match the required text
file_header_template = This file is subject to the terms and conditions defined\nin file 'LICENSE', which is part of this source code package.
dotnet_diagnostic.IDE0073.severity = error
# Newline settings
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true

2
.gitattributes vendored

@ -1,4 +1,4 @@
*.cs text eol=lf
*.csproj text eol=lf
*.csproj text eol=crlf
*.config eol=lf
*.json eol=lf

@ -1,60 +0,0 @@
name: Bug Report
description: File a bug report
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: what-should-have-happened
attributes:
label: What did you expect to happen?
placeholder: I expected that...
validations:
required: true
- type: textarea
id: what-actually-happened
attributes:
label: Instead of that, what actually happened?
placeholder: ... but instead, what happened was...
validations:
required: true
- type: dropdown
id: operating-system
attributes:
label: Which operating system are you running on?
options:
- Linux
- macOS
- Windows
- Other
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of DepotDownloader are using?
validations:
required: true
- type: input
id: command
attributes:
label: Command
description: Specify the full command you used (except for username and password)
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. Run with `-debug` parameter to get additional output.
render: shell
- type: textarea
id: additional-info
attributes:
label: Additional Information
description: Is there anything else that you think we should know?
validations:
required: false

@ -1,5 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: Discussions
url: https://github.com/SteamRE/DepotDownloader/discussions/new
about: Please ask and answer questions here.

@ -1,36 +0,0 @@
name: Feature Request
description: Suggest an idea for this project
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
Thanks, we appreciate good ideas!
- type: textarea
id: problem-area
attributes:
label: What problem is this feature trying to solve?
placeholder: I'm really frustrated when...
validations:
required: true
- type: textarea
id: proposed-solution
attributes:
label: How would you like it to be solved?
placeholder: I think that it could be solved by...
validations:
required: true
- type: textarea
id: alternative-solutions
attributes:
label: Have you considered any alternative solutions
placeholder: I did think that that it also could be solved by ..., but...
validations:
required: true
- type: textarea
id: additional-info
attributes:
label: Additional Information
description: Is there anything else that you think we should know?
validations:
required: false

@ -3,13 +3,5 @@ updates:
- package-ecosystem: nuget
directory: "/"
schedule:
interval: weekly
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: weekly
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]

@ -15,156 +15,23 @@ on:
jobs:
build:
name: .NET on ${{ matrix.runs-on }} (${{ matrix.configuration }})
runs-on: ${{ matrix.runs-on }}
name: .NET on ${{ matrix.os }} (${{ matrix.configuration }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
runs-on: [macos-latest, macos-14, ubuntu-latest, windows-latest]
os: [macos-latest, ubuntu-latest, windows-latest]
configuration: [Release, Debug]
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
- name: Setup .NET Core
uses: actions/setup-dotnet@v5
uses: actions/setup-dotnet@v1
- name: Build
run: dotnet publish DepotDownloader/DepotDownloader.csproj -c ${{ matrix.configuration }} -o artifacts /p:ContinuousIntegrationBuild=true
run: dotnet publish -c ${{ matrix.configuration }} -o artifacts
- name: Upload artifact
uses: actions/upload-artifact@v4
if: matrix.configuration == 'Release' && matrix.runs-on == 'windows-latest'
uses: actions/upload-artifact@v2
if: matrix.configuration == 'Release'
with:
name: DepotDownloader-framework
name: DepotDownloader-${{ runner.os }}
path: artifacts
if-no-files-found: error
- name: Publish Windows-x64
if: matrix.configuration == 'Release' && matrix.runs-on == 'windows-latest'
run: dotnet publish DepotDownloader/DepotDownloader.csproj --configuration Release -p:PublishSingleFile=true -p:DebugType=embedded --self-contained --runtime win-x64 --output selfcontained-win-x64
- name: Publish Windows-arm64
if: matrix.configuration == 'Release' && matrix.runs-on == 'windows-latest'
run: dotnet publish DepotDownloader/DepotDownloader.csproj --configuration Release -p:PublishSingleFile=true -p:DebugType=embedded --self-contained --runtime win-arm64 --output selfcontained-win-arm64
- name: Publish Linux-x64
if: matrix.configuration == 'Release' && matrix.runs-on == 'ubuntu-latest'
run: dotnet publish DepotDownloader/DepotDownloader.csproj --configuration Release -p:PublishSingleFile=true -p:DebugType=embedded --self-contained --runtime linux-x64 --output selfcontained-linux-x64
- name: Publish Linux-arm
if: matrix.configuration == 'Release' && matrix.runs-on == 'ubuntu-latest'
run: dotnet publish DepotDownloader/DepotDownloader.csproj --configuration Release -p:PublishSingleFile=true -p:DebugType=embedded --self-contained --runtime linux-arm --output selfcontained-linux-arm
- name: Publish Linux-arm64
if: matrix.configuration == 'Release' && matrix.runs-on == 'ubuntu-latest'
run: dotnet publish DepotDownloader/DepotDownloader.csproj --configuration Release -p:PublishSingleFile=true -p:DebugType=embedded --self-contained --runtime linux-arm64 --output selfcontained-linux-arm64
- name: Publish macOS-x64
if: matrix.configuration == 'Release' && matrix.runs-on == 'macos-latest'
run: dotnet publish DepotDownloader/DepotDownloader.csproj --configuration Release -p:PublishSingleFile=true -p:DebugType=embedded --self-contained --runtime osx-x64 --output selfcontained-osx-x64
- name: Publish macOS-arm64
if: matrix.configuration == 'Release' && matrix.runs-on == 'macos-14'
run: dotnet publish DepotDownloader/DepotDownloader.csproj --configuration Release -p:PublishSingleFile=true -p:DebugType=embedded --self-contained --runtime osx-arm64 --output selfcontained-osx-arm64
- name: Upload Windows-x64
uses: actions/upload-artifact@v4
if: matrix.configuration == 'Release' && matrix.runs-on == 'windows-latest'
with:
name: DepotDownloader-windows-x64
path: selfcontained-win-x64
if-no-files-found: error
- name: Upload Windows-arm64
uses: actions/upload-artifact@v4
if: matrix.configuration == 'Release' && matrix.runs-on == 'windows-latest'
with:
name: DepotDownloader-windows-arm64
path: selfcontained-win-arm64
if-no-files-found: error
- name: Upload Linux-x64
uses: actions/upload-artifact@v4
if: matrix.configuration == 'Release' && matrix.runs-on == 'ubuntu-latest'
with:
name: DepotDownloader-linux-x64
path: selfcontained-linux-x64
if-no-files-found: error
- name: Upload Linux-arm
uses: actions/upload-artifact@v4
if: matrix.configuration == 'Release' && matrix.runs-on == 'ubuntu-latest'
with:
name: DepotDownloader-linux-arm
path: selfcontained-linux-arm
if-no-files-found: error
- name: Upload Linux-arm64
uses: actions/upload-artifact@v4
if: matrix.configuration == 'Release' && matrix.runs-on == 'ubuntu-latest'
with:
name: DepotDownloader-linux-arm64
path: selfcontained-linux-arm64
if-no-files-found: error
- name: Upload macOS-x64
uses: actions/upload-artifact@v4
if: matrix.configuration == 'Release' && matrix.runs-on == 'macos-latest'
with:
name: DepotDownloader-macos-x64
path: selfcontained-osx-x64
if-no-files-found: error
- name: Upload macOS-arm64
uses: actions/upload-artifact@v4
if: matrix.configuration == 'Release' && matrix.runs-on == 'macos-14'
with:
name: DepotDownloader-macos-arm64
path: selfcontained-osx-arm64
if-no-files-found: error
release:
if: startsWith(github.ref, 'refs/tags/')
needs: build
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Display artifacts folder structure
run: ls -Rl
working-directory: artifacts
- name: Create release files
run: |
set -eux
mkdir release
chmod +x artifacts/DepotDownloader-linux-x64/DepotDownloader
chmod +x artifacts/DepotDownloader-linux-arm/DepotDownloader
chmod +x artifacts/DepotDownloader-linux-arm64/DepotDownloader
chmod +x artifacts/DepotDownloader-macos-x64/DepotDownloader
chmod +x artifacts/DepotDownloader-macos-arm64/DepotDownloader
zip -9j release/DepotDownloader-framework.zip artifacts/DepotDownloader-framework/*
zip -9j release/DepotDownloader-windows-x64.zip artifacts/DepotDownloader-windows-x64/*
zip -9j release/DepotDownloader-windows-arm64.zip artifacts/DepotDownloader-windows-arm64/*
zip -9j release/DepotDownloader-linux-x64.zip artifacts/DepotDownloader-linux-x64/*
zip -9j release/DepotDownloader-linux-arm.zip artifacts/DepotDownloader-linux-arm/*
zip -9j release/DepotDownloader-linux-arm64.zip artifacts/DepotDownloader-linux-arm64/*
zip -9j release/DepotDownloader-macos-x64.zip artifacts/DepotDownloader-macos-x64/*
zip -9j release/DepotDownloader-macos-arm64.zip artifacts/DepotDownloader-macos-arm64/*
- name: Display structure of archived files
run: ls -Rl
working-directory: release
- name: Release
uses: softprops/action-gh-release@v2
with:
draft: true
files: release/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

@ -1,39 +0,0 @@
name: SteamKit2 Continuous Integration
on:
schedule:
- cron: '0 1 * * SUN'
workflow_dispatch:
jobs:
build:
name: .NET on ${{ matrix.runs-on }} (${{ matrix.configuration }})
runs-on: ${{ matrix.runs-on }}
strategy:
fail-fast: false
matrix:
runs-on: [ macos-latest, macos-14, ubuntu-latest, windows-latest ]
configuration: [ Release, Debug ]
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
steps:
- uses: actions/checkout@v5
- name: Setup .NET Core
uses: actions/setup-dotnet@v5
- name: Configure NuGet
run: |
dotnet nuget add source --username USERNAME --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/SteamRE/index.json"
dotnet add DepotDownloader/DepotDownloader.csproj package SteamKit2 --prerelease
- name: Build
run: dotnet publish DepotDownloader/DepotDownloader.csproj -c ${{ matrix.configuration }} -o artifacts /p:ContinuousIntegrationBuild=true
- name: Upload artifact
uses: actions/upload-artifact@v4
if: matrix.configuration == 'Release'
with:
name: DepotDownloader-${{ matrix.runs-on }}
path: artifacts
if-no-files-found: error

@ -1,33 +0,0 @@
name: WinGet submission on release
on:
workflow_dispatch:
release:
types: [published]
jobs:
winget:
name: Publish winget package
runs-on: windows-latest
steps:
- name: Submit package to Windows Package Manager Community Repository
run: |
$wingetPackage = "SteamRE.DepotDownloader"
$headers = @{
Authorization = "Bearer ${{ secrets.GITHUB_TOKEN }}"
}
$github = Invoke-RestMethod -uri "https://api.github.com/repos/SteamRE/DepotDownloader/releases" -Headers $headers
$targetRelease = $github | Where-Object -Property name -match '^DepotDownloader' | Select -First 1
$assets = $targetRelease | Select -ExpandProperty assets -First 1
$zipX64Url = $assets | Where-Object -Property name -match 'DepotDownloader-windows-x64.zip' | Select -ExpandProperty browser_download_url
$zipArm64Url = $assets | Where-Object -Property name -match 'DepotDownloader-windows-arm64.zip' | Select -ExpandProperty browser_download_url
$ver = $targetRelease.tag_name -ireplace '^(DepotDownloader[ _])?v?'
# getting latest wingetcreate file
iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
# how to create a token: https://github.com/microsoft/winget-create?tab=readme-ov-file#github-personal-access-token-classic-permissions
.\wingetcreate.exe update $wingetPackage --submit --version $ver --urls "$zipX64Url" "$zipArm64Url" --token "${{ secrets.PT_WINGET }}"

5
.gitignore vendored

@ -60,7 +60,7 @@ _ReSharper*
*.ncrunch*
.*crunch*.local.xml
# Installshield output folder
# Installshield output folder
[Ee]xpress
# DocProject is a documentation generator add-in
@ -117,5 +117,4 @@ protobuf/
cryptopp/
# misc
Thumbs.db
launchSettings.json
Thumbs.db

@ -1,7 +1,4 @@
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
@ -14,26 +11,22 @@ namespace DepotDownloader
[ProtoContract]
class AccountSettingsStore
{
// Member 1 was a Dictionary<string, byte[]> for SentryData.
[ProtoMember(1, IsRequired = false)]
public Dictionary<string, byte[]> SentryData { get; private set; }
[ProtoMember(2, IsRequired = false)]
public ConcurrentDictionary<string, int> ContentServerPenalty { get; private set; }
// Member 3 was a Dictionary<string, string> for LoginKeys.
[ProtoMember(4, IsRequired = false)]
public Dictionary<string, string> LoginTokens { get; private set; }
[ProtoMember(5, IsRequired = false)]
public Dictionary<string, string> GuardData { get; private set; }
[ProtoMember(3, IsRequired = false)]
public Dictionary<string, string> LoginKeys { get; private set; }
string FileName;
AccountSettingsStore()
{
SentryData = new Dictionary<string, byte[]>();
ContentServerPenalty = new ConcurrentDictionary<string, int>();
LoginTokens = new(StringComparer.OrdinalIgnoreCase);
GuardData = new(StringComparer.OrdinalIgnoreCase);
LoginKeys = new Dictionary<string, string>();
}
static bool Loaded
@ -53,9 +46,11 @@ namespace DepotDownloader
{
try
{
using var fs = IsolatedStorage.OpenFile(filename, FileMode.Open, FileAccess.Read);
using var ds = new DeflateStream(fs, CompressionMode.Decompress);
Instance = Serializer.Deserialize<AccountSettingsStore>(ds);
using (var fs = IsolatedStorage.OpenFile(filename, FileMode.Open, FileAccess.Read))
using (var ds = new DeflateStream(fs, CompressionMode.Decompress))
{
Instance = Serializer.Deserialize<AccountSettingsStore>(ds);
}
}
catch (IOException ex)
{
@ -78,9 +73,11 @@ namespace DepotDownloader
try
{
using var fs = IsolatedStorage.OpenFile(Instance.FileName, FileMode.Create, FileAccess.Write);
using var ds = new DeflateStream(fs, CompressionMode.Compress);
Serializer.Serialize(ds, Instance);
using (var fs = IsolatedStorage.OpenFile(Instance.FileName, FileMode.Create, FileAccess.Write))
using (var ds = new DeflateStream(fs, CompressionMode.Compress))
{
Serializer.Serialize(ds, Instance);
}
}
catch (IOException ex)
{

@ -1,59 +0,0 @@
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System;
using Spectre.Console;
namespace DepotDownloader;
static class Ansi
{
// https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
// https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
public enum ProgressState
{
Hidden = 0,
Default = 1,
Error = 2,
Indeterminate = 3,
Warning = 4,
}
const char ESC = (char)0x1B;
const char BEL = (char)0x07;
private static bool useProgress;
public static void Init()
{
if (Console.IsInputRedirected || Console.IsOutputRedirected)
{
return;
}
if (OperatingSystem.IsLinux())
{
return;
}
var (supportsAnsi, legacyConsole) = AnsiDetector.Detect(stdError: false, upgrade: true);
useProgress = supportsAnsi && !legacyConsole;
}
public static void Progress(ulong downloaded, ulong total)
{
var progress = (byte)MathF.Round(downloaded / (float)total * 100.0f);
Progress(ProgressState.Default, progress);
}
public static void Progress(ProgressState state, byte progress = 0)
{
if (!useProgress)
{
return;
}
Console.Write($"{ESC}]9;4;{(byte)state};{progress}{BEL}");
}
}

@ -1,117 +0,0 @@
// Copied from https://github.com/spectreconsole/spectre.console/blob/d79e6adc5f8e637fb35c88f987023ffda6707243/src/Spectre.Console/Internal/Backends/Ansi/AnsiDetector.cs
// MIT License - Copyright(c) 2020 Patrik Svensson, Phil Scott, Nils Andresen
// which is partially based on https://github.com/keqingrong/supports-ansi/blob/master/index.js
// <auto-generated/>
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Windows.Win32;
using Windows.Win32.System.Console;
namespace Spectre.Console;
internal static class AnsiDetector
{
private static readonly Regex[] _regexes =
[
new("^xterm"), // xterm, PuTTY, Mintty
new("^rxvt"), // RXVT
new("^eterm"), // Eterm
new("^screen"), // GNU screen, tmux
new("tmux"), // tmux
new("^vt100"), // DEC VT series
new("^vt102"), // DEC VT series
new("^vt220"), // DEC VT series
new("^vt320"), // DEC VT series
new("ansi"), // ANSI
new("scoansi"), // SCO ANSI
new("cygwin"), // Cygwin, MinGW
new("linux"), // Linux console
new("konsole"), // Konsole
new("bvterm"), // Bitvise SSH Client
new("^st-256color"), // Suckless Simple Terminal, st
new("alacritty"), // Alacritty
];
public static (bool SupportsAnsi, bool LegacyConsole) Detect(bool stdError, bool upgrade)
{
// Running on Windows?
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Running under ConEmu?
var conEmu = Environment.GetEnvironmentVariable("ConEmuANSI");
if (!string.IsNullOrEmpty(conEmu) && conEmu.Equals("On", StringComparison.OrdinalIgnoreCase))
{
return (true, false);
}
var supportsAnsi = WindowsSupportsAnsi(upgrade, stdError, out var legacyConsole);
return (supportsAnsi, legacyConsole);
}
return DetectFromTerm();
}
private static (bool SupportsAnsi, bool LegacyConsole) DetectFromTerm()
{
// Check if the terminal is of type ANSI/VT100/xterm compatible.
var term = Environment.GetEnvironmentVariable("TERM");
if (!string.IsNullOrWhiteSpace(term))
{
if (_regexes.Any(regex => regex.IsMatch(term)))
{
return (true, false);
}
}
return (false, true);
}
private static bool WindowsSupportsAnsi(bool upgrade, bool stdError, out bool isLegacy)
{
isLegacy = false;
try
{
var @out = PInvoke.GetStdHandle_SafeHandle(stdError ? STD_HANDLE.STD_ERROR_HANDLE :STD_HANDLE.STD_OUTPUT_HANDLE);
if (!PInvoke.GetConsoleMode(@out, out var mode))
{
// Could not get console mode, try TERM (set in cygwin, WSL-Shell).
var (ansiFromTerm, legacyFromTerm) = DetectFromTerm();
isLegacy = ansiFromTerm ? legacyFromTerm : isLegacy;
return ansiFromTerm;
}
if ((mode & CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0||true)
{
isLegacy = true;
if (!upgrade)
{
return false;
}
// Try enable ANSI support.
mode |= CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING | CONSOLE_MODE.DISABLE_NEWLINE_AUTO_RETURN;
if (!PInvoke.SetConsoleMode(@out, mode))
{
// Enabling failed.
return false;
}
isLegacy = false;
}
return true;
}
catch
{
// All we know here is that we don't support ANSI.
return false;
}
}
}

@ -1,11 +1,11 @@
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using SteamKit2.CDN;
using SteamKit2;
namespace DepotDownloader
{
@ -14,80 +14,173 @@ namespace DepotDownloader
/// </summary>
class CDNClientPool
{
private const int ServerEndpointMinimumSize = 8;
private readonly Steam3Session steamSession;
private readonly uint appId;
public Client CDNClient { get; }
public Server ProxyServer { get; private set; }
public CDNClient CDNClient { get; }
public CDNClient.Server ProxyServer { get; private set; }
private readonly ConcurrentStack<CDNClient.Server> activeConnectionPool;
private readonly BlockingCollection<CDNClient.Server> availableServerEndpoints;
private readonly List<Server> servers = [];
private int nextServer;
private readonly AutoResetEvent populatePoolEvent;
private readonly Task monitorTask;
private readonly CancellationTokenSource shutdownToken;
public CancellationTokenSource ExhaustedToken { get; set; }
public CDNClientPool(Steam3Session steamSession, uint appId)
{
this.steamSession = steamSession;
this.appId = appId;
CDNClient = new Client(steamSession.steamClient);
CDNClient = new CDNClient(steamSession.steamClient);
activeConnectionPool = new ConcurrentStack<CDNClient.Server>();
availableServerEndpoints = new BlockingCollection<CDNClient.Server>();
populatePoolEvent = new AutoResetEvent(true);
shutdownToken = new CancellationTokenSource();
monitorTask = Task.Factory.StartNew(ConnectionPoolMonitorAsync).Unwrap();
}
public async Task UpdateServerList()
public void Shutdown()
{
var servers = await this.steamSession.steamContent.GetServersForSteamPipe();
shutdownToken.Cancel();
monitorTask.Wait();
}
ProxyServer = servers.Where(x => x.UseAsProxy).FirstOrDefault();
private async Task<IReadOnlyCollection<CDNClient.Server>> FetchBootstrapServerListAsync()
{
var backoffDelay = 0;
var weightedCdnServers = servers
.Where(server =>
while (!shutdownToken.IsCancellationRequested)
{
try
{
var isEligibleForApp = server.AllowedAppIds.Length == 0 || server.AllowedAppIds.Contains(appId);
return isEligibleForApp && (server.Type == "SteamCache" || server.Type == "CDN");
})
.Select(server =>
var cdnServers = await ContentServerDirectoryService.LoadAsync(this.steamSession.steamClient.Configuration, ContentDownloader.Config.CellID, shutdownToken.Token);
if (cdnServers != null)
{
return cdnServers;
}
}
catch (Exception ex)
{
AccountSettingsStore.Instance.ContentServerPenalty.TryGetValue(server.Host, out var penalty);
Console.WriteLine("Failed to retrieve content server list: {0}", ex.Message);
if (ex is SteamKitWebRequestException e && e.StatusCode == (HttpStatusCode)429)
{
// If we're being throttled, add a delay to the next request
backoffDelay = Math.Min(5, ++backoffDelay);
await Task.Delay(TimeSpan.FromSeconds(backoffDelay));
}
}
}
return (server, penalty);
})
.OrderBy(pair => pair.penalty).ThenBy(pair => pair.server.WeightedLoad);
return null;
}
private async Task ConnectionPoolMonitorAsync()
{
var didPopulate = false;
foreach (var (server, weight) in weightedCdnServers)
while (!shutdownToken.IsCancellationRequested)
{
for (var i = 0; i < server.NumEntries; i++)
populatePoolEvent.WaitOne(TimeSpan.FromSeconds(1));
// We want the Steam session so we can take the CellID from the session and pass it through to the ContentServer Directory Service
if (availableServerEndpoints.Count < ServerEndpointMinimumSize && steamSession.steamClient.IsConnected)
{
var servers = await FetchBootstrapServerListAsync().ConfigureAwait(false);
if (servers == null || servers.Count == 0)
{
ExhaustedToken?.Cancel();
return;
}
ProxyServer = servers.Where(x => x.UseAsProxy).FirstOrDefault();
var weightedCdnServers = servers
.Where(server =>
{
var isEligibleForApp = server.AllowedAppIds == null || server.AllowedAppIds.Contains(appId);
return isEligibleForApp && (server.Type == "SteamCache" || server.Type == "CDN");
})
.Select(server =>
{
AccountSettingsStore.Instance.ContentServerPenalty.TryGetValue(server.Host, out var penalty);
return (server, penalty);
})
.OrderBy(pair => pair.penalty).ThenBy(pair => pair.server.WeightedLoad);
foreach (var (server, weight) in weightedCdnServers)
{
for (var i = 0; i < server.NumEntries; i++)
{
availableServerEndpoints.Add(server);
}
}
didPopulate = true;
}
else if (availableServerEndpoints.Count == 0 && !steamSession.steamClient.IsConnected && didPopulate)
{
this.servers.Add(server);
ExhaustedToken?.Cancel();
return;
}
}
}
if (this.servers.Count == 0)
private CDNClient.Server BuildConnection(CancellationToken token)
{
if (availableServerEndpoints.Count < ServerEndpointMinimumSize)
{
throw new Exception("Failed to retrieve any download servers.");
populatePoolEvent.Set();
}
return availableServerEndpoints.Take(token);
}
public Server GetConnection()
public CDNClient.Server GetConnection(CancellationToken token)
{
return servers[nextServer % servers.Count];
if (!activeConnectionPool.TryPop(out var connection))
{
connection = BuildConnection(token);
}
return connection;
}
public void ReturnConnection(Server server)
public async Task<string> AuthenticateConnection(uint appId, uint depotId, CDNClient.Server server)
{
if (server == null) return;
var host = steamSession.ResolveCDNTopLevelHost(server.Host);
var cdnKey = $"{depotId:D}:{host}";
steamSession.RequestCDNAuthToken(appId, depotId, host, cdnKey);
if (steamSession.CDNAuthTokens.TryGetValue(cdnKey, out var authTokenCallbackPromise))
{
var result = await authTokenCallbackPromise.Task;
return result.Token;
}
// nothing to do, maybe remove from ContentServerPenalty?
throw new Exception($"Failed to retrieve CDN token for server {server.Host} depot {depotId}");
}
public void ReturnBrokenConnection(Server server)
public void ReturnConnection(CDNClient.Server server)
{
if (server == null) return;
lock (servers)
{
if (servers[nextServer % servers.Count] == server)
{
nextServer++;
activeConnectionPool.Push(server);
}
// TODO: Add server to ContentServerPenalty
}
}
public void ReturnBrokenConnection(CDNClient.Server server)
{
if (server == null) return;
// Broken connections are not returned to the pool
}
}
}

@ -1,76 +0,0 @@
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System;
using System.Threading.Tasks;
using SteamKit2.Authentication;
namespace DepotDownloader
{
// This is practically copied from https://github.com/SteamRE/SteamKit/blob/master/SteamKit2/SteamKit2/Steam/Authentication/UserConsoleAuthenticator.cs
internal class ConsoleAuthenticator : IAuthenticator
{
/// <inheritdoc />
public Task<string> GetDeviceCodeAsync(bool previousCodeWasIncorrect)
{
if (previousCodeWasIncorrect)
{
Console.Error.WriteLine("The previous 2-factor auth code you have provided is incorrect.");
}
string code;
do
{
Console.Error.Write("STEAM GUARD! Please enter your 2-factor auth code from your authenticator app: ");
code = Console.ReadLine()?.Trim();
if (code == null)
{
break;
}
}
while (string.IsNullOrEmpty(code));
return Task.FromResult(code!);
}
/// <inheritdoc />
public Task<string> GetEmailCodeAsync(string email, bool previousCodeWasIncorrect)
{
if (previousCodeWasIncorrect)
{
Console.Error.WriteLine("The previous 2-factor auth code you have provided is incorrect.");
}
string code;
do
{
Console.Error.Write($"STEAM GUARD! Please enter the auth code sent to the email at {email}: ");
code = Console.ReadLine()?.Trim();
if (code == null)
{
break;
}
}
while (string.IsNullOrEmpty(code));
return Task.FromResult(code!);
}
/// <inheritdoc />
public Task<bool> AcceptDeviceConfirmationAsync()
{
if (ContentDownloader.Config.SkipAppConfirmation)
{
return Task.FromResult(false);
}
Console.Error.WriteLine("STEAM GUARD! Use the Steam Mobile App to confirm your sign in...");
return Task.FromResult(true);
}
}
}

File diff suppressed because it is too large Load Diff

@ -1,7 +1,4 @@
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
@ -19,7 +16,7 @@ namespace DepotDownloader
DepotConfigStore()
{
InstalledManifestIDs = [];
InstalledManifestIDs = new Dictionary<uint, ulong>();
}
static bool Loaded
@ -36,9 +33,9 @@ namespace DepotDownloader
if (File.Exists(filename))
{
using var fs = File.Open(filename, FileMode.Open);
using var ds = new DeflateStream(fs, CompressionMode.Decompress);
Instance = Serializer.Deserialize<DepotConfigStore>(ds);
using (var fs = File.Open(filename, FileMode.Open))
using (var ds = new DeflateStream(fs, CompressionMode.Decompress))
Instance = Serializer.Deserialize<DepotConfigStore>(ds);
}
else
{
@ -53,9 +50,9 @@ namespace DepotDownloader
if (!Loaded)
throw new Exception("Saved config before loading");
using var fs = File.Open(Instance.FileName, FileMode.Create);
using var ds = new DeflateStream(fs, CompressionMode.Compress);
Serializer.Serialize(ds, Instance);
using (var fs = File.Open(Instance.FileName, FileMode.Create))
using (var ds = new DeflateStream(fs, CompressionMode.Compress))
Serializer.Serialize(ds, Instance);
}
}
}

@ -1,32 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<RollForward>LatestMajor</RollForward>
<Version>3.4.0</Version>
<Version>2.4.5</Version>
<Description>Steam Downloading Utility</Description>
<Authors>SteamRE Team</Authors>
<Copyright>Copyright © SteamRE Team 2025</Copyright>
<ApplicationIcon>..\Icon\DepotDownloader.ico</ApplicationIcon>
<Deterministic>true</Deterministic>
<TreatWarningsAsErrors Condition="'$(ContinuousIntegrationBuild)' == 'true'">true</TreatWarningsAsErrors>
<InvariantGlobalization>true</InvariantGlobalization>
<Copyright>Copyright © SteamRE Team 2021</Copyright>
</PropertyGroup>
<ItemGroup>
<None Include="..\LICENSE" Link="LICENSE">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="protobuf-net" Version="3.2.52" />
<PackageReference Include="QRCoder" Version="1.6.0" />
<PackageReference Include="SteamKit2" Version="3.3.0" />
<PackageReference Include="protobuf-net" Version="3.0.101" />
<PackageReference Include="SteamKit2" Version="2.4.0-Alpha.3" />
</ItemGroup>
</Project>

@ -1,7 +1,4 @@
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System.Collections.Generic;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace DepotDownloader
@ -10,7 +7,6 @@ namespace DepotDownloader
{
public int CellID { get; set; }
public bool DownloadAllPlatforms { get; set; }
public bool DownloadAllArchs { get; set; }
public bool DownloadAllLanguages { get; set; }
public bool DownloadManifestOnly { get; set; }
public string InstallDirectory { get; set; }
@ -23,14 +19,13 @@ namespace DepotDownloader
public bool VerifyAll { get; set; }
public int MaxServers { get; set; }
public int MaxDownloads { get; set; }
public string SuppliedPassword { get; set; }
public bool RememberPassword { get; set; }
// A Steam LoginID to allow multiple concurrent connections
public uint? LoginID { get; set; }
public bool UseQrCode { get; set; }
public bool SkipAppConfirmation { get; set; }
}
}

@ -1,7 +1,4 @@
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System.IO;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Sockets;
@ -32,10 +29,8 @@ namespace DepotDownloader
// By default, we create dual-mode sockets:
// Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
{
NoDelay = true
};
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.NoDelay = true;
try
{

@ -1,7 +1,4 @@
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System;
using System;
using System.Diagnostics.Tracing;
using System.Text;
@ -38,7 +35,7 @@ namespace DepotDownloader
}
}
sb.Append(')');
sb.Append(")");
Console.WriteLine(sb.ToString());
}
}

@ -1,5 +0,0 @@
GetConsoleMode
GetConsoleProcessList
GetStdHandle
MessageBox
SetConsoleMode

@ -1,51 +1,84 @@
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System.IO;
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace DepotDownloader
{
static class PlatformUtilities
public static class PlatformUtilities
{
public static void SetExecutable(string path, bool value)
private const int ModeExecuteOwner = 0x0040;
private const int ModeExecuteGroup = 0x0008;
private const int ModeExecuteOther = 0x0001;
private const int ModeExecute = ModeExecuteOwner | ModeExecuteGroup | ModeExecuteOther;
[StructLayout(LayoutKind.Explicit, Size = 144)]
private readonly struct StatLinux
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
[FieldOffset(24)] public readonly uint st_mode;
}
[StructLayout(LayoutKind.Explicit, Size = 144)]
private readonly struct StatOSX
{
[FieldOffset(4)] public readonly ushort st_mode;
}
[DllImport("libc", EntryPoint = "__xstat", SetLastError = true)]
private static extern int statLinux(int version, string path, out StatLinux statLinux);
[DllImport("libc", EntryPoint = "stat", SetLastError = true)]
private static extern int statOSX(string path, out StatOSX stat);
const UnixFileMode ModeExecute = UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute;
[DllImport("libc", EntryPoint = "stat$INODE64", SetLastError = true)]
private static extern int statOSXCompat(string path, out StatOSX stat);
var mode = File.GetUnixFileMode(path);
var hasExecuteMask = (mode & ModeExecute) == ModeExecute;
if (hasExecuteMask != value)
[DllImport("libc", SetLastError = true)]
private static extern int chmod(string path, uint mode);
[DllImport("libc", SetLastError = true)]
private static extern int chmod(string path, ushort mode);
[DllImport("libc", CallingConvention = CallingConvention.Cdecl, SetLastError = true)]
private static extern IntPtr strerror(int errno);
private static void ThrowIf(int i)
{
if (i == -1)
{
File.SetUnixFileMode(path, value
? mode | ModeExecute
: mode & ~ModeExecute);
var errno = Marshal.GetLastWin32Error();
throw new Exception(Marshal.PtrToStringAnsi(strerror(errno)));
}
}
[SupportedOSPlatform("windows5.0")]
public static void VerifyConsoleLaunch()
public static void SetExecutable(string path, bool value)
{
// Reference: https://devblogs.microsoft.com/oldnewthing/20160125-00/?p=92922
var processList = new uint[2];
var processCount = Windows.Win32.PInvoke.GetConsoleProcessList(processList);
if (processCount != 1)
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return;
ThrowIf(statLinux(1, path, out var stat));
var hasExecuteMask = (stat.st_mode & ModeExecute) == ModeExecute;
if (hasExecuteMask != value)
{
ThrowIf(chmod(path, (uint)(value
? stat.st_mode | ModeExecute
: stat.st_mode & ~ModeExecute)));
}
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
StatOSX stat;
_ = Windows.Win32.PInvoke.MessageBox(
Windows.Win32.Foundation.HWND.Null,
"Depot Downloader is a console application; there is no GUI.\n\nIf you do not pass any command line parameters, it prints usage info and exits.\n\nYou must use this from a terminal/console.",
"Depot Downloader",
Windows.Win32.UI.WindowsAndMessaging.MESSAGEBOX_STYLE.MB_OK | Windows.Win32.UI.WindowsAndMessaging.MESSAGEBOX_STYLE.MB_ICONWARNING
);
ThrowIf(RuntimeInformation.ProcessArchitecture == Architecture.Arm64
? statOSX(path, out stat)
: statOSXCompat(path, out stat));
var hasExecuteMask = (stat.st_mode & ModeExecute) == ModeExecute;
if (hasExecuteMask != value)
{
ThrowIf(chmod(path, (ushort)(value
? stat.st_mode | ModeExecute
: stat.st_mode & ~ModeExecute)));
}
}
}
}
}

@ -1,60 +1,35 @@
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using SteamKit2;
using SteamKit2.CDN;
namespace DepotDownloader
{
class Program
{
private static bool[] consumedArgs;
static int Main(string[] args)
=> MainAsync(args).GetAwaiter().GetResult();
static async Task<int> Main(string[] args)
static async Task<int> MainAsync(string[] args)
{
if (args.Length == 0)
{
PrintVersion();
PrintUsage();
if (OperatingSystem.IsWindowsVersionAtLeast(5, 0))
{
PlatformUtilities.VerifyConsoleLaunch();
}
return 0;
return 1;
}
Ansi.Init();
DebugLog.Enabled = false;
AccountSettingsStore.LoadFromFile("account.config");
#region Common Options
// Not using HasParameter because it is case insensitive
if (args.Length == 1 && (args[0] == "-V" || args[0] == "--version"))
{
PrintVersion(true);
return 0;
}
consumedArgs = new bool[args.Length];
if (HasParameter(args, "-debug"))
{
PrintVersion(true);
DebugLog.Enabled = true;
DebugLog.AddListener((category, message) =>
{
@ -67,22 +42,6 @@ namespace DepotDownloader
var username = GetParameter<string>(args, "-username") ?? GetParameter<string>(args, "-user");
var password = GetParameter<string>(args, "-password") ?? GetParameter<string>(args, "-pass");
ContentDownloader.Config.RememberPassword = HasParameter(args, "-remember-password");
ContentDownloader.Config.UseQrCode = HasParameter(args, "-qr");
ContentDownloader.Config.SkipAppConfirmation = HasParameter(args, "-no-mobile");
if (username == null)
{
if (ContentDownloader.Config.RememberPassword && !ContentDownloader.Config.UseQrCode)
{
Console.WriteLine("Error: -remember-password can not be used without -username or -qr.");
return 1;
}
}
else if (ContentDownloader.Config.UseQrCode)
{
Console.WriteLine("Error: -qr can not be used with -username.");
return 1;
}
ContentDownloader.Config.DownloadManifestOnly = HasParameter(args, "-manifest-only");
@ -98,26 +57,20 @@ namespace DepotDownloader
if (fileList != null)
{
const string RegexPrefix = "regex:";
try
{
var fileListData = await File.ReadAllTextAsync(fileList);
var files = fileListData.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
ContentDownloader.Config.UsingFileList = true;
ContentDownloader.Config.FilesToDownload = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
ContentDownloader.Config.FilesToDownloadRegex = [];
var files = await File.ReadAllLinesAsync(fileList);
ContentDownloader.Config.FilesToDownloadRegex = new List<Regex>();
foreach (var fileEntry in files)
{
if (string.IsNullOrWhiteSpace(fileEntry))
{
continue;
}
if (fileEntry.StartsWith(RegexPrefix))
if (fileEntry.StartsWith("regex:"))
{
var rgx = new Regex(fileEntry[RegexPrefix.Length..], RegexOptions.Compiled | RegexOptions.IgnoreCase);
var rgx = new Regex(fileEntry.Substring(6), RegexOptions.Compiled | RegexOptions.IgnoreCase);
ContentDownloader.Config.FilesToDownloadRegex.Add(rgx);
}
else
@ -137,24 +90,9 @@ namespace DepotDownloader
ContentDownloader.Config.InstallDirectory = GetParameter<string>(args, "-dir");
ContentDownloader.Config.VerifyAll = HasParameter(args, "-verify-all") || HasParameter(args, "-verify_all") || HasParameter(args, "-validate");
if (HasParameter(args, "-use-lancache"))
{
await Client.DetectLancacheServerAsync();
if (Client.UseLancacheServer)
{
Console.WriteLine("Detected Lancache server! Downloads will be directed through the Lancache.");
// Increasing the number of concurrent downloads when the cache is detected since the downloads will likely
// be served much faster than over the internet. Steam internally has this behavior as well.
if (!HasParameter(args, "-max-downloads"))
{
ContentDownloader.Config.MaxDownloads = 25;
}
}
}
ContentDownloader.Config.MaxServers = GetParameter(args, "-max-servers", 20);
ContentDownloader.Config.MaxDownloads = GetParameter(args, "-max-downloads", 8);
ContentDownloader.Config.MaxServers = Math.Max(ContentDownloader.Config.MaxServers, ContentDownloader.Config.MaxDownloads);
ContentDownloader.Config.LoginID = HasParameter(args, "-loginid") ? GetParameter<uint>(args, "-loginid") : null;
#endregion
@ -172,8 +110,6 @@ namespace DepotDownloader
{
#region Pubfile Downloading
PrintUnconsumedArgs(args);
if (InitializeSteam(username, password))
{
try
@ -209,8 +145,6 @@ namespace DepotDownloader
{
#region UGC Downloading
PrintUnconsumedArgs(args);
if (InitializeSteam(username, password))
{
try
@ -247,38 +181,23 @@ namespace DepotDownloader
#region App downloading
var branch = GetParameter<string>(args, "-branch") ?? GetParameter<string>(args, "-beta") ?? ContentDownloader.DEFAULT_BRANCH;
ContentDownloader.Config.BetaPassword = GetParameter<string>(args, "-branchpassword") ?? GetParameter<string>(args, "-betapassword");
if (!string.IsNullOrEmpty(ContentDownloader.Config.BetaPassword) && string.IsNullOrEmpty(branch))
{
Console.WriteLine("Error: Cannot specify -branchpassword when -branch is not specified.");
return 1;
}
ContentDownloader.Config.BetaPassword = GetParameter<string>(args, "-betapassword");
ContentDownloader.Config.DownloadAllPlatforms = HasParameter(args, "-all-platforms");
var os = GetParameter<string>(args, "-os");
if (ContentDownloader.Config.DownloadAllPlatforms && !string.IsNullOrEmpty(os))
if (ContentDownloader.Config.DownloadAllPlatforms && !String.IsNullOrEmpty(os))
{
Console.WriteLine("Error: Cannot specify -os when -all-platforms is specified.");
return 1;
}
ContentDownloader.Config.DownloadAllArchs = HasParameter(args, "-all-archs");
var arch = GetParameter<string>(args, "-osarch");
if (ContentDownloader.Config.DownloadAllArchs && !string.IsNullOrEmpty(arch))
{
Console.WriteLine("Error: Cannot specify -osarch when -all-archs is specified.");
return 1;
}
ContentDownloader.Config.DownloadAllLanguages = HasParameter(args, "-all-languages");
var language = GetParameter<string>(args, "-language");
if (ContentDownloader.Config.DownloadAllLanguages && !string.IsNullOrEmpty(language))
if (ContentDownloader.Config.DownloadAllLanguages && !String.IsNullOrEmpty(language))
{
Console.WriteLine("Error: Cannot specify -language when -all-languages is specified.");
return 1;
@ -307,8 +226,6 @@ namespace DepotDownloader
depotManifestIds.AddRange(depotIdList.Select(depotId => (depotId, ContentDownloader.INVALID_MANIFEST_ID)));
}
PrintUnconsumedArgs(args);
if (InitializeSteam(username, password))
{
try
@ -346,52 +263,32 @@ namespace DepotDownloader
static bool InitializeSteam(string username, string password)
{
if (!ContentDownloader.Config.UseQrCode)
if (username != null && password == null && (!ContentDownloader.Config.RememberPassword || !AccountSettingsStore.Instance.LoginKeys.ContainsKey(username)))
{
if (username != null && password == null && (!ContentDownloader.Config.RememberPassword || !AccountSettingsStore.Instance.LoginTokens.ContainsKey(username)))
do
{
if (AccountSettingsStore.Instance.LoginTokens.ContainsKey(username))
Console.Write("Enter account password for \"{0}\": ", username);
if (Console.IsInputRedirected)
{
Console.WriteLine($"Account \"{username}\" has stored credentials. Did you forget to specify -remember-password?");
password = Console.ReadLine();
}
do
else
{
Console.Write("Enter account password for \"{0}\": ", username);
if (Console.IsInputRedirected)
{
password = Console.ReadLine();
}
else
{
// Avoid console echoing of password
password = Util.ReadPassword();
}
// Avoid console echoing of password
password = Util.ReadPassword();
}
Console.WriteLine();
} while (string.Empty == password);
}
else if (username == null)
{
Console.WriteLine("No username given. Using anonymous account with dedicated server subscription.");
}
Console.WriteLine();
} while (String.Empty == password);
}
if (!string.IsNullOrEmpty(password))
else if (username == null)
{
const int MAX_PASSWORD_SIZE = 64;
if (password.Length > MAX_PASSWORD_SIZE)
{
Console.Error.WriteLine($"Warning: Password is longer than {MAX_PASSWORD_SIZE} characters, which is not supported by Steam.");
}
if (!password.All(char.IsAscii))
{
Console.Error.WriteLine("Warning: Password contains non-ASCII characters, which is not supported by Steam.");
}
Console.WriteLine("No username given. Using anonymous account with dedicated server subscription.");
}
// capture the supplied password in case we need to re-use it after checking the login key
ContentDownloader.Config.SuppliedPassword = password;
return ContentDownloader.InitializeSteam3(username, password);
}
@ -400,10 +297,7 @@ namespace DepotDownloader
for (var x = 0; x < args.Length; ++x)
{
if (args[x].Equals(param, StringComparison.OrdinalIgnoreCase))
{
consumedArgs[x] = true;
return x;
}
}
return -1;
@ -414,7 +308,7 @@ namespace DepotDownloader
return IndexOfParam(args, param) > -1;
}
static T GetParameter<T>(string[] args, string param, T defaultValue = default)
static T GetParameter<T>(string[] args, string param, T defaultValue = default(T))
{
var index = IndexOfParam(args, param);
@ -426,11 +320,10 @@ namespace DepotDownloader
var converter = TypeDescriptor.GetConverter(typeof(T));
if (converter != null)
{
consumedArgs[index + 1] = true;
return (T)converter.ConvertFromString(strParam);
}
return default;
return default(T);
}
static List<T> GetParameterList<T>(string[] args, string param)
@ -452,7 +345,6 @@ namespace DepotDownloader
var converter = TypeDescriptor.GetConverter(typeof(T));
if (converter != null)
{
consumedArgs[index] = true;
list.Add((T)converter.ConvertFromString(strParam));
}
@ -462,89 +354,47 @@ namespace DepotDownloader
return list;
}
static void PrintUnconsumedArgs(string[] args)
{
var printError = false;
for (var index = 0; index < consumedArgs.Length; index++)
{
if (!consumedArgs[index])
{
printError = true;
Console.Error.WriteLine($"Argument #{index + 1} {args[index]} was not used.");
}
}
if (printError)
{
Console.Error.WriteLine("Make sure you specified the arguments correctly. Check --help for correct arguments.");
Console.Error.WriteLine();
}
}
static void PrintUsage()
{
// Do not use tabs to align parameters here because tab size may differ
Console.WriteLine();
Console.WriteLine("Usage: downloading one or all depots for an app:");
Console.WriteLine(" depotdownloader -app <id> [-depot <id> [-manifest <id>]]");
Console.WriteLine(" [-username <username> [-password <password>]] [other options]");
Console.WriteLine("Usage - downloading one or all depots for an app:");
Console.WriteLine("\tdepotdownloader -app <id> [-depot <id> [-manifest <id>]]");
Console.WriteLine("\t\t[-username <username> [-password <password>]] [other options]");
Console.WriteLine();
Console.WriteLine("Usage: downloading a workshop item using pubfile id");
Console.WriteLine(" depotdownloader -app <id> -pubfile <id> [-username <username> [-password <password>]]");
Console.WriteLine("Usage: downloading a workshop item using ugc id");
Console.WriteLine(" depotdownloader -app <id> -ugc <id> [-username <username> [-password <password>]]");
Console.WriteLine("Usage - downloading a workshop item using pubfile id");
Console.WriteLine("\tdepotdownloader -app <id> -pubfile <id> [-username <username> [-password <password>]]");
Console.WriteLine("Usage - downloading a workshop item using ugc id");
Console.WriteLine("\tdepotdownloader -app <id> -ugc <id> [-username <username> [-password <password>]]");
Console.WriteLine();
Console.WriteLine("Parameters:");
Console.WriteLine(" -app <#> - the AppID to download.");
Console.WriteLine(" -depot <#> - the DepotID to download.");
Console.WriteLine(" -manifest <id> - manifest id of content to download (requires -depot, default: current for branch).");
Console.WriteLine($" -branch <branchname> - download from specified branch if available (default: {ContentDownloader.DEFAULT_BRANCH}).");
Console.WriteLine(" -branchpassword <pass> - branch password if applicable.");
Console.WriteLine(" -all-platforms - downloads all platform-specific depots when -app is used.");
Console.WriteLine(" -all-archs - download all architecture-specific depots when -app is used.");
Console.WriteLine(" -os <os> - the operating system for which to download the game (windows, macos or linux, default: OS the program is currently running on)");
Console.WriteLine(" -osarch <arch> - the architecture for which to download the game (32 or 64, default: the host's architecture)");
Console.WriteLine(" -all-languages - download all language-specific depots when -app is used.");
Console.WriteLine(" -language <lang> - the language for which to download the game (default: english)");
Console.WriteLine(" -lowviolence - download low violence depots when -app is used.");
Console.WriteLine("\t-app <#>\t\t\t\t- the AppID to download.");
Console.WriteLine("\t-depot <#>\t\t\t\t- the DepotID to download.");
Console.WriteLine("\t-manifest <id>\t\t\t- manifest id of content to download (requires -depot, default: current for branch).");
Console.WriteLine("\t-beta <branchname>\t\t\t- download from specified branch if available (default: Public).");
Console.WriteLine("\t-betapassword <pass>\t\t- branch password if applicable.");
Console.WriteLine("\t-all-platforms\t\t\t- downloads all platform-specific depots when -app is used.");
Console.WriteLine("\t-os <os>\t\t\t\t- the operating system for which to download the game (windows, macos or linux, default: OS the program is currently running on)");
Console.WriteLine("\t-osarch <arch>\t\t\t\t- the architecture for which to download the game (32 or 64, default: the host's architecture)");
Console.WriteLine("\t-all-languages\t\t\t\t- download all language-specific depots when -app is used.");
Console.WriteLine("\t-language <lang>\t\t\t\t- the language for which to download the game (default: english)");
Console.WriteLine("\t-lowviolence\t\t\t\t- download low violence depots when -app is used.");
Console.WriteLine();
Console.WriteLine(" -ugc <#> - the UGC ID to download.");
Console.WriteLine(" -pubfile <#> - the PublishedFileId to download. (Will automatically resolve to UGC id)");
Console.WriteLine("\t-ugc <#>\t\t\t\t- the UGC ID to download.");
Console.WriteLine("\t-pubfile <#>\t\t\t- the PublishedFileId to download. (Will automatically resolve to UGC id)");
Console.WriteLine();
Console.WriteLine(" -username <user> - the username of the account to login to for restricted content.");
Console.WriteLine(" -password <pass> - the password of the account to login to for restricted content.");
Console.WriteLine(" -remember-password - if set, remember the password for subsequent logins of this user.");
Console.WriteLine(" use -username <username> -remember-password as login credentials.");
Console.WriteLine(" -qr - display a login QR code to be scanned with the Steam mobile app");
Console.WriteLine(" -no-mobile - prefer entering a 2FA code instead of prompting to accept in the Steam mobile app");
Console.WriteLine("\t-username <user>\t\t- the username of the account to login to for restricted content.");
Console.WriteLine("\t-password <pass>\t\t- the password of the account to login to for restricted content.");
Console.WriteLine("\t-remember-password\t\t- if set, remember the password for subsequent logins of this user.");
Console.WriteLine();
Console.WriteLine(" -dir <installdir> - the directory in which to place downloaded files.");
Console.WriteLine(" -filelist <file.txt> - the name of a local file that contains a list of files to download (from the manifest).");
Console.WriteLine(" prefix file path with `regex:` if you want to match with regex. each file path should be on their own line.");
Console.WriteLine("\t-dir <installdir>\t\t- the directory in which to place downloaded files.");
Console.WriteLine("\t-filelist <file.txt>\t- a list of files to download (from the manifest). Prefix file path with 'regex:' if you want to match with regex.");
Console.WriteLine("\t-validate\t\t\t\t- Include checksum verification of files already downloaded");
Console.WriteLine();
Console.WriteLine(" -validate - include checksum verification of files already downloaded");
Console.WriteLine(" -manifest-only - downloads a human readable manifest for any depots that would be downloaded.");
Console.WriteLine(" -cellid <#> - the overridden CellID of the content server to download from.");
Console.WriteLine(" -max-downloads <#> - maximum number of chunks to download concurrently. (default: 8).");
Console.WriteLine(" -loginid <#> - a unique 32-bit integer Steam LogonID in decimal, required if running multiple instances of DepotDownloader concurrently.");
Console.WriteLine(" -use-lancache - forces downloads over the local network via a Lancache instance.");
Console.WriteLine();
Console.WriteLine(" -debug - enable verbose debug logging.");
Console.WriteLine(" -V or --version - print version and runtime.");
}
static void PrintVersion(bool printExtra = false)
{
var version = typeof(Program).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
Console.WriteLine($"DepotDownloader v{version}");
if (!printExtra)
{
return;
}
Console.WriteLine($"Runtime: {RuntimeInformation.FrameworkDescription} on {RuntimeInformation.OSDescription}");
Console.WriteLine("\t-manifest-only\t\t\t- downloads a human readable manifest for any depots that would be downloaded.");
Console.WriteLine("\t-cellid <#>\t\t\t\t- the overridden CellID of the content server to download from.");
Console.WriteLine("\t-max-servers <#>\t\t- maximum number of content servers to use. (default: 20).");
Console.WriteLine("\t-max-downloads <#>\t\t- maximum number of chunks to download concurrently. (default: 8).");
Console.WriteLine("\t-loginid <#>\t\t- a unique 32-bit integer Steam LogonID in decimal, required if running multiple instances of DepotDownloader concurrently.");
}
}
}

@ -1,12 +1,7 @@
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
using ProtoBuf;
using SteamKit2;
@ -18,7 +13,7 @@ namespace DepotDownloader
// Proto ctor
private ProtoManifest()
{
Files = [];
Files = new List<FileData>();
}
public ProtoManifest(DepotManifest sourceManifest, ulong id) : this()
@ -34,7 +29,7 @@ namespace DepotDownloader
// Proto ctor
private FileData()
{
Chunks = [];
Chunks = new List<ChunkData>();
}
public FileData(DepotManifest.FileData sourceData) : this()
@ -80,7 +75,7 @@ namespace DepotDownloader
public ChunkData(DepotManifest.ChunkData sourceChunk)
{
ChunkID = sourceChunk.ChunkID;
Checksum = BitConverter.GetBytes(sourceChunk.Checksum);
Checksum = sourceChunk.Checksum;
Offset = sourceChunk.Offset;
CompressedLength = sourceChunk.CompressedLength;
UncompressedLength = sourceChunk.UncompressedLength;
@ -134,63 +129,33 @@ namespace DepotDownloader
return null;
}
using var ms = new MemoryStream();
using (var fs = File.Open(filename, FileMode.Open))
using (var ds = new DeflateStream(fs, CompressionMode.Decompress))
ds.CopyTo(ms);
using (var ms = new MemoryStream())
{
using (var fs = File.Open(filename, FileMode.Open))
using (var ds = new DeflateStream(fs, CompressionMode.Decompress))
ds.CopyTo(ms);
checksum = SHA1.HashData(ms.ToArray());
checksum = Util.SHAHash(ms.ToArray());
ms.Seek(0, SeekOrigin.Begin);
return Serializer.Deserialize<ProtoManifest>(ms);
ms.Seek(0, SeekOrigin.Begin);
return Serializer.Deserialize<ProtoManifest>(ms);
}
}
public void SaveToFile(string filename, out byte[] checksum)
{
using var ms = new MemoryStream();
Serializer.Serialize(ms, this);
checksum = SHA1.HashData(ms.ToArray());
ms.Seek(0, SeekOrigin.Begin);
using var fs = File.Open(filename, FileMode.Create);
using var ds = new DeflateStream(fs, CompressionMode.Compress);
ms.CopyTo(ds);
}
public DepotManifest ConvertToSteamManifest(uint depotId)
{
ulong uncompressedSize = 0, compressedSize = 0;
var newManifest = new DepotManifest();
newManifest.Files = new List<DepotManifest.FileData>(Files.Count);
foreach (var file in Files)
using (var ms = new MemoryStream())
{
var fileNameHash = SHA1.HashData(Encoding.UTF8.GetBytes(file.FileName.Replace('/', '\\').ToLowerInvariant()));
var newFile = new DepotManifest.FileData(file.FileName, fileNameHash, file.Flags, file.TotalSize, file.FileHash, null, false, file.Chunks.Count);
Serializer.Serialize(ms, this);
foreach (var chunk in file.Chunks)
{
var newChunk = new DepotManifest.ChunkData(chunk.ChunkID, BitConverter.ToUInt32(chunk.Checksum, 0), chunk.Offset, chunk.CompressedLength, chunk.UncompressedLength);
newFile.Chunks.Add(newChunk);
checksum = Util.SHAHash(ms.ToArray());
uncompressedSize += chunk.UncompressedLength;
compressedSize += chunk.CompressedLength;
}
ms.Seek(0, SeekOrigin.Begin);
newManifest.Files.Add(newFile);
using (var fs = File.Open(filename, FileMode.Create))
using (var ds = new DeflateStream(fs, CompressionMode.Compress))
ms.CopyTo(ds);
}
newManifest.FilenamesEncrypted = false;
newManifest.DepotID = depotId;
newManifest.ManifestGID = ID;
newManifest.CreationTime = CreationTime;
newManifest.TotalUncompressedSize = uncompressedSize;
newManifest.TotalCompressedSize = compressedSize;
newManifest.EncryptedCRC = 0;
return newManifest;
}
}
}

@ -1,24 +1,28 @@
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using QRCoder;
using SteamKit2;
using SteamKit2.Authentication;
using SteamKit2.CDN;
using SteamKit2.Internal;
namespace DepotDownloader
{
class Steam3Session
{
public bool IsLoggedOn { get; private set; }
public class Credentials
{
public bool LoggedOn { get; set; }
public ulong SessionToken { get; set; }
public bool IsValid
{
get { return LoggedOn; }
}
}
public ReadOnlyCollection<SteamApps.LicenseListCallback.License> Licenses
{
@ -26,45 +30,68 @@ namespace DepotDownloader
private set;
}
public Dictionary<uint, ulong> AppTokens { get; } = [];
public Dictionary<uint, ulong> PackageTokens { get; } = [];
public Dictionary<uint, byte[]> DepotKeys { get; } = [];
public ConcurrentDictionary<(uint, string), TaskCompletionSource<SteamContent.CDNAuthToken>> CDNAuthTokens { get; } = [];
public Dictionary<uint, SteamApps.PICSProductInfoCallback.PICSProductInfo> AppInfo { get; } = [];
public Dictionary<uint, SteamApps.PICSProductInfoCallback.PICSProductInfo> PackageInfo { get; } = [];
public Dictionary<string, byte[]> AppBetaPasswords { get; } = [];
public Dictionary<uint, ulong> AppTokens { get; private set; }
public Dictionary<uint, ulong> PackageTokens { get; private set; }
public Dictionary<uint, byte[]> DepotKeys { get; private set; }
public ConcurrentDictionary<string, TaskCompletionSource<SteamApps.CDNAuthTokenCallback>> CDNAuthTokens { get; private set; }
public Dictionary<uint, SteamApps.PICSProductInfoCallback.PICSProductInfo> AppInfo { get; private set; }
public Dictionary<uint, SteamApps.PICSProductInfoCallback.PICSProductInfo> PackageInfo { get; private set; }
public Dictionary<string, byte[]> AppBetaPasswords { get; private set; }
public SteamClient steamClient;
public SteamUser steamUser;
public SteamContent steamContent;
readonly SteamApps steamApps;
readonly SteamCloud steamCloud;
readonly PublishedFile steamPublishedFile;
readonly SteamUnifiedMessages.UnifiedService<IPublishedFile> steamPublishedFile;
readonly CallbackManager callbacks;
readonly bool authenticatedUser;
bool bConnected;
bool bConnecting;
bool bAborted;
bool bExpectingDisconnectRemote;
bool bDidDisconnect;
bool bDidReceiveLoginKey;
bool bIsConnectionRecovery;
int connectionBackoff;
int seq; // more hack fixes
AuthSession authSession;
readonly CancellationTokenSource abortedToken = new();
DateTime connectTime;
// input
readonly SteamUser.LogOnDetails logonDetails;
// output
readonly Credentials credentials;
static readonly TimeSpan STEAM3_TIMEOUT = TimeSpan.FromSeconds(30);
public Steam3Session(SteamUser.LogOnDetails details)
{
this.logonDetails = details;
this.authenticatedUser = details.Username != null || ContentDownloader.Config.UseQrCode;
this.authenticatedUser = details.Username != null;
this.credentials = new Credentials();
this.bConnected = false;
this.bConnecting = false;
this.bAborted = false;
this.bExpectingDisconnectRemote = false;
this.bDidDisconnect = false;
this.bDidReceiveLoginKey = false;
this.seq = 0;
this.AppTokens = new Dictionary<uint, ulong>();
this.PackageTokens = new Dictionary<uint, ulong>();
this.DepotKeys = new Dictionary<uint, byte[]>();
this.CDNAuthTokens = new ConcurrentDictionary<string, TaskCompletionSource<SteamApps.CDNAuthTokenCallback>>();
this.AppInfo = new Dictionary<uint, SteamApps.PICSProductInfoCallback.PICSProductInfo>();
this.PackageInfo = new Dictionary<uint, SteamApps.PICSProductInfoCallback.PICSProductInfo>();
this.AppBetaPasswords = new Dictionary<string, byte[]>();
var clientConfiguration = SteamConfiguration.Create(config =>
config
.WithHttpClientFactory(static purpose => HttpClientFactory.CreateHttpClient())
.WithHttpClientFactory(HttpClientFactory.CreateHttpClient)
);
this.steamClient = new SteamClient(clientConfiguration);
@ -73,23 +100,42 @@ namespace DepotDownloader
this.steamApps = this.steamClient.GetHandler<SteamApps>();
this.steamCloud = this.steamClient.GetHandler<SteamCloud>();
var steamUnifiedMessages = this.steamClient.GetHandler<SteamUnifiedMessages>();
this.steamPublishedFile = steamUnifiedMessages.CreateService<PublishedFile>();
this.steamContent = this.steamClient.GetHandler<SteamContent>();
this.steamPublishedFile = steamUnifiedMessages.CreateService<IPublishedFile>();
this.callbacks = new CallbackManager(this.steamClient);
this.callbacks.Subscribe<SteamClient.ConnectedCallback>(ConnectedCallback);
this.callbacks.Subscribe<SteamClient.DisconnectedCallback>(DisconnectedCallback);
this.callbacks.Subscribe<SteamUser.LoggedOnCallback>(LogOnCallback);
this.callbacks.Subscribe<SteamUser.SessionTokenCallback>(SessionTokenCallback);
this.callbacks.Subscribe<SteamApps.LicenseListCallback>(LicenseListCallback);
this.callbacks.Subscribe<SteamUser.UpdateMachineAuthCallback>(UpdateMachineAuthCallback);
this.callbacks.Subscribe<SteamUser.LoginKeyCallback>(LoginKeyCallback);
Console.Write("Connecting to Steam3...");
if (authenticatedUser)
{
var fi = new FileInfo(String.Format("{0}.sentryFile", logonDetails.Username));
if (AccountSettingsStore.Instance.SentryData != null && AccountSettingsStore.Instance.SentryData.ContainsKey(logonDetails.Username))
{
logonDetails.SentryFileHash = Util.SHAHash(AccountSettingsStore.Instance.SentryData[logonDetails.Username]);
}
else if (fi.Exists && fi.Length > 0)
{
var sentryData = File.ReadAllBytes(fi.FullName);
logonDetails.SentryFileHash = Util.SHAHash(sentryData);
AccountSettingsStore.Instance.SentryData[logonDetails.Username] = sentryData;
AccountSettingsStore.Save();
}
}
Connect();
}
public delegate bool WaitCondition();
private readonly Lock steamLock = new();
private readonly object steamLock = new object();
public bool WaitUntilCallback(Action submitter, WaitCondition waiter)
{
@ -105,7 +151,7 @@ namespace DepotDownloader
{
lock (steamLock)
{
callbacks.RunWaitCallbacks(TimeSpan.FromSeconds(1));
WaitForCallbacks();
}
} while (!bAborted && this.seq == seq && !waiter());
}
@ -113,61 +159,46 @@ namespace DepotDownloader
return bAborted;
}
public bool WaitForCredentials()
public Credentials WaitForCredentials()
{
if (IsLoggedOn || bAborted)
return IsLoggedOn;
if (credentials.IsValid || bAborted)
return credentials;
WaitUntilCallback(() => { }, () => IsLoggedOn);
WaitUntilCallback(() => { }, () => { return credentials.IsValid; });
return IsLoggedOn;
return credentials;
}
public async Task TickCallbacks()
public void RequestAppInfo(uint appId, bool bForce = false)
{
var token = abortedToken.Token;
if ((AppInfo.ContainsKey(appId) && !bForce) || bAborted)
return;
try
var completed = false;
Action<SteamApps.PICSTokensCallback> cbMethodTokens = appTokens =>
{
while (!token.IsCancellationRequested)
completed = true;
if (appTokens.AppTokensDenied.Contains(appId))
{
await callbacks.RunWaitCallbackAsync(token);
Console.WriteLine("Insufficient privileges to get access token for app {0}", appId);
}
}
catch (OperationCanceledException)
{
//
}
}
public async Task RequestAppInfo(uint appId, bool bForce = false)
{
if ((AppInfo.ContainsKey(appId) && !bForce) || bAborted)
return;
var appTokens = await steamApps.PICSGetAccessTokens([appId], []);
if (appTokens.AppTokensDenied.Contains(appId))
{
Console.WriteLine("Insufficient privileges to get access token for app {0}", appId);
}
foreach (var token_dict in appTokens.AppTokens)
{
this.AppTokens[token_dict.Key] = token_dict.Value;
}
};
foreach (var token_dict in appTokens.AppTokens)
WaitUntilCallback(() =>
{
this.AppTokens[token_dict.Key] = token_dict.Value;
}
var request = new SteamApps.PICSRequest(appId);
callbacks.Subscribe(steamApps.PICSGetAccessTokens(new List<uint> { appId }, new List<uint>()), cbMethodTokens);
}, () => { return completed; });
if (AppTokens.TryGetValue(appId, out var token))
completed = false;
Action<SteamApps.PICSProductInfoCallback> cbMethod = appInfo =>
{
request.AccessToken = token;
}
var appInfoMultiple = await steamApps.PICSGetProductInfo([request], []);
completed = !appInfo.ResponsePending;
foreach (var appInfo in appInfoMultiple.Results)
{
foreach (var app_value in appInfo.Apps)
{
var app = app_value.Value;
@ -180,17 +211,45 @@ namespace DepotDownloader
{
AppInfo[app] = null;
}
};
var request = new SteamApps.PICSRequest(appId);
if (AppTokens.ContainsKey(appId))
{
request.AccessToken = AppTokens[appId];
}
WaitUntilCallback(() =>
{
callbacks.Subscribe(steamApps.PICSGetProductInfo(new List<SteamApps.PICSRequest> { request }, new List<SteamApps.PICSRequest>()), cbMethod);
}, () => { return completed; });
}
public async Task RequestPackageInfo(IEnumerable<uint> packageIds)
public void RequestPackageInfo(IEnumerable<uint> packageIds)
{
var packages = packageIds.ToList();
packages.RemoveAll(PackageInfo.ContainsKey);
packages.RemoveAll(pid => PackageInfo.ContainsKey(pid));
if (packages.Count == 0 || bAborted)
return;
var completed = false;
Action<SteamApps.PICSProductInfoCallback> cbMethod = packageInfo =>
{
completed = !packageInfo.ResponsePending;
foreach (var package_value in packageInfo.Packages)
{
var package = package_value.Value;
PackageInfo[package.ID] = package;
}
foreach (var package in packageInfo.UnknownPackages)
{
PackageInfo[package] = null;
}
};
var packageRequests = new List<SteamApps.PICSRequest>();
foreach (var package in packages)
@ -205,162 +264,183 @@ namespace DepotDownloader
packageRequests.Add(request);
}
var packageInfoMultiple = await steamApps.PICSGetProductInfo([], packageRequests);
foreach (var packageInfo in packageInfoMultiple.Results)
WaitUntilCallback(() =>
{
foreach (var package_value in packageInfo.Packages)
{
var package = package_value.Value;
PackageInfo[package.ID] = package;
}
foreach (var package in packageInfo.UnknownPackages)
{
PackageInfo[package] = null;
}
}
callbacks.Subscribe(steamApps.PICSGetProductInfo(new List<SteamApps.PICSRequest>(), packageRequests), cbMethod);
}, () => { return completed; });
}
public async Task<bool> RequestFreeAppLicense(uint appId)
public bool RequestFreeAppLicense(uint appId)
{
try
var success = false;
var completed = false;
Action<SteamApps.FreeLicenseCallback> cbMethod = resultInfo =>
{
var resultInfo = await steamApps.RequestFreeLicense(appId);
completed = true;
success = resultInfo.GrantedApps.Contains(appId);
};
return resultInfo.GrantedApps.Contains(appId);
}
catch (Exception ex)
WaitUntilCallback(() =>
{
Console.WriteLine($"Failed to request FreeOnDemand license for app {appId}: {ex.Message}");
return false;
}
callbacks.Subscribe(steamApps.RequestFreeLicense(appId), cbMethod);
}, () => { return completed; });
return success;
}
public async Task RequestDepotKey(uint depotId, uint appid = 0)
public void RequestDepotKey(uint depotId, uint appid = 0)
{
if (DepotKeys.ContainsKey(depotId) || bAborted)
return;
var depotKey = await steamApps.GetDepotDecryptionKey(depotId, appid);
Console.WriteLine("Got depot key for {0} result: {1}", depotKey.DepotID, depotKey.Result);
var completed = false;
if (depotKey.Result != EResult.OK)
Action<SteamApps.DepotKeyCallback> cbMethod = depotKey =>
{
return;
}
DepotKeys[depotKey.DepotID] = depotKey.DepotKey;
}
completed = true;
Console.WriteLine("Got depot key for {0} result: {1}", depotKey.DepotID, depotKey.Result);
public async Task<ulong> GetDepotManifestRequestCodeAsync(uint depotId, uint appId, ulong manifestId, string branch)
{
if (bAborted)
return 0;
if (depotKey.Result != EResult.OK)
{
Abort();
return;
}
var requestCode = await steamContent.GetManifestRequestCode(depotId, appId, manifestId, branch);
DepotKeys[depotKey.DepotID] = depotKey.DepotKey;
};
if (requestCode == 0)
WaitUntilCallback(() =>
{
Console.WriteLine($"No manifest request code was returned for depot {depotId} from app {appId}, manifest {manifestId}");
callbacks.Subscribe(steamApps.GetDepotDecryptionKey(depotId, appid), cbMethod);
}, () => { return completed; });
}
if (!authenticatedUser)
{
Console.WriteLine("Suggestion: Try logging in with -username as old manifests may not be available for anonymous accounts.");
}
public string ResolveCDNTopLevelHost(string host)
{
// SteamPipe CDN shares tokens with all hosts
if (host.EndsWith(".steampipe.steamcontent.com"))
{
return "steampipe.steamcontent.com";
}
else
if (host.EndsWith(".steamcontent.com"))
{
Console.WriteLine($"Got manifest request code for depot {depotId} from app {appId}, manifest {manifestId}, result: {requestCode}");
return "steamcontent.com";
}
return requestCode;
return host;
}
public async Task RequestCDNAuthToken(uint appid, uint depotid, Server server)
public void RequestCDNAuthToken(uint appid, uint depotid, string host, string cdnKey)
{
var cdnKey = (depotid, server.Host);
var completion = new TaskCompletionSource<SteamContent.CDNAuthToken>();
if (bAborted || !CDNAuthTokens.TryAdd(cdnKey, completion))
{
if (CDNAuthTokens.ContainsKey(cdnKey) || bAborted)
return;
}
DebugLog.WriteLine(nameof(Steam3Session), $"Requesting CDN auth token for {server.Host}");
var cdnAuth = await steamContent.GetCDNAuthToken(appid, depotid, server.Host);
Console.WriteLine($"Got CDN auth token for {server.Host} result: {cdnAuth.Result} (expires {cdnAuth.Expiration})");
if (cdnAuth.Result != EResult.OK)
{
if (!CDNAuthTokens.TryAdd(cdnKey, new TaskCompletionSource<SteamApps.CDNAuthTokenCallback>()))
return;
}
completion.TrySetResult(cdnAuth);
}
var completed = false;
var timeoutDate = DateTime.Now.AddSeconds(10);
Action<SteamApps.CDNAuthTokenCallback> cbMethod = cdnAuth =>
{
completed = true;
Console.WriteLine("Got CDN auth token for {0} result: {1} (expires {2})", host, cdnAuth.Result, cdnAuth.Expiration);
public async Task CheckAppBetaPassword(uint appid, string password)
{
var appPassword = await steamApps.CheckAppBetaPassword(appid, password);
if (cdnAuth.Result != EResult.OK)
{
Abort();
return;
}
Console.WriteLine("Retrieved {0} beta keys with result: {1}", appPassword.BetaPasswords.Count, appPassword.Result);
CDNAuthTokens[cdnKey].TrySetResult(cdnAuth);
};
foreach (var entry in appPassword.BetaPasswords)
WaitUntilCallback(() =>
{
AppBetaPasswords[entry.Key] = entry.Value;
}
callbacks.Subscribe(steamApps.GetCDNAuthToken(appid, depotid, host), cbMethod);
}, () => { return completed || DateTime.Now >= timeoutDate; });
}
public async Task<KeyValue> GetPrivateBetaDepotSection(uint appid, string branch)
public void CheckAppBetaPassword(uint appid, string password)
{
if (!AppBetaPasswords.TryGetValue(branch, out var branchPassword)) // Should be filled by CheckAppBetaPassword
var completed = false;
Action<SteamApps.CheckAppBetaPasswordCallback> cbMethod = appPassword =>
{
return new KeyValue();
}
AppTokens.TryGetValue(appid, out var accessToken); // Should be filled by RequestAppInfo
completed = true;
var privateBeta = await steamApps.PICSGetPrivateBeta(appid, accessToken, branch, branchPassword);
Console.WriteLine("Retrieved {0} beta keys with result: {1}", appPassword.BetaPasswords.Count, appPassword.Result);
Console.WriteLine($"Retrieved private beta depot section for {appid} with result: {privateBeta.Result}");
foreach (var entry in appPassword.BetaPasswords)
{
AppBetaPasswords[entry.Key] = entry.Value;
}
};
return privateBeta.DepotSection;
WaitUntilCallback(() =>
{
callbacks.Subscribe(steamApps.CheckAppBetaPassword(appid, password), cbMethod);
}, () => { return completed; });
}
public async Task<PublishedFileDetails> GetPublishedFileDetails(uint appId, PublishedFileID pubFile)
public PublishedFileDetails GetPublishedFileDetails(uint appId, PublishedFileID pubFile)
{
var pubFileRequest = new CPublishedFile_GetDetails_Request { appid = appId };
pubFileRequest.publishedfileids.Add(pubFile);
var details = await steamPublishedFile.GetDetails(pubFileRequest);
var completed = false;
PublishedFileDetails details = null;
if (details.Result == EResult.OK)
Action<SteamUnifiedMessages.ServiceMethodResponse> cbMethod = callback =>
{
return details.Body.publishedfiledetails.FirstOrDefault();
}
completed = true;
if (callback.Result == EResult.OK)
{
var response = callback.GetDeserializedResponse<CPublishedFile_GetDetails_Response>();
details = response.publishedfiledetails.FirstOrDefault();
}
else
{
throw new Exception($"EResult {(int)callback.Result} ({callback.Result}) while retrieving file details for pubfile {pubFile}.");
}
};
WaitUntilCallback(() =>
{
callbacks.Subscribe(steamPublishedFile.SendMessage(api => api.GetDetails(pubFileRequest)), cbMethod);
}, () => { return completed; });
throw new Exception($"EResult {(int)details.Result} ({details.Result}) while retrieving file details for pubfile {pubFile}.");
return details;
}
public async Task<SteamCloud.UGCDetailsCallback> GetUGCDetails(UGCHandle ugcHandle)
public SteamCloud.UGCDetailsCallback GetUGCDetails(UGCHandle ugcHandle)
{
var callback = await steamCloud.RequestUGCDetails(ugcHandle);
var completed = false;
SteamCloud.UGCDetailsCallback details = null;
if (callback.Result == EResult.OK)
Action<SteamCloud.UGCDetailsCallback> cbMethod = callback =>
{
return callback;
}
else if (callback.Result == EResult.FileNotFound)
completed = true;
if (callback.Result == EResult.OK)
{
details = callback;
}
else if (callback.Result == EResult.FileNotFound)
{
details = null;
}
else
{
throw new Exception($"EResult {(int)callback.Result} ({callback.Result}) while retrieving UGC details for {ugcHandle}.");
}
};
WaitUntilCallback(() =>
{
return null;
}
callbacks.Subscribe(steamCloud.RequestUGCDetails(ugcHandle), cbMethod);
}, () => { return completed; });
throw new Exception($"EResult {(int)callback.Result} ({callback.Result}) while retrieving UGC details for {ugcHandle}.");
return details;
}
private void ResetConnectionFlags()
@ -368,16 +448,19 @@ namespace DepotDownloader
bExpectingDisconnectRemote = false;
bDidDisconnect = false;
bIsConnectionRecovery = false;
bDidReceiveLoginKey = false;
}
void Connect()
{
bAborted = false;
bConnected = false;
bConnecting = true;
connectionBackoff = 0;
authSession = null;
ResetConnectionFlags();
this.connectTime = DateTime.Now;
this.steamClient.Connect();
}
@ -394,13 +477,11 @@ namespace DepotDownloader
}
bAborted = true;
bConnected = false;
bConnecting = false;
bIsConnectionRecovery = false;
abortedToken.Cancel();
steamClient.Disconnect();
Ansi.Progress(Ansi.ProgressState.Hidden);
// flush callbacks until our disconnected event
while (!bDidDisconnect)
{
@ -414,15 +495,41 @@ namespace DepotDownloader
steamClient.Disconnect();
}
private async void ConnectedCallback(SteamClient.ConnectedCallback connected)
public void TryWaitForLoginKey()
{
Console.WriteLine(" Done!");
bConnecting = false;
if (logonDetails.Username == null || !credentials.LoggedOn || !ContentDownloader.Config.RememberPassword) return;
// Update our tracking so that we don't time out, even if we need to reconnect multiple times,
// e.g. if the authentication phase takes a while and therefore multiple connections.
connectionBackoff = 0;
var totalWaitPeriod = DateTime.Now.AddSeconds(3);
while (true)
{
var now = DateTime.Now;
if (now >= totalWaitPeriod) break;
if (bDidReceiveLoginKey) break;
callbacks.RunWaitAllCallbacks(TimeSpan.FromMilliseconds(100));
}
}
private void WaitForCallbacks()
{
callbacks.RunWaitCallbacks(TimeSpan.FromSeconds(1));
var diff = DateTime.Now - connectTime;
if (diff > STEAM3_TIMEOUT && !bConnected)
{
Console.WriteLine("Timeout connecting to Steam3.");
Abort();
}
}
private void ConnectedCallback(SteamClient.ConnectedCallback connected)
{
Console.WriteLine(" Done!");
bConnecting = false;
bConnected = true;
if (!authenticatedUser)
{
Console.Write("Logging anonymously into Steam3...");
@ -430,119 +537,7 @@ namespace DepotDownloader
}
else
{
if (logonDetails.Username != null)
{
Console.WriteLine("Logging '{0}' into Steam3...", logonDetails.Username);
}
if (authSession is null)
{
if (logonDetails.Username != null && logonDetails.Password != null && logonDetails.AccessToken is null)
{
try
{
_ = AccountSettingsStore.Instance.GuardData.TryGetValue(logonDetails.Username, out var guarddata);
authSession = await steamClient.Authentication.BeginAuthSessionViaCredentialsAsync(new AuthSessionDetails
{
DeviceFriendlyName = nameof(DepotDownloader),
Username = logonDetails.Username,
Password = logonDetails.Password,
IsPersistentSession = ContentDownloader.Config.RememberPassword,
GuardData = guarddata,
Authenticator = new ConsoleAuthenticator(),
});
}
catch (TaskCanceledException)
{
return;
}
catch (Exception ex)
{
Console.Error.WriteLine("Failed to authenticate with Steam: " + ex.Message);
Abort(false);
return;
}
}
else if (logonDetails.AccessToken is null && ContentDownloader.Config.UseQrCode)
{
Console.WriteLine("Logging in with QR code...");
try
{
var session = await steamClient.Authentication.BeginAuthSessionViaQRAsync(new AuthSessionDetails
{
DeviceFriendlyName = nameof(DepotDownloader),
IsPersistentSession = ContentDownloader.Config.RememberPassword,
});
authSession = session;
// Steam will periodically refresh the challenge url, so we need a new QR code.
session.ChallengeURLChanged = () =>
{
Console.WriteLine();
Console.WriteLine("The QR code has changed:");
DisplayQrCode(session.ChallengeURL);
};
// Draw initial QR code immediately
DisplayQrCode(session.ChallengeURL);
}
catch (TaskCanceledException)
{
return;
}
catch (Exception ex)
{
Console.Error.WriteLine("Failed to authenticate with Steam: " + ex.Message);
Abort(false);
return;
}
}
}
if (authSession != null)
{
try
{
var result = await authSession.PollingWaitForResultAsync();
logonDetails.Username = result.AccountName;
logonDetails.Password = null;
logonDetails.AccessToken = result.RefreshToken;
if (result.NewGuardData != null)
{
AccountSettingsStore.Instance.GuardData[result.AccountName] = result.NewGuardData;
if (ContentDownloader.Config.UseQrCode)
{
Console.WriteLine($"Success! Next time you can login with -username {result.AccountName} -remember-password instead of -qr.");
}
}
else
{
AccountSettingsStore.Instance.GuardData.Remove(result.AccountName);
}
AccountSettingsStore.Instance.LoginTokens[result.AccountName] = result.RefreshToken;
AccountSettingsStore.Save();
}
catch (TaskCanceledException)
{
return;
}
catch (Exception ex)
{
Console.Error.WriteLine("Failed to authenticate with Steam: " + ex.Message);
Abort(false);
return;
}
authSession = null;
}
Console.Write("Logging '{0}' into Steam3...", logonDetails.Username);
steamUser.LogOn(logonDetails);
}
}
@ -551,8 +546,6 @@ namespace DepotDownloader
{
bDidDisconnect = true;
DebugLog.WriteLine(nameof(Steam3Session), $"Disconnected: bIsConnectionRecovery = {bIsConnectionRecovery}, UserInitiated = {disconnected.UserInitiated}, bExpectingDisconnectRemote = {bExpectingDisconnectRemote}");
// When recovering the connection, we want to reconnect even if the remote disconnects us
if (!bIsConnectionRecovery && (disconnected.UserInitiated || bExpectingDisconnectRemote))
{
@ -568,18 +561,16 @@ namespace DepotDownloader
}
else if (!bAborted)
{
connectionBackoff += 1;
if (bConnecting)
{
Console.WriteLine($"Connection to Steam failed. Trying again (#{connectionBackoff})...");
Console.WriteLine("Connection to Steam failed. Trying again");
}
else
{
Console.WriteLine("Lost connection to Steam. Reconnecting");
}
Thread.Sleep(1000 * connectionBackoff);
Thread.Sleep(1000 * ++connectionBackoff);
// Any connection related flags need to be reset here to match the state after Connect
ResetConnectionFlags();
@ -591,48 +582,45 @@ namespace DepotDownloader
{
var isSteamGuard = loggedOn.Result == EResult.AccountLogonDenied;
var is2FA = loggedOn.Result == EResult.AccountLoginDeniedNeedTwoFactor;
var isAccessToken = ContentDownloader.Config.RememberPassword && logonDetails.AccessToken != null &&
loggedOn.Result is EResult.InvalidPassword
or EResult.InvalidSignature
or EResult.AccessDenied
or EResult.Expired
or EResult.Revoked;
var isLoginKey = ContentDownloader.Config.RememberPassword && logonDetails.LoginKey != null && loggedOn.Result == EResult.InvalidPassword;
if (isSteamGuard || is2FA || isAccessToken)
if (isSteamGuard || is2FA || isLoginKey)
{
bExpectingDisconnectRemote = true;
Abort(false);
if (!isAccessToken)
if (!isLoginKey)
{
Console.WriteLine("This account is protected by Steam Guard.");
}
if (is2FA)
{
do
{
Console.Write("Please enter your 2 factor auth code from your authenticator app: ");
logonDetails.TwoFactorCode = Console.ReadLine();
} while (string.Empty == logonDetails.TwoFactorCode);
Console.Write("Please enter your 2 factor auth code from your authenticator app: ");
logonDetails.TwoFactorCode = Console.ReadLine();
}
else if (isAccessToken)
else if (isLoginKey)
{
AccountSettingsStore.Instance.LoginTokens.Remove(logonDetails.Username);
AccountSettingsStore.Instance.LoginKeys.Remove(logonDetails.Username);
AccountSettingsStore.Save();
// TODO: Handle gracefully by falling back to password prompt?
Console.WriteLine($"Access token was rejected ({loggedOn.Result}).");
Abort(false);
return;
logonDetails.LoginKey = null;
if (ContentDownloader.Config.SuppliedPassword != null)
{
Console.WriteLine("Login key was expired. Connecting with supplied password.");
logonDetails.Password = ContentDownloader.Config.SuppliedPassword;
}
else
{
Console.Write("Login key was expired. Please enter your password: ");
logonDetails.Password = Util.ReadPassword();
}
}
else
{
do
{
Console.Write("Please enter the authentication code sent to your email address: ");
logonDetails.AuthCode = Console.ReadLine();
} while (string.Empty == logonDetails.AuthCode);
Console.Write("Please enter the authentication code sent to your email address: ");
logonDetails.AuthCode = Console.ReadLine();
}
Console.Write("Retrying Steam3 connection...");
@ -669,7 +657,7 @@ namespace DepotDownloader
Console.WriteLine(" Done!");
this.seq++;
IsLoggedOn = true;
credentials.LoggedOn = true;
if (ContentDownloader.Config.CellID == 0)
{
@ -678,6 +666,12 @@ namespace DepotDownloader
}
}
private void SessionTokenCallback(SteamUser.SessionTokenCallback sessionToken)
{
Console.WriteLine("Got session token!");
credentials.SessionToken = sessionToken.SessionToken;
}
private void LicenseListCallback(SteamApps.LicenseListCallback licenseList)
{
if (licenseList.Result != EResult.OK)
@ -700,20 +694,45 @@ namespace DepotDownloader
}
}
private static void DisplayQrCode(string challengeUrl)
private void UpdateMachineAuthCallback(SteamUser.UpdateMachineAuthCallback machineAuth)
{
// Encode the link as a QR code
using var qrGenerator = new QRCodeGenerator();
var qrCodeData = qrGenerator.CreateQrCode(challengeUrl, QRCodeGenerator.ECCLevel.L);
using var qrCode = new AsciiQRCode(qrCodeData);
var qrCodeAsAsciiArt = qrCode.GetLineByLineGraphic(1, drawQuietZones: true);
var hash = Util.SHAHash(machineAuth.Data);
Console.WriteLine("Got Machine Auth: {0} {1} {2} {3}", machineAuth.FileName, machineAuth.Offset, machineAuth.BytesToWrite, machineAuth.Data.Length, hash);
Console.WriteLine("Use the Steam Mobile App to sign in with this QR code:");
AccountSettingsStore.Instance.SentryData[logonDetails.Username] = machineAuth.Data;
AccountSettingsStore.Save();
foreach (var line in qrCodeAsAsciiArt)
var authResponse = new SteamUser.MachineAuthDetails
{
Console.WriteLine(line);
}
BytesWritten = machineAuth.BytesToWrite,
FileName = machineAuth.FileName,
FileSize = machineAuth.BytesToWrite,
Offset = machineAuth.Offset,
SentryFileHash = hash, // should be the sha1 hash of the sentry file we just wrote
OneTimePassword = machineAuth.OneTimePassword, // not sure on this one yet, since we've had no examples of steam using OTPs
LastError = 0, // result from win32 GetLastError
Result = EResult.OK, // if everything went okay, otherwise ~who knows~
JobID = machineAuth.JobID, // so we respond to the correct server job
};
// send off our response
steamUser.SendMachineAuthResponse(authResponse);
}
private void LoginKeyCallback(SteamUser.LoginKeyCallback loginKey)
{
Console.WriteLine("Accepted new login key for account {0}", logonDetails.Username);
AccountSettingsStore.Instance.LoginKeys[logonDetails.Username] = loginKey.LoginKey;
AccountSettingsStore.Save();
steamUser.AcceptNewLoginKey(loginKey);
bDidReceiveLoginKey = true;
}
}
}

@ -1,7 +1,4 @@
// This file is subject to the terms and conditions defined
// in file 'LICENSE', which is part of this source code package.
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -9,7 +6,6 @@ using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using SteamKit2;
namespace DepotDownloader
{
@ -32,12 +28,6 @@ namespace DepotDownloader
return "linux";
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD))
{
// Return linux as freebsd steam client doesn't exist yet
return "linux";
}
return "unknown";
}
@ -79,16 +69,30 @@ namespace DepotDownloader
}
// Validate a file against Steam3 Chunk data
public static List<DepotManifest.ChunkData> ValidateSteam3FileChecksums(FileStream fs, DepotManifest.ChunkData[] chunkdata)
public static List<ProtoManifest.ChunkData> ValidateSteam3FileChecksums(FileStream fs, ProtoManifest.ChunkData[] chunkdata)
{
var neededChunks = new List<DepotManifest.ChunkData>();
var neededChunks = new List<ProtoManifest.ChunkData>();
int read;
foreach (var data in chunkdata)
{
var chunk = new byte[data.UncompressedLength];
fs.Seek((long)data.Offset, SeekOrigin.Begin);
read = fs.Read(chunk, 0, (int)data.UncompressedLength);
byte[] tempchunk;
if (read < data.UncompressedLength)
{
tempchunk = new byte[read];
Array.Copy(chunk, 0, tempchunk, 0, read);
}
else
{
tempchunk = chunk;
}
var adler = AdlerHash(fs, (int)data.UncompressedLength);
if (!adler.SequenceEqual(BitConverter.GetBytes(data.Checksum)))
var adler = AdlerHash(tempchunk);
if (!adler.SequenceEqual(data.Checksum))
{
neededChunks.Add(data);
}
@ -97,114 +101,28 @@ namespace DepotDownloader
return neededChunks;
}
public static byte[] AdlerHash(Stream stream, int length)
public static byte[] AdlerHash(byte[] input)
{
uint a = 0, b = 0;
for (var i = 0; i < length; i++)
for (var i = 0; i < input.Length; i++)
{
var c = (uint)stream.ReadByte();
a = (a + c) % 65521;
a = (a + input[i]) % 65521;
b = (b + a) % 65521;
}
return BitConverter.GetBytes(a | (b << 16));
}
public static byte[] FileSHAHash(string filename)
public static byte[] SHAHash(byte[] input)
{
using (var fs = File.Open(filename, FileMode.Open))
using (var sha = SHA1.Create())
{
var output = sha.ComputeHash(fs);
var output = sha.ComputeHash(input);
return output;
}
}
public static DepotManifest LoadManifestFromFile(string directory, uint depotId, ulong manifestId, bool badHashWarning)
{
// Try loading Steam format manifest first.
var filename = Path.Combine(directory, string.Format("{0}_{1}.manifest", depotId, manifestId));
if (File.Exists(filename))
{
byte[] expectedChecksum;
try
{
expectedChecksum = File.ReadAllBytes(filename + ".sha");
}
catch (IOException)
{
expectedChecksum = null;
}
var currentChecksum = FileSHAHash(filename);
if (expectedChecksum != null && expectedChecksum.SequenceEqual(currentChecksum))
{
return DepotManifest.LoadFromFile(filename);
}
else if (badHashWarning)
{
Console.WriteLine("Manifest {0} on disk did not match the expected checksum.", manifestId);
}
}
// Try converting legacy manifest format.
filename = Path.Combine(directory, string.Format("{0}_{1}.bin", depotId, manifestId));
if (File.Exists(filename))
{
byte[] expectedChecksum;
try
{
expectedChecksum = File.ReadAllBytes(filename + ".sha");
}
catch (IOException)
{
expectedChecksum = null;
}
byte[] currentChecksum;
var oldManifest = ProtoManifest.LoadFromFile(filename, out currentChecksum);
if (oldManifest != null && (expectedChecksum == null || !expectedChecksum.SequenceEqual(currentChecksum)))
{
oldManifest = null;
if (badHashWarning)
{
Console.WriteLine("Manifest {0} on disk did not match the expected checksum.", manifestId);
}
}
if (oldManifest != null)
{
return oldManifest.ConvertToSteamManifest(depotId);
}
}
return null;
}
public static bool SaveManifestToFile(string directory, DepotManifest manifest)
{
try
{
var filename = Path.Combine(directory, string.Format("{0}_{1}.manifest", manifest.DepotID, manifest.ManifestGID));
manifest.SaveToFile(filename);
File.WriteAllBytes(filename + ".sha", FileSHAHash(filename));
return true; // If serialization completes without throwing an exception, return true
}
catch (Exception)
{
return false; // Return false if an error occurs
}
}
public static byte[] DecodeHexString(string hex)
{
if (hex == null)
@ -219,21 +137,43 @@ namespace DepotDownloader
return bytes;
}
/// <summary>
/// Decrypts using AES/ECB/PKCS7
/// </summary>
public static byte[] SymmetricDecryptECB(byte[] input, byte[] key)
public static string EncodeHexString(byte[] input)
{
return input.Aggregate(new StringBuilder(),
(sb, v) => sb.Append(v.ToString("x2"))
).ToString();
}
public static async Task InvokeAsync(IEnumerable<Func<Task>> taskFactories, int maxDegreeOfParallelism)
{
using var aes = Aes.Create();
aes.BlockSize = 128;
aes.KeySize = 256;
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.PKCS7;
if (taskFactories == null) throw new ArgumentNullException(nameof(taskFactories));
if (maxDegreeOfParallelism <= 0) throw new ArgumentException(nameof(maxDegreeOfParallelism));
var queue = taskFactories.ToArray();
if (queue.Length == 0)
{
return;
}
var tasksInFlight = new List<Task>(maxDegreeOfParallelism);
var index = 0;
do
{
while (tasksInFlight.Count < maxDegreeOfParallelism && index < queue.Length)
{
var taskFactory = queue[index++];
tasksInFlight.Add(taskFactory());
}
var completedTask = await Task.WhenAny(tasksInFlight).ConfigureAwait(false);
using var aesTransform = aes.CreateDecryptor(key, null);
var output = aesTransform.TransformFinalBlock(input, 0, input.Length);
await completedTask.ConfigureAwait(false);
return output;
tasksInFlight.Remove(completedTask);
} while (index < queue.Length || tasksInFlight.Count != 0);
}
}
}

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/>
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="protobuf-net" publicKeyToken="257b51d87d2e4d67" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-2.0.0.640" newVersion="2.0.0.640"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

@ -0,0 +1,3 @@
{
"rollForwardOnNoCandidateFx": 2
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 65 65" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="B" transform="matrix(1,0,0,1,0.5,0.5)">
<g>
<path d="M1.305,41.202C5.259,54.386 17.488,64 31.959,64C49.632,64 63.959,49.673 63.959,32C63.959,14.327 49.632,0 31.959,0C15.001,0 1.124,13.193 0.028,29.874C2.102,33.351 2.907,35.502 1.303,41.202L1.305,41.202Z" style="fill:rgb(56,61,72);fill-rule:nonzero;"/>
<g transform="matrix(-0.984183,-1.19698e-16,9.80712e-17,-0.795301,95.4776,96.4352)">
<g id="Arrow">
<path d="M45.613,41.896C45.811,41.658 46.125,41.658 46.322,41.896C48.069,44.002 55.776,53.297 58.421,56.487C58.569,56.666 58.615,56.938 58.537,57.174C58.459,57.411 58.273,57.566 58.067,57.566C55.616,57.566 50.417,57.566 50.417,57.566L50.417,80.833L41.519,90.628L41.519,57.566L33.869,57.566C33.662,57.566 33.476,57.411 33.399,57.174C33.321,56.938 33.367,56.666 33.515,56.487C36.16,53.297 43.867,44.002 45.613,41.896Z" style="fill:white;"/>
<path d="M44.904,40.996C43.158,43.102 35.45,52.397 32.805,55.587C32.361,56.122 32.224,56.937 32.457,57.648C32.691,58.359 33.249,58.824 33.869,58.824L40.502,58.824C40.502,58.824 40.502,90.628 40.502,90.628L42.194,91.567L51.433,81.398L51.433,58.824C51.433,58.824 58.067,58.824 58.067,58.824C58.687,58.824 59.245,58.359 59.478,57.648C59.712,56.937 59.574,56.122 59.13,55.587C56.485,52.397 48.778,43.102 47.032,40.996C46.44,40.282 45.496,40.282 44.904,40.996L44.904,40.996ZM45.613,41.896C45.811,41.658 46.125,41.658 46.322,41.896C48.069,44.002 55.776,53.297 58.421,56.487C58.569,56.666 58.615,56.938 58.537,57.174C58.459,57.411 58.273,57.566 58.067,57.566C55.616,57.566 50.417,57.566 50.417,57.566L50.417,80.833L41.519,90.628L41.519,57.566L33.869,57.566C33.662,57.566 33.476,57.411 33.399,57.174C33.321,56.938 33.367,56.666 33.515,56.487C36.16,53.297 43.867,44.002 45.613,41.896Z" style="fill:rgb(56,61,72);"/>
</g>
</g>
<path id="Steam" d="M30.31,23.985L30.313,24.143L22.483,35.518C21.215,35.46 19.943,35.683 18.735,36.18C18.209,36.394 17.707,36.662 17.237,36.98L0.042,29.893C0.042,29.893 -0.356,36.439 1.302,41.317L13.458,46.333C14.058,49.061 15.938,51.453 18.7,52.603C23.194,54.47 28.43,52.313 30.303,47.821C30.786,46.664 31.019,45.418 30.987,44.165L42.18,36.16L42.455,36.165C49.16,36.165 54.61,30.699 54.61,23.985C54.61,17.271 49.17,11.825 42.455,11.811C35.753,11.811 30.3,17.271 30.3,23.985L30.31,23.985ZM28.43,47.035C26.976,50.535 22.964,52.182 19.477,50.729C17.93,50.079 16.675,48.882 15.953,47.367L19.91,49.007C20.524,49.262 21.182,49.394 21.847,49.394C23.879,49.394 25.721,48.165 26.501,46.288C27.565,43.733 26.34,40.754 23.786,39.687L19.686,37.992C21.264,37.392 23.058,37.372 24.736,38.069C26.436,38.772 27.736,40.096 28.432,41.789C29.128,43.482 29.124,45.349 28.422,47.035M42.466,32.1C38.022,32.088 34.372,28.431 34.368,23.987C34.373,19.544 38.023,15.888 42.466,15.876C46.91,15.887 50.561,19.543 50.566,23.987C50.562,28.431 46.91,32.089 42.466,32.1M36.398,23.974C36.395,20.635 39.139,17.884 42.478,17.879C45.833,17.879 48.562,20.609 48.562,23.974C48.564,27.314 45.818,30.064 42.478,30.067C39.139,30.062 36.395,27.313 36.397,23.974L36.398,23.974Z" style="fill:rgb(175,212,255);fill-rule:nonzero;"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

@ -1,131 +1,55 @@
DepotDownloader
===============
Steam depot downloader utilizing the SteamKit2 library. Supports .NET 8.0
This program must be run from a console, it has no GUI.
## Installation
### Directly from GitHub
Download a binary from [the releases page](https://github.com/SteamRE/DepotDownloader/releases/latest).
### via Windows Package Manager CLI (aka winget)
On Windows, [winget](https://github.com/microsoft/winget-cli) users can download and install
the latest Terminal release by installing the `SteamRE.DepotDownloader`
package:
```powershell
winget install --exact --id SteamRE.DepotDownloader
```
### via Homebrew
On macOS, [Homebrew](https://brew.sh) users can download and install that latest release by running the following commands:
```shell
brew tap steamre/tools
brew install depotdownloader
```
## Usage
Steam depot downloader utilizing the SteamKit2 library. Supports .NET 5.0
### Downloading one or all depots for an app
```powershell
./DepotDownloader -app <id> [-depot <id> [-manifest <id>]]
[-username <username> [-password <password>]] [other options]
```
dotnet DepotDownloader.dll -app <id> [-depot <id> [-manifest <id>]]
[-username <username> [-password <password>]] [other options]
```
For example: `./DepotDownloader -app 730 -depot 731 -manifest 7617088375292372759`
By default it will use anonymous account ([view which apps are available on it here](https://steamdb.info/sub/17906/)).
To use your account, specify the `-username <username>` parameter. Password will be asked interactively if you do
not use specify the `-password` parameter.
For example: `dotnet DepotDownloader.dll -app 730 -depot 731 -manifest 7617088375292372759`
### Downloading a workshop item using pubfile id
```powershell
./DepotDownloader -app <id> -pubfile <id> [-username <username> [-password <password>]]
```
dotnet DepotDownloader.dll -app <id> -pubfile <id> [-username <username> [-password <password>]]
```
For example: `./DepotDownloader -app 730 -pubfile 1885082371`
For example: `dotnet DepotDownloader.dll -app 730 -pubfile 1885082371`
### Downloading a workshop item using ugc id
```powershell
./DepotDownloader -app <id> -ugc <id> [-username <username> [-password <password>]]
```
dotnet DepotDownloader.dll -app <id> -ugc <id> [-username <username> [-password <password>]]
```
For example: `./DepotDownloader -app 730 -ugc 770604181014286929`
For example: `dotnet DepotDownloader.dll -app 730 -ugc 770604181014286929`
## Parameters
#### Authentication
Parameter | Description
----------------------- | -----------
`-username <user>` | the username of the account to login to for restricted content.
`-password <pass>` | the password of the account to login to for restricted content.
`-remember-password` | if set, remember the password for subsequent logins of this user. (Use `-username <username> -remember-password` as login credentials)
`-qr` | display a login QR code to be scanned with the Steam mobile app
`-no-mobile` | prefer entering a 2FA code instead of prompting to accept in the Steam mobile app.
`-loginid <#>` | a unique 32-bit integer Steam LogonID in decimal, required if running multiple instances of DepotDownloader concurrently.
#### Downloading
Parameter | Description
------------------------ | -----------
`-app <#>` | the AppID to download.
`-depot <#>` | the DepotID to download.
`-manifest <id>` | manifest id of content to download (requires `-depot`, default: current for branch).
`-ugc <#>` | the UGC ID to download.
`-pubfile <#>` | the PublishedFileId to download. (Will automatically resolve to UGC id)
`-branch <branchname>` | download from specified branch if available (default: Public).
`-branchpassword <pass>` | branch password if applicable.
#### Download configuration
Parameter | Description
----------------------- | -----------
`-all-platforms` | downloads all platform-specific depots when `-app` is used.
`-os <os>` | the operating system for which to download the game (windows, macos or linux, default: OS the program is currently running on)
`-osarch <arch>` | the architecture for which to download the game (32 or 64, default: the host's architecture)
`-all-archs` | download all architecture-specific depots when `-app` is used.
`-all-languages` | download all language-specific depots when `-app` is used.
`-language <lang>` | the language for which to download the game (default: english)
`-lowviolence` | download low violence depots when `-app` is used.
`-dir <installdir>` | the directory in which to place downloaded files.
`-filelist <file.txt>` | the name of a local file that contains a list of files to download (from the manifest). prefix file path with `regex:` if you want to match with regex. each file path should be on their own line.
`-validate` | include checksum verification of files already downloaded.
`-manifest-only` | downloads a human readable manifest for any depots that would be downloaded.
`-cellid <#>` | the overridden CellID of the content server to download from.
`-max-downloads <#>` | maximum number of chunks to download concurrently. (default: 8).
`-use-lancache` | forces downloads over the local network via a Lancache instance.
#### Other
Parameter | Description
----------------------- | -----------
`-debug` | enable verbose debug logging.
`-V` or `--version` | print version and runtime.
## Frequently Asked Questions
### Why am I prompted to enter a 2-factor code every time I run the app?
Your 2-factor code authenticates a Steam session. You need to "remember" your session with `-remember-password` which persists the login key for your Steam session.
### Can I run DepotDownloader while an account is already connected to Steam?
Any connection to Steam will be closed if they share a LoginID. You can specify a different LoginID with `-loginid`.
### Why doesn't my password containing special characters work? Do I have to specify the password on the command line?
If you pass the `-password` parameter with a password that contains special characters, you will need to escape the command appropriately for the shell you are using. You do not have to include the `-password` parameter on the command line as long as you include a `-username`. You will be prompted to enter your password interactively.
### I am getting error 401 or no manifest code returned for old manifest ids
Try logging in with a Steam account, this may happen when using anonymous account.
Steam allows developers to block downloading old manifests, in which case no manifest code is returned even when parameters appear correct.
### Why am I getting slow download speeds and frequent connection timeouts?
When downloading old builds, cache server may not have the chunks readily available which makes downloading slower.
Try increasing `-max-downloads` to saturate the network more.
Parameter | Description
--------- | -----------
-app \<#> | the AppID to download.
-depot \<#> | the DepotID to download.
-manifest \<id> | manifest id of content to download (requires -depot, default: current for branch).
-ugc \<#> | the UGC ID to download.
-beta \<branchname> | download from specified branch if available (default: Public).
-betapassword \<pass> | branch password if applicable.
-all-platforms | downloads all platform-specific depots when -app is used.
-os \<os> | the operating system for which to download the game (windows, macos or linux, default: OS the program is currently running on)
-osarch \<arch> | the architecture for which to download the game (32 or 64, default: the host's architecture)
-all-languages | download all language-specific depots when -app is used.
-language \<lang> | the language for which to download the game (default: english)
-lowviolence | download low violence depots when -app is used.
-pubfile \<#> | the PublishedFileId to download. (Will automatically resolve to UGC id)
-username \<user> | the username of the account to login to for restricted content.
-password \<pass> | the password of the account to login to for restricted content.
-remember-password | if set, remember the password for subsequent logins of this user.
-dir \<installdir> | the directory in which to place downloaded files.
-filelist \<file.txt> | a list of files to download (from the manifest). Prefix file path with `regex:` if you want to match with regex.
-validate | Include checksum verification of files already downloaded
-manifest-only | downloads a human readable manifest for any depots that would be downloaded.
-cellid \<#> | the overridden CellID of the content server to download from.
-max-servers \<#> | maximum number of content servers to use. (default: 20).
-max-downloads \<#> | maximum number of chunks to download concurrently. (default: 8).
-loginid \<#> | a unique 32-bit integer Steam LogonID in decimal, required if running multiple instances of DepotDownloader concurrently.

@ -1,6 +0,0 @@
{
"sdk": {
"version": "9.0.100",
"rollForward": "latestMinor"
}
}
Loading…
Cancel
Save