localsend_server.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. // server.go — LocalSend v2.2 接收服务器。
  2. //
  3. // 监听 HTTPS 端口 53317,实现 LocalSend 协议 v2.2 的接收端点:
  4. // - POST /api/localsend/v2/register 设备注册/发现响应
  5. // - GET /api/localsend/v2/info 本机信息
  6. // - POST /api/localsend/v2/prepare-upload 发起发送:声明文件列表 → sessionId+fileToken
  7. // - POST /api/localsend/v2/upload 上传文件体 → 保存到接收目录
  8. // - POST /api/localsend/v2/cancel 取消传输
  9. //
  10. // 本机使用自签证书(mTLS),客户端须携带自签客户端证书;对端证书为自签,
  11. // 此处不校验(指纹校验由对端/应用层完成)。与官方 LocalSend 客户端完全互通。
  12. package main
  13. import (
  14. "crypto/rand"
  15. "crypto/tls"
  16. "encoding/json"
  17. "fmt"
  18. "io"
  19. "net"
  20. "net/http"
  21. "os"
  22. "path/filepath"
  23. "strings"
  24. "sync"
  25. "time"
  26. "TengTuToolbox/localsend/crypto"
  27. "TengTuToolbox/localsend/model"
  28. )
  29. // lsServer 是 LocalSend 接收服务器实例。
  30. type lsServer struct {
  31. mu sync.Mutex
  32. httpServer *http.Server
  33. listener net.Listener
  34. port int
  35. alias string
  36. receiveDir string
  37. autoAccept bool
  38. secCtx *crypto.StoredSecurityContext
  39. // 等待中的 prepare-upload 请求:sessionId → 确认/拒绝信号
  40. pending map[string]*pendingUpload
  41. pendingMu sync.Mutex
  42. onFileOffer func(*fileOffer) // 收到文件时回调(通知前端,手动模式等待确认)
  43. }
  44. // pendingUpload 记录一个等待用户确认(手动接收)或已接受(自动接收)的上传会话。
  45. type pendingUpload struct {
  46. sessionID string
  47. info model.InfoDto
  48. files map[string]model.FileDto
  49. // 手动接收:wait 收到 true=接受、false=拒绝
  50. wait chan bool
  51. // 自动接收:已接受,无需等待
  52. autoAccepted bool
  53. }
  54. // fileOffer 描述一次文件发送请求,转发给前端做提示。
  55. type fileOffer struct {
  56. SessionID string `json:"sessionId"`
  57. Alias string `json:"alias"`
  58. Files []fileOfferItem `json:"files"`
  59. Auto bool `json:"auto"`
  60. }
  61. // fileOfferItem 是单文件的提示信息。
  62. type fileOfferItem struct {
  63. ID string `json:"id"`
  64. FileName string `json:"fileName"`
  65. Size int64 `json:"size"`
  66. FileType string `json:"fileType"`
  67. }
  68. // newLSServer 创建一个接收服务器。port<=0 时用默认 53317。
  69. func newLSServer(port int, secCtx *crypto.StoredSecurityContext) *lsServer {
  70. if port <= 0 {
  71. port = model.DefaultPort
  72. }
  73. return &lsServer{
  74. port: port,
  75. secCtx: secCtx,
  76. pending: make(map[string]*pendingUpload),
  77. }
  78. }
  79. // setConfig 更新别名、接收目录、接收模式(在线程安全)。
  80. func (s *lsServer) setConfig(alias, receiveDir string, autoAccept bool) {
  81. s.mu.Lock()
  82. defer s.mu.Unlock()
  83. s.alias = alias
  84. s.receiveDir = receiveDir
  85. s.autoAccept = autoAccept
  86. }
  87. // start 启动 HTTPS 监听(阻塞式,调用方应放入 goroutine)。
  88. func (s *lsServer) start() error {
  89. if s.secCtx == nil {
  90. return fmt.Errorf("security context is nil")
  91. }
  92. cert, err := tls.X509KeyPair([]byte(s.secCtx.Certificate), []byte(s.secCtx.PrivateKey))
  93. if err != nil {
  94. return fmt.Errorf("failed to load server certificate: %w", err)
  95. }
  96. tlsCfg := &tls.Config{
  97. Certificates: []tls.Certificate{cert},
  98. // 官方 LocalSend 接收端不强制客户端证书(无 ClientAuth):协议靠应用层
  99. // sessionId/fileId/token + 发送方主动校验接收方指纹来保证安全。某些客户端
  100. // (如官方手机端)在部分请求不提供客户端证书,强制 RequireAnyClientCert
  101. // 会导致 TLS 握手失败、对端报 httpConnectionException。故与官方一致不设 ClientAuth。
  102. MinVersion: tls.VersionTLS12,
  103. }
  104. mux := http.NewServeMux()
  105. mux.HandleFunc("/api/localsend/v2/register", s.handleRegister)
  106. mux.HandleFunc("/api/localsend/v2/info", s.handleInfo)
  107. mux.HandleFunc("/api/localsend/v2/prepare-upload", s.handlePrepareUpload)
  108. mux.HandleFunc("/api/localsend/v2/upload", s.handleUpload)
  109. mux.HandleFunc("/api/localsend/v2/cancel", s.handleCancel)
  110. ln, err := net.Listen("tcp", fmt.Sprintf(":%d", s.port))
  111. if err != nil {
  112. return fmt.Errorf("failed to listen on port %d: %w", s.port, err)
  113. }
  114. s.mu.Lock()
  115. s.listener = ln
  116. s.httpServer = &http.Server{Handler: mux, TLSConfig: tlsCfg}
  117. s.mu.Unlock()
  118. err = s.httpServer.ServeTLS(ln, "", "")
  119. if err != nil && err != http.ErrServerClosed {
  120. return err
  121. }
  122. return nil
  123. }
  124. // stop 停止服务器。
  125. func (s *lsServer) stop() {
  126. s.mu.Lock()
  127. hs := s.httpServer
  128. s.httpServer = nil
  129. s.mu.Unlock()
  130. if hs != nil {
  131. hs.Close()
  132. }
  133. }
  134. // infoResponse 构造本机的 InfoDto(含 fingerprint/alias/port/protocol/download)。
  135. func (s *lsServer) infoResponse() model.InfoDto {
  136. s.mu.Lock()
  137. alias := s.alias
  138. s.mu.Unlock()
  139. return model.InfoDto{
  140. Alias: alias,
  141. Version: "2.2",
  142. Fingerprint: s.secCtx.CertificateHash,
  143. Port: s.port,
  144. Protocol: model.ProtocolTypeHTTPS,
  145. DeviceType: model.DeviceTypeDesktop,
  146. Download: false,
  147. }
  148. }
  149. // handleRegister 处理设备注册:返回本机信息。
  150. func (s *lsServer) handleRegister(w http.ResponseWriter, r *http.Request) {
  151. info := s.infoResponse()
  152. writeJSON(w, http.StatusOK, info)
  153. }
  154. // handleInfo 处理信息查询:返回本机信息。
  155. func (s *lsServer) handleInfo(w http.ResponseWriter, r *http.Request) {
  156. info := s.infoResponse()
  157. writeJSON(w, http.StatusOK, info)
  158. }
  159. // handlePrepareUpload 处理发送发起:返回 sessionId + 各文件 fileToken。
  160. // 自动接收:直接接受。手动接收:阻塞等待用户确认(onFileOffer 通知前端,
  161. // 等待 confirm/reject 信号),超时或拒绝返回 500。
  162. func (s *lsServer) handlePrepareUpload(w http.ResponseWriter, r *http.Request) {
  163. var req model.PrepareUploadRequestDto
  164. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  165. writeJSON(w, http.StatusBadRequest, map[string]string{"message": "invalid request"})
  166. return
  167. }
  168. sessionID := randomID()
  169. s.mu.Lock()
  170. auto := s.autoAccept
  171. receiveDir := s.receiveDir
  172. s.mu.Unlock()
  173. pu := &pendingUpload{
  174. sessionID: sessionID,
  175. info: req.Info,
  176. files: req.Files,
  177. wait: make(chan bool, 1),
  178. }
  179. s.pendingMu.Lock()
  180. s.pending[sessionID] = pu
  181. s.pendingMu.Unlock()
  182. // 手动接收:通知前端等待确认;确认或超时后决定。
  183. if !auto {
  184. offer := &fileOffer{SessionID: sessionID, Alias: req.Info.Alias, Auto: false}
  185. for _, f := range req.Files {
  186. offer.Files = append(offer.Files, fileOfferItem{
  187. ID: f.ID, FileName: f.FileName, Size: f.Size, FileType: f.FileType,
  188. })
  189. }
  190. if s.onFileOffer != nil {
  191. s.onFileOffer(offer)
  192. }
  193. select {
  194. case ok := <-pu.wait:
  195. if !ok {
  196. s.pendingMu.Lock()
  197. delete(s.pending, sessionID)
  198. s.pendingMu.Unlock()
  199. writeJSON(w, http.StatusInternalServerError, map[string]string{"message": "declined"})
  200. return
  201. }
  202. case <-time.After(60 * time.Second):
  203. s.pendingMu.Lock()
  204. delete(s.pending, sessionID)
  205. s.pendingMu.Unlock()
  206. writeJSON(w, http.StatusRequestTimeout, map[string]string{"message": "timeout"})
  207. return
  208. }
  209. }
  210. // 生成各文件 token
  211. files := make(map[string]string, len(req.Files))
  212. for id := range req.Files {
  213. files[id] = randomID()
  214. }
  215. if receiveDir == "" {
  216. receiveDir = os.TempDir()
  217. }
  218. resp := model.PrepareUploadResponseDto{SessionID: sessionID, Files: files}
  219. writeJSON(w, http.StatusOK, resp)
  220. }
  221. // handleUpload 处理文件上传:将请求体保存到接收目录。
  222. // 查询参数:sessionId、fileId、token(来自 prepare-upload 响应)。
  223. func (s *lsServer) handleUpload(w http.ResponseWriter, r *http.Request) {
  224. sessionID := r.URL.Query().Get("sessionId")
  225. fileID := r.URL.Query().Get("fileId")
  226. _ = r.URL.Query().Get("token")
  227. s.pendingMu.Lock()
  228. pu, ok := s.pending[sessionID]
  229. s.pendingMu.Unlock()
  230. if !ok {
  231. writeJSON(w, http.StatusNotFound, map[string]string{"message": "session not found"})
  232. return
  233. }
  234. fd, ok := pu.files[fileID]
  235. if !ok {
  236. writeJSON(w, http.StatusNotFound, map[string]string{"message": "file not found"})
  237. return
  238. }
  239. s.mu.Lock()
  240. receiveDir := s.receiveDir
  241. s.mu.Unlock()
  242. if receiveDir == "" {
  243. receiveDir = os.TempDir()
  244. }
  245. if err := os.MkdirAll(receiveDir, 0o755); err != nil {
  246. writeJSON(w, http.StatusInternalServerError, map[string]string{"message": "mkdir failed"})
  247. return
  248. }
  249. // 保存文件名:用 fileName 但防路径穿越
  250. name := filepath.Base(fd.FileName)
  251. if name == "." || name == "" {
  252. name = fileID
  253. }
  254. dst := filepath.Join(receiveDir, name)
  255. out, err := os.Create(dst)
  256. if err != nil {
  257. writeJSON(w, http.StatusInternalServerError, map[string]string{"message": "create failed"})
  258. return
  259. }
  260. defer out.Close()
  261. if _, err := io.Copy(out, r.Body); err != nil {
  262. writeJSON(w, http.StatusInternalServerError, map[string]string{"message": "copy failed"})
  263. return
  264. }
  265. writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
  266. }
  267. // handleCancel 处理取消传输:移除 pending 会话。
  268. func (s *lsServer) handleCancel(w http.ResponseWriter, r *http.Request) {
  269. sessionID := r.URL.Query().Get("sessionId")
  270. s.pendingMu.Lock()
  271. if pu, ok := s.pending[sessionID]; ok {
  272. select {
  273. case pu.wait <- false:
  274. default:
  275. }
  276. delete(s.pending, sessionID)
  277. }
  278. s.pendingMu.Unlock()
  279. writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
  280. }
  281. // confirm 手动接收确认:接受(true)或拒绝(false)某次发送。
  282. func (s *lsServer) confirm(sessionID string, accept bool) {
  283. s.pendingMu.Lock()
  284. pu, ok := s.pending[sessionID]
  285. s.pendingMu.Unlock()
  286. if !ok {
  287. return
  288. }
  289. select {
  290. case pu.wait <- accept:
  291. default:
  292. }
  293. }
  294. // writeJSON 写 JSON 响应。
  295. func writeJSON(w http.ResponseWriter, code int, v interface{}) {
  296. w.Header().Set("Content-Type", "application/json")
  297. w.WriteHeader(code)
  298. _ = json.NewEncoder(w).Encode(v)
  299. }
  300. // randomID 生成一个随机会话/文件 token(16 字节十六进制)。
  301. func randomID() string {
  302. b := make([]byte, 16)
  303. if _, err := rand.Read(b); err != nil {
  304. return fmt.Sprintf("%d", time.Now().UnixNano())
  305. }
  306. const hexChars = "0123456789abcdef"
  307. var sb strings.Builder
  308. for _, c := range b {
  309. sb.WriteByte(hexChars[c>>4])
  310. sb.WriteByte(hexChars[c&0x0f])
  311. }
  312. return sb.String()
  313. }