Documentation
¶
Overview ¶
Package cdn 实现 Steam Content Delivery Network(CDN)的 depot chunk 下载与解码。
Steam CDN 把 depot 内容拆成多个 chunk(默认 1 MB)存储。下载流程:
- 从 PICS / manifest 获取目标 depot 的 ContentManifestPayload.FileMapping 列表;
- 用 steam.Apps.GetDepotDecryptionKey 拿到 depot 的 AES 解密密钥;
- 用 steam.Apps.GetCDNAuthToken 拿到访问 CDN 主机的临时鉴权 token;
- 用本包的 CDNClient.DownloadChunk 按 chunk SHA 下载、解密、解压、校验 Adler32;
- 按 chunk.Offset 把所有 chunk 拼成完整文件。
Chunk 在 CDN 上的 URL 格式:
http://<host>/depot/<depotID>/chunk/<sha-hex>?token=<cdnAuthToken>
Chunk 字节流的处理顺序(与 SteamKit2 CDN/DepotChunk.cs 一致):
- AES-CBC 解密:前 16 字节用 ECB 解密得到 IV,剩余用 CBC + PKCS7 解密;
- 解密后字节是压缩载荷,按 magic 检测压缩格式: - "VSZa" → VZstd(zstd) - "VZa" → VZip(LZMA,Steam 私有容器) - "PK\x03\x04" → PKZip - 无 magic → 未压缩,直接拷贝
- 解压后用 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
- Variables
- func DecompressChunk(data []byte, expectedSize int) ([]byte, error)
- func DecompressVZip(data []byte) ([]byte, error)
- func RegisterVZipDecompressor(d VZipDecompressor)
- func RegisterZstdDecompressor(d ZstdDecompressor)
- func VerifyChecksum(data []byte, expected uint32) error
- type CDNClient
- func (c *CDNClient) DownloadChunk(host string, depotID uint32, token string, ...) ([]byte, error)
- func (c *CDNClient) DownloadChunkContext(ctx context.Context, host string, depotID uint32, token string, ...) ([]byte, error)
- func (c *CDNClient) DownloadFile(host string, depotID uint32, token string, ...) ([]byte, error)
- func (c *CDNClient) DownloadFileContext(ctx context.Context, host string, depotID uint32, token string, ...) ([]byte, error)
- type VZipDecompressor
- type ZstdDecompressor
Constants ¶
const MaxChunkSize = 32 * 1024 * 1024
MaxChunkSize 是单个 chunk 响应体的最大字节数(32MB)。 防御性上限:恶意/异常 CDN 返回 GB 级响应会导致 OOM。 chunk 实际大小可从 manifest 的 CbCompressed 预判,32MB 足够覆盖压缩后最大 chunk。
Variables ¶
var ( // ErrDepotKeyRequired 表示未设置 depot key,无法解密 chunk。 ErrDepotKeyRequired = errors.New("cdn: depot key 未设置") // ErrHTTPNotOK 表示 CDN 返回非 2xx 状态码。 ErrHTTPNotOK = errors.New("cdn: CDN 返回非 2xx 状态码") )
预声明错误。
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 一致):
- AES-CBC + PKCS7 解密:前 16 字节 ECB 解密得到 IV,剩余 CBC 解密
- 按 magic bytes 检测压缩格式(VZstd/VZip/PKZip/BZip2/GZip/None)并解压
- 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 控制;如需更细粒度容错,调用方应自行实现指数退避。
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 解压器实例。