cdn

package
v1.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package cdn 实现 Steam Content Delivery Network(CDN)的 depot chunk 下载与解码。

Steam CDN 把 depot 内容拆成多个 chunk(默认 1 MB)存储。下载流程:

  1. 从 PICS / manifest 获取目标 depot 的 ContentManifestPayload.FileMapping 列表;
  2. 用 steam.Apps.GetDepotDecryptionKey 拿到 depot 的 AES 解密密钥;
  3. 用 steam.Apps.GetCDNAuthToken 拿到访问 CDN 主机的临时鉴权 token;
  4. 用本包的 CDNClient.DownloadChunk 按 chunk SHA 下载、解密、解压、校验 Adler32;
  5. 按 chunk.Offset 把所有 chunk 拼成完整文件。

Chunk 在 CDN 上的 URL 格式:

http://<host>/depot/<depotID>/chunk/<sha-hex>?token=<cdnAuthToken>

Chunk 字节流的处理顺序(与 SteamKit2 CDN/DepotChunk.cs 一致):

  1. AES-CBC 解密:前 16 字节用 ECB 解密得到 IV,剩余用 CBC + PKCS7 解密;
  2. 解密后字节是压缩载荷,按 magic 检测压缩格式: - "VSZa" → VZstd(zstd) - "VZa" → VZip(LZMA,Steam 私有容器) - "PK\x03\x04" → PKZip - 无 magic → 未压缩,直接拷贝
  3. 解压后用 Adler32 校验(与 SteamKit2 Adler32.Calculate 一致)。

压缩格式说明(与 SteamKit2 Util/VZstdUtil.cs / VZipUtil.cs / ZipUtil.cs 对齐):

  • VZstd:使用 zstd。Go 标准库不含 zstd;需通过 RegisterZstdDecompressor 注入第三方实现 (如 github.com/klauspost/compress/zstd)。
  • VZip:Steam 私有 LZMA 容器,前 4 字节 magic "VZa",后续含版本/crc/uncompressed_size/lzma 流。 需通过 RegisterVZipDecompressor 注入第三方 LZMA 实现 (如 github.com/ulikunitz/xz/lzma)。
  • PKZip:标准 ZIP 格式。Go 标准库 archive/zip 支持,但 chunk 的 ZIP 是单个文件的 deflate 流, 本实现使用 archive/zip 解压。

Package cdn - vzip_default 提供 Steam VZip(私有 LZMA 容器)的内置解压实现。

VZip 格式(与 SteamKit2 VZipUtil.cs 对齐):

偏移  长度  字段             说明
0     2     VZipHeader       固定 0x5A56 (LE: "VZ")
2     1     Version          固定 'a' (0x61)
3     4     CRC32/timestamp  解压后数据的 CRC32 或创建时间戳
7     1     propertyBits     LZMA 属性字节 (lc, lp, pb 编码)
8     4     dictionarySize   LZMA 字典大小
12    N     compressedData   LZMA 压缩数据(无 size 字段,长度由 footer 位置决定)
末尾  10    footer           [outputCRC 4B][sizeDecompressed 4B][VZipFooter 2B]

VZipFooter = 0x767A (LE: "vz")

关键区别与经典 LZMA 格式:

  • 经典 LZMA: [props 5B][uncompressedSize 8B][compressed data]
  • VZip LZMA: [props 5B][compressed data](size 在 footer 中,不在 header 中)

因此不能用 lzma.NewReader(它期望 header 中有 size 字段), 需要用 lzma.Reader2(支持无 size 的 LZMA 流)或手动构造解码器。

对标 SteamKit2: SteamKit2/Util/VZipUtil.cs。

Index

Constants

View Source
const MaxChunkSize = 32 * 1024 * 1024

MaxChunkSize 是单个 chunk 响应体的最大字节数(32MB)。 防御性上限:恶意/异常 CDN 返回 GB 级响应会导致 OOM。 chunk 实际大小可从 manifest 的 CbCompressed 预判,32MB 足够覆盖压缩后最大 chunk。

Variables

View Source
var (
	// ErrDepotKeyRequired 表示未设置 depot key,无法解密 chunk。
	ErrDepotKeyRequired = errors.New("cdn: depot key 未设置")
	// ErrHTTPNotOK 表示 CDN 返回非 2xx 状态码。
	ErrHTTPNotOK = errors.New("cdn: CDN 返回非 2xx 状态码")
)

预声明错误。

View Source
var (
	// ErrChunkTooShort 表示解密后的 chunk 数据少于 4 字节,无法读取 magic。
	ErrChunkTooShort = errors.New("cdn: chunk data too short (less than 4 bytes)")
	// ErrUnknownCompression 表示 chunk 使用了未知的压缩方法。
	ErrUnknownCompression = errors.New("cdn: unknown compression method")
	// ErrZstdNotRegistered 表示 chunk 使用 VZstd 压缩,但未调用 RegisterZstdDecompressor 注入实现。
	ErrZstdNotRegistered = errors.New("cdn: zstd decompressor not registered; call cdn.RegisterZstdDecompressor first")
	// ErrVZipNotRegistered 表示 chunk 使用 VZip 压缩,但未调用 RegisterVZipDecompressor 注入实现。
	ErrVZipNotRegistered = errors.New("cdn: VZip decompressor not registered; call cdn.RegisterVZipDecompressor first")
	// ErrCRCMismatch 表示 chunk 解压后 Adler32 校验失败。
	// 通常意味着 depot key 错误或 chunk 数据在传输中被损坏。
	ErrCRCMismatch = errors.New("cdn: chunk Adler32 checksum mismatch")
)

预声明错误,避免热路径上分配。

Functions

func DecompressChunk

func DecompressChunk(data []byte, expectedSize int) ([]byte, error)

DecompressChunk 把解密后的 chunk 字节解压为原始字节。

输入 data 是解密后的 chunk 载荷(不含加密 IV)。 本函数按 magic bytes 检测压缩格式:

  • "VZst" → VZstd(需注册 ZstdDecompressor)
  • "VZa" → VZip(需注册 VZipDecompressor)
  • "PK\x03\x04" → PKZip(标准 zip,本包内置 archive/zip 解压)
  • "BZh" → BZip2(本包内置 compress/bzip2 解压,兼容旧格式)
  • 0x1f 0x8b → GZip(本包内置 compress/gzip 解压,兼容旧格式)
  • 无 magic → 未压缩,直接拷贝

expectedSize 是 manifest 中的 CbOriginal 字段,用于预分配缓冲区。

func DecompressVZip

func DecompressVZip(data []byte) ([]byte, error)

DecompressVZip 解压 VZip 格式的字节数据。

格式详见包注释。与 SteamKit2 VZipUtil.Decompress 行为一致。

与 SteamKit2 对齐的关键点:

  • dictionarySize 字段若 < (1<<12),强制设为 (1<<12)(LZMA 规范要求)
  • 不校验 footer 中的 CRC32(与 SteamKit2 depot chunk 流程 verifyChecksum=false 一致)。 原因:Steam CDN 的 depot chunk 流程只信任 Adler32 校验(在 cdn.DownloadChunk 中做), VZip 内嵌 CRC32 在某些历史 chunk 上可能不正确,强制校验会导致误报失败。

func RegisterVZipDecompressor

func RegisterVZipDecompressor(d VZipDecompressor)

RegisterVZipDecompressor 注册 VZip 解压器实现。

调用本函数后,DownloadChunk 才能处理 VZip 压缩的 chunk。 注册是全局的;重复注册会覆盖前一次实现。传 nil 等价于取消注册。

func RegisterZstdDecompressor

func RegisterZstdDecompressor(d ZstdDecompressor)

RegisterZstdDecompressor 注册 zstd 解压器实现。

调用本函数后,DownloadChunk 才能处理 VZstd 压缩的 chunk。 注册是全局的;重复注册会覆盖前一次实现。传 nil 等价于取消注册。

func VerifyChecksum

func VerifyChecksum(data []byte, expected uint32) error

VerifyChecksum 用 Adler32 校验解压后的 chunk 字节。

与 SteamKit2 Adler32.Calculate 一致:初始 seed 为 0(不是标准 Adler-32 的 1)。 Go 的 hash/adler32.Checksum 初始 s1=1,不能直接使用。 这里手动实现与 SteamKit2 完全一致的 Adler32(seed=0)。

返回 nil 表示校验通过,ErrCRCMismatch 表示不匹配。

Types

type CDNClient

type CDNClient struct {
	HTTPClient *http.Client
	Scheme     string
	DepotKey   []byte
	ChunkRetry int
}

CDNClient 用于从 Steam CDN 下载并解码 depot chunk。

字段说明:

  • HTTPClient:用于实际 HTTP 请求的客户端。可定制 User-Agent、代理、超时等。 若为 nil,使用 60 秒超时的默认客户端。
  • Scheme:CDN URL 协议,默认 "http";某些 CDN(如 cs.steampowered.com)支持 https。
  • DepotKey:depot 解密密钥,由 steam.Apps.GetDepotDecryptionKey 获取。 DownloadChunk / DownloadFile 调用前必须设置。
  • ChunkRetry:单个 chunk 下载失败时的重试次数,默认 0(不重试)。 SteamKit2 也不内置重试,但实际场景下网络抖动重试可显著提升成功率。

func NewCDNClient

func NewCDNClient(depotKey []byte) *CDNClient

NewCDNClient 创建一个 CDNClient。depotKey 可为 nil,但调用下载方法前必须设置。

func (*CDNClient) DownloadChunk

func (c *CDNClient) DownloadChunk(host string, depotID uint32, token string, chunk *protobuf.ContentManifestPayload_FileMapping_ChunkData) ([]byte, error)

DownloadChunk 下载、解密、解压、校验一个 chunk,返回原始字节。

参数:

  • host:CDN 主机名(来自 PICS appinfo 的 cdns 列表,如 "cs.steampowered.com")
  • depotID:目标 depot id
  • token:CDN 鉴权 token(来自 steam.Apps.GetCDNAuthToken;公开 depot 可为空)
  • chunk:manifest 中的 ChunkData(提供 Sha、Crc、CbOriginal)

返回的 []byte 长度应等于 chunk.CbOriginal。若 Adler32 校验失败,返回 ErrCRCMismatch。

解密流程(与 SteamKit2 Steam/CDN/DepotChunk.cs 一致):

  1. AES-CBC + PKCS7 解密:前 16 字节 ECB 解密得到 IV,剩余 CBC 解密
  2. 按 magic bytes 检测压缩格式(VZstd/VZip/PKZip/BZip2/GZip/None)并解压
  3. Adler32 校验(与 SteamKit2 Adler32.Calculate 等价)

func (*CDNClient) DownloadChunkContext added in v1.1.0

func (c *CDNClient) DownloadChunkContext(ctx context.Context, host string, depotID uint32, token string, chunk *protobuf.ContentManifestPayload_FileMapping_ChunkData) ([]byte, error)

DownloadChunkContext 与 DownloadChunk 相同,并支持取消下载和重试退避。

func (*CDNClient) DownloadFile

func (c *CDNClient) DownloadFile(host string, depotID uint32, token string, mapping *protobuf.ContentManifestPayload_FileMapping) ([]byte, error)

DownloadFile 下载 manifest 中一个 FileMapping 对应的完整文件。

内部按 chunk.Offset 把各 chunk 拼接到结果缓冲区,返回完整文件字节。 任一 chunk 下载失败(含重试耗尽)立即返回错误。

重试由 CDNClient.ChunkRetry 控制;如需更细粒度容错,调用方应自行实现指数退避。

func (*CDNClient) DownloadFileContext added in v1.1.0

func (c *CDNClient) DownloadFileContext(ctx context.Context, host string, depotID uint32, token string, mapping *protobuf.ContentManifestPayload_FileMapping) ([]byte, error)

DownloadFileContext 下载并拼装文件,支持取消。

type VZipDecompressor

type VZipDecompressor interface {
	// Decompress 从 r 读取 VZip 压缩字节(含 "VZa" magic 头),返回解压后的原始字节。
	Decompress(r io.Reader) ([]byte, error)
}

VZipDecompressor 是 VZip(Steam 私有 LZMA 容器)解压器的抽象接口。

VZip 格式:4 字节 "VZa" magic + 1 字节 version + 4 字节 CRC32 + 4 字节 uncompressed_size + LZMA 流 + 4 字节 CRC32。 调用方应使用第三方 LZMA 库(如 github.com/ulikunitz/xz/lzma)实现本接口, 并通过 RegisterVZipDecompressor 注册到本包。

func NewDefaultVZipDecompressor

func NewDefaultVZipDecompressor() VZipDecompressor

NewDefaultVZipDecompressor 创建一个内置的 VZip 解压器实例。

type ZstdDecompressor

type ZstdDecompressor interface {
	// Decompress 从 r 读取 zstd 压缩字节,返回解压后的原始字节。
	Decompress(r io.Reader) ([]byte, error)
}

ZstdDecompressor 是 zstd 解压器的抽象接口。

Go 标准库不含 zstd。调用方应使用第三方库(如 github.com/klauspost/compress/zstd) 实现本接口,并通过 RegisterZstdDecompressor 注册到本包。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL