diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..24028e1 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,23 @@ +version: "2" +linters: + default: standard + enable: + - staticcheck + - gosec + exclusions: + rules: + - linters: + - gosec + text: "G304:" + - linters: + - errcheck + text: "is not checked" +formatters: + enable: + - gofmt + - gofumpt + - goimports + - gci + - golines +output: + path-mode: abs diff --git a/internal/config/config.go b/internal/config/config.go index bd558ae..cec16ed 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,12 +3,12 @@ package config import ( "encoding/json" "log/slog" - "mesh-drop/internal/security" "os" "path/filepath" "sync" "github.com/google/uuid" + "mesh-drop/internal/security" ) // WindowState 定义窗口状态 @@ -66,7 +66,7 @@ func GetUserHomeDir() string { // New 读取配置 func Load(defaultState WindowState) *Config { configDir := GetConfigDir() - _ = os.MkdirAll(configDir, 0755) + _ = os.MkdirAll(configDir, 0o750) configFile := filepath.Join(configDir, "config.json") // 设置默认值 @@ -88,7 +88,9 @@ func Load(defaultState WindowState) *Config { TrustedPeer: make(map[string]string), } - fileBytes, err := os.ReadFile(configFile) + fileBytes, err := os.ReadFile( + configFile, + ) if err != nil { if !os.IsNotExist(err) { slog.Error("Failed to read config file", "error", err) @@ -107,7 +109,7 @@ func Load(defaultState WindowState) *Config { } // 确保默认保存路径存在 - err = os.MkdirAll(defaultSavePath, 0755) + err = os.MkdirAll(defaultSavePath, 0o750) if err != nil { slog.Error("Failed to create default save path", "path", defaultSavePath, "error", err) } @@ -145,7 +147,7 @@ func (c *Config) Save() error { func (c *Config) save() error { dir := filepath.Dir(c.configPath) - if err := os.MkdirAll(dir, 0755); err != nil { + if err := os.MkdirAll(dir, 0o750); err != nil { return err } @@ -156,7 +158,7 @@ func (c *Config) save() error { // 设置配置文件权限为 0600 (仅所有者读写) if c.configPath != "" { - if err := os.WriteFile(c.configPath, jsonData, 0600); err != nil { + if err := os.WriteFile(c.configPath, jsonData, 0o600); err != nil { slog.Warn("Failed to write config file", "error", err) return err } @@ -181,7 +183,7 @@ func (c *Config) update(fn func()) { func (c *Config) SetSavePath(savePath string) { c.update(func() { c.data.SavePath = savePath - _ = os.MkdirAll(savePath, 0755) + _ = os.MkdirAll(savePath, 0o750) }) } diff --git a/internal/discovery/service.go b/internal/discovery/service.go index 1eef246..64b3568 100644 --- a/internal/discovery/service.go +++ b/internal/discovery/service.go @@ -4,8 +4,6 @@ import ( "encoding/json" "fmt" "log/slog" - "mesh-drop/internal/config" - "mesh-drop/internal/security" "net" "runtime" "sort" @@ -13,6 +11,8 @@ import ( "time" "github.com/wailsapp/wails/v3/pkg/application" + "mesh-drop/internal/config" + "mesh-drop/internal/security" ) const ( @@ -112,7 +112,13 @@ func (s *Service) GetLocalIPInSameSubnet(receiverIP string) (string, bool) { } } } - slog.Error("Failed to get local IP in same subnet", "receiverIP", receiverIP, "component", "discovery") + slog.Error( + "Failed to get local IP in same subnet", + "receiverIP", + receiverIP, + "component", + "discovery", + ) return "", false } @@ -222,7 +228,13 @@ func (s *Service) startListening() { sigData := packet.SignPayload() valid, err := security.Verify(packet.PublicKey, sigData, sig) if err != nil || !valid { - slog.Warn("Received invalid discovery packet signature", "id", packet.ID, "ip", remoteAddr.IP.String()) + slog.Warn( + "Received invalid discovery packet signature", + "id", + packet.ID, + "ip", + remoteAddr.IP.String(), + ) continue } @@ -231,7 +243,15 @@ func (s *Service) startListening() { trustedKeys := s.config.GetTrusted() if knownKey, ok := trustedKeys[packet.ID]; ok { if knownKey != packet.PublicKey { - slog.Warn("SECURITY ALERT: Peer ID mismatch with known public key (Spoofing attempt?)", "id", packet.ID, "known_key", knownKey, "received_key", packet.PublicKey) + slog.Warn( + "SECURITY ALERT: Peer ID mismatch with known public key (Spoofing attempt?)", + "id", + packet.ID, + "known_key", + knownKey, + "received_key", + packet.PublicKey, + ) trustMismatch = true // 当发现 ID 欺骗时,不更新 peer,而是标记为 trustMismatch // 用户可以手动重新添加信任 diff --git a/internal/security/cert.go b/internal/security/cert.go index 563b4c3..b0dc627 100644 --- a/internal/security/cert.go +++ b/internal/security/cert.go @@ -52,7 +52,13 @@ func generateSelfSignedCert(certPath, keyPath string) error { // 在实际的动态环境中,我们可能希望添加所有当前接口的 IP 地址 // 实际上,在客户端跳过 IP 验证对于本地 P2P 来说是很常见的。 - derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + derBytes, err := x509.CreateCertificate( + rand.Reader, + &template, + &template, + &priv.PublicKey, + priv, + ) if err != nil { return err } @@ -73,7 +79,10 @@ func generateSelfSignedCert(certPath, keyPath string) error { return err } defer keyOut.Close() - if err := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil { + if err := pem.Encode( + keyOut, + &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}, + ); err != nil { return err } diff --git a/internal/transfer/client.go b/internal/transfer/client.go index 24aba92..d21cba9 100644 --- a/internal/transfer/client.go +++ b/internal/transfer/client.go @@ -10,13 +10,13 @@ import ( "io" "log/slog" "math" - "mesh-drop/internal/discovery" "net/http" "net/url" "os" "path/filepath" "github.com/google/uuid" + "mesh-drop/internal/discovery" ) func (s *Service) SendFiles(target *discovery.Peer, targetIP string, filePaths []string) { @@ -32,7 +32,15 @@ func (s *Service) SendFile(target *discovery.Peer, targetIP string, filePath str file, err := os.Open(filePath) if err != nil { - slog.Error("Failed to open file", "path", filePath, "error", err, "component", "transfer-client") + slog.Error( + "Failed to open file", + "path", + filePath, + "error", + err, + "component", + "transfer-client", + ) return } @@ -101,7 +109,15 @@ func (s *Service) SendFolder(target *discovery.Peer, targetIP string, folderPath size, err := calculateTarSize(ctx, folderPath) if err != nil { - slog.Error("Failed to calculate folder size", "path", folderPath, "error", err, "component", "transfer-client") + slog.Error( + "Failed to calculate folder size", + "path", + folderPath, + "error", + err, + "component", + "transfer-client", + ) return } @@ -137,7 +153,13 @@ func (s *Service) SendFolder(target *discovery.Peer, targetIP string, folderPath go func(ctx context.Context) { defer w.Close() if err := streamFolderToTar(ctx, w, folderPath); err != nil { - slog.Error("Failed to stream folder to tar", "error", err, "component", "transfer-client") + slog.Error( + "Failed to stream folder to tar", + "error", + err, + "component", + "transfer-client", + ) w.CloseWithError(err) } }(ctx) @@ -199,7 +221,12 @@ func (s *Service) SendText(target *discovery.Peer, targetIP string, text string) } // ask 向接收端发送传输请求 -func (s *Service) ask(ctx context.Context, target *discovery.Peer, targetIP string, task *Transfer) (TransferAskResponse, error) { +func (s *Service) ask( + ctx context.Context, + target *discovery.Peer, + targetIP string, + task *Transfer, +) (TransferAskResponse, error) { if err := ctx.Err(); err != nil { return TransferAskResponse{}, err } @@ -232,7 +259,14 @@ func (s *Service) ask(ctx context.Context, target *discovery.Peer, targetIP stri } // processTransfer 传输数据 -func (s *Service) processTransfer(ctx context.Context, askResp TransferAskResponse, target *discovery.Peer, targetIP string, task *Transfer, payload io.Reader) { +func (s *Service) processTransfer( + ctx context.Context, + askResp TransferAskResponse, + target *discovery.Peer, + targetIP string, + task *Transfer, + payload io.Reader, +) { defer func() { s.NotifyTransferListUpdate() }() @@ -240,7 +274,9 @@ func (s *Service) processTransfer(ctx context.Context, askResp TransferAskRespon if err := ctx.Err(); err != nil { return } - uploadUrl, _ := url.Parse(fmt.Sprintf("https://%s:%d/transfer/upload/%s", targetIP, target.Port, task.ID)) + uploadUrl, _ := url.Parse( + fmt.Sprintf("https://%s:%d/transfer/upload/%s", targetIP, target.Port, task.ID), + ) query := uploadUrl.Query() query.Add("token", askResp.Token) uploadUrl.RawQuery = query.Encode() @@ -273,7 +309,15 @@ func (s *Service) processTransfer(ctx context.Context, askResp TransferAskRespon } else { task.Status = TransferStatusError task.ErrorMsg = fmt.Sprintf("Failed to upload file: %v", err) - slog.Error("Failed to upload file", "url", uploadUrl.String(), "error", err, "component", "transfer-client") + slog.Error( + "Failed to upload file", + "url", + uploadUrl.String(), + "error", + err, + "component", + "transfer-client", + ) } return } @@ -384,7 +428,15 @@ func streamFolderToTar(ctx context.Context, w io.Writer, srcPath string) error { if relPath == "." { return nil } - slog.Debug("Processing file", "path", path, "relPath", relPath, "component", "transfer-client") + slog.Debug( + "Processing file", + "path", + path, + "relPath", + relPath, + "component", + "transfer-client", + ) header, err := tar.FileInfoHeader(info, "") if err != nil { diff --git a/internal/transfer/history.go b/internal/transfer/history.go index aecf6d3..72bd28d 100644 --- a/internal/transfer/history.go +++ b/internal/transfer/history.go @@ -3,9 +3,10 @@ package transfer import ( "encoding/json" "log/slog" - "mesh-drop/internal/config" "os" "path/filepath" + + "mesh-drop/internal/config" ) func (s *Service) SaveHistory() { @@ -24,7 +25,7 @@ func (s *Service) SaveHistory() { } // 写入临时文件 - if err := os.WriteFile(tempPath, historyJson, 0644); err != nil { + if err := os.WriteFile(tempPath, historyJson, 0o600); err != nil { slog.Error("Failed to write temp history file", "error", err, "component", "transfer") return } diff --git a/internal/transfer/model.go b/internal/transfer/model.go index f84a0d8..a369a78 100644 --- a/internal/transfer/model.go +++ b/internal/transfer/model.go @@ -1,8 +1,9 @@ package transfer import ( - "mesh-drop/internal/discovery" "time" + + "mesh-drop/internal/discovery" ) type TransferStatus string @@ -34,9 +35,9 @@ const ( // Transfer type Transfer struct { - ID string `json:"id" binding:"required"` // 传输会话 ID - CreateTime int64 `json:"create_time"` // 创建时间 - Sender discovery.Peer `json:"sender" binding:"required"` // 发送者 + ID string `json:"id" binding:"required"` // 传输会话 ID + CreateTime int64 `json:"create_time"` // 创建时间 + Sender discovery.Peer `json:"sender" binding:"required"` // 发送者 // FileName 如果 ContentType 为 file,文件名;如果 ContentType 为 folder,文件夹名;如果 ContentType 为 text,空 FileName string `json:"file_name"` // 文件名 FileSize int64 `json:"file_size"` // 文件大小 (字节) diff --git a/internal/transfer/server.go b/internal/transfer/server.go index 8f9f7e1..6f038f9 100644 --- a/internal/transfer/server.go +++ b/internal/transfer/server.go @@ -49,7 +49,8 @@ func (s *Service) handleAsk(c *gin.Context) { task.Sender.TrustMismatch = peer.TrustMismatch } - if s.config.GetAutoAccept() || (s.config.IsTrusted(task.Sender.ID) && !task.Sender.TrustMismatch) { + if s.config.GetAutoAccept() || + (s.config.IsTrusted(task.Sender.ID) && !task.Sender.TrustMismatch) { task.DecisionChan <- Decision{ ID: task.ID, Accepted: true, @@ -179,7 +180,15 @@ func (s *Service) handleUpload(c *gin.Context) { _, err := os.Stat(destPath) counter := 1 for err == nil { - destPath = filepath.Join(savePath, fmt.Sprintf("%s (%d)%s", strings.TrimSuffix(task.FileName, filepath.Ext(task.FileName)), counter, filepath.Ext(task.FileName))) + destPath = filepath.Join( + savePath, + fmt.Sprintf( + "%s (%d)%s", + strings.TrimSuffix(task.FileName, filepath.Ext(task.FileName)), + counter, + filepath.Ext(task.FileName), + ), + ) counter++ _, err = os.Stat(destPath) } @@ -227,7 +236,13 @@ func (s *Service) receive(c *gin.Context, task *Transfer, writer Writer, ctxRead if err != nil { // 发送端断线,任务取消 if c.Request.Context().Err() != nil { - slog.Info("Sender canceled transfer (Network/Context disconnected)", "id", task.ID, "raw_err", err) + slog.Info( + "Sender canceled transfer (Network/Context disconnected)", + "id", + task.ID, + "raw_err", + err, + ) task.ErrorMsg = "Sender disconnected" task.Status = TransferStatusCanceled return @@ -273,7 +288,12 @@ func (s *Service) receive(c *gin.Context, task *Transfer, writer Writer, ctxRead task.Status = TransferStatusCompleted } -func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer, ctxReader io.Reader) { +func (s *Service) receiveFolder( + c *gin.Context, + savePath string, + task *Transfer, + ctxReader io.Reader, +) { defer s.NotifyTransferListUpdate() // 创建根目录 @@ -286,7 +306,7 @@ func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer, counter++ _, err = os.Stat(destPath) } - if err := os.MkdirAll(destPath, 0755); err != nil { + if err := os.MkdirAll(destPath, 0o750); err != nil { c.JSON(http.StatusInternalServerError, TransferUploadResponse{ ID: task.ID, Message: "Receiver failed to create folder", @@ -318,7 +338,13 @@ func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer, return false } if c.Request.Context().Err() != nil { - slog.Info("Transfer canceled by sender (Network disconnect)", "id", task.ID, "stage", stage) + slog.Info( + "Transfer canceled by sender (Network disconnect)", + "id", + task.ID, + "stage", + stage, + ) task.Status = TransferStatusCanceled task.ErrorMsg = "Sender disconnected" // 发送端已断开,无需也不应再发送 c.JSON @@ -350,6 +376,14 @@ func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer, return true } + // 获取绝对路径以防止 Zip Slip (G305) + // 必须先转换成绝对路径再判断 + absDestPath, err := filepath.Abs(destPath) + if err != nil { + handleError(err, "resolve_abs_path") + return + } + tr := tar.NewReader(reader) for { header, err := tr.Next() @@ -360,32 +394,52 @@ func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer, return } - target := filepath.Join(destPath, header.Name) - // 确保路径没有越界 - if !strings.HasPrefix(target, filepath.Clean(destPath)+string(os.PathSeparator)) { - // 非法路径 + target := filepath.Join(destPath, filepath.Clean(header.Name)) + absTarget, err := filepath.Abs(target) + if err != nil { + slog.Error("Failed to resolve absolute path", "path", target, "error", err) continue } + // 确保路径在目标目录内 + if !strings.HasPrefix(absTarget, absDestPath+string(os.PathSeparator)) { + slog.Warn( + "Zip Slip attempt detected", + "header_name", + header.Name, + "resolved_path", + absTarget, + ) + continue + } + + // 使用安全的绝对路径 + target = absTarget + switch header.Typeflag { case tar.TypeDir: - if err := os.MkdirAll(target, 0755); err != nil { + if err := os.MkdirAll(target, 0o750); err != nil { slog.Error("Failed to create dir", "path", target, "error", err) } case tar.TypeReg: - f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + f, err := os.OpenFile( + target, + os.O_CREATE|os.O_RDWR, + os.FileMode(header.Mode), + ) //nolint:gosec if err != nil { slog.Error("Failed to create file", "path", target, "error", err) continue } + // nolint: gosec if _, err := io.Copy(f, tr); err != nil { - f.Close() + _ = f.Close() if handleError(err, "write_file_content") { return } } - f.Close() + _ = f.Close() } } diff --git a/internal/transfer/service.go b/internal/transfer/service.go index 066153f..17fb743 100644 --- a/internal/transfer/service.go +++ b/internal/transfer/service.go @@ -5,9 +5,6 @@ import ( "crypto/tls" "fmt" "log/slog" - "mesh-drop/internal/config" - "mesh-drop/internal/discovery" - "mesh-drop/internal/security" "net/http" "path/filepath" "sort" @@ -16,6 +13,9 @@ import ( "github.com/gin-gonic/gin" "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/services/notifications" + "mesh-drop/internal/config" + "mesh-drop/internal/discovery" + "mesh-drop/internal/security" ) type Service struct { @@ -37,12 +37,18 @@ type Service struct { httpClient *http.Client } -func NewService(config *config.Config, app *application.App, notifier *notifications.NotificationService, port int, discoveryService *discovery.Service) *Service { +func NewService( + config *config.Config, + app *application.App, + notifier *notifications.NotificationService, + port int, + discoveryService *discovery.Service, +) *Service { gin.SetMode(gin.ReleaseMode) // 配置自定义 HTTP 客户端以跳过自签名证书验证 tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec } httpClient := &http.Client{ Transport: tr, @@ -95,7 +101,7 @@ func (s *Service) GetTransferSyncMap() *sync.Map { } func (s *Service) GetTransferList() []*Transfer { - var requests []*Transfer = make([]*Transfer, 0) + requests := make([]*Transfer, 0) s.transfers.Range(func(key, value any) bool { transfer := value.(*Transfer) requests = append(requests, transfer) diff --git a/main.go b/main.go index 3bb17e0..dff4c6b 100644 --- a/main.go +++ b/main.go @@ -3,15 +3,15 @@ package main import ( "embed" "log/slog" - "mesh-drop/internal/config" - "mesh-drop/internal/discovery" - "mesh-drop/internal/transfer" "os" "path/filepath" "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/events" "github.com/wailsapp/wails/v3/pkg/services/notifications" + "mesh-drop/internal/config" + "mesh-drop/internal/discovery" + "mesh-drop/internal/transfer" ) //go:embed all:frontend/dist @@ -67,7 +67,17 @@ func NewApp() *App { if screen != nil { defaultWidth = int(float64(screen.Size.Width) * 0.8) defaultHeight = int(float64(screen.Size.Height) * 0.8) - slog.Info("Primary screen found", "width", screen.Size.Width, "height", screen.Size.Height, "defaultWidth", defaultWidth, "defaultHeight", defaultHeight) + slog.Info( + "Primary screen found", + "width", + screen.Size.Width, + "height", + screen.Size.Height, + "defaultWidth", + defaultWidth, + "defaultHeight", + defaultHeight, + ) } else { slog.Info("No primary screen found, using defaults") } @@ -137,20 +147,23 @@ func (a *App) registerCustomEvents() { func (a *App) setupEvents() { // 窗口文件拖拽事件 - a.mainWindows.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) { - files := make([]File, 0) - for _, file := range event.Context().DroppedFiles() { - files = append(files, File{ - Name: filepath.Base(file), - Path: file, + a.mainWindows.OnWindowEvent( + events.Common.WindowFilesDropped, + func(event *application.WindowEvent) { + files := make([]File, 0) + for _, file := range event.Context().DroppedFiles() { + files = append(files, File{ + Name: filepath.Base(file), + Path: file, + }) + } + details := event.Context().DropTargetDetails() + a.app.Event.Emit("files-dropped", FilesDroppedEvent{ + Files: files, + Target: details.ElementID, }) - } - details := event.Context().DropTargetDetails() - a.app.Event.Emit("files-dropped", FilesDroppedEvent{ - Files: files, - Target: details.ElementID, - }) - }) + }, + ) // 窗口关闭事件 a.mainWindows.OnWindowEvent(events.Common.WindowClosing, func(event *application.WindowEvent) {