package jingle import ( "errors" "fmt" "strings" ) func appendTransportAttrs(t *Transport, attrs *[]kv) error { if t == nil { return errors.New("content missing transport") } if t.Ufrag == "" { return errors.New("transport missing ufrag") } if t.Pwd == "" { return errors.New("transport missing pwd") } *attrs = append(*attrs, kv{"ice-ufrag", t.Ufrag}) *attrs = append(*attrs, kv{"ice-pwd", t.Pwd}) var iceOptions []string if t.Trickle != nil { iceOptions = append(iceOptions, "trickle") } if t.Renomination != nil { iceOptions = append(iceOptions, "renomination") } if len(iceOptions) == 0 { iceOptions = defaultICEOptions } *attrs = append(*attrs, kv{"ice-options", strings.Join(iceOptions, " ")}) fp := t.Fingerprint if fp == nil { return errors.New("transport missing DTLS fingerprint") } if fp.Hash == "" || fp.Text == "" { return errors.New("fingerprint missing hash or body") } if strings.ContainsAny(fp.Hash, " \t\r\n") { return fmt.Errorf("fingerprint hash contains whitespace: %q", fp.Hash) } *attrs = append(*attrs, kv{"fingerprint", fp.Hash + " " + fp.Text}) if fp.Setup != "" { *attrs = append(*attrs, kv{"setup", fp.Setup}) } return nil } // falls back to session-level fingerprint/setup when the media block omits // them (matches Conversations SessionDescription) func buildJingleTransport(m *mediaSection, sessAttrs []kv) (*Transport, error) { t := &Transport{ Ufrag: mmFirst(m.attrs, "ice-ufrag", ""), Pwd: mmFirst(m.attrs, "ice-pwd", ""), } fpRaw := mmFirst(m.attrs, "fingerprint", mmFirst(sessAttrs, "fingerprint", "")) setupVal := mmFirst(m.attrs, "setup", mmFirst(sessAttrs, "setup", "")) if fpRaw != "" && setupVal != "" { sp := strings.SplitN(fpRaw, " ", 2) if len(sp) >= 2 { t.Fingerprint = &Fingerprint{ Hash: sp[0], Setup: setupVal, Text: sp[1], } } } if iceOptions := mmFirst(m.attrs, "ice-options", ""); iceOptions != "" { for _, opt := range strings.Fields(iceOptions) { if !wellKnownICEOptions[opt] { continue } switch opt { case "trickle": t.Trickle = &ICEOptionTrickle{} case "renomination": t.Renomination = &ICEOptionRenomination{} } } } for _, val := range mmGetAll(m.attrs, "candidate") { cand, err := candidateFromSDP(val) if err != nil { return nil, err } if cand != nil { t.Candidates = append(t.Candidates, *cand) } } return t, nil } func candidateToSDP(c *Candidate) (string, error) { for _, kv := range []struct{ name, val string }{ {"foundation", c.Foundation}, {"component", c.Component}, {"protocol", c.Protocol}, {"priority", c.Priority}, {"ip", c.IP}, {"port", c.Port}, } { if kv.val == "" { return "", fmt.Errorf("candidate missing %s", kv.name) } } proto := strings.ToLower(c.Protocol) if proto != "udp" { return "", fmt.Errorf("'%s' is not a supported protocol", proto) } var extra []string if c.Type != "" { extra = append(extra, "typ", c.Type) } if c.RelAddr != "" { extra = append(extra, "raddr", c.RelAddr) } if c.RelPort != "" { extra = append(extra, "rport", c.RelPort) } if c.Generation != "" { extra = append(extra, "generation", c.Generation) } line := c.Foundation + " " + c.Component + " " + proto + " " + c.Priority + " " + c.IP + " " + c.Port if len(extra) > 0 { line += " " + strings.Join(extra, " ") } return line, nil } func candidateFromSDP(value string) (*Candidate, error) { parts := strings.Fields(value) if len(parts) < 6 { return nil, nil } cand := &Candidate{ Foundation: parts[0], Component: parts[1], Protocol: strings.ToLower(parts[2]), Priority: parts[3], IP: parts[4], Port: parts[5], } if cand.Protocol != "udp" { return nil, fmt.Errorf("'%s' is not a supported protocol", cand.Protocol) } extra := parts[6:] for i := 0; i+1 < len(extra); i += 2 { k := extra[i] v := extra[i+1] switch k { case "typ": cand.Type = v case "raddr": cand.RelAddr = v case "rport": cand.RelPort = v case "generation": cand.Generation = v } } return cand, nil }