Packages

Elixir libraries and OTP behaviours for building telecom applications with SIP protocol and media handling

Current section

Files

Jump to
parrot_platform lib parrot sip parser.ex
Raw

lib/parrot/sip/parser.ex

defmodule Parrot.Sip.Parser do
@moduledoc """
SIP message parser using NimbleParsec.
This module provides the core functionality for parsing SIP messages
according to RFC 3261 and related specifications.
"""
import NimbleParsec
require Logger
alias Parrot.Sip.Message
alias Parrot.Sip.Headers
# Constants
@sip_methods [
:invite,
:ack,
:bye,
:cancel,
:options,
:register,
:prack,
:subscribe,
:notify,
:publish,
:info,
:refer,
:message,
:update
]
# Common parsers
whitespace = ascii_string([?\s, ?\t], min: 1)
optional_whitespace = ascii_string([?\s, ?\t], min: 0)
crlf = string("\r\n")
token_char = [
?!,
?%,
?',
?*,
?+,
?-,
?.,
?0,
?1,
?2,
?3,
?4,
?5,
?6,
?7,
?8,
?9,
?A,
?B,
?C,
?D,
?E,
?F,
?G,
?H,
?I,
?J,
?K,
?L,
?M,
?N,
?O,
?P,
?Q,
?R,
?S,
?T,
?U,
?V,
?W,
?X,
?Y,
?Z,
?_,
?`,
?a,
?b,
?c,
?d,
?e,
?f,
?g,
?h,
?i,
?j,
?k,
?l,
?m,
?n,
?o,
?p,
?q,
?r,
?s,
?t,
?u,
?v,
?w,
?x,
?y,
?z,
?~
]
token = ascii_string(token_char, min: 1)
# Request-Line parsers
method =
choice([
string("INVITE") |> replace(:invite),
string("ACK") |> replace(:ack),
string("BYE") |> replace(:bye),
string("CANCEL") |> replace(:cancel),
string("OPTIONS") |> replace(:options),
string("REGISTER") |> replace(:register),
string("PRACK") |> replace(:prack),
string("SUBSCRIBE") |> replace(:subscribe),
string("NOTIFY") |> replace(:notify),
string("PUBLISH") |> replace(:publish),
string("INFO") |> replace(:info),
string("REFER") |> replace(:refer),
string("MESSAGE") |> replace(:message),
string("UPDATE") |> replace(:update)
])
sip_uri = ascii_string([not: ?\s], min: 1)
sip_version = string("SIP/2.0")
request_line =
method
|> ignore(whitespace)
|> concat(sip_uri)
|> ignore(whitespace)
|> concat(sip_version)
|> ignore(crlf)
|> tag(:request_line)
# Status-Line parsers
status_code = integer(min: 1, max: 3)
reason_phrase = ascii_string([not: ?\r], min: 0)
status_line =
sip_version
|> ignore(whitespace)
|> concat(status_code)
|> ignore(whitespace)
|> concat(reason_phrase)
|> ignore(crlf)
|> tag(:status_line)
# Header parsers
header_name =
token
|> map({String, :downcase, []})
# Parse a header value
header_value =
ascii_string([not: ?\r], min: 0)
|> ignore(crlf)
header =
header_name
|> ignore(string(":"))
|> ignore(optional_whitespace)
|> concat(header_value)
|> tag(:header)
# Body parser
body =
ascii_string([], min: 0)
|> tag(:body)
# Complete message parser
defparsec(
:parse_message,
choice([request_line, status_line])
|> times(header, min: 0)
|> ignore(crlf)
|> optional(body)
)
@doc """
Parse a SIP message from a binary string.
Returns `{:ok, message}` or `{:error, reason}`.
"""
@spec parse(binary()) :: {:ok, Message.t()} | {:error, String.t()}
def parse(raw_message) when is_binary(raw_message) do
# Pre-process the raw message to handle folded headers
processed_message = unfold_headers(raw_message)
case parse_message(processed_message) do
{:ok, parsed, "", _, _, _} ->
process_parsed_message(parsed)
{:ok, _, _rest, _, _, _} ->
{:error, "Invalid SIP message: unparsed content remains"}
{:error, _reason, _rest, _context, _line, _col} ->
{:error, "Invalid SIP message format"}
end
end
# Unfold header lines that are continued on the next line with whitespace
defp unfold_headers(message) do
String.replace(message, ~r/\r\n[ \t]+/, " ")
end
# Process the parsed message and convert it to a Message struct
defp process_parsed_message(parsed) do
# Extract parts from parsed result
{type, parts} = extract_message_parts(parsed)
# Create basic message structure
base_message =
case type do
:request ->
%Message{
method: parts.method,
request_uri: parts.request_uri,
version: parts.version,
headers: parts.headers,
body: parts.body,
type: :request,
direction: :incoming
}
:response ->
%Message{
status_code: parts.status_code,
reason_phrase: parts.reason_phrase,
version: parts.version,
headers: parts.headers,
body: parts.body,
type: :response,
direction: :incoming
}
end
# RFC 3261 Section 17.1.3: Transaction ID is the branch parameter from the top Via header
transaction_id =
case Map.get(base_message.headers, "via") do
nil ->
nil
via when is_list(via) ->
case via do
[top | _] -> Map.get(top.parameters, "branch")
_ -> nil
end
via ->
Map.get(via.parameters, "branch")
end
# RFC 3261 Section 12.1.1: Dialog ID is Call-ID + tags
dialog_id =
try do
Logger.debug("Getting dialog_id from message")
Parrot.Sip.DialogId.from_message(base_message)
rescue
_ -> nil
end
message = %Message{base_message | transaction_id: transaction_id, dialog_id: dialog_id}
# Validate required headers and content length
with :ok <- validate_message(message),
:ok <- validate_content_length(message) do
{:ok, message}
else
{:error, reason} -> {:error, reason}
end
rescue
e -> {:error, "Error processing SIP message in parser: #{inspect(e)}"}
end
# Extract components from the parsed message
defp extract_message_parts(parsed) do
# Initialize with empty values
parts = %{
headers: %{},
body: ""
}
# Process each part
{type, updated_parts} =
Enum.reduce(parsed, {nil, parts}, fn
{:request_line, [method, request_uri, version]}, {_, parts} ->
{:request,
Map.merge(parts, %{
method: method,
request_uri: request_uri,
version: version
})}
{:status_line, [version, status_code, reason_phrase]}, {_, parts} ->
{:response,
Map.merge(parts, %{
version: version,
status_code: status_code,
reason_phrase: reason_phrase
})}
{:header, [name, value]}, {type, parts} ->
# Process headers
headers = process_header(name, value, parts.headers)
{type, %{parts | headers: headers}}
{:body, [body]}, {type, parts} ->
{type, %{parts | body: body}}
_, acc ->
acc
end)
{type, updated_parts}
end
# Process individual headers
defp process_header(name, value, headers) do
# Trim leading/trailing whitespace from value
value = String.trim(value)
# Process based on header name
case name do
"via" ->
# Via headers can be repeated
parsed = Headers.Via.parse(value)
case Map.get(headers, "via") do
nil ->
Map.put(headers, "via", parsed)
existing ->
if is_list(existing) do
Map.put(headers, "via", existing ++ [parsed])
else
Map.put(headers, "via", [existing, parsed])
end
end
"accept" ->
# For Accept headers, we need to keep the first one or create a list
parsed = Headers.Accept.parse(value)
case Map.get(headers, "accept") do
nil ->
Map.put(headers, "accept", parsed)
existing ->
if is_list(existing) do
Map.put(headers, "accept", existing ++ [parsed])
else
Map.put(headers, "accept", [existing, parsed])
end
end
"from" ->
Map.put(headers, "from", Headers.From.parse(value))
"to" ->
Map.put(headers, "to", Headers.To.parse(value))
"contact" ->
Map.put(headers, "contact", Headers.Contact.parse(value))
"call-id" ->
Map.put(headers, "call-id", Headers.CallId.parse(value))
"cseq" ->
Map.put(headers, "cseq", Headers.CSeq.parse(value))
"content-length" ->
Map.put(headers, "content-length", Headers.ContentLength.parse(value))
"max-forwards" ->
Map.put(headers, "max-forwards", Headers.MaxForwards.parse(value))
"expires" ->
Map.put(headers, "expires", Headers.Expires.parse(value))
"content-type" ->
Map.put(headers, "content-type", Headers.ContentType.parse(value))
"refer-to" ->
Map.put(headers, "refer-to", Headers.ReferTo.parse(value))
"event" ->
Map.put(headers, "event", Headers.Event.parse(value))
"subscription-state" ->
Map.put(headers, "subscription-state", Headers.SubscriptionState.parse(value))
"subject" ->
Map.put(headers, "subject", Headers.Subject.parse(value))
"allow" ->
Map.put(headers, "allow", Headers.Allow.parse(value))
"supported" ->
Map.put(headers, "supported", Headers.Supported.parse(value))
# For other headers, just store the raw value
_ ->
Map.put(headers, name, value)
end
end
# Validate that Content-Length matches the actual body length
def validate_content_length(message) do
if Map.has_key?(message.headers, "content-length") do
declared_length = message.headers["content-length"].value
actual_length = byte_size(message.body)
cond do
# Reject negative Content-Length values
declared_length < 0 ->
{:error, "Content-Length header value cannot be negative (#{declared_length})"}
actual_length != declared_length ->
# Be lenient with Content-Length mismatches for UDP
# Many SIP implementations have minor discrepancies
# Log a warning but don't reject the message
require Logger
Logger.debug(
"Content-Length mismatch: declared #{declared_length}, actual #{actual_length}"
)
:ok
true ->
:ok
end
else
# If no Content-Length header, it's valid (though not recommended for TCP)
:ok
end
end
def validate_content_length!(message) do
case validate_content_length(message) do
:ok -> :ok
{:error, reason} -> raise ArgumentError, reason
end
end
# All header-specific parsing is now handled by their respective Headers modules
# Validate that the message has all required headers
defp validate_message(message) do
required_request_headers = ["via", "to", "from", "call-id", "cseq"]
required_response_headers = ["via", "to", "from", "call-id", "cseq"]
required_headers =
case message.type do
:request -> required_request_headers
:response -> required_response_headers
_ -> required_request_headers
end
missing_headers =
Enum.filter(required_headers, fn header ->
not Map.has_key?(message.headers, header)
end)
cond do
message.type == :request and not Enum.member?(@sip_methods, message.method) ->
{:error, "Invalid SIP method: #{message.method}"}
message.type == :response and (message.status_code < 100 or message.status_code > 699) ->
{:error, "Invalid SIP message format: Invalid status code: #{message.status_code}"}
length(missing_headers) > 0 ->
{:error,
"Invalid SIP message format: Missing required headers: #{Enum.join(missing_headers, ", ")}"}
message.type == :request and
Map.has_key?(message.headers, "cseq") and
not Enum.member?(@sip_methods, message.headers["cseq"].method) ->
{:error, "Invalid CSeq method: #{message.headers["cseq"].method}"}
Map.has_key?(message.headers, "via") and is_binary(message.headers["via"]) ->
# If Via is still a string, try to parse it properly
try do
via = Headers.Via.parse(message.headers["via"])
# Update the message, but we don't need to use it since we just return :ok
_updated_message = %{message | headers: Map.put(message.headers, "via", via)}
:ok
rescue
_ -> {:error, "Invalid Via header format"}
end
true ->
:ok
end
end
end