peerapi.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package taildrop
  4. import (
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "net/http"
  9. "net/url"
  10. "strings"
  11. "time"
  12. "tailscale.com/ipn/ipnlocal"
  13. "tailscale.com/tailcfg"
  14. "tailscale.com/tstime"
  15. "tailscale.com/util/clientmetric"
  16. "tailscale.com/util/httphdr"
  17. )
  18. func init() {
  19. ipnlocal.RegisterPeerAPIHandler("/v0/put/", handlePeerPut)
  20. }
  21. var (
  22. metricPutCalls = clientmetric.NewCounter("peerapi_put")
  23. )
  24. // canPutFile reports whether h can put a file ("Taildrop") to this node.
  25. func canPutFile(h ipnlocal.PeerAPIHandler) bool {
  26. if h.Peer().UnsignedPeerAPIOnly() {
  27. // Unsigned peers can't send files.
  28. return false
  29. }
  30. return h.IsSelfUntagged() || h.PeerCaps().HasCapability(tailcfg.PeerCapabilityFileSharingSend)
  31. }
  32. func handlePeerPut(h ipnlocal.PeerAPIHandler, w http.ResponseWriter, r *http.Request) {
  33. ext, ok := ipnlocal.GetExt[*Extension](h.LocalBackend())
  34. if !ok {
  35. http.Error(w, "miswired", http.StatusInternalServerError)
  36. return
  37. }
  38. handlePeerPutWithBackend(h, ext, w, r)
  39. }
  40. // extensionForPut is the subset of taildrop extension that taildrop
  41. // file put needs. This is pulled out for testability.
  42. type extensionForPut interface {
  43. manager() *manager
  44. hasCapFileSharing() bool
  45. Clock() tstime.Clock
  46. }
  47. func handlePeerPutWithBackend(h ipnlocal.PeerAPIHandler, ext extensionForPut, w http.ResponseWriter, r *http.Request) {
  48. if r.Method == "PUT" {
  49. metricPutCalls.Add(1)
  50. }
  51. taildropMgr := ext.manager()
  52. if taildropMgr == nil {
  53. h.Logf("taildrop: no taildrop manager")
  54. http.Error(w, "failed to get taildrop manager", http.StatusInternalServerError)
  55. return
  56. }
  57. if !canPutFile(h) {
  58. http.Error(w, ErrNoTaildrop.Error(), http.StatusForbidden)
  59. return
  60. }
  61. if !ext.hasCapFileSharing() {
  62. http.Error(w, ErrNoTaildrop.Error(), http.StatusForbidden)
  63. return
  64. }
  65. rawPath := r.URL.EscapedPath()
  66. prefix, ok := strings.CutPrefix(rawPath, "/v0/put/")
  67. if !ok {
  68. http.Error(w, "misconfigured internals", http.StatusForbidden)
  69. return
  70. }
  71. baseName, err := url.PathUnescape(prefix)
  72. if err != nil {
  73. http.Error(w, ErrInvalidFileName.Error(), http.StatusBadRequest)
  74. return
  75. }
  76. enc := json.NewEncoder(w)
  77. switch r.Method {
  78. case "GET":
  79. id := clientID(h.Peer().StableID())
  80. if prefix == "" {
  81. // List all the partial files.
  82. files, err := taildropMgr.PartialFiles(id)
  83. if err != nil {
  84. http.Error(w, err.Error(), http.StatusInternalServerError)
  85. return
  86. }
  87. if err := enc.Encode(files); err != nil {
  88. http.Error(w, err.Error(), http.StatusInternalServerError)
  89. h.Logf("json.Encoder.Encode error: %v", err)
  90. return
  91. }
  92. } else {
  93. // Stream all the block hashes for the specified file.
  94. next, close, err := taildropMgr.HashPartialFile(id, baseName)
  95. if err != nil {
  96. http.Error(w, err.Error(), http.StatusInternalServerError)
  97. return
  98. }
  99. defer close()
  100. for {
  101. switch cs, err := next(); {
  102. case err == io.EOF:
  103. return
  104. case err != nil:
  105. http.Error(w, err.Error(), http.StatusInternalServerError)
  106. h.Logf("HashPartialFile.next error: %v", err)
  107. return
  108. default:
  109. if err := enc.Encode(cs); err != nil {
  110. http.Error(w, err.Error(), http.StatusInternalServerError)
  111. h.Logf("json.Encoder.Encode error: %v", err)
  112. return
  113. }
  114. }
  115. }
  116. }
  117. case "PUT":
  118. t0 := ext.Clock().Now()
  119. id := clientID(h.Peer().StableID())
  120. var offset int64
  121. if rangeHdr := r.Header.Get("Range"); rangeHdr != "" {
  122. ranges, ok := httphdr.ParseRange(rangeHdr)
  123. if !ok || len(ranges) != 1 || ranges[0].Length != 0 {
  124. http.Error(w, "invalid Range header", http.StatusBadRequest)
  125. return
  126. }
  127. offset = ranges[0].Start
  128. }
  129. n, err := taildropMgr.PutFile(clientID(fmt.Sprint(id)), baseName, r.Body, offset, r.ContentLength)
  130. switch err {
  131. case nil:
  132. d := ext.Clock().Since(t0).Round(time.Second / 10)
  133. h.Logf("got put of %s in %v from %v/%v", approxSize(n), d, h.RemoteAddr().Addr(), h.Peer().ComputedName)
  134. io.WriteString(w, "{}\n")
  135. case ErrNoTaildrop:
  136. http.Error(w, err.Error(), http.StatusForbidden)
  137. case ErrInvalidFileName:
  138. http.Error(w, err.Error(), http.StatusBadRequest)
  139. case ErrFileExists:
  140. http.Error(w, err.Error(), http.StatusConflict)
  141. default:
  142. http.Error(w, err.Error(), http.StatusInternalServerError)
  143. }
  144. default:
  145. http.Error(w, "expected method GET or PUT", http.StatusMethodNotAllowed)
  146. }
  147. }
  148. func approxSize(n int64) string {
  149. if n <= 1<<10 {
  150. return "<=1KB"
  151. }
  152. if n <= 1<<20 {
  153. return "<=1MB"
  154. }
  155. return fmt.Sprintf("~%dMB", n>>20)
  156. }