mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-12 17:59:06 +02:00
validateOffer bounded the file count and inline text but only checked the sign of an announced size, and nothing limited the aggregate or how many offers a sender could keep open. Every announced byte is staged in the spool before delivery, so a peer decided how much of the receiver's disk to take: 512 files of 8 GiB were accepted unchallenged, and a flood of offers each raised its own consent prompt. Sizes are now capped per file and per offer, and a sender is held to a fixed number of open offers. The aggregate accumulates against the remaining headroom instead of summing first, because 512 files of 2^60 wrap an int64 back through zero and a plain sum would report such an offer as nil bytes. The count is of open offers, not lifetime ones, so settling one frees a slot.
225 lines
5.2 KiB
Go
225 lines
5.2 KiB
Go
package filedrop
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Port is the file drop port over the tunnel. It stays fixed whatever the
|
|
// receiver ends up binding locally: a receiver that cannot take this port binds
|
|
// another one and redirects this port to it, so senders never negotiate.
|
|
const Port uint16 = 22042
|
|
|
|
// EnvPort overrides the local listen port; the tunnel-side port stays Port.
|
|
const EnvPort = "NB_FILEDROP_PORT"
|
|
|
|
// HeaderReceivedBytes carries the receiver's confirmed byte count in a HEAD response.
|
|
const HeaderReceivedBytes = "Netbird-Received-Bytes"
|
|
|
|
// DefaultOfferTTL bounds how long an offer waits for the receiver's decision.
|
|
const DefaultOfferTTL = 5 * time.Minute
|
|
|
|
// MaxOfferFiles bounds the number of items a single offer may announce.
|
|
const MaxOfferFiles = 512
|
|
|
|
// MaxInlineTextSize bounds an inline text snippet, which is held in memory.
|
|
const MaxInlineTextSize = 64 * 1024
|
|
|
|
// MaxFileSize bounds one announced payload, and MaxOfferSize the whole offer.
|
|
// Every announced byte is staged in the spool before it is delivered, so
|
|
// without these one peer decides how much of the receiver's disk to consume.
|
|
const (
|
|
MaxFileSize int64 = 100 << 30
|
|
MaxOfferSize int64 = 200 << 30
|
|
)
|
|
|
|
// MaxSenderOffers bounds how many offers one sender may have outstanding, so a
|
|
// peer cannot flood the offer store or the consent prompts behind it.
|
|
const MaxSenderOffers = 16
|
|
|
|
const maxOfferBodySize = 1 << 20
|
|
|
|
const (
|
|
pathOffers = "/v1/offers"
|
|
pathOffersSlash = pathOffers + "/"
|
|
|
|
segmentFiles = "files"
|
|
)
|
|
|
|
// The decisions an offer can carry. Pending is the only non-final one.
|
|
const (
|
|
DecisionPending Decision = iota
|
|
DecisionAccepted
|
|
DecisionDeclined
|
|
DecisionExpired
|
|
)
|
|
|
|
// The receiving modes a profile can be in.
|
|
const (
|
|
ModeOff Mode = iota
|
|
ModeAsk
|
|
ModeAutoAccept
|
|
)
|
|
|
|
// The payload kinds an offer can announce.
|
|
const (
|
|
KindFile Kind = iota
|
|
KindText
|
|
)
|
|
|
|
// The states a transfer moves through.
|
|
const (
|
|
StatePending State = iota
|
|
StateTransferring
|
|
StateCompleted
|
|
StateDeclined
|
|
StateExpired
|
|
StateCancelled
|
|
StateFailed
|
|
)
|
|
|
|
var (
|
|
ErrOfferNotFound = errors.New("offer not found")
|
|
ErrRefused = errors.New("offer refused by receiver")
|
|
ErrDeclined = errors.New("offer declined")
|
|
ErrExpired = errors.New("offer expired")
|
|
ErrNotAccepted = errors.New("offer not accepted")
|
|
ErrUnknownPeer = errors.New("unknown peer")
|
|
ErrInvalidOffer = errors.New("invalid offer")
|
|
)
|
|
|
|
// OfferID identifies a single transfer offer on the receiving peer.
|
|
type OfferID string
|
|
|
|
// PeerKey is the remote peer's public key, used as the identity for per-sender policy.
|
|
type PeerKey string
|
|
|
|
// Decision is the receiver's answer to an offer.
|
|
type Decision uint8
|
|
|
|
// Mode is the receiver's profile-local policy for incoming offers.
|
|
type Mode uint8
|
|
|
|
// Kind distinguishes payloads that are written to the spool from inline text snippets.
|
|
type Kind uint8
|
|
|
|
// State is the lifecycle state of a transfer, on either side.
|
|
type State uint8
|
|
|
|
// FileMeta describes one payload item announced in an offer.
|
|
type FileMeta struct {
|
|
Name string `json:"name"`
|
|
Size int64 `json:"size"`
|
|
ContentType string `json:"contentType,omitempty"`
|
|
Kind Kind `json:"kind,omitempty"`
|
|
Text string `json:"text,omitempty"`
|
|
}
|
|
|
|
// OfferRequest is the JSON body of POST /v1/offers. It carries metadata only.
|
|
type OfferRequest struct {
|
|
SenderName string `json:"senderName,omitempty"`
|
|
Files []FileMeta `json:"files"`
|
|
}
|
|
|
|
// OfferResponse is returned for an offer and for every poll of its status.
|
|
type OfferResponse struct {
|
|
ID OfferID `json:"id"`
|
|
Decision Decision `json:"decision"`
|
|
}
|
|
|
|
// String implements fmt.Stringer.
|
|
func (d Decision) String() string {
|
|
switch d {
|
|
case DecisionPending:
|
|
return "pending"
|
|
case DecisionAccepted:
|
|
return "accepted"
|
|
case DecisionDeclined:
|
|
return "declined"
|
|
case DecisionExpired:
|
|
return "expired"
|
|
default:
|
|
return fmt.Sprintf("unknown(%d)", uint8(d))
|
|
}
|
|
}
|
|
|
|
func (d Decision) valid() bool {
|
|
switch d {
|
|
case DecisionPending, DecisionAccepted, DecisionDeclined, DecisionExpired:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// String implements fmt.Stringer.
|
|
func (m Mode) String() string {
|
|
switch m {
|
|
case ModeOff:
|
|
return "off"
|
|
case ModeAsk:
|
|
return "ask"
|
|
case ModeAutoAccept:
|
|
return "auto"
|
|
default:
|
|
return fmt.Sprintf("unknown(%d)", uint8(m))
|
|
}
|
|
}
|
|
|
|
func (m Mode) valid() bool {
|
|
switch m {
|
|
case ModeOff, ModeAsk, ModeAutoAccept:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// String implements fmt.Stringer.
|
|
func (k Kind) String() string {
|
|
switch k {
|
|
case KindFile:
|
|
return "file"
|
|
case KindText:
|
|
return "text"
|
|
default:
|
|
return fmt.Sprintf("unknown(%d)", uint8(k))
|
|
}
|
|
}
|
|
|
|
func (k Kind) valid() bool {
|
|
return k == KindFile || k == KindText
|
|
}
|
|
|
|
// String implements fmt.Stringer.
|
|
func (s State) String() string {
|
|
switch s {
|
|
case StatePending:
|
|
return "pending"
|
|
case StateTransferring:
|
|
return "transferring"
|
|
case StateCompleted:
|
|
return "completed"
|
|
case StateDeclined:
|
|
return "declined"
|
|
case StateExpired:
|
|
return "expired"
|
|
case StateCancelled:
|
|
return "cancelled"
|
|
case StateFailed:
|
|
return "failed"
|
|
default:
|
|
return fmt.Sprintf("unknown(%d)", uint8(s))
|
|
}
|
|
}
|
|
|
|
func (s State) terminal() bool {
|
|
switch s {
|
|
case StateCompleted, StateDeclined, StateExpired, StateCancelled, StateFailed:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|