@@ -2,6 +2,10 @@ package deploys
22
33import (
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,避免前后端版本短暂不一致时部署失败。
171182func 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 复制整个目录
258404func CopyDirectory (src , dst string ) error {
259405 return filepath .WalkDir (src , func (path string , d fs.DirEntry , err error ) error {
0 commit comments