366 lines
10 KiB
Go
366 lines
10 KiB
Go
// Copyright 2023 LiveKit, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package livekit
|
|
|
|
import (
|
|
"errors"
|
|
fmt "fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// RFC 3261 compliant validation functions for SIP headers and messages
|
|
|
|
type allowedCharacters struct {
|
|
ascii [128]bool
|
|
utf8 bool
|
|
}
|
|
|
|
func NewAllowedCharacters() *allowedCharacters {
|
|
return &allowedCharacters{}
|
|
}
|
|
|
|
func (a *allowedCharacters) AddUTF8() error {
|
|
a.utf8 = true
|
|
return nil
|
|
}
|
|
|
|
func (a *allowedCharacters) AddNumbers() error {
|
|
for r := '0'; r <= '9'; r++ {
|
|
a.ascii[r] = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *allowedCharacters) AddLowercaseASCII() error {
|
|
for r := 'a'; r <= 'z'; r++ {
|
|
a.ascii[r] = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *allowedCharacters) AddUppercaseASCII() error {
|
|
for r := 'A'; r <= 'Z'; r++ {
|
|
a.ascii[r] = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *allowedCharacters) AddPrintableLienarASCII() {
|
|
// Anything between 0x20 and 0x7E
|
|
for i := 0x20; i <= 0x7E; i++ {
|
|
a.ascii[i] = true
|
|
}
|
|
a.ascii[0x09] = true // \t or HTAB
|
|
}
|
|
|
|
func (a *allowedCharacters) Add(chars string) error {
|
|
for _, char := range chars {
|
|
if int(char) >= len(a.ascii) {
|
|
return fmt.Errorf("char %d out of range, consider explicilty adding utf8 characters", char)
|
|
}
|
|
a.ascii[char] = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *allowedCharacters) Remove(chars string) error {
|
|
for _, char := range chars {
|
|
if int(char) >= len(a.ascii) {
|
|
return fmt.Errorf("char %d out of range, consider explicilty adding utf8 characters", char)
|
|
}
|
|
a.ascii[char] = false
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *allowedCharacters) Copy() *allowedCharacters {
|
|
return &allowedCharacters{
|
|
ascii: a.ascii,
|
|
utf8: a.utf8,
|
|
}
|
|
}
|
|
|
|
func (a *allowedCharacters) Validate(target string) error {
|
|
for _, char := range target {
|
|
if int(char) >= len(a.ascii) {
|
|
if a.utf8 {
|
|
continue
|
|
}
|
|
return fmt.Errorf("char %d out of range, consider explicilty adding utf8 characters", char)
|
|
}
|
|
if !a.ascii[char] {
|
|
return fmt.Errorf("char %d not allowed", char)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var tokenCharacters *allowedCharacters
|
|
var displayNameCharacters *allowedCharacters
|
|
var headerValuesCharacters *allowedCharacters
|
|
|
|
func init() {
|
|
// Per RFC 3261 Section 25.1
|
|
// SIP-message = Request / Response
|
|
// Request = Request-Line *( message-header ) CRLF [ message-body ]
|
|
// Response = Status-Line *( message-header ) CRLF [ message-body ]
|
|
// Request-Line = Method SP Request-URI SP SIP-Version CRLF
|
|
// Method = (CAPITAL ASCII)
|
|
// Request-URI = SIP-URI / SIPS-URI / absoluteURI
|
|
// SIP-Version = "SIP" "/" 1*DIGIT "." 1*DIGIT (CAPITAL ASCII, DIGITS, "/.")
|
|
// Status-Line = SIP-Version SP Status-Code SP Reason-Phrase CRLF
|
|
// Status-Code = (Alphanum + "-")
|
|
// Reason-Phrase = (Basically whatever...)
|
|
// extension-header = header-name (token) ":" header-value (Basically whatever...)
|
|
|
|
// URIs
|
|
// SIP-URI = "sip:" [ userinfo ] hostport uri-parameters [ headers ]
|
|
// SIPS-URI = "sips:" [ userinfo ] hostport uri-parameters [ headers ]
|
|
|
|
// One specific header form we care about:
|
|
// name-addr = [ display-name ] LAQUOT addr-spec RAQUOT
|
|
// display-name = *(token LWS)/ quoted-string
|
|
// addr-spec = SIP-URI / SIPS-URI / absoluteURI
|
|
|
|
tokenCharacters = NewAllowedCharacters()
|
|
tokenCharacters.AddNumbers()
|
|
tokenCharacters.AddLowercaseASCII()
|
|
tokenCharacters.AddUppercaseASCII()
|
|
tokenCharacters.Add("-.!%*_+`'~")
|
|
|
|
displayNameCharacters = tokenCharacters.Copy()
|
|
displayNameCharacters.Add(" \t")
|
|
|
|
headerValuesCharacters = NewAllowedCharacters()
|
|
headerValuesCharacters.AddPrintableLienarASCII()
|
|
headerValuesCharacters.AddUTF8()
|
|
}
|
|
|
|
// Required headers for SIP requests per RFC 3261 Section 8.1.1
|
|
var RequiredRequestHeaders = map[string]bool{
|
|
"via": true,
|
|
"from": true,
|
|
"to": true,
|
|
"call-id": true,
|
|
"cseq": true,
|
|
"max-forwards": true,
|
|
}
|
|
|
|
// Required headers for SIP responses per RFC 3261 Section 8.2.1
|
|
var RequiredResponseHeaders = map[string]bool{
|
|
"via": true,
|
|
"from": true,
|
|
"to": true,
|
|
"call-id": true,
|
|
"cseq": true,
|
|
}
|
|
|
|
// Crucial headers that can't be overridden by the user, and their shorthands
|
|
var FrobiddenSipHeaderNames = map[string]bool{
|
|
"accept": true,
|
|
"accept-encoding": true,
|
|
"accept-language": true,
|
|
"allow": true,
|
|
"allow-events": true, // rfc3903
|
|
"call-id": true,
|
|
"contact": true,
|
|
"content-encoding": true,
|
|
"content-length": true,
|
|
"content-type": true,
|
|
"cseq": true,
|
|
"event": true, // rfc3903
|
|
"expires": true,
|
|
"from": true, // We might allow this in the future, but for now we're printing
|
|
"max-forwards": true,
|
|
"record-route": true,
|
|
"refer-to": true, // rfc3515
|
|
"reply-to": true,
|
|
"k": true, // Supported
|
|
"l": true, // Content-Length
|
|
"m": true, // Contact
|
|
"o": true, // Event; rfc3903
|
|
"r": true, // Refer-To; rfc3515
|
|
"t": true, // To
|
|
"u": true, // Allow-Events; rfc3903
|
|
"v": true, // Via
|
|
}
|
|
|
|
// Headers that must comply with name-addr specification per RFC 3261 Section 20.10
|
|
// name-addr = [display-name] <addr-spec>
|
|
// addr-spec = SIP-URI / SIPS-URI / absoluteURI
|
|
var nameAddrHeaders = map[string]bool{
|
|
"from": true,
|
|
"to": true,
|
|
"contact": true,
|
|
"route": true,
|
|
"record-route": true,
|
|
"reply-to": true,
|
|
"p-asserted-identity": true, // RFC 3325 Section 9.1
|
|
}
|
|
|
|
// ValidateHeaderName validates a SIP header name per RFC 3261 Section 25.1
|
|
func ValidateHeaderName(name string, restrictNames bool) error {
|
|
if name == "" {
|
|
return errors.New("header name cannot be empty")
|
|
}
|
|
|
|
if len(name) > 255 {
|
|
return errors.New("header name too long (max 255 characters)")
|
|
}
|
|
|
|
if err := tokenCharacters.Validate(name); err != nil {
|
|
return fmt.Errorf("header name %s contains invalid characters: %w", name, err)
|
|
}
|
|
|
|
// Convert to lowercase for case-insensitive comparison
|
|
if restrictNames {
|
|
lowerName := strings.ToLower(name)
|
|
if forbidden, exists := FrobiddenSipHeaderNames[lowerName]; exists && forbidden {
|
|
return fmt.Errorf("header name %s not supported", name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateHeaderValue validates a SIP header value per RFC 3261 Section 25.1
|
|
func ValidateHeaderValue(name, value string) error {
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
|
|
if len(value) > 1024 {
|
|
return fmt.Errorf("header %s: value too long (max 1024 characters)", name)
|
|
}
|
|
|
|
// Basic character validation - printable ASCII. We're stricter than the spec here - no UTF-8 for now
|
|
if err := headerValuesCharacters.Validate(value); err != nil {
|
|
return fmt.Errorf("header %s: value: %w", name, err)
|
|
}
|
|
|
|
// Convert to lowercase for case-insensitive comparison
|
|
lowerName := strings.ToLower(name)
|
|
if _, exists := nameAddrHeaders[lowerName]; exists && false {
|
|
// TODO: Disabled since all supported headers are forbidden, re-enable when we allow some
|
|
if err := validateNameAddrHeader(value); err != nil {
|
|
return fmt.Errorf("header %s: value: %w", name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// findAngleBrackets efficiently finds angle brackets in a single scan
|
|
// Returns: start, end positions (-1 = missing), and error status
|
|
func findAngleBrackets(value string) (int, int, error) {
|
|
start := -1
|
|
end := -1
|
|
|
|
for i, r := range value {
|
|
switch r {
|
|
case '<':
|
|
if start != -1 {
|
|
return -1, -1, errors.New("multiple opening brackets")
|
|
}
|
|
start = i
|
|
case '>':
|
|
if end != -1 {
|
|
return -1, -1, errors.New("multiple closing brackets")
|
|
}
|
|
end = i
|
|
}
|
|
}
|
|
|
|
// Check for mismatched brackets
|
|
if (start == -1) != (end == -1) {
|
|
return -1, -1, errors.New("mismatched angle brackets")
|
|
}
|
|
|
|
// Check that < comes before >
|
|
if start > end {
|
|
return -1, -1, errors.New("malformed angle brackets")
|
|
}
|
|
|
|
return start, end, nil
|
|
}
|
|
|
|
// validateNameAddrHeader validates headers that use name-addr format per RFC 3261 Section 20.10
|
|
func validateNameAddrHeader(value string) error {
|
|
// RFC 3261 Section 20.10 - name-addr format
|
|
// name-addr = [display-name] <addr-spec>
|
|
// addr-spec = SIP-URI / SIPS-URI / absoluteURI
|
|
|
|
uri := value
|
|
start, end, err := findAngleBrackets(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if start >= 0 || end >= 0 {
|
|
uri = value[start+1 : end]
|
|
if err := validateDisplayName(strings.TrimSpace(value[:start])); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// This is a bare URI, and should comply with addr-spec, no special characters
|
|
if strings.ContainsAny(value, ";,? ") {
|
|
return errors.New("bare URI with special characters")
|
|
}
|
|
}
|
|
return validateURI(uri)
|
|
}
|
|
|
|
// validateDisplayName validates a display name in name-addr format
|
|
func validateDisplayName(displayName string) error {
|
|
if displayName == "" {
|
|
return nil
|
|
}
|
|
|
|
// Check if display name is quoted
|
|
if strings.HasPrefix(displayName, `"`) && strings.HasSuffix(displayName, `"`) {
|
|
// Quoted display name - use strconv.Unquote to validate proper escaping
|
|
_, err := strconv.Unquote(displayName)
|
|
if err != nil {
|
|
return fmt.Errorf("display name: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Unquoted display name - must not contain special characters
|
|
if err := displayNameCharacters.Validate(displayName); err != nil {
|
|
return fmt.Errorf("display name: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateURI validates URIs that can appear in name-addr format
|
|
func validateURI(uri string) error {
|
|
// Just do the basics, full validation should be done by sip service
|
|
scheme := strings.SplitN(uri, ":", 2)[0]
|
|
if scheme != "sip" && scheme != "sips" && scheme != "tel" {
|
|
// Technically, it either needs to be sip/s: or scheme://...
|
|
// Thus, tel: uri should not be supported here... but we allow it because of de-facto usage.
|
|
return errors.New("uri: scheme not one of sip, sips, or tel")
|
|
}
|
|
|
|
// Just no spaces, proper validation should be done in sip service
|
|
if strings.Contains(uri, " ") {
|
|
return errors.New("uri: contains spaces")
|
|
}
|
|
|
|
return nil
|
|
}
|