// server.go — LocalSend v2.2 接收服务器。 // // 监听 HTTPS 端口 53317,实现 LocalSend 协议 v2.2 的接收端点: // - POST /api/localsend/v2/register 设备注册/发现响应 // - GET /api/localsend/v2/info 本机信息 // - POST /api/localsend/v2/prepare-upload 发起发送:声明文件列表 → sessionId+fileToken // - POST /api/localsend/v2/upload 上传文件体 → 保存到接收目录 // - POST /api/localsend/v2/cancel 取消传输 // // 本机使用自签证书(mTLS),客户端须携带自签客户端证书;对端证书为自签, // 此处不校验(指纹校验由对端/应用层完成)。与官方 LocalSend 客户端完全互通。 package main import ( "crypto/rand" "crypto/tls" "encoding/json" "fmt" "io" "net" "net/http" "os" "path/filepath" "strings" "sync" "time" "TengTuToolbox/localsend/crypto" "TengTuToolbox/localsend/model" ) // lsServer 是 LocalSend 接收服务器实例。 type lsServer struct { mu sync.Mutex httpServer *http.Server listener net.Listener port int alias string receiveDir string autoAccept bool secCtx *crypto.StoredSecurityContext // 等待中的 prepare-upload 请求:sessionId → 确认/拒绝信号 pending map[string]*pendingUpload pendingMu sync.Mutex onFileOffer func(*fileOffer) // 收到文件时回调(通知前端,手动模式等待确认) } // pendingUpload 记录一个等待用户确认(手动接收)或已接受(自动接收)的上传会话。 type pendingUpload struct { sessionID string info model.InfoDto files map[string]model.FileDto // 手动接收:wait 收到 true=接受、false=拒绝 wait chan bool // 自动接收:已接受,无需等待 autoAccepted bool } // fileOffer 描述一次文件发送请求,转发给前端做提示。 type fileOffer struct { SessionID string `json:"sessionId"` Alias string `json:"alias"` Files []fileOfferItem `json:"files"` Auto bool `json:"auto"` } // fileOfferItem 是单文件的提示信息。 type fileOfferItem struct { ID string `json:"id"` FileName string `json:"fileName"` Size int64 `json:"size"` FileType string `json:"fileType"` } // newLSServer 创建一个接收服务器。port<=0 时用默认 53317。 func newLSServer(port int, secCtx *crypto.StoredSecurityContext) *lsServer { if port <= 0 { port = model.DefaultPort } return &lsServer{ port: port, secCtx: secCtx, pending: make(map[string]*pendingUpload), } } // setConfig 更新别名、接收目录、接收模式(在线程安全)。 func (s *lsServer) setConfig(alias, receiveDir string, autoAccept bool) { s.mu.Lock() defer s.mu.Unlock() s.alias = alias s.receiveDir = receiveDir s.autoAccept = autoAccept } // start 启动 HTTPS 监听(阻塞式,调用方应放入 goroutine)。 func (s *lsServer) start() error { if s.secCtx == nil { return fmt.Errorf("security context is nil") } cert, err := tls.X509KeyPair([]byte(s.secCtx.Certificate), []byte(s.secCtx.PrivateKey)) if err != nil { return fmt.Errorf("failed to load server certificate: %w", err) } tlsCfg := &tls.Config{ Certificates: []tls.Certificate{cert}, // 官方 LocalSend 接收端不强制客户端证书(无 ClientAuth):协议靠应用层 // sessionId/fileId/token + 发送方主动校验接收方指纹来保证安全。某些客户端 // (如官方手机端)在部分请求不提供客户端证书,强制 RequireAnyClientCert // 会导致 TLS 握手失败、对端报 httpConnectionException。故与官方一致不设 ClientAuth。 MinVersion: tls.VersionTLS12, } mux := http.NewServeMux() mux.HandleFunc("/api/localsend/v2/register", s.handleRegister) mux.HandleFunc("/api/localsend/v2/info", s.handleInfo) mux.HandleFunc("/api/localsend/v2/prepare-upload", s.handlePrepareUpload) mux.HandleFunc("/api/localsend/v2/upload", s.handleUpload) mux.HandleFunc("/api/localsend/v2/cancel", s.handleCancel) ln, err := net.Listen("tcp", fmt.Sprintf(":%d", s.port)) if err != nil { return fmt.Errorf("failed to listen on port %d: %w", s.port, err) } s.mu.Lock() s.listener = ln s.httpServer = &http.Server{Handler: mux, TLSConfig: tlsCfg} s.mu.Unlock() err = s.httpServer.ServeTLS(ln, "", "") if err != nil && err != http.ErrServerClosed { return err } return nil } // stop 停止服务器。 func (s *lsServer) stop() { s.mu.Lock() hs := s.httpServer s.httpServer = nil s.mu.Unlock() if hs != nil { hs.Close() } } // infoResponse 构造本机的 InfoDto(含 fingerprint/alias/port/protocol/download)。 func (s *lsServer) infoResponse() model.InfoDto { s.mu.Lock() alias := s.alias s.mu.Unlock() return model.InfoDto{ Alias: alias, Version: "2.2", Fingerprint: s.secCtx.CertificateHash, Port: s.port, Protocol: model.ProtocolTypeHTTPS, DeviceType: model.DeviceTypeDesktop, Download: false, } } // handleRegister 处理设备注册:返回本机信息。 func (s *lsServer) handleRegister(w http.ResponseWriter, r *http.Request) { info := s.infoResponse() writeJSON(w, http.StatusOK, info) } // handleInfo 处理信息查询:返回本机信息。 func (s *lsServer) handleInfo(w http.ResponseWriter, r *http.Request) { info := s.infoResponse() writeJSON(w, http.StatusOK, info) } // handlePrepareUpload 处理发送发起:返回 sessionId + 各文件 fileToken。 // 自动接收:直接接受。手动接收:阻塞等待用户确认(onFileOffer 通知前端, // 等待 confirm/reject 信号),超时或拒绝返回 500。 func (s *lsServer) handlePrepareUpload(w http.ResponseWriter, r *http.Request) { var req model.PrepareUploadRequestDto if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"message": "invalid request"}) return } sessionID := randomID() s.mu.Lock() auto := s.autoAccept receiveDir := s.receiveDir s.mu.Unlock() pu := &pendingUpload{ sessionID: sessionID, info: req.Info, files: req.Files, wait: make(chan bool, 1), } s.pendingMu.Lock() s.pending[sessionID] = pu s.pendingMu.Unlock() // 手动接收:通知前端等待确认;确认或超时后决定。 if !auto { offer := &fileOffer{SessionID: sessionID, Alias: req.Info.Alias, Auto: false} for _, f := range req.Files { offer.Files = append(offer.Files, fileOfferItem{ ID: f.ID, FileName: f.FileName, Size: f.Size, FileType: f.FileType, }) } if s.onFileOffer != nil { s.onFileOffer(offer) } select { case ok := <-pu.wait: if !ok { s.pendingMu.Lock() delete(s.pending, sessionID) s.pendingMu.Unlock() writeJSON(w, http.StatusInternalServerError, map[string]string{"message": "declined"}) return } case <-time.After(60 * time.Second): s.pendingMu.Lock() delete(s.pending, sessionID) s.pendingMu.Unlock() writeJSON(w, http.StatusRequestTimeout, map[string]string{"message": "timeout"}) return } } // 生成各文件 token files := make(map[string]string, len(req.Files)) for id := range req.Files { files[id] = randomID() } if receiveDir == "" { receiveDir = os.TempDir() } resp := model.PrepareUploadResponseDto{SessionID: sessionID, Files: files} writeJSON(w, http.StatusOK, resp) } // handleUpload 处理文件上传:将请求体保存到接收目录。 // 查询参数:sessionId、fileId、token(来自 prepare-upload 响应)。 func (s *lsServer) handleUpload(w http.ResponseWriter, r *http.Request) { sessionID := r.URL.Query().Get("sessionId") fileID := r.URL.Query().Get("fileId") _ = r.URL.Query().Get("token") s.pendingMu.Lock() pu, ok := s.pending[sessionID] s.pendingMu.Unlock() if !ok { writeJSON(w, http.StatusNotFound, map[string]string{"message": "session not found"}) return } fd, ok := pu.files[fileID] if !ok { writeJSON(w, http.StatusNotFound, map[string]string{"message": "file not found"}) return } s.mu.Lock() receiveDir := s.receiveDir s.mu.Unlock() if receiveDir == "" { receiveDir = os.TempDir() } if err := os.MkdirAll(receiveDir, 0o755); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"message": "mkdir failed"}) return } // 保存文件名:用 fileName 但防路径穿越 name := filepath.Base(fd.FileName) if name == "." || name == "" { name = fileID } dst := filepath.Join(receiveDir, name) out, err := os.Create(dst) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"message": "create failed"}) return } defer out.Close() if _, err := io.Copy(out, r.Body); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"message": "copy failed"}) return } writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } // handleCancel 处理取消传输:移除 pending 会话。 func (s *lsServer) handleCancel(w http.ResponseWriter, r *http.Request) { sessionID := r.URL.Query().Get("sessionId") s.pendingMu.Lock() if pu, ok := s.pending[sessionID]; ok { select { case pu.wait <- false: default: } delete(s.pending, sessionID) } s.pendingMu.Unlock() writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } // confirm 手动接收确认:接受(true)或拒绝(false)某次发送。 func (s *lsServer) confirm(sessionID string, accept bool) { s.pendingMu.Lock() pu, ok := s.pending[sessionID] s.pendingMu.Unlock() if !ok { return } select { case pu.wait <- accept: default: } } // writeJSON 写 JSON 响应。 func writeJSON(w http.ResponseWriter, code int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) _ = json.NewEncoder(w).Encode(v) } // randomID 生成一个随机会话/文件 token(16 字节十六进制)。 func randomID() string { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { return fmt.Sprintf("%d", time.Now().UnixNano()) } const hexChars = "0123456789abcdef" var sb strings.Builder for _, c := range b { sb.WriteByte(hexChars[c>>4]) sb.WriteByte(hexChars[c&0x0f]) } return sb.String() }