| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347 |
- // 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()
- }
|