|
|
|
@ -305,12 +305,10 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Set("X-Frame-Options", "DENY")
|
|
|
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
|
|
|
}
|
|
|
|
|
if strings.HasPrefix(r.URL.Path, "/v0/partial-files/") {
|
|
|
|
|
h.handlePartialFileGet(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if strings.HasPrefix(r.URL.Path, "/v0/put/") {
|
|
|
|
|
metricPutCalls.Add(1)
|
|
|
|
|
if r.Method == "PUT" {
|
|
|
|
|
metricPutCalls.Add(1)
|
|
|
|
|
}
|
|
|
|
|
h.handlePeerPut(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
@ -631,112 +629,85 @@ func (h *peerAPIHandler) peerHasCap(wantCap tailcfg.PeerCapability) bool {
|
|
|
|
|
return h.ps.b.PeerCaps(h.remoteAddr.Addr()).HasCapability(wantCap)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var errMisconfiguredInternals = errors.New("misconfigured internals")
|
|
|
|
|
|
|
|
|
|
func (h *peerAPIHandler) extractBaseName(rawPath, prefix string) (ret string, err error) {
|
|
|
|
|
prefix, ok := strings.CutPrefix(rawPath, prefix)
|
|
|
|
|
if !ok {
|
|
|
|
|
return "", errMisconfiguredInternals
|
|
|
|
|
}
|
|
|
|
|
if prefix == "" {
|
|
|
|
|
return "", taildrop.ErrInvalidFileName
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(prefix, "/") {
|
|
|
|
|
return "", taildrop.ErrInvalidFileName
|
|
|
|
|
}
|
|
|
|
|
baseName, err := url.PathUnescape(prefix)
|
|
|
|
|
if err == errMisconfiguredInternals {
|
|
|
|
|
return "", errMisconfiguredInternals
|
|
|
|
|
} else if err != nil {
|
|
|
|
|
return "", taildrop.ErrInvalidFileName
|
|
|
|
|
}
|
|
|
|
|
return baseName, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *peerAPIHandler) handlePartialFileGet(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !h.ps.b.hasCapFileSharing() {
|
|
|
|
|
http.Error(w, taildrop.ErrNoTaildrop.Error(), http.StatusForbidden)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if r.Method != "GET" {
|
|
|
|
|
http.Error(w, "expected method GET", http.StatusMethodNotAllowed)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var resp any
|
|
|
|
|
var err error
|
|
|
|
|
id := taildrop.ClientID(h.peerNode.StableID())
|
|
|
|
|
|
|
|
|
|
if r.URL.Path == "/v0/partial-files/" {
|
|
|
|
|
resp, err = h.ps.taildrop.PartialFiles(id)
|
|
|
|
|
} else {
|
|
|
|
|
baseName, _ := h.extractBaseName(r.URL.EscapedPath(), "/v0/partial-files/")
|
|
|
|
|
ranges, ok := httphdr.ParseRange(r.Header.Get("Range"))
|
|
|
|
|
if !ok || len(ranges) != 1 || ranges[0].Length < 0 {
|
|
|
|
|
http.Error(w, "invalid Range header", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
offset := ranges[0].Start
|
|
|
|
|
length := ranges[0].Length
|
|
|
|
|
if length == 0 {
|
|
|
|
|
length = -1 // httphdr.Range.Length == 0 implies reading the rest of file
|
|
|
|
|
}
|
|
|
|
|
resp, err = h.ps.taildrop.HashPartialFile(id, baseName, offset, length)
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !h.canPutFile() {
|
|
|
|
|
http.Error(w, taildrop.ErrNoTaildrop.Error(), http.StatusForbidden)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !h.ps.b.hasCapFileSharing() {
|
|
|
|
|
http.Error(w, "file sharing not enabled by Tailscale admin", http.StatusForbidden)
|
|
|
|
|
http.Error(w, taildrop.ErrNoTaildrop.Error(), http.StatusForbidden)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if r.Method != "PUT" {
|
|
|
|
|
http.Error(w, "expected method PUT", http.StatusMethodNotAllowed)
|
|
|
|
|
rawPath := r.URL.EscapedPath()
|
|
|
|
|
prefix, ok := strings.CutPrefix(rawPath, "/v0/put/")
|
|
|
|
|
if !ok {
|
|
|
|
|
http.Error(w, "misconfigured internals", http.StatusForbidden)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
baseName, err := h.extractBaseName(r.URL.EscapedPath(), "/v0/put/")
|
|
|
|
|
baseName, err := url.PathUnescape(prefix)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
|
http.Error(w, taildrop.ErrInvalidFileName.Error(), http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
t0 := h.ps.b.clock.Now()
|
|
|
|
|
id := taildrop.ClientID(h.peerNode.StableID())
|
|
|
|
|
switch r.Method {
|
|
|
|
|
case "GET":
|
|
|
|
|
var resp any
|
|
|
|
|
var err error
|
|
|
|
|
id := taildrop.ClientID(h.peerNode.StableID())
|
|
|
|
|
|
|
|
|
|
var offset int64
|
|
|
|
|
if rangeHdr := r.Header.Get("Range"); rangeHdr != "" {
|
|
|
|
|
ranges, ok := httphdr.ParseRange(rangeHdr)
|
|
|
|
|
if !ok || len(ranges) != 1 || ranges[0].Length != 0 {
|
|
|
|
|
http.Error(w, "invalid Range header", http.StatusBadRequest)
|
|
|
|
|
if r.URL.Path == "/v0/put/"+baseName {
|
|
|
|
|
resp, err = h.ps.taildrop.PartialFiles(id)
|
|
|
|
|
} else {
|
|
|
|
|
ranges, ok := httphdr.ParseRange(r.Header.Get("Range"))
|
|
|
|
|
if !ok || len(ranges) != 1 || ranges[0].Length < 0 {
|
|
|
|
|
http.Error(w, "invalid Range header", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
offset := ranges[0].Start
|
|
|
|
|
length := ranges[0].Length
|
|
|
|
|
if length == 0 {
|
|
|
|
|
length = -1 // httphdr.Range.Length == 0 implies reading the rest of file
|
|
|
|
|
}
|
|
|
|
|
resp, err = h.ps.taildrop.HashPartialFile(id, baseName, offset, length)
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
offset = ranges[0].Start
|
|
|
|
|
}
|
|
|
|
|
n, err := h.ps.taildrop.PutFile(taildrop.ClientID(fmt.Sprint(id)), baseName, r.Body, offset, r.ContentLength)
|
|
|
|
|
switch err {
|
|
|
|
|
case nil:
|
|
|
|
|
d := h.ps.b.clock.Since(t0).Round(time.Second / 10)
|
|
|
|
|
h.logf("got put of %s in %v from %v/%v", approxSize(n), d, h.remoteAddr.Addr(), h.peerNode.ComputedName)
|
|
|
|
|
io.WriteString(w, "{}\n")
|
|
|
|
|
case taildrop.ErrNoTaildrop:
|
|
|
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
|
|
|
case taildrop.ErrInvalidFileName:
|
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
|
case taildrop.ErrFileExists:
|
|
|
|
|
http.Error(w, err.Error(), http.StatusConflict)
|
|
|
|
|
case "PUT":
|
|
|
|
|
t0 := h.ps.b.clock.Now()
|
|
|
|
|
id := taildrop.ClientID(h.peerNode.StableID())
|
|
|
|
|
|
|
|
|
|
var offset int64
|
|
|
|
|
if rangeHdr := r.Header.Get("Range"); rangeHdr != "" {
|
|
|
|
|
ranges, ok := httphdr.ParseRange(rangeHdr)
|
|
|
|
|
if !ok || len(ranges) != 1 || ranges[0].Length != 0 {
|
|
|
|
|
http.Error(w, "invalid Range header", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
offset = ranges[0].Start
|
|
|
|
|
}
|
|
|
|
|
n, err := h.ps.taildrop.PutFile(taildrop.ClientID(fmt.Sprint(id)), baseName, r.Body, offset, r.ContentLength)
|
|
|
|
|
switch err {
|
|
|
|
|
case nil:
|
|
|
|
|
d := h.ps.b.clock.Since(t0).Round(time.Second / 10)
|
|
|
|
|
h.logf("got put of %s in %v from %v/%v", approxSize(n), d, h.remoteAddr.Addr(), h.peerNode.ComputedName)
|
|
|
|
|
io.WriteString(w, "{}\n")
|
|
|
|
|
case taildrop.ErrNoTaildrop:
|
|
|
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
|
|
|
case taildrop.ErrInvalidFileName:
|
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
|
case taildrop.ErrFileExists:
|
|
|
|
|
http.Error(w, err.Error(), http.StatusConflict)
|
|
|
|
|
default:
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
http.Error(w, "expected method GET or PUT", http.StatusMethodNotAllowed)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|