sdp.go (9809B)
1 // Package sdp implements encoding and decoding of 2 // Session Description Protocol formatted data as specified in 3 // RFC 8866. 4 package sdp 5 6 import ( 7 "bufio" 8 "fmt" 9 "io" 10 "net/mail" 11 "net/netip" 12 "net/url" 13 "strconv" 14 "strings" 15 "time" 16 ) 17 18 type Session struct { 19 Origin Origin 20 Name string 21 22 Info string 23 URI *url.URL 24 Email *mail.Address 25 Phone string 26 Connection *ConnInfo 27 Bandwidth *Bandwidth 28 // Time holds the start time and stop time of the Session, at 29 // the first and second index respectively. 30 Time [2]time.Time 31 // Repeat points to a repetition cycle describing for how long 32 // and when the session may reoccur. 33 Repeat *Repeat 34 // Adjustments holds any time adjustments that may occur, for 35 // example daylight savings, throughout the period a repetition 36 // cycle is active. 37 Adjustments []TimeAdjustment 38 Attributes []string 39 Media []Media 40 } 41 42 const NoUsername string = "-" 43 44 // Origin represents the originator of the session as described in RFC 8866 section 5.2. 45 type Origin struct { 46 // Username is a named identity on the originating host. If unset, 47 // the encoded value will be NoUsername. 48 Username string 49 50 // ID is a globally unique identifier for the session. 51 // The recommended value is a timestamp from Now(). 52 ID int 53 54 // Version is a version number of the session. It should be 55 // incremented each time the session is modified. The recommended 56 // value is a timestamp from Now(). 57 Version int 58 59 // Address is the originating address of the session. 60 Address netip.Addr 61 } 62 63 func (o Origin) String() string { 64 ipv := "IP6" 65 if o.Address.Is4() { 66 ipv = "IP4" 67 } 68 return fmt.Sprintf("o=%s %d %d IN %s %s", o.Username, o.ID, o.Version, ipv, o.Address) 69 } 70 71 func ReadSession(rd io.Reader) (*Session, error) { 72 parser := &parser{Scanner: bufio.NewScanner(rd)} 73 if err := parser.parse(); err != nil { 74 return nil, fmt.Errorf("parse session: %w", err) 75 } 76 return &parser.session, nil 77 } 78 79 // cleanPhone returns the phone number in s stripped of "-" and space 80 // characters. Since "+1 617 555-6011" is semantically equal to 81 // "+16175556011", storing the number in the latter form lets us test for 82 // equality more easily. 83 func cleanPhone(s string) string { 84 s = strings.ReplaceAll(s, " ", "") 85 return strings.ReplaceAll(s, "-", "") 86 } 87 88 func parseOrigin(line string) (Origin, error) { 89 fields := strings.Fields(line) 90 if len(fields) != 6 { 91 return Origin{}, fmt.Errorf("need %d fields but only have %d", 6, len(fields)) 92 } 93 o := Origin{Username: fields[0]} 94 var err error 95 o.ID, err = strconv.Atoi(fields[1]) 96 if err != nil { 97 return o, fmt.Errorf("parse session id: %w", err) 98 } 99 o.Version, err = strconv.Atoi(fields[2]) 100 if err != nil { 101 return o, fmt.Errorf("parse version: %w", err) 102 } 103 if fields[3] != "IN" { 104 return o, fmt.Errorf("unknown network class %q", fields[3]) 105 } 106 107 // skip IP4/IP6 in fields[4]; netip handles the IP version for us. 108 addr, err := netip.ParseAddr(fields[5]) 109 if err != nil { 110 return o, fmt.Errorf("parse address: %w", err) 111 } 112 o.Address = addr 113 return o, nil 114 } 115 116 // parseEmail returns the parsed email address from s. 117 // Addresses should be in RFC 5322 form, for instance 118 // "Oliver Lowe <o@olowe.co>" or just "o@olowe.co". 119 // They can also be in the form detailed in the SDP specification, for instance 120 // "Oliver Lowe (o@olowe.co)". 121 func parseEmail(s string) (*mail.Address, error) { 122 // Oliver Lowe (o@olowe.co) to RFC 5322 format 123 // Oliver Lowe <o@olowe.co> 124 s = strings.ReplaceAll(s, "(", "<") 125 s = strings.ReplaceAll(s, ")", ">") 126 return mail.ParseAddress(s) 127 } 128 129 const ( 130 BandwidthConferenceTotal = "CT" 131 BandwidthAppSpecific = "AS" 132 ) 133 134 type Bandwidth struct { 135 // Type describes the value of Bitrate, usually one of 136 // BandwidthConferenceTotal or BandwidthAppSpecific. 137 Type string 138 // Bitrate is the measure of bits per second. 139 Bitrate int 140 } 141 142 func (b Bandwidth) String() string { 143 // need kilobits per second as per section 5.8. 144 return fmt.Sprintf("b=%s:%d", b.Type, b.Bitrate/1e3) 145 } 146 147 func parseBandwidth(s string) (Bandwidth, error) { 148 t, b, ok := strings.Cut(s, ":") 149 if !ok { 150 return Bandwidth{}, fmt.Errorf("missing %s separator", ":") 151 } 152 if t == "" { 153 return Bandwidth{}, fmt.Errorf("missing bandwidth type") 154 } 155 kbps, err := strconv.Atoi(b) 156 if err != nil { 157 return Bandwidth{}, fmt.Errorf("parse bitrate: %w", err) 158 } 159 // convert to bits per second 160 return Bandwidth{t, kbps * 1e3}, nil 161 } 162 163 // Media represents a media description. 164 type Media struct { 165 Type MediaType 166 Port int // IP port 167 PortCount int // count of subsequent ports from Port 168 Transport TransportProto 169 // Format describes the media format. Interpretation of the 170 // entries depends on the value of Transport. For example, if 171 // Transport is ProtoRTP, Format contains RTP payload type 172 // numbers. For more, see the <fmt> description in section 5.14 173 // of RFC 8866. 174 Format []string 175 176 // Optional fields 177 Title string 178 Connection *ConnInfo 179 Bandwidth *Bandwidth 180 Attributes []string 181 } 182 183 func (m Media) String() string { 184 buf := &strings.Builder{} 185 if m.PortCount == 0 { 186 fmt.Fprintf(buf, "m=%s %d %s %s\n", m.Type, m.Port, m.Transport, strings.Join(m.Format, " ")) 187 } else { 188 fmt.Fprintf(buf, "m=%s %d/%d %s %s\n", m.Type, m.Port, m.PortCount, m.Transport, strings.Join(m.Format, " ")) 189 } 190 191 if m.Title != "" { 192 fmt.Fprintf(buf, "i=%s\n", m.Title) 193 } 194 if m.Connection != nil { 195 fmt.Fprintln(buf, m.Connection) 196 } 197 if m.Bandwidth != nil { 198 fmt.Fprintln(buf, m.Bandwidth) 199 } 200 if m.Attributes != nil { 201 fmt.Fprintf(buf, "a=%s", strings.Join(m.Attributes, " ")) 202 } 203 return strings.TrimSpace(buf.String()) 204 } 205 206 type MediaType uint8 207 208 const ( 209 MediaTypeAudio MediaType = iota 210 MediaTypeVideo 211 MediaTypeText 212 MediaTypeApplication 213 MediaTypeMessage 214 MediaTypeImage 215 ) 216 217 func (t MediaType) String() string { 218 switch t { 219 case MediaTypeAudio: 220 return "audio" 221 case MediaTypeVideo: 222 return "video" 223 case MediaTypeText: 224 return "text" 225 case MediaTypeApplication: 226 return "application" 227 case MediaTypeMessage: 228 return "message" 229 case MediaTypeImage: 230 return "image" 231 } 232 return "unknown" 233 } 234 235 func parseMediaType(s string) (MediaType, error) { 236 switch s { 237 case MediaTypeAudio.String(): 238 return MediaTypeAudio, nil 239 case MediaTypeVideo.String(): 240 return MediaTypeVideo, nil 241 case MediaTypeText.String(): 242 return MediaTypeText, nil 243 case MediaTypeApplication.String(): 244 return MediaTypeApplication, nil 245 case MediaTypeMessage.String(): 246 return MediaTypeMessage, nil 247 case MediaTypeImage.String(): 248 return MediaTypeImage, nil 249 } 250 return 0, fmt.Errorf("unknown media type %s", s) 251 } 252 253 type TransportProto uint8 254 255 const ( 256 ProtoUDP TransportProto = iota 257 ProtoRTP 258 ProtoRTPSecure 259 ProtoRTPSecureFeedback 260 ) 261 262 func (tp TransportProto) String() string { 263 switch tp { 264 case ProtoUDP: 265 return "udp" 266 case ProtoRTP: 267 return "RTP/AVP" 268 case ProtoRTPSecure: 269 return "RTP/SAVP" 270 case ProtoRTPSecureFeedback: 271 return "RTP/SAVPF" 272 } 273 return "unknown" 274 } 275 276 func parseMedia(s string) (Media, error) { 277 fields := strings.Fields(s) 278 if len(fields) < 4 { 279 return Media{}, fmt.Errorf("found %d fields, need at least %d", len(fields), 4) 280 } 281 282 mtyp, err := parseMediaType(fields[0]) 283 if err != nil { 284 return Media{}, fmt.Errorf("media type: %w", err) 285 } 286 m := Media{Type: mtyp} 287 288 p, n, found := strings.Cut(fields[1], "/") 289 m.Port, err = strconv.Atoi(p) 290 if err != nil { 291 return Media{}, fmt.Errorf("parse port: %w", err) 292 } 293 if found { 294 m.PortCount, err = strconv.Atoi(n) 295 if err != nil { 296 return Media{}, fmt.Errorf("parse port count: %w", err) 297 } 298 } 299 300 switch fields[2] { 301 case ProtoUDP.String(): 302 m.Transport = ProtoUDP 303 case ProtoRTP.String(): 304 m.Transport = ProtoRTP 305 case ProtoRTPSecure.String(): 306 m.Transport = ProtoRTPSecure 307 case ProtoRTPSecureFeedback.String(): 308 m.Transport = ProtoRTPSecureFeedback 309 default: 310 return Media{}, fmt.Errorf("unknown protocol %s", fields[2]) 311 } 312 313 m.Format = fields[3:] 314 return m, nil 315 } 316 317 // ConnInfo represents connection information. 318 type ConnInfo struct { 319 Address netip.Addr 320 // TTL is the time-to-live of IPv4 multicast packets. 321 TTL uint8 322 // Count is the number of subsequent IP addresses after 323 // Address used in the session. 324 Count int 325 } 326 327 func (c *ConnInfo) String() string { 328 ipv := "IP6" 329 if c.Address.Is4() { 330 ipv = "IP4" 331 } 332 s := fmt.Sprintf("c=%s %s %s", "IN", ipv, c.Address) 333 if c.Address.Is4() && c.TTL > 0 { 334 s += fmt.Sprintf("/%d", c.TTL) 335 } 336 if c.Count > 0 { 337 s += fmt.Sprintf("/%d", c.Count) 338 } 339 return s 340 } 341 342 func parseConnInfo(s string) (ConnInfo, error) { 343 fields := strings.Fields(s) 344 if len(fields) != 3 { 345 return ConnInfo{}, fmt.Errorf("expected %d fields, got %d", 3, len(fields)) 346 } 347 if fields[0] != "IN" { 348 return ConnInfo{}, fmt.Errorf("unsupported class %q, expected IN", fields[0]) 349 } 350 351 var conn ConnInfo 352 if fields[1] != "IP4" && fields[1] != "IP6" { 353 return conn, fmt.Errorf("unsupported network type %s", fields[2]) 354 } 355 addr := strings.Split(fields[2], "/") 356 var err error 357 conn.Address, err = netip.ParseAddr(addr[0]) 358 if err != nil { 359 return conn, fmt.Errorf("parse address: %w", err) 360 } 361 if len(addr) == 1 { 362 return conn, nil 363 } 364 365 subfields := make([]int, len(addr[1:])) 366 for i := range subfields { 367 var err error 368 subfields[i], err = strconv.Atoi(addr[i+1]) 369 if err != nil { 370 return conn, fmt.Errorf("parse address subfield %d: %w", i, err) 371 } 372 } 373 374 if conn.Address.Is4() { 375 if subfields[0] < 0 || subfields[0] > 255 { 376 return conn, fmt.Errorf("ttl: %d is outside uint8 range", subfields[0]) 377 } 378 conn.TTL = uint8(subfields[0]) 379 if len(subfields) == 2 { 380 conn.Count = subfields[1] 381 } 382 } 383 384 if conn.Address.Is6() && len(subfields) > 1 { 385 return conn, fmt.Errorf("parse address: only 1 subfield allowed, read %d", len(subfields)) 386 } else if conn.Address.Is6() && len(subfields) == 1 { 387 conn.Count = subfields[0] 388 } 389 return conn, nil 390 }