Skip to content

Commit eca7f59

Browse files
committed
fix: 兼容证书压缩包格式
1 parent 0c57079 commit eca7f59

3 files changed

Lines changed: 308 additions & 13 deletions

File tree

internal/client/common.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package client
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
67
"io"
78
"net/http"
89
"net/url"
910
"os"
1011
"path/filepath"
12+
"strings"
1113
"sync/atomic"
1214
"time"
1315
)
@@ -68,6 +70,9 @@ func DownloadFile(ctx context.Context, httpClient *http.Client, accessKey, downl
6870
if resp.StatusCode != http.StatusOK {
6971
return fmt.Errorf("下载失败,状态码: %d", resp.StatusCode)
7072
}
73+
if err := ensureDownloadContentType(resp); err != nil {
74+
return err
75+
}
7176

7277
// 确保目标目录存在
7378
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
@@ -114,3 +119,26 @@ func DownloadFile(ctx context.Context, httpClient *http.Client, accessKey, downl
114119
completed = true
115120
return nil
116121
}
122+
123+
func ensureDownloadContentType(resp *http.Response) error {
124+
contentType := strings.ToLower(resp.Header.Get("Content-Type"))
125+
if contentType == "" {
126+
return nil
127+
}
128+
129+
invalidTypes := []string{
130+
"application/json",
131+
"text/html",
132+
"text/plain",
133+
}
134+
for _, invalidType := range invalidTypes {
135+
if strings.Contains(contentType, invalidType) {
136+
preview, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
137+
resp.Body.Close()
138+
resp.Body = io.NopCloser(bytes.NewReader(preview))
139+
return fmt.Errorf("下载内容不是证书压缩包: content-type=%s body=%q", contentType, string(preview))
140+
}
141+
}
142+
143+
return nil
144+
}

internal/client/deploys/deploy.go

Lines changed: 159 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ package deploys
22

33
import (
44
"archive/tar"
5+
"archive/zip"
6+
"bufio"
7+
"compress/gzip"
8+
"encoding/hex"
59
"errors"
610
"fmt"
711
"io"
@@ -167,7 +171,14 @@ func (cd *CertDeployer) DeployCertificate(domain, url string) error {
167171
return nil
168172
}
169173

170-
// ExtractTar 解压tar文件
174+
const (
175+
archiveFormatTar = "tar"
176+
archiveFormatTarGzip = "tar.gz"
177+
archiveFormatZip = "zip"
178+
)
179+
180+
// ExtractTar 解压证书压缩包。
181+
// 当前证书包标准格式为 tar;这里兼容 tar.gz 和旧版 zip,避免前后端版本短暂不一致时部署失败。
171182
func ExtractTar(tarFile, extractDir string) error {
172183
// 创建解压目录
173184
if err := os.MkdirAll(extractDir, 0755); err != nil {
@@ -181,17 +192,77 @@ func ExtractTar(tarFile, extractDir string) error {
181192
}
182193
defer reader.Close()
183194

184-
tarReader := tar.NewReader(reader)
195+
bufferedReader := bufio.NewReader(reader)
196+
header, err := bufferedReader.Peek(512)
197+
if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, bufio.ErrBufferFull) {
198+
return fmt.Errorf("读取压缩包文件头失败: %w", err)
199+
}
200+
201+
switch detectArchiveFormat(header) {
202+
case archiveFormatTarGzip:
203+
gzipReader, err := gzip.NewReader(bufferedReader)
204+
if err != nil {
205+
return fmt.Errorf("打开gzip压缩tar失败: %w", err)
206+
}
207+
defer gzipReader.Close()
208+
return extractTarReader(tar.NewReader(gzipReader), extractDir, archiveHeaderSummary(header))
209+
case archiveFormatZip:
210+
return extractZipArchive(tarFile, extractDir)
211+
default:
212+
return extractTarReader(tar.NewReader(bufferedReader), extractDir, archiveHeaderSummary(header))
213+
}
214+
}
215+
216+
func detectArchiveFormat(header []byte) string {
217+
if len(header) >= 2 && header[0] == 0x1f && header[1] == 0x8b {
218+
return archiveFormatTarGzip
219+
}
220+
if len(header) >= 4 {
221+
switch string(header[:4]) {
222+
case "PK\x03\x04", "PK\x05\x06", "PK\x07\x08":
223+
return archiveFormatZip
224+
}
225+
}
226+
return archiveFormatTar
227+
}
228+
229+
func archiveHeaderSummary(header []byte) string {
230+
if len(header) == 0 {
231+
return "<empty>"
232+
}
233+
234+
const maxHeaderSummaryBytes = 64
235+
if len(header) > maxHeaderSummaryBytes {
236+
header = header[:maxHeaderSummaryBytes]
237+
}
238+
239+
printable := make([]byte, 0, len(header))
240+
for _, b := range header {
241+
if b >= 32 && b <= 126 {
242+
printable = append(printable, b)
243+
} else {
244+
printable = append(printable, '.')
245+
}
246+
}
247+
248+
return fmt.Sprintf("ascii=%q hex=%s", string(printable), hex.EncodeToString(header))
249+
}
185250

251+
func extractTarReader(tarReader *tar.Reader, extractDir, headerSummary string) error {
186252
// 解压所有文件
253+
firstEntry := true
187254
for {
188255
header, err := tarReader.Next()
189256
if errors.Is(err, io.EOF) {
190257
return nil
191258
}
192259
if err != nil {
260+
if firstEntry {
261+
return fmt.Errorf("读取tar文件失败: %w, 文件头: %s", err, headerSummary)
262+
}
193263
return fmt.Errorf("读取tar文件失败: %w", err)
194264
}
265+
firstEntry = false
195266

196267
if err := extractTarFile(header, tarReader, extractDir); err != nil {
197268
return err
@@ -205,19 +276,11 @@ func extractTarFile(header *tar.Header, reader io.Reader, extractDir string) err
205276
return nil
206277
}
207278

208-
// 使用 filepath.Rel 安全地检查路径
209-
targetPath := filepath.Join(extractDir, header.Name)
210-
211-
// 清理路径并检查符号链接
212-
cleanTarget := filepath.Clean(targetPath)
213-
rel, err := filepath.Rel(extractDir, cleanTarget)
214-
if err != nil || strings.HasPrefix(rel, "..") || strings.Contains(rel, ".."+string(filepath.Separator)) {
215-
return fmt.Errorf("不安全的文件路径: %s", header.Name)
279+
targetPath, err := safeArchiveTarget(extractDir, header.Name)
280+
if err != nil {
281+
return err
216282
}
217283

218-
// 使用清理后的路径
219-
targetPath = cleanTarget
220-
221284
mode := os.FileMode(header.Mode)
222285
switch header.Typeflag {
223286
case tar.TypeDir:
@@ -254,6 +317,89 @@ func extractTarFile(header *tar.Header, reader io.Reader, extractDir string) err
254317
return nil
255318
}
256319

320+
func safeArchiveTarget(extractDir, entryName string) (string, error) {
321+
// 使用 filepath.Rel 安全地检查路径
322+
targetPath := filepath.Join(extractDir, entryName)
323+
324+
// 清理路径并检查符号链接
325+
cleanTarget := filepath.Clean(targetPath)
326+
rel, err := filepath.Rel(extractDir, cleanTarget)
327+
if err != nil || strings.HasPrefix(rel, "..") || strings.Contains(rel, ".."+string(filepath.Separator)) {
328+
return "", fmt.Errorf("不安全的文件路径: %s", entryName)
329+
}
330+
331+
return cleanTarget, nil
332+
}
333+
334+
func extractZipArchive(zipFile, extractDir string) error {
335+
reader, err := zip.OpenReader(zipFile)
336+
if err != nil {
337+
return fmt.Errorf("打开zip文件失败: %w", err)
338+
}
339+
defer reader.Close()
340+
341+
for _, file := range reader.File {
342+
if err := extractZipFile(file, extractDir); err != nil {
343+
return err
344+
}
345+
}
346+
347+
return nil
348+
}
349+
350+
func extractZipFile(file *zip.File, extractDir string) error {
351+
if file == nil || file.Name == "" {
352+
return nil
353+
}
354+
355+
targetPath, err := safeArchiveTarget(extractDir, file.Name)
356+
if err != nil {
357+
return err
358+
}
359+
360+
info := file.FileInfo()
361+
if info.IsDir() {
362+
mode := info.Mode().Perm()
363+
if mode == 0 {
364+
mode = 0755
365+
}
366+
return os.MkdirAll(targetPath, mode)
367+
}
368+
if !info.Mode().IsRegular() {
369+
return nil
370+
}
371+
372+
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
373+
return fmt.Errorf("创建文件目录失败: %w", err)
374+
}
375+
376+
reader, err := file.Open()
377+
if err != nil {
378+
return fmt.Errorf("打开zip文件条目失败: %w", err)
379+
}
380+
defer reader.Close()
381+
382+
mode := info.Mode().Perm()
383+
if mode == 0 {
384+
mode = 0644
385+
}
386+
outFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
387+
if err != nil {
388+
return fmt.Errorf("创建文件失败: %w", err)
389+
}
390+
defer outFile.Close()
391+
392+
if _, err := io.Copy(outFile, reader); err != nil {
393+
return fmt.Errorf("复制文件内容失败: %w", err)
394+
}
395+
396+
if err := os.Chmod(targetPath, mode); err != nil {
397+
return fmt.Errorf("设置文件权限失败: %w", err)
398+
}
399+
400+
return nil
401+
}
402+
257403
// CopyDirectory 复制整个目录
258404
func CopyDirectory(src, dst string) error {
259405
return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {

0 commit comments

Comments
 (0)