Skip to content

Commit 27e7d73

Browse files
committed
fix: 部署证书包改用 tar 解压
1 parent ca0dcee commit 27e7d73

9 files changed

Lines changed: 214 additions & 93 deletions

File tree

internal/client/deploys/1panel.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -154,26 +154,26 @@ func (cd *CertDeployer) DeployCertificateTo1Panel(domain, url string) error {
154154
}
155155

156156
safeDomain := SanitizeDomain(domain)
157-
fileName := fmt.Sprintf("%s_certificates.zip", safeDomain)
158-
zipFile := filepath.Join(CertsDir, fileName)
157+
fileName := fmt.Sprintf("%s_certificates.tar", safeDomain)
158+
tarFile := filepath.Join(CertsDir, fileName)
159159

160-
// 下载zip文件
161-
if err := cd.downloadFunc(url, zipFile); err != nil {
160+
// 下载tar文件
161+
if err := cd.downloadFunc(url, tarFile); err != nil {
162162
return fmt.Errorf("下载证书失败: %w", err)
163163
}
164164

165-
logger.Info("证书下载完成", "file", zipFile)
165+
logger.Info("证书下载完成", "file", tarFile)
166166

167167
defer func() {
168-
if _, err := os.Stat(zipFile); err == nil {
169-
os.Remove(zipFile)
168+
if _, err := os.Stat(tarFile); err == nil {
169+
os.Remove(tarFile)
170170
}
171171
}()
172172

173173
folderName := safeDomain
174174
extractDir := filepath.Join(CertsDir, folderName)
175175

176-
if err := ExtractZip(zipFile, extractDir); err != nil {
176+
if err := ExtractTar(tarFile, extractDir); err != nil {
177177
os.RemoveAll(extractDir)
178178
return fmt.Errorf("解压证书失败: %w", err)
179179
}

internal/client/deploys/apache.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,26 +54,26 @@ func (cd *CertDeployer) DeployCertificateToApache(domain, url string) error {
5454
}
5555

5656
safeDomain := SanitizeDomain(domain)
57-
fileName := fmt.Sprintf("%s_certificates.zip", safeDomain)
58-
zipFile := filepath.Join(CertsDir, fileName)
57+
fileName := fmt.Sprintf("%s_certificates.tar", safeDomain)
58+
tarFile := filepath.Join(CertsDir, fileName)
5959

60-
// 下载zip文件
61-
if err := cd.downloadFunc(url, zipFile); err != nil {
60+
// 下载tar文件
61+
if err := cd.downloadFunc(url, tarFile); err != nil {
6262
return fmt.Errorf("下载证书失败: %w", err)
6363
}
6464

65-
logger.Info("证书下载完成", "file", zipFile)
65+
logger.Info("证书下载完成", "file", tarFile)
6666

6767
defer func() {
68-
if _, err := os.Stat(zipFile); err == nil {
69-
os.Remove(zipFile)
68+
if _, err := os.Stat(tarFile); err == nil {
69+
os.Remove(tarFile)
7070
}
7171
}()
7272

7373
folderName := safeDomain
7474
extractDir := filepath.Join(CertsDir, folderName)
7575

76-
if err := ExtractZip(zipFile, extractDir); err != nil {
76+
if err := ExtractTar(tarFile, extractDir); err != nil {
7777
os.RemoveAll(extractDir)
7878
return fmt.Errorf("解压证书失败: %w", err)
7979
}

internal/client/deploys/deploy.go

Lines changed: 50 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package deploys
22

33
import (
4-
"archive/zip"
4+
"archive/tar"
55
"errors"
66
"fmt"
77
"io"
@@ -52,22 +52,22 @@ func (cd *CertDeployer) DeployCertificate(domain, url string) error {
5252
// 处理泛域名,将 * 转换为 _
5353
safeDomain := SanitizeDomain(domain)
5454

55-
// 文件名格式为 {domain}_certificates.zip
56-
fileName := fmt.Sprintf("%s_certificates.zip", safeDomain)
57-
zipFile := filepath.Join(CertsDir, fileName)
55+
// 文件名格式为 {domain}_certificates.tar
56+
fileName := fmt.Sprintf("%s_certificates.tar", safeDomain)
57+
tarFile := filepath.Join(CertsDir, fileName)
5858

59-
// 下载zip文件
60-
if err := cd.downloadFunc(url, zipFile); err != nil {
59+
// 下载tar文件
60+
if err := cd.downloadFunc(url, tarFile); err != nil {
6161
return fmt.Errorf("下载证书失败: %w", err)
6262
}
6363

64-
logger.Info("证书下载完成", "file", zipFile)
64+
logger.Info("证书下载完成", "file", tarFile)
6565

6666
// 确保下载失败时清理
6767
defer func() {
68-
if _, err := os.Stat(zipFile); err == nil {
69-
// 部署成功后删除zip文件
70-
os.Remove(zipFile)
68+
if _, err := os.Stat(tarFile); err == nil {
69+
// 部署成功后删除tar文件
70+
os.Remove(tarFile)
7171
}
7272
}()
7373

@@ -80,16 +80,16 @@ func (cd *CertDeployer) DeployCertificate(domain, url string) error {
8080
onePanelEnabled := sslConfig.OnePanel != nil && sslConfig.OnePanel.URL != ""
8181

8282
if nginxPath == "" && apachePath == "" && rustFSPath == "" && !feiNiuEnabled && !onePanelEnabled {
83-
logger.Info("未配置SSL目录,证书已下载", "file", zipFile)
83+
logger.Info("未配置SSL目录,证书已下载", "file", tarFile)
8484
return nil
8585
}
8686

8787
// 证书文件夹名(使用处理后的安全域名)
8888
folderName := safeDomain
8989
extractDir := filepath.Join(CertsDir, folderName)
9090

91-
// 1. 解压zip文件
92-
if err := ExtractZip(zipFile, extractDir); err != nil {
91+
// 1. 解压tar文件
92+
if err := ExtractTar(tarFile, extractDir); err != nil {
9393
// 清理失败的解压文件
9494
os.RemoveAll(extractDir)
9595
return fmt.Errorf("解压证书失败: %w", err)
@@ -167,76 +167,87 @@ func (cd *CertDeployer) DeployCertificate(domain, url string) error {
167167
return nil
168168
}
169169

170-
// ExtractZip 解压zip文件
171-
func ExtractZip(zipFile, extractDir string) error {
170+
// ExtractTar 解压tar文件
171+
func ExtractTar(tarFile, extractDir string) error {
172172
// 创建解压目录
173173
if err := os.MkdirAll(extractDir, 0755); err != nil {
174174
return fmt.Errorf("创建解压目录失败: %w", err)
175175
}
176176

177-
// 打开zip文件
178-
reader, err := zip.OpenReader(zipFile)
177+
// 打开tar文件
178+
reader, err := os.Open(tarFile)
179179
if err != nil {
180-
return fmt.Errorf("打开zip文件失败: %w", err)
180+
return fmt.Errorf("打开tar文件失败: %w", err)
181181
}
182182
defer reader.Close()
183183

184+
tarReader := tar.NewReader(reader)
185+
184186
// 解压所有文件
185-
for _, file := range reader.File {
186-
if err := extractZipFile(file, extractDir); err != nil {
187+
for {
188+
header, err := tarReader.Next()
189+
if errors.Is(err, io.EOF) {
190+
return nil
191+
}
192+
if err != nil {
193+
return fmt.Errorf("读取tar文件失败: %w", err)
194+
}
195+
196+
if err := extractTarFile(header, tarReader, extractDir); err != nil {
187197
return err
188198
}
189199
}
190-
191-
return nil
192200
}
193201

194-
// extractZipFile 解压单个zip文件条目
195-
func extractZipFile(file *zip.File, extractDir string) error {
202+
// extractTarFile 解压单个tar文件条目
203+
func extractTarFile(header *tar.Header, reader io.Reader, extractDir string) error {
204+
if header == nil || header.Name == "" {
205+
return nil
206+
}
207+
196208
// 使用 filepath.Rel 安全地检查路径
197-
targetPath := filepath.Join(extractDir, file.Name)
209+
targetPath := filepath.Join(extractDir, header.Name)
198210

199211
// 清理路径并检查符号链接
200212
cleanTarget := filepath.Clean(targetPath)
201213
rel, err := filepath.Rel(extractDir, cleanTarget)
202214
if err != nil || strings.HasPrefix(rel, "..") || strings.Contains(rel, ".."+string(filepath.Separator)) {
203-
return fmt.Errorf("不安全的文件路径: %s", file.Name)
215+
return fmt.Errorf("不安全的文件路径: %s", header.Name)
204216
}
205217

206218
// 使用清理后的路径
207219
targetPath = cleanTarget
208220

209-
// 创建目录
210-
if file.FileInfo().IsDir() {
211-
return os.MkdirAll(targetPath, file.FileInfo().Mode())
221+
mode := os.FileMode(header.Mode)
222+
switch header.Typeflag {
223+
case tar.TypeDir:
224+
return os.MkdirAll(targetPath, mode)
225+
case tar.TypeReg, tar.TypeRegA:
226+
// 继续处理普通文件
227+
default:
228+
// 跳过符号链接、硬链接和设备文件等特殊条目
229+
return nil
212230
}
213231

214232
// 创建文件目录
215233
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
216234
return fmt.Errorf("创建文件目录失败: %w", err)
217235
}
218236

219-
// 打开zip文件中的文件
220-
rc, err := file.Open()
221-
if err != nil {
222-
return fmt.Errorf("打开zip中的文件失败: %w", err)
223-
}
224-
defer rc.Close()
225-
226237
// 创建目标文件
227-
outFile, err := os.Create(targetPath)
238+
outFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
228239
if err != nil {
229240
return fmt.Errorf("创建文件失败: %w", err)
230241
}
231242
defer outFile.Close()
232243

233244
// 复制文件内容
234-
if _, err := io.Copy(outFile, rc); err != nil {
245+
if _, err := io.Copy(outFile, reader); err != nil {
235246
return fmt.Errorf("复制文件内容失败: %w", err)
236247
}
237248

238249
// 设置文件权限
239-
if err := os.Chmod(targetPath, file.FileInfo().Mode()); err != nil {
250+
if err := os.Chmod(targetPath, mode); err != nil {
240251
return fmt.Errorf("设置文件权限失败: %w", err)
241252
}
242253

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package deploys
2+
3+
import (
4+
"archive/tar"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
"testing"
9+
)
10+
11+
type testTarEntry struct {
12+
name string
13+
body string
14+
mode int64
15+
typeflag byte
16+
}
17+
18+
func TestExtractTarExtractsRegularFiles(t *testing.T) {
19+
tempDir := t.TempDir()
20+
tarPath := filepath.Join(tempDir, "certificates.tar")
21+
extractDir := filepath.Join(tempDir, "extract")
22+
23+
writeTestTar(t, tarPath, []testTarEntry{
24+
{name: "nested", mode: 0o755, typeflag: tar.TypeDir},
25+
{name: "cert.pem", body: "cert", mode: 0o644, typeflag: tar.TypeReg},
26+
{name: "nested/privateKey.key", body: "key", mode: 0o600, typeflag: tar.TypeReg},
27+
{name: "ignored-link", mode: 0o777, typeflag: tar.TypeSymlink},
28+
})
29+
30+
if err := ExtractTar(tarPath, extractDir); err != nil {
31+
t.Fatalf("ExtractTar() error = %v", err)
32+
}
33+
34+
assertFileContent(t, filepath.Join(extractDir, "cert.pem"), "cert")
35+
assertFileContent(t, filepath.Join(extractDir, "nested", "privateKey.key"), "key")
36+
37+
if _, err := os.Stat(filepath.Join(extractDir, "ignored-link")); !os.IsNotExist(err) {
38+
t.Fatalf("expected symlink entry to be skipped, got err=%v", err)
39+
}
40+
}
41+
42+
func TestExtractTarRejectsUnsafePath(t *testing.T) {
43+
tempDir := t.TempDir()
44+
tarPath := filepath.Join(tempDir, "certificates.tar")
45+
extractDir := filepath.Join(tempDir, "extract")
46+
47+
writeTestTar(t, tarPath, []testTarEntry{
48+
{name: "../escape.pem", body: "bad", mode: 0o644, typeflag: tar.TypeReg},
49+
})
50+
51+
err := ExtractTar(tarPath, extractDir)
52+
if err == nil {
53+
t.Fatal("ExtractTar() expected unsafe path error")
54+
}
55+
if !strings.Contains(err.Error(), "不安全的文件路径") {
56+
t.Fatalf("expected unsafe path error, got %v", err)
57+
}
58+
59+
if _, statErr := os.Stat(filepath.Join(tempDir, "escape.pem")); !os.IsNotExist(statErr) {
60+
t.Fatalf("unsafe file should not be written, stat err=%v", statErr)
61+
}
62+
}
63+
64+
func writeTestTar(t *testing.T, tarPath string, entries []testTarEntry) {
65+
t.Helper()
66+
67+
file, err := os.Create(tarPath)
68+
if err != nil {
69+
t.Fatalf("create tar: %v", err)
70+
}
71+
defer file.Close()
72+
73+
writer := tar.NewWriter(file)
74+
for _, entry := range entries {
75+
header := &tar.Header{
76+
Name: entry.name,
77+
Mode: entry.mode,
78+
Size: int64(len(entry.body)),
79+
Typeflag: entry.typeflag,
80+
}
81+
if entry.typeflag == tar.TypeDir {
82+
header.Size = 0
83+
}
84+
85+
if err := writer.WriteHeader(header); err != nil {
86+
t.Fatalf("write tar header %q: %v", entry.name, err)
87+
}
88+
if header.Size > 0 {
89+
if _, err := writer.Write([]byte(entry.body)); err != nil {
90+
t.Fatalf("write tar body %q: %v", entry.name, err)
91+
}
92+
}
93+
}
94+
95+
if err := writer.Close(); err != nil {
96+
t.Fatalf("close tar writer: %v", err)
97+
}
98+
}
99+
100+
func assertFileContent(t *testing.T, path, want string) {
101+
t.Helper()
102+
103+
got, err := os.ReadFile(path)
104+
if err != nil {
105+
t.Fatalf("read %s: %v", path, err)
106+
}
107+
if string(got) != want {
108+
t.Fatalf("expected %s to contain %q, got %q", path, want, string(got))
109+
}
110+
}

internal/client/deploys/feiniu.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -324,26 +324,26 @@ func (cd *CertDeployer) DeployCertificateToFeiNiu(domain, url string) error {
324324
}
325325

326326
safeDomain := SanitizeDomain(domain)
327-
fileName := fmt.Sprintf("%s_certificates.zip", safeDomain)
328-
zipFile := filepath.Join(CertsDir, fileName)
327+
fileName := fmt.Sprintf("%s_certificates.tar", safeDomain)
328+
tarFile := filepath.Join(CertsDir, fileName)
329329

330-
// 下载zip文件
331-
if err := cd.downloadFunc(url, zipFile); err != nil {
330+
// 下载tar文件
331+
if err := cd.downloadFunc(url, tarFile); err != nil {
332332
return fmt.Errorf("下载证书失败: %w", err)
333333
}
334334

335-
logger.Info("证书下载完成", "file", zipFile)
335+
logger.Info("证书下载完成", "file", tarFile)
336336

337337
defer func() {
338-
if _, err := os.Stat(zipFile); err == nil {
339-
os.Remove(zipFile)
338+
if _, err := os.Stat(tarFile); err == nil {
339+
os.Remove(tarFile)
340340
}
341341
}()
342342

343343
folderName := safeDomain
344344
extractDir := filepath.Join(CertsDir, folderName)
345345

346-
if err := ExtractZip(zipFile, extractDir); err != nil {
346+
if err := ExtractTar(tarFile, extractDir); err != nil {
347347
os.RemoveAll(extractDir)
348348
return fmt.Errorf("解压证书失败: %w", err)
349349
}

0 commit comments

Comments
 (0)