mirror of
https://github.com/pion/ice.git
synced 2025-09-26 19:41:11 +08:00
Parse Candidate Extensions (RFC5245)
- Rewrote `UnmarshalCandidate` to better align with RFC5245. - Added Candidate `Extensions` and `GetExtension`. - Updated `Equal` and `Marshal` to accommodate these changes. - New Type `CandidateExtension` to handle.
This commit is contained in:
16
candidate.go
16
candidate.go
@@ -52,12 +52,28 @@ type Candidate interface {
|
||||
// candidate, which is useful for diagnostics and other purposes
|
||||
RelatedAddress() *CandidateRelatedAddress
|
||||
|
||||
// Extensions returns a copy of all extension attributes associated with the ICECandidate.
|
||||
// In the order of insertion, *(key value).
|
||||
// Extension attributes are defined in RFC 5245, Section 15.1:
|
||||
// https://datatracker.ietf.org/doc/html/rfc5245#section-15.1
|
||||
//.
|
||||
Extensions() []CandidateExtension
|
||||
|
||||
// GetExtension returns the value of the extension attribute associated with the ICECandidate.
|
||||
// Extension attributes are defined in RFC 5245, Section 15.1:
|
||||
// https://datatracker.ietf.org/doc/html/rfc5245#section-15.1
|
||||
//.
|
||||
GetExtension(key string) (value CandidateExtension, ok bool)
|
||||
|
||||
String() string
|
||||
Type() CandidateType
|
||||
TCPType() TCPType
|
||||
|
||||
Equal(other Candidate) bool
|
||||
|
||||
// DeepEqual same as Equal, But it also compares the candidate extensions.
|
||||
DeepEqual(other Candidate) bool
|
||||
|
||||
Marshal() string
|
||||
|
||||
addr() net.Addr
|
||||
|
@@ -45,6 +45,7 @@ type candidateBase struct {
|
||||
|
||||
remoteCandidateCaches map[AddrPort]Candidate
|
||||
isLocationTracked bool
|
||||
extensions []CandidateExtension
|
||||
}
|
||||
|
||||
// Done implements context.Context
|
||||
@@ -406,6 +407,7 @@ func (c *candidateBase) Equal(other Candidate) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return c.NetworkType() == other.NetworkType() &&
|
||||
c.Type() == other.Type() &&
|
||||
c.Address() == other.Address() &&
|
||||
@@ -414,6 +416,11 @@ func (c *candidateBase) Equal(other Candidate) bool {
|
||||
c.RelatedAddress().Equal(other.RelatedAddress())
|
||||
}
|
||||
|
||||
// DeepEqual is same as Equal but also compares the extensions
|
||||
func (c *candidateBase) DeepEqual(other Candidate) bool {
|
||||
return c.Equal(other) && c.extensionsEqual(other.Extensions())
|
||||
}
|
||||
|
||||
// String makes the candidateBase printable
|
||||
func (c *candidateBase) String() string {
|
||||
return fmt.Sprintf("%s %s %s%s (resolved: %v)", c.NetworkType(), c.Type(), net.JoinHostPort(c.Address(), strconv.Itoa(c.Port())), c.relatedAddress, c.resolvedAddr)
|
||||
@@ -496,10 +503,6 @@ func (c *candidateBase) Marshal() string {
|
||||
c.Port(),
|
||||
c.Type())
|
||||
|
||||
if c.tcpType != TCPTypeUnspecified {
|
||||
val += fmt.Sprintf(" tcptype %s", c.tcpType.String())
|
||||
}
|
||||
|
||||
if r := c.RelatedAddress(); r != nil && r.Address != "" && r.Port != 0 {
|
||||
val = fmt.Sprintf("%s raddr %s rport %d",
|
||||
val,
|
||||
@@ -507,92 +510,468 @@ func (c *candidateBase) Marshal() string {
|
||||
r.Port)
|
||||
}
|
||||
|
||||
extensions := c.marshalExtensions()
|
||||
|
||||
if extensions != "" {
|
||||
val = fmt.Sprintf("%s %s", val, extensions)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
// UnmarshalCandidate creates a Candidate from its string representation
|
||||
func UnmarshalCandidate(raw string) (Candidate, error) {
|
||||
split := strings.Fields(raw)
|
||||
// Foundation not specified: not RFC 8445 compliant but seen in the wild
|
||||
if len(raw) != 0 && raw[0] == ' ' {
|
||||
split = append([]string{" "}, split...)
|
||||
}
|
||||
if len(split) < 8 {
|
||||
return nil, fmt.Errorf("%w (%d)", errAttributeTooShortICECandidate, len(split))
|
||||
// CandidateExtension represents a single candidate extension
|
||||
// as defined in https://tools.ietf.org/html/rfc5245#section-15.1
|
||||
// .
|
||||
type CandidateExtension struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (c *candidateBase) Extensions() []CandidateExtension {
|
||||
// IF Extensions were not parsed using UnmarshalCandidate
|
||||
// For backwards compatibility when the TCPType is set manually
|
||||
if len(c.extensions) == 0 && c.TCPType() != TCPTypeUnspecified {
|
||||
return []CandidateExtension{{
|
||||
Key: "tcptype",
|
||||
Value: c.TCPType().String(),
|
||||
}}
|
||||
}
|
||||
|
||||
// Foundation
|
||||
foundation := split[0]
|
||||
extensions := make([]CandidateExtension, len(c.extensions))
|
||||
copy(extensions, c.extensions)
|
||||
|
||||
// Component
|
||||
rawComponent, err := strconv.ParseUint(split[1], 10, 16)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", errParseComponent, err) //nolint:errorlint
|
||||
}
|
||||
component := uint16(rawComponent)
|
||||
return extensions
|
||||
}
|
||||
|
||||
// Protocol
|
||||
protocol := split[2]
|
||||
// Get returns the value of the given key if it exists.
|
||||
func (c *candidateBase) GetExtension(key string) (CandidateExtension, bool) {
|
||||
extension := CandidateExtension{Key: key}
|
||||
|
||||
// Priority
|
||||
priorityRaw, err := strconv.ParseUint(split[3], 10, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", errParsePriority, err) //nolint:errorlint
|
||||
}
|
||||
priority := uint32(priorityRaw)
|
||||
for i := range c.extensions {
|
||||
if c.extensions[i].Key == key {
|
||||
extension.Value = c.extensions[i].Value
|
||||
|
||||
// Address
|
||||
address := removeZoneIDFromAddress(split[4])
|
||||
|
||||
// Port
|
||||
rawPort, err := strconv.ParseUint(split[5], 10, 16)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", errParsePort, err) //nolint:errorlint
|
||||
}
|
||||
port := int(rawPort)
|
||||
typ := split[7]
|
||||
|
||||
relatedAddress := ""
|
||||
relatedPort := 0
|
||||
tcpType := TCPTypeUnspecified
|
||||
|
||||
if len(split) > 8 {
|
||||
split = split[8:]
|
||||
|
||||
if split[0] == "raddr" {
|
||||
if len(split) < 4 {
|
||||
return nil, fmt.Errorf("%w: incorrect length", errParseRelatedAddr)
|
||||
}
|
||||
|
||||
// RelatedAddress
|
||||
relatedAddress = split[1]
|
||||
|
||||
// RelatedPort
|
||||
rawRelatedPort, parseErr := strconv.ParseUint(split[3], 10, 16)
|
||||
if parseErr != nil {
|
||||
return nil, fmt.Errorf("%w: %v", errParsePort, parseErr) //nolint:errorlint
|
||||
}
|
||||
relatedPort = int(rawRelatedPort)
|
||||
} else if split[0] == "tcptype" {
|
||||
if len(split) < 2 {
|
||||
return nil, fmt.Errorf("%w: incorrect length", errParseTCPType)
|
||||
}
|
||||
|
||||
tcpType = NewTCPType(split[1])
|
||||
return extension, true
|
||||
}
|
||||
}
|
||||
|
||||
switch typ {
|
||||
case "host":
|
||||
return NewCandidateHost(&CandidateHostConfig{"", protocol, address, port, component, priority, foundation, tcpType, false})
|
||||
case "srflx":
|
||||
return NewCandidateServerReflexive(&CandidateServerReflexiveConfig{"", protocol, address, port, component, priority, foundation, relatedAddress, relatedPort})
|
||||
case "prflx":
|
||||
return NewCandidatePeerReflexive(&CandidatePeerReflexiveConfig{"", protocol, address, port, component, priority, foundation, relatedAddress, relatedPort})
|
||||
case "relay":
|
||||
return NewCandidateRelay(&CandidateRelayConfig{"", protocol, address, port, component, priority, foundation, relatedAddress, relatedPort, "", nil})
|
||||
default:
|
||||
// TCPType was manually set.
|
||||
if key == "tcptype" && c.TCPType() != TCPTypeUnspecified {
|
||||
extension.Value = c.TCPType().String()
|
||||
|
||||
return extension, true
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w (%s)", ErrUnknownCandidateTyp, typ)
|
||||
return extension, false
|
||||
}
|
||||
|
||||
// marshalExtensions returns the string representation of the candidate extensions.
|
||||
func (c *candidateBase) marshalExtensions() string {
|
||||
value := ""
|
||||
exts := c.Extensions()
|
||||
|
||||
for i := range exts {
|
||||
if value != "" {
|
||||
value += " "
|
||||
}
|
||||
|
||||
value += exts[i].Key + " " + exts[i].Value
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// Equal returns true if the candidate extensions are equal.
|
||||
func (c *candidateBase) extensionsEqual(other []CandidateExtension) bool {
|
||||
freq1 := make(map[CandidateExtension]int)
|
||||
freq2 := make(map[CandidateExtension]int)
|
||||
|
||||
if len(c.extensions) != len(other) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(c.extensions) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(c.extensions) == 1 {
|
||||
return c.extensions[0] == other[0]
|
||||
}
|
||||
|
||||
for i := range c.extensions {
|
||||
freq1[c.extensions[i]]++
|
||||
freq2[other[i]]++
|
||||
}
|
||||
|
||||
for k, v := range freq1 {
|
||||
if freq2[k] != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *candidateBase) setExtensions(extensions []CandidateExtension) {
|
||||
c.extensions = extensions
|
||||
}
|
||||
|
||||
// UnmarshalCandidate Parses a candidate from a string
|
||||
// https://datatracker.ietf.org/doc/html/rfc5245#section-15.1
|
||||
func UnmarshalCandidate(raw string) (Candidate, error) {
|
||||
// rfc5245
|
||||
|
||||
pos := 0
|
||||
|
||||
// foundation ( 1*32ice-char ) But we allow for empty foundation,
|
||||
foundation, pos, err := readCandidateCharToken(raw, pos, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v in %s", errParseFoundation, err, raw) //nolint:errorlint // we wrap the error
|
||||
}
|
||||
|
||||
// Empty foundation, not RFC 8445 compliant but seen in the wild
|
||||
if foundation == "" {
|
||||
foundation = " "
|
||||
}
|
||||
|
||||
if pos >= len(raw) {
|
||||
return nil, fmt.Errorf("%w: expected component in %s", errAttributeTooShortICECandidate, raw)
|
||||
}
|
||||
|
||||
// component-id ( 1*5DIGIT )
|
||||
component, pos, err := readCandidateDigitToken(raw, pos, 5)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v in %s", errParseComponent, err, raw) //nolint:errorlint // we wrap the error
|
||||
}
|
||||
|
||||
if pos >= len(raw) {
|
||||
return nil, fmt.Errorf("%w: expected transport in %s", errAttributeTooShortICECandidate, raw)
|
||||
}
|
||||
|
||||
// transport ( "UDP" / transport-extension ; from RFC 3261 ) SP
|
||||
protocol, pos := readCandidateStringToken(raw, pos)
|
||||
|
||||
if pos >= len(raw) {
|
||||
return nil, fmt.Errorf("%w: expected priority in %s", errAttributeTooShortICECandidate, raw)
|
||||
}
|
||||
|
||||
// priority ( 1*10DIGIT ) SP
|
||||
priority, pos, err := readCandidateDigitToken(raw, pos, 10)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v in %s", errParsePriority, err, raw) //nolint:errorlint // we wrap the error
|
||||
}
|
||||
|
||||
if pos >= len(raw) {
|
||||
return nil, fmt.Errorf("%w: expected address in %s", errAttributeTooShortICECandidate, raw)
|
||||
}
|
||||
|
||||
// connection-address SP ;from RFC 4566
|
||||
address, pos := readCandidateStringToken(raw, pos)
|
||||
|
||||
// Remove IPv6 ZoneID: https://github.com/pion/ice/pull/704
|
||||
address = removeZoneIDFromAddress(address)
|
||||
|
||||
if pos >= len(raw) {
|
||||
return nil, fmt.Errorf("%w: expected port in %s", errAttributeTooShortICECandidate, raw)
|
||||
}
|
||||
|
||||
// port from RFC 4566
|
||||
port, pos, err := readCandidatePort(raw, pos)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v in %s", errParsePort, err, raw) //nolint:errorlint // we wrap the error
|
||||
}
|
||||
|
||||
// "typ" SP
|
||||
typeKey, pos := readCandidateStringToken(raw, pos)
|
||||
if typeKey != "typ" {
|
||||
return nil, fmt.Errorf("%w (%s)", ErrUnknownCandidateTyp, typeKey)
|
||||
}
|
||||
|
||||
if pos >= len(raw) {
|
||||
return nil, fmt.Errorf("%w: expected candidate type in %s", errAttributeTooShortICECandidate, raw)
|
||||
}
|
||||
|
||||
// SP cand-type ("host" / "srflx" / "prflx" / "relay")
|
||||
typ, pos := readCandidateStringToken(raw, pos)
|
||||
|
||||
raddr, rport, pos, err := tryReadRelativeAddrs(raw, pos)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tcpType := TCPTypeUnspecified
|
||||
var extensions []CandidateExtension
|
||||
var tcpTypeRaw string
|
||||
|
||||
if pos < len(raw) {
|
||||
extensions, tcpTypeRaw, err = unmarshalCandidateExtensions(raw[pos:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", errParseExtension, err) //nolint:errorlint // we wrap the error
|
||||
}
|
||||
|
||||
if tcpTypeRaw != "" {
|
||||
tcpType = NewTCPType(tcpTypeRaw)
|
||||
if tcpType == TCPTypeUnspecified {
|
||||
return nil, fmt.Errorf("%w: invalid or unsupported TCPtype %s", errParseTCPType, tcpTypeRaw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this code is ugly because we can't break backwards compatibility
|
||||
// with the old way of parsing candidates
|
||||
switch typ {
|
||||
case "host":
|
||||
candidate, err := NewCandidateHost(&CandidateHostConfig{
|
||||
"",
|
||||
protocol,
|
||||
address,
|
||||
port,
|
||||
uint16(component), //nolint:gosec // G115 no overflow we read 5 digits
|
||||
uint32(priority), //nolint:gosec // G115 no overflow we read 5 digits
|
||||
foundation,
|
||||
tcpType,
|
||||
false,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
candidate.setExtensions(extensions)
|
||||
|
||||
return candidate, nil
|
||||
case "srflx":
|
||||
candidate, err := NewCandidateServerReflexive(&CandidateServerReflexiveConfig{
|
||||
"",
|
||||
protocol,
|
||||
address,
|
||||
port,
|
||||
uint16(component), //nolint:gosec // G115 no overflow we read 5 digits
|
||||
uint32(priority), //nolint:gosec // G115 no overflow we read 5 digits
|
||||
foundation,
|
||||
raddr,
|
||||
rport,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
candidate.setExtensions(extensions)
|
||||
|
||||
return candidate, nil
|
||||
case "prflx":
|
||||
candidate, err := NewCandidatePeerReflexive(&CandidatePeerReflexiveConfig{
|
||||
"",
|
||||
protocol,
|
||||
address,
|
||||
port,
|
||||
uint16(component), //nolint:gosec // G115 no overflow we read 5 digits
|
||||
uint32(priority), //nolint:gosec // G115 no overflow we read 5 digits
|
||||
foundation,
|
||||
raddr,
|
||||
rport,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
candidate.setExtensions(extensions)
|
||||
|
||||
return candidate, nil
|
||||
case "relay":
|
||||
candidate, err := NewCandidateRelay(&CandidateRelayConfig{
|
||||
"",
|
||||
protocol,
|
||||
address,
|
||||
port,
|
||||
uint16(component), //nolint:gosec // G115 no overflow we read 5 digits
|
||||
uint32(priority), //nolint:gosec // G115 no overflow we read 5 digits
|
||||
foundation,
|
||||
raddr,
|
||||
rport,
|
||||
"",
|
||||
nil,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
candidate.setExtensions(extensions)
|
||||
|
||||
return candidate, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("%w (%s)", ErrUnknownCandidateTyp, typ)
|
||||
}
|
||||
}
|
||||
|
||||
// Read an ice-char token from the raw string
|
||||
// ice-char = ALPHA / DIGIT / "+" / "/"
|
||||
// stop reading when a space is encountered or the end of the string
|
||||
func readCandidateCharToken(raw string, start int, limit int) (string, int, error) {
|
||||
for i, char := range raw[start:] {
|
||||
if char == 0x20 { // SP
|
||||
return raw[start : start+i], start + i + 1, nil
|
||||
}
|
||||
|
||||
if i == limit {
|
||||
return "", 0, fmt.Errorf("token too long: %s expected 1x%d", raw[start:start+i], limit) //nolint: err113 // handled by caller
|
||||
}
|
||||
|
||||
if !(char >= 'A' && char <= 'Z' ||
|
||||
char >= 'a' && char <= 'z' ||
|
||||
char >= '0' && char <= '9' ||
|
||||
char == '+' || char == '/') {
|
||||
return "", 0, fmt.Errorf("invalid ice-char token: %c", char) //nolint: err113 // handled by caller
|
||||
}
|
||||
}
|
||||
|
||||
return raw[start:], len(raw), nil
|
||||
}
|
||||
|
||||
// Read an ice string token from the raw string until a space is encountered
|
||||
// Or the end of the string, we imply that ice string are UTF-8 encoded
|
||||
func readCandidateStringToken(raw string, start int) (string, int) {
|
||||
for i, char := range raw[start:] {
|
||||
if char == 0x20 { // SP
|
||||
return raw[start : start+i], start + i + 1
|
||||
}
|
||||
}
|
||||
|
||||
return raw[start:], len(raw)
|
||||
}
|
||||
|
||||
// Read a digit token from the raw string
|
||||
// stop reading when a space is encountered or the end of the string
|
||||
func readCandidateDigitToken(raw string, start, limit int) (int, int, error) {
|
||||
var val int
|
||||
for i, char := range raw[start:] {
|
||||
if char == 0x20 { // SP
|
||||
return val, start + i + 1, nil
|
||||
}
|
||||
|
||||
if i == limit {
|
||||
return 0, 0, fmt.Errorf("token too long: %s expected 1x%d", raw[start:start+i], limit) //nolint: err113 // handled by caller
|
||||
}
|
||||
|
||||
if !(char >= '0' && char <= '9') {
|
||||
return 0, 0, fmt.Errorf("invalid digit token: %c", char) //nolint: err113 // handled by caller
|
||||
}
|
||||
|
||||
val = val*10 + int(char-'0')
|
||||
}
|
||||
|
||||
return val, len(raw), nil
|
||||
}
|
||||
|
||||
// Read and validate RFC 4566 port from the raw string
|
||||
func readCandidatePort(raw string, start int) (int, int, error) {
|
||||
port, pos, err := readCandidateDigitToken(raw, start, 5)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if port > 65535 {
|
||||
return 0, 0, fmt.Errorf("invalid RFC 4566 port %d", port) //nolint: err113 // handled by caller
|
||||
}
|
||||
|
||||
return port, pos, nil
|
||||
}
|
||||
|
||||
// Read a byte-string token from the raw string
|
||||
// As defined in RFC 4566 1*(%x01-09/%x0B-0C/%x0E-FF) ;any byte except NUL, CR, or LF
|
||||
// we imply that extensions byte-string are UTF-8 encoded
|
||||
func readCandidateByteString(raw string, start int) (string, int, error) {
|
||||
for i, char := range raw[start:] {
|
||||
if char == 0x20 { // SP
|
||||
return raw[start : start+i], start + i + 1, nil
|
||||
}
|
||||
|
||||
// 1*(%x01-09/%x0B-0C/%x0E-FF)
|
||||
if !(char >= 0x01 && char <= 0x09 ||
|
||||
char >= 0x0B && char <= 0x0C ||
|
||||
char >= 0x0E && char <= 0xFF) {
|
||||
return "", 0, fmt.Errorf("invalid byte-string character: %c", char) //nolint: err113 // handled by caller
|
||||
}
|
||||
}
|
||||
|
||||
return raw[start:], len(raw), nil
|
||||
}
|
||||
|
||||
// Read and validate raddr and rport from the raw string
|
||||
// [SP rel-addr] [SP rel-port]
|
||||
// defined in https://datatracker.ietf.org/doc/html/rfc5245#section-15.1
|
||||
// .
|
||||
func tryReadRelativeAddrs(raw string, start int) (raddr string, rport, pos int, err error) {
|
||||
key, pos := readCandidateStringToken(raw, start)
|
||||
|
||||
if key != "raddr" {
|
||||
return "", 0, start, nil
|
||||
}
|
||||
|
||||
if pos >= len(raw) {
|
||||
return "", 0, 0, fmt.Errorf("%w: expected raddr value in %s", errParseRelatedAddr, raw)
|
||||
}
|
||||
|
||||
raddr, pos = readCandidateStringToken(raw, pos)
|
||||
|
||||
if pos >= len(raw) {
|
||||
return "", 0, 0, fmt.Errorf("%w: expected rport in %s", errParseRelatedAddr, raw)
|
||||
}
|
||||
|
||||
key, pos = readCandidateStringToken(raw, pos)
|
||||
if key != "rport" {
|
||||
return "", 0, 0, fmt.Errorf("%w: expected rport in %s", errParseRelatedAddr, raw)
|
||||
}
|
||||
|
||||
if pos >= len(raw) {
|
||||
return "", 0, 0, fmt.Errorf("%w: expected rport value in %s", errParseRelatedAddr, raw)
|
||||
}
|
||||
|
||||
rport, pos, err = readCandidatePort(raw, pos)
|
||||
if err != nil {
|
||||
return "", 0, 0, fmt.Errorf("%w: %v", errParseRelatedAddr, err) //nolint:errorlint // we wrap the error
|
||||
}
|
||||
|
||||
return raddr, rport, pos, nil
|
||||
}
|
||||
|
||||
// UnmarshalCandidateExtensions parses the candidate extensions from the raw string.
|
||||
// *(SP extension-att-name SP extension-att-value)
|
||||
// Where extension-att-name, and extension-att-value are byte-strings
|
||||
// as defined in https://tools.ietf.org/html/rfc5245#section-15.1
|
||||
func unmarshalCandidateExtensions(raw string) (extensions []CandidateExtension, rawTCPTypeRaw string, err error) {
|
||||
extensions = make([]CandidateExtension, 0)
|
||||
|
||||
if raw == "" {
|
||||
return extensions, "", nil
|
||||
}
|
||||
|
||||
if raw[0] == 0x20 { // SP
|
||||
return extensions, "", fmt.Errorf("%w: unexpected space %s", errParseExtension, raw)
|
||||
}
|
||||
|
||||
for i := 0; i < len(raw); {
|
||||
key, next, err := readCandidateByteString(raw, i)
|
||||
if err != nil {
|
||||
return extensions, "", fmt.Errorf("%w: failed to read key %v", errParseExtension, err) //nolint: errorlint // we wrap the error
|
||||
}
|
||||
i = next
|
||||
|
||||
if i >= len(raw) {
|
||||
return extensions, "", fmt.Errorf("%w: missing value for %s in %s", errParseExtension, key, raw)
|
||||
}
|
||||
|
||||
value, next, err := readCandidateByteString(raw, i)
|
||||
if err != nil {
|
||||
return extensions, "", fmt.Errorf("%w: failed to read value %v", errParseExtension, err) //nolint: errorlint // we are wrapping the error
|
||||
}
|
||||
i = next
|
||||
|
||||
if key == "tcptype" {
|
||||
rawTCPTypeRaw = value
|
||||
}
|
||||
|
||||
extensions = append(extensions, CandidateExtension{key, value})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
@@ -274,6 +274,19 @@ func mustCandidateHost(conf *CandidateHostConfig) Candidate {
|
||||
return cand
|
||||
}
|
||||
|
||||
func mustCandidateHostWithExtensions(t *testing.T, conf *CandidateHostConfig, extensions []CandidateExtension) Candidate {
|
||||
t.Helper()
|
||||
|
||||
cand, err := NewCandidateHost(conf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cand.setExtensions(extensions)
|
||||
|
||||
return cand
|
||||
}
|
||||
|
||||
func mustCandidateRelay(conf *CandidateRelayConfig) Candidate {
|
||||
cand, err := NewCandidateRelay(conf)
|
||||
if err != nil {
|
||||
@@ -282,6 +295,19 @@ func mustCandidateRelay(conf *CandidateRelayConfig) Candidate {
|
||||
return cand
|
||||
}
|
||||
|
||||
func mustCandidateRelayWithExtensions(t *testing.T, conf *CandidateRelayConfig, extensions []CandidateExtension) Candidate {
|
||||
t.Helper()
|
||||
|
||||
cand, err := NewCandidateRelay(conf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cand.setExtensions(extensions)
|
||||
|
||||
return cand
|
||||
}
|
||||
|
||||
func mustCandidateServerReflexive(conf *CandidateServerReflexiveConfig) Candidate {
|
||||
cand, err := NewCandidateServerReflexive(conf)
|
||||
if err != nil {
|
||||
@@ -290,6 +316,32 @@ func mustCandidateServerReflexive(conf *CandidateServerReflexiveConfig) Candidat
|
||||
return cand
|
||||
}
|
||||
|
||||
func mustCandidateServerReflexiveWithExtensions(t *testing.T, conf *CandidateServerReflexiveConfig, extensions []CandidateExtension) Candidate {
|
||||
t.Helper()
|
||||
|
||||
cand, err := NewCandidateServerReflexive(conf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cand.setExtensions(extensions)
|
||||
|
||||
return cand
|
||||
}
|
||||
|
||||
func mustCandidatePeerReflexiveWithExtensions(t *testing.T, conf *CandidatePeerReflexiveConfig, extensions []CandidateExtension) Candidate {
|
||||
t.Helper()
|
||||
|
||||
cand, err := NewCandidatePeerReflexive(conf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cand.setExtensions(extensions)
|
||||
|
||||
return cand
|
||||
}
|
||||
|
||||
func TestCandidateMarshal(t *testing.T) {
|
||||
for idx, test := range []struct {
|
||||
candidate Candidate
|
||||
@@ -327,6 +379,25 @@ func TestCandidateMarshal(t *testing.T) {
|
||||
"647372371 1 udp 1694498815 191.228.238.68 53991 typ srflx raddr 192.168.0.274 rport 53991",
|
||||
false,
|
||||
},
|
||||
{
|
||||
mustCandidatePeerReflexiveWithExtensions(
|
||||
t,
|
||||
&CandidatePeerReflexiveConfig{
|
||||
Network: NetworkTypeTCP4.String(),
|
||||
Address: "192.0.2.15",
|
||||
Port: 50000,
|
||||
RelAddr: "10.0.0.1",
|
||||
RelPort: 12345,
|
||||
},
|
||||
[]CandidateExtension{
|
||||
{"generation", "0"},
|
||||
{"network-id", "2"},
|
||||
{"network-cost", "10"},
|
||||
},
|
||||
),
|
||||
"4207374052 1 tcp 1685790463 192.0.2.15 50000 typ prflx raddr 10.0.0.1 rport 12345 generation 0 network-id 2 network-cost 10",
|
||||
false,
|
||||
},
|
||||
{
|
||||
mustCandidateRelay(&CandidateRelayConfig{
|
||||
Network: NetworkTypeUDP4.String(),
|
||||
@@ -368,6 +439,28 @@ func TestCandidateMarshal(t *testing.T) {
|
||||
" 1 udp 500 " + localhostIPStr + " 80 typ host",
|
||||
false,
|
||||
},
|
||||
{
|
||||
mustCandidateHost(&CandidateHostConfig{
|
||||
Network: NetworkTypeUDP4.String(),
|
||||
Address: localhostIPStr,
|
||||
Port: 80,
|
||||
Priority: 500,
|
||||
Foundation: "+/3713fhi",
|
||||
}),
|
||||
"+/3713fhi 1 udp 500 " + localhostIPStr + " 80 typ host",
|
||||
false,
|
||||
},
|
||||
{
|
||||
mustCandidateHost(&CandidateHostConfig{
|
||||
Network: NetworkTypeTCP4.String(),
|
||||
Address: "172.28.142.173",
|
||||
Port: 7686,
|
||||
Priority: 1671430143,
|
||||
Foundation: "+/3713fhi",
|
||||
}),
|
||||
"3359356140 1 tcp 1671430143 172.28.142.173 7686 typ host",
|
||||
false,
|
||||
},
|
||||
|
||||
// Invalid candidates
|
||||
{nil, "", true},
|
||||
@@ -382,11 +475,52 @@ func TestCandidateMarshal(t *testing.T) {
|
||||
{nil, "4207374051 INVALID udp 2130706431 10.0.75.1 INVALID typ host", true},
|
||||
{nil, "4207374051 1 udp 2130706431 10.0.75.1 53634 typ INVALID", true},
|
||||
{nil, "4207374051 1 INVALID 2130706431 10.0.75.1 53634 typ host", true},
|
||||
{nil, "4207374051 1 INVALID 2130706431 10.0.75.1 53634 typ", true},
|
||||
{nil, "4207374051 1 INVALID 2130706431 10.0.75.1 53634", true},
|
||||
{nil, "848194626 1 udp 16777215 50.0.0.^^1 5000 typ relay raddr 192.168.0.1 rport 5001", true},
|
||||
{nil, "4207374052 1 tcp 1685790463 192.0#.2.15 50000 typ prflx raddr 10.0.0.1 rport 12345 rport 5001", true},
|
||||
{nil, "647372371 1 udp 1694498815 191.228.2@338.68 53991 typ srflx raddr 192.168.0.274 rport 53991", true},
|
||||
// invalid foundion; longer than 32 characters
|
||||
{nil, "111111111111111111111111111111111 1 udp 500 " + localhostIPStr + " 80 typ host", true},
|
||||
// Invalid ice-char
|
||||
{nil, "3$3 1 udp 500 " + localhostIPStr + " 80 typ host", true},
|
||||
// invalid component; longer than 5 digits
|
||||
{nil, "4207374051 123456 udp 500 " + localhostIPStr + " 0 typ host", true},
|
||||
// invalid priority; longer than 10 digits
|
||||
{nil, "4207374051 99999 udp 12345678910 " + localhostIPStr + " 99999 typ host", true},
|
||||
// invalid port;
|
||||
{nil, "4207374051 99999 udp 500 " + localhostIPStr + " 65536 typ host", true},
|
||||
{nil, "4207374051 99999 udp 500 " + localhostIPStr + " 999999 typ host", true},
|
||||
{nil, "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr 192.168.0.1 rport 999999", true},
|
||||
|
||||
// bad byte-string in extension value
|
||||
{nil, "750 1 udp 500 fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a 53987 typ host ext valu\nu", true},
|
||||
{nil, "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr 192.168.0.1 rport 654 ext valu\nu", true},
|
||||
{nil, "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr 192.168.0.1 rport 654 ext valu\000e", true},
|
||||
|
||||
// bad byte-string in extension key
|
||||
{nil, "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr 192.168.0.1 rport 654 ext\r value", true},
|
||||
|
||||
// invalid tcptype
|
||||
{nil, "1052353102 1 tcp 2128609279 192.168.0.196 0 typ host tcptype INVALID", true},
|
||||
|
||||
// expect rport after raddr
|
||||
{nil, "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr 192.168.0.1 extension 322", true},
|
||||
{nil, "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr 192.168.0.1 rport", true},
|
||||
{nil, "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr 192.168.0.1", true},
|
||||
{nil, "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr", true},
|
||||
{nil, "4207374051 99999 udp 500 " + localhostIPStr + " 80 typ", true},
|
||||
{nil, "4207374051 99999 udp 500 " + localhostIPStr + " 80", true},
|
||||
{nil, "4207374051 99999 udp 500 " + localhostIPStr, true},
|
||||
{nil, "4207374051 99999 udp 500 ", true},
|
||||
{nil, "4207374051 99999 udp", true},
|
||||
{nil, "4207374051 99999", true},
|
||||
{nil, "4207374051", true},
|
||||
} {
|
||||
t.Run(strconv.Itoa(idx), func(t *testing.T) {
|
||||
actualCandidate, err := UnmarshalCandidate(test.marshaled)
|
||||
if test.expectError {
|
||||
require.Error(t, err)
|
||||
require.Error(t, err, "expected error", test.marshaled)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -466,3 +600,635 @@ func TestMarshalUnmarshalCandidateWithZoneID(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Truef(t, candidate.Equal(candidate2), "%s != %s", candidate.String(), candidate2.String())
|
||||
}
|
||||
|
||||
func TestCandidateExtensionsMarshal(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Extensions []CandidateExtension
|
||||
candidate string
|
||||
}{
|
||||
{
|
||||
[]CandidateExtension{
|
||||
{"generation", "0"},
|
||||
{"ufrag", "QNvE"},
|
||||
{"network-id", "4"},
|
||||
},
|
||||
"1299692247 1 udp 2122134271 fdc8:cc8:c835:e400:343c:feb:32c8:17b9 58240 typ host generation 0 ufrag QNvE network-id 4",
|
||||
},
|
||||
{
|
||||
[]CandidateExtension{
|
||||
{"generation", "1"},
|
||||
{"network-id", "2"},
|
||||
{"network-cost", "50"},
|
||||
},
|
||||
"647372371 1 udp 1694498815 191.228.238.68 53991 typ srflx raddr 192.168.0.274 rport 53991 generation 1 network-id 2 network-cost 50",
|
||||
},
|
||||
{
|
||||
[]CandidateExtension{
|
||||
{"generation", "0"},
|
||||
{"network-id", "2"},
|
||||
{"network-cost", "10"},
|
||||
},
|
||||
"4207374052 1 tcp 1685790463 192.0.2.15 50000 typ prflx raddr 10.0.0.1 rport 12345 generation 0 network-id 2 network-cost 10",
|
||||
},
|
||||
{
|
||||
[]CandidateExtension{
|
||||
{"generation", "0"},
|
||||
{"network-id", "1"},
|
||||
{"network-cost", "20"},
|
||||
{"ufrag", "frag42abcdef"},
|
||||
{"password", "abc123exp123"},
|
||||
},
|
||||
"848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr 192.168.0.1 rport 5001 generation 0 network-id 1 network-cost 20 ufrag frag42abcdef password abc123exp123",
|
||||
},
|
||||
{
|
||||
[]CandidateExtension{
|
||||
{"tcptype", "active"},
|
||||
{"generation", "0"},
|
||||
},
|
||||
"1052353102 1 tcp 2128609279 192.168.0.196 0 typ host tcptype active generation 0",
|
||||
},
|
||||
{
|
||||
[]CandidateExtension{
|
||||
{"tcptype", "active"},
|
||||
{"generation", "0"},
|
||||
},
|
||||
"1052353102 1 tcp 2128609279 192.168.0.196 0 typ host tcptype active generation 0",
|
||||
},
|
||||
{
|
||||
[]CandidateExtension{},
|
||||
"1052353102 1 tcp 2128609279 192.168.0.196 0 typ host",
|
||||
},
|
||||
{
|
||||
[]CandidateExtension{},
|
||||
"1052353102 1 tcp 2128609279 192.168.0.196 0 typ host",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
candidate, err := UnmarshalCandidate(tc.candidate)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.Extensions, candidate.Extensions(), "Extensions should be equal", tc.candidate)
|
||||
|
||||
valueStr := candidate.Marshal()
|
||||
candidate2, err := UnmarshalCandidate(valueStr)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.Extensions, candidate2.Extensions(), "Marshal() should preserve extensions")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandidateExtensionsDeepEqual(t *testing.T) {
|
||||
noExt, err := UnmarshalCandidate("750 0 udp 500 fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a 53987 typ host")
|
||||
require.NoError(t, err)
|
||||
|
||||
generation := "0"
|
||||
ufrag := "QNvE"
|
||||
networkID := "4"
|
||||
|
||||
extensions := []CandidateExtension{
|
||||
{"generation", generation},
|
||||
{"ufrag", ufrag},
|
||||
{"network-id", networkID},
|
||||
}
|
||||
|
||||
candidate, err := UnmarshalCandidate(
|
||||
"750 0 udp 500 fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a 53987 typ host generation " +
|
||||
generation + " ufrag " + ufrag + " network-id " + networkID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
a Candidate
|
||||
b Candidate
|
||||
equal bool
|
||||
}{
|
||||
{
|
||||
mustCandidateHost(&CandidateHostConfig{
|
||||
Network: NetworkTypeUDP4.String(),
|
||||
Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a",
|
||||
Port: 53987,
|
||||
Priority: 500,
|
||||
Foundation: "750",
|
||||
}),
|
||||
noExt,
|
||||
true,
|
||||
},
|
||||
{
|
||||
mustCandidateHostWithExtensions(
|
||||
t,
|
||||
&CandidateHostConfig{
|
||||
Network: NetworkTypeUDP4.String(),
|
||||
Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a",
|
||||
Port: 53987,
|
||||
Priority: 500,
|
||||
Foundation: "750",
|
||||
},
|
||||
[]CandidateExtension{},
|
||||
),
|
||||
noExt,
|
||||
true,
|
||||
},
|
||||
{
|
||||
mustCandidateHostWithExtensions(
|
||||
t,
|
||||
&CandidateHostConfig{
|
||||
Network: NetworkTypeUDP4.String(),
|
||||
Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a",
|
||||
Port: 53987,
|
||||
Priority: 500,
|
||||
Foundation: "750",
|
||||
},
|
||||
extensions,
|
||||
),
|
||||
candidate,
|
||||
true,
|
||||
},
|
||||
{
|
||||
mustCandidateRelayWithExtensions(
|
||||
t,
|
||||
&CandidateRelayConfig{
|
||||
Network: NetworkTypeUDP4.String(),
|
||||
Address: "10.0.0.10",
|
||||
Port: 5000,
|
||||
RelAddr: "10.0.0.2",
|
||||
RelPort: 5001,
|
||||
},
|
||||
[]CandidateExtension{
|
||||
{"generation", "0"},
|
||||
{"network-id", "1"},
|
||||
},
|
||||
),
|
||||
mustCandidateRelayWithExtensions(
|
||||
t,
|
||||
&CandidateRelayConfig{
|
||||
Network: NetworkTypeUDP4.String(),
|
||||
Address: "10.0.0.10",
|
||||
Port: 5000,
|
||||
RelAddr: "10.0.0.2",
|
||||
RelPort: 5001,
|
||||
},
|
||||
[]CandidateExtension{
|
||||
{"network-id", "1"},
|
||||
{"generation", "0"},
|
||||
},
|
||||
),
|
||||
true,
|
||||
},
|
||||
{
|
||||
mustCandidatePeerReflexiveWithExtensions(
|
||||
t,
|
||||
&CandidatePeerReflexiveConfig{
|
||||
Network: NetworkTypeTCP4.String(),
|
||||
Address: "192.0.2.15",
|
||||
Port: 50000,
|
||||
RelAddr: "10.0.0.1",
|
||||
RelPort: 12345,
|
||||
},
|
||||
[]CandidateExtension{
|
||||
{"generation", "0"},
|
||||
{"network-id", "2"},
|
||||
{"network-cost", "10"},
|
||||
},
|
||||
),
|
||||
mustCandidatePeerReflexiveWithExtensions(
|
||||
t,
|
||||
&CandidatePeerReflexiveConfig{
|
||||
Network: NetworkTypeTCP4.String(),
|
||||
Address: "192.0.2.15",
|
||||
Port: 50000,
|
||||
RelAddr: "10.0.0.1",
|
||||
RelPort: 12345,
|
||||
},
|
||||
[]CandidateExtension{
|
||||
{"generation", "0"},
|
||||
{"network-id", "2"},
|
||||
{"network-cost", "10"},
|
||||
},
|
||||
),
|
||||
true,
|
||||
},
|
||||
{
|
||||
mustCandidateServerReflexiveWithExtensions(
|
||||
t,
|
||||
&CandidateServerReflexiveConfig{
|
||||
Network: NetworkTypeUDP4.String(),
|
||||
Address: "191.228.238.68",
|
||||
Port: 53991,
|
||||
RelAddr: "192.168.0.274",
|
||||
RelPort: 53991,
|
||||
},
|
||||
[]CandidateExtension{
|
||||
{"generation", "0"},
|
||||
{"network-id", "2"},
|
||||
{"network-cost", "10"},
|
||||
},
|
||||
),
|
||||
mustCandidateServerReflexiveWithExtensions(
|
||||
t,
|
||||
&CandidateServerReflexiveConfig{
|
||||
Network: NetworkTypeUDP4.String(),
|
||||
Address: "191.228.238.68",
|
||||
Port: 53991,
|
||||
RelAddr: "192.168.0.274",
|
||||
RelPort: 53991,
|
||||
},
|
||||
[]CandidateExtension{
|
||||
{"generation", "0"},
|
||||
{"network-id", "2"},
|
||||
{"network-cost", "10"},
|
||||
},
|
||||
),
|
||||
true,
|
||||
},
|
||||
{
|
||||
mustCandidateHostWithExtensions(
|
||||
t,
|
||||
&CandidateHostConfig{
|
||||
Network: NetworkTypeUDP4.String(),
|
||||
Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a",
|
||||
Port: 53987,
|
||||
Priority: 500,
|
||||
Foundation: "750",
|
||||
},
|
||||
[]CandidateExtension{
|
||||
{"generation", "5"},
|
||||
{"ufrag", ufrag},
|
||||
{"network-id", networkID},
|
||||
},
|
||||
),
|
||||
candidate,
|
||||
false,
|
||||
},
|
||||
{
|
||||
mustCandidateHostWithExtensions(
|
||||
t,
|
||||
&CandidateHostConfig{
|
||||
Network: NetworkTypeTCP4.String(),
|
||||
Address: "192.168.0.196",
|
||||
Port: 0,
|
||||
Priority: 2128609279,
|
||||
Foundation: "1052353102",
|
||||
TCPType: TCPTypeActive,
|
||||
},
|
||||
[]CandidateExtension{
|
||||
{"tcptype", TCPTypeActive.String()},
|
||||
{"generation", "0"},
|
||||
},
|
||||
),
|
||||
mustCandidateHostWithExtensions(
|
||||
t,
|
||||
&CandidateHostConfig{
|
||||
Network: NetworkTypeTCP4.String(),
|
||||
Address: "192.168.0.197",
|
||||
Port: 0,
|
||||
Priority: 2128609279,
|
||||
Foundation: "1052353102",
|
||||
TCPType: TCPTypeActive,
|
||||
},
|
||||
[]CandidateExtension{
|
||||
{"tcptype", TCPTypeActive.String()},
|
||||
{"generation", "0"},
|
||||
},
|
||||
),
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
require.Equal(t, tc.a.DeepEqual(tc.b), tc.equal, "a: %s, b: %s", tc.a.Marshal(), tc.b.Marshal())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalCandidateExtensions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
value string
|
||||
expected []CandidateExtension
|
||||
fail bool
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
value: "",
|
||||
expected: []CandidateExtension{},
|
||||
fail: false,
|
||||
},
|
||||
{
|
||||
name: "valid extension string",
|
||||
value: "a b c d",
|
||||
expected: []CandidateExtension{{"a", "b"}, {"c", "d"}},
|
||||
fail: false,
|
||||
},
|
||||
{
|
||||
name: "valid extension string",
|
||||
value: "a b empty c d",
|
||||
expected: []CandidateExtension{
|
||||
{"a", "b"},
|
||||
{"empty", ""},
|
||||
{"c", "d"},
|
||||
},
|
||||
fail: false,
|
||||
},
|
||||
{
|
||||
name: "invalid extension string",
|
||||
value: "invalid",
|
||||
expected: []CandidateExtension{},
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
name: "invalid extension",
|
||||
value: " a b",
|
||||
expected: []CandidateExtension{{"a", "b"}, {"c", "d"}},
|
||||
fail: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
actual, _, err := unmarshalCandidateExtensions(testCase.value)
|
||||
if testCase.fail {
|
||||
req.Error(err)
|
||||
} else {
|
||||
req.NoError(err)
|
||||
req.EqualValuesf(
|
||||
testCase.expected,
|
||||
actual,
|
||||
"UnmarshalCandidateExtensions() did not return the expected value %v",
|
||||
testCase.value,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandidateGetExtension(t *testing.T) {
|
||||
t.Run("Get extension", func(t *testing.T) {
|
||||
extensions := []CandidateExtension{
|
||||
{"a", "b"},
|
||||
{"c", "d"},
|
||||
}
|
||||
candidate, err := NewCandidateHost(&CandidateHostConfig{
|
||||
Network: NetworkTypeUDP4.String(),
|
||||
Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a",
|
||||
Port: 53987,
|
||||
Priority: 500,
|
||||
Foundation: "750",
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
candidate.setExtensions(extensions)
|
||||
|
||||
value, ok := candidate.GetExtension("c")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "c", value.Key)
|
||||
require.Equal(t, "d", value.Value)
|
||||
|
||||
value, ok = candidate.GetExtension("a")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "a", value.Key)
|
||||
require.Equal(t, "b", value.Value)
|
||||
|
||||
value, ok = candidate.GetExtension("b")
|
||||
require.False(t, ok)
|
||||
require.Equal(t, "b", value.Key)
|
||||
require.Equal(t, "", value.Value)
|
||||
})
|
||||
|
||||
// This is undefined behavior in the spec; extension-att-name is not unique
|
||||
// but it implied that it's unique in the implementation
|
||||
t.Run("Extension with multiple values", func(t *testing.T) {
|
||||
extensions := []CandidateExtension{
|
||||
{"a", "1"},
|
||||
{"a", "2"},
|
||||
}
|
||||
|
||||
candidate, err := NewCandidateHost(&CandidateHostConfig{
|
||||
Network: NetworkTypeUDP4.String(),
|
||||
Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a",
|
||||
Port: 53987,
|
||||
Priority: 500,
|
||||
Foundation: "750",
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
candidate.setExtensions(extensions)
|
||||
|
||||
value, ok := candidate.GetExtension("a")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "a", value.Key)
|
||||
require.Equal(t, "1", value.Value)
|
||||
})
|
||||
|
||||
t.Run("TCPType extension", func(t *testing.T) {
|
||||
extensions := []CandidateExtension{
|
||||
{"tcptype", "passive"},
|
||||
}
|
||||
|
||||
candidate, err := NewCandidateHost(&CandidateHostConfig{
|
||||
Network: NetworkTypeTCP4.String(),
|
||||
Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a",
|
||||
Port: 53987,
|
||||
Priority: 500,
|
||||
Foundation: "750",
|
||||
TCPType: TCPTypeActive,
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tcpType, ok := candidate.GetExtension("tcptype")
|
||||
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "tcptype", tcpType.Key)
|
||||
require.Equal(t, TCPTypeActive.String(), tcpType.Value)
|
||||
|
||||
candidate.setExtensions(extensions)
|
||||
|
||||
tcpType, ok = candidate.GetExtension("tcptype")
|
||||
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "tcptype", tcpType.Key)
|
||||
require.Equal(t, "passive", tcpType.Value)
|
||||
|
||||
candidate2, err := NewCandidateHost(&CandidateHostConfig{
|
||||
Network: NetworkTypeTCP4.String(),
|
||||
Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a",
|
||||
Port: 53987,
|
||||
Priority: 500,
|
||||
Foundation: "750",
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tcpType, ok = candidate2.GetExtension("tcptype")
|
||||
|
||||
require.False(t, ok)
|
||||
require.Equal(t, "tcptype", tcpType.Key)
|
||||
require.Equal(t, "", tcpType.Value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBaseCandidateMarshalExtensions(t *testing.T) {
|
||||
t.Run("Marshal extension", func(t *testing.T) {
|
||||
extensions := []CandidateExtension{
|
||||
{"generation", "0"},
|
||||
{"ValuE", "KeE"},
|
||||
{"empty", ""},
|
||||
{"another", "value"},
|
||||
}
|
||||
candidate, err := NewCandidateHost(&CandidateHostConfig{
|
||||
Network: NetworkTypeUDP4.String(),
|
||||
Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a",
|
||||
Port: 53987,
|
||||
Priority: 500,
|
||||
Foundation: "750",
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
candidate.setExtensions(extensions)
|
||||
|
||||
value := candidate.marshalExtensions()
|
||||
require.Equal(t, "generation 0 ValuE KeE empty another value", value)
|
||||
})
|
||||
|
||||
t.Run("Marshal Empty", func(t *testing.T) {
|
||||
candidate, err := NewCandidateHost(&CandidateHostConfig{
|
||||
Network: NetworkTypeUDP4.String(),
|
||||
Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a",
|
||||
Port: 53987,
|
||||
Priority: 500,
|
||||
Foundation: "750",
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
value := candidate.marshalExtensions()
|
||||
require.Equal(t, "", value)
|
||||
})
|
||||
|
||||
t.Run("Marshal TCPType no extension", func(t *testing.T) {
|
||||
candidate, err := NewCandidateHost(&CandidateHostConfig{
|
||||
Network: NetworkTypeUDP4.String(),
|
||||
Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a",
|
||||
Port: 53987,
|
||||
Priority: 500,
|
||||
Foundation: "750",
|
||||
TCPType: TCPTypeActive,
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
value := candidate.marshalExtensions()
|
||||
require.Equal(t, "tcptype active", value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBaseCandidateExtensionsEqual(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
extensions1 []CandidateExtension
|
||||
extensions2 []CandidateExtension
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Empty extensions",
|
||||
extensions1: []CandidateExtension{},
|
||||
extensions2: []CandidateExtension{},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Single value extensions",
|
||||
extensions1: []CandidateExtension{{"a", "b"}},
|
||||
extensions2: []CandidateExtension{{"a", "b"}},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "multiple value extensions",
|
||||
extensions1: []CandidateExtension{
|
||||
{"a", "b"},
|
||||
{"c", "d"},
|
||||
},
|
||||
extensions2: []CandidateExtension{
|
||||
{"a", "b"},
|
||||
{"c", "d"},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "unsorted extensions",
|
||||
extensions1: []CandidateExtension{
|
||||
{"c", "d"},
|
||||
{"a", "b"},
|
||||
},
|
||||
extensions2: []CandidateExtension{
|
||||
{"a", "b"},
|
||||
{"c", "d"},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "different values",
|
||||
extensions1: []CandidateExtension{
|
||||
{"a", "b"},
|
||||
{"c", "d"},
|
||||
},
|
||||
extensions2: []CandidateExtension{
|
||||
{"a", "b"},
|
||||
{"c", "e"},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "different size",
|
||||
extensions1: []CandidateExtension{
|
||||
{"a", "b"},
|
||||
{"c", "d"},
|
||||
},
|
||||
extensions2: []CandidateExtension{
|
||||
{"a", "b"},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "different keys",
|
||||
extensions1: []CandidateExtension{
|
||||
{"a", "b"},
|
||||
{"c", "d"},
|
||||
},
|
||||
extensions2: []CandidateExtension{
|
||||
{"a", "b"},
|
||||
{"e", "d"},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
cand, err := NewCandidateHost(&CandidateHostConfig{
|
||||
Network: NetworkTypeUDP4.String(),
|
||||
Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a",
|
||||
Port: 53987,
|
||||
Priority: 500,
|
||||
Foundation: "750",
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
cand.setExtensions(testCase.extensions1)
|
||||
|
||||
require.Equal(t, testCase.expected, cand.extensionsEqual(testCase.extensions2))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -125,10 +125,12 @@ var (
|
||||
errNotImplemented = errors.New("not implemented yet")
|
||||
errNoUDPMuxAvailable = errors.New("no UDP mux is available")
|
||||
errNoXorAddrMapping = errors.New("no address mapping")
|
||||
errParseFoundation = errors.New("failed to parse foundation")
|
||||
errParseComponent = errors.New("failed to parse component")
|
||||
errParsePort = errors.New("failed to parse port")
|
||||
errParsePriority = errors.New("failed to parse priority")
|
||||
errParseRelatedAddr = errors.New("failed to parse related addresses")
|
||||
errParseExtension = errors.New("failed to parse extension")
|
||||
errParseTCPType = errors.New("failed to parse TCP type")
|
||||
errRead = errors.New("failed to read")
|
||||
errUDPMuxDisabled = errors.New("UDPMux is not enabled")
|
||||
|
Reference in New Issue
Block a user