この記事は NTTコムウェア Advent Calendar 2023 24日目の記事です。
NTTコムウェアの近江です。
皆様は新しいプログラミング言語を学ぶときにどのように学習を進めますか?基本文法を学んだあとは何かしら作ってみたいけれど、さて何を作ろう?と迷うことはないでしょうか。
そんな悩みを知人に相談したところ、通信を行うソフトウェアを実装することを勧められました。同じプロトコルを具備した他のソフトウェアと通信させることで正しさを確認できるから、だそうです。
この記事は、遅ればせながら以前より気になっていたRustを用いて、VoIPで使われることが多いプロトコルであるSIPで規定されているStateless Proxyを元に、私の独断と偏見でギリ通信できる程度に要件を削ぎ落としまくったなんちゃってStateless Proxyの実装を進めていった際の検討や調査のメモ、あるいはRFC3261を独自に解釈してプログラミングを楽しんだ記録です。
本記事はIETFのRFCを多数引用しています。記事にする際に読みにくくなる場合には、意味が変わらない範囲で空白文字やページ区切りを削除、記述順序の入れ替えなどをしていることがあります。
プロジェクトを作る
プロジェクトの名称は、プロキシに適した名前として直感で「sotafugy8」という名づけることにしました。この名前に特に意味はなく、個人的にプロキシ感を感じることができた語感のみで決めました。以降、今回実装するものについて言及する先にはsotafugy8と表記します。
事前にRustの環境は整えられている前提で、sotafugy8というプロジェクトを作ります。
cargo new sotafugy8
これによりこのコマンドを実行したディレクトリ内にsotafugy8というディレクトリが生成され、さらにその中に生成されたsrcディレクトリ内にmain.rsというソースファイルが生成されているので、このファイルにコードを書いていきます。
sotafugy8が依存するクレートがいくつかあるので、Cargo.tomlに書いておきます。(後で書いてもかまいません)
[dependencies]
+ regex = "1.10.2"
+ lazy_static = "1"
+ md5 = "0.7.0"
つくるもの
SIPのシーケンス例がRFC3665にいくつか記載されています。
分かりやすそうな例として、RFC3665「3.4. Successful Session with Proxy Failure」のシーケンスの後半を切り取って以下に示します。(なお、Proxy 2はStateful Proxyの動作になっています。Stateless Proxyの場合は若干異なり、F13
の信号を自ら送信することはしません。またsotafugy8ではF18
以降の信号はProxyを経由せずに送信されるように実装します。)
Alice Proxy 2 Bob
| | |
| INVITE F11 | |
|-------------------------------->| INVITE F12 |
| 100 F13 |--------------->|
|<--------------------------------| |
| | 180 F14 |
| 180 F15 |<---------------|
|<--------------------------------| |
| | 200 F16 |
| 200 F17 |<---------------|
|<--------------------------------| |
| ACK F18 | |
|-------------------------------->| ACK F19 |
| |--------------->|
| Both Way RTP Media |
|<================================================>|
| | BYE F20 |
| BYE F21 |<---------------|
|<--------------------------------| |
| 200 F22 | |
|-------------------------------->| 200 F23 |
| |--------------->|
| | |
具体的な信号も同じRFC3665「3.4. Successful Session with Proxy Failure」に記載されており、その中でProxyの役割がわかりやすいところとして、F11とF12の箇所のメッセージを以下に引用します。
F11 INVITE Alice -> Proxy 2
INVITE sip:bob@biloxi.example.com SIP/2.0
Via: SIP/2.0/UDP client.atlanta.example.com:5060;branch=z9hG4bK74bf9
Max-Forwards: 70
From: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
To: Bob <sip:bob@biloxi.example.com>
Call-ID: 4Fde34wkd11wsGFDs3@atlanta.example.com
CSeq: 2 INVITE
Contact: <sip:alice@client.atlanta.example.com>
Proxy-Authorization: Digest username="alice",
realm="biloxi.example.com",
nonce="1ae6cbe5ea9c8e8df84fqnlec434a359", opaque="",
uri="sip:bob@biloxi.example.com",
response="8a880c919d1a52f20a1593e228adf599"
Content-Type: application/sdp
Content-Length: 151
v=0
o=alice 2890844526 2890844526 IN IP4 client.atlanta.example.com
s=-
c=IN IP4 192.0.2.101
t=0 0
m=audio 49172 RTP/AVP 0
a=rtpmap:0 PCMU/8000
F12 INVITE Proxy 2 -> Bob
INVITE sip:bob@client.biloxi.example.com SIP/2.0
Via: SIP/2.0/UDP ss2.biloxi.example.com:5060;branch=z9hG4bK721e4.1
Via: SIP/2.0/UDP client.atlanta.example.com:5060;branch=z9hG4bK74bf9
;received=192.0.2.101
Max-Forwards: 69
Record-Route: <sip:ss2.biloxi.example.com;lr>
From: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
To: Bob <sip:bob@biloxi.example.com>
Call-ID: 4Fde34wkd11wsGFDs3@atlanta.example.com
CSeq: 2 INVITE
Contact: <sip:alice@client.atlanta.example.com>
Content-Type: application/sdp
Content-Length: 151
v=0
o=alice 2890844526 2890844526 IN IP4 client.atlanta.example.com
s=-
c=IN IP4 192.0.2.101
t=0 0
m=audio 49172 RTP/AVP 0
a=rtpmap:0 PCMU/8000
見比べると、
- proxyを通る前後でINVITEという文字列の右側が
sip:bob@biloxi.example.com
からsip:bob@client.biloxi.example.com
に変わっている - Viaで始まる行が1行増えている
- 元々あったViaで始まる行の次の行にスペースをひとつおいた後に
;received=
で始まる行が追加されている - Max-Forwardsで始まる行の数字が1減っている
などの差分があリます。(他にも差分がありますが、それらを扱う機能はsotafugy8では実装しません)
また、先ほどのメッセージはリクエストと呼ばれるものの一種なのですが、それに対するレスポンスの例としてF16とF17を以下に引用します。
F16 200 OK Bob -> Proxy 2
SIP/2.0 200 OK
Via: SIP/2.0/UDP ss2.biloxi.example.com:5060;branch=z9hG4bK721e4.1
;received=192.0.2.222
Via: SIP/2.0/UDP client.atlanta.example.com:5060;branch=z9hG4bK74bf9
;received=192.0.2.101
Record-Route: <sip:ss2.biloxi.example.com;lr>
From: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
To: Bob <sip:bob@biloxi.example.com>;tag=314159
Call-ID: 4Fde34wkd11wsGFDs3@atlanta.example.com
CSeq: 2 INVITE
Contact: <sip:bob@client.biloxi.example.com>
Content-Type: application/sdp
Content-Length: 147
v=0
o=bob 2890844527 2890844527 IN IP4 client.biloxi.example.com
s=-
c=IN IP4 192.0.2.201
t=0 0
m=audio 3456 RTP/AVP 0
a=rtpmap:0 PCMU/8000
F17 200 OK Proxy 2 -> Alice
SIP/2.0 200 OK
Via: SIP/2.0/UDP client.atlanta.example.com:5060;branch=z9hG4bK74bf9
;received=192.0.2.101
Record-Route: <sip:ss2.biloxi.example.com;lr>
From: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
To: Bob <sip:bob@biloxi.example.com>;tag=314159
Call-ID: 4Fde34wkd11wsGFDs3@atlanta.example.com
CSeq: 2 INVITE
Contact: <sip:bob@client.biloxi.example.com>
Content-Type: application/sdp
Content-Length: 147
v=0
o=bob 2890844527 2890844527 IN IP4 client.biloxi.example.com
s=-
c=IN IP4 192.0.2.201
t=0 0
m=audio 3456 RTP/AVP 0
a=rtpmap:0 PCMU/8000
F16
に比べるとF17
は
- Viaで始まる行が一行減っている
ことがわかります。
以上のように、Proxyは受信したリクエストもしくはレスポンスを一部編集しつつ別の誰かに中継する、という機能を提供します。
Proxyの詳しい要件についてはRFC3261のPage 91から始まる「16 Proxy Behavior」に書いてありますが、今回はあくまでもRust言語の学習が主な目的なので、先ほども述べた通り、RFC3261で定義されている本来のProxyではなく、私自身の独断と偏見で勝手に要件を落としまくったミニマムスペックにすら達していないStateless Proxy的な何かを実装していきます。実装する/しないの主な判断基準を以下に記しておきます。
- 下位レイヤのプロトコルはUDP/IPv4のみにする
- マルチキャスト対応しない
- 通信相手がRFC3261に準拠しているものと想定する
- 受信したメッセージを解析する際には厳格にはチェックせず、ゆる~く解析する
- addr-specはSIP-URIだけに対応
- リクエスト中継時にRecord-Routeは挿入しない
- 拡張機能の類には一切対応しない
- 認証関連機能には一切対応しない(※簡単にイタズラできちゃいます)
- RegistrarとLocation Service機能も内包する
- REGISTERで登録できるアドレスはひとつだけ、優先順位を指定しても無視
- REGISTERで登録したアドレスは有効期限切れしない
- 同じ意味になるが異なる表現ができる文字列を比較する際に完全一致の比較のみを行う
- フリーのSIPソフトフォンを探してきて通話できればそれでよしとする
メッセージの受信
メッセージの受信を契機に諸々の処理が動き始めるので、まずは受信から実装していきます。
RFC3261のPage 142に
All SIP elements MUST implement UDP and TCP. SIP elements MAY
implement other protocols.
と書いてあり、UDPだけでなくTCPへの対応も必須ということになっていますが、RFC3261のPage 143に
If an element sends a request over TCP because of these message size
constraints, and that request would have otherwise been sent over
UDP, if the attempt to establish the connection generates either an
ICMP Protocol Not Supported, or results in a TCP reset, the element
SHOULD retry the request, using UDP. This is only to provide
backwards compatibility with RFC 2543 compliant implementations that
do not support TCP. It is anticipated that this behavior will be
deprecated in a future revision of this specification.
と書いてあり、行儀のよいSIPクライアントであればTCPでの通信に失敗したらUDPで送ってくれそうなので、相手に甘えることにしてUDPだけに対応します。また、IPv4のみに対応し、IPv6には対応しません。(なお、IPv6対応したい場合はRFC5954も参照したほうがよさそうです)
UDPを使ってパケットを受信するには、以下のようなコードを書けばよいようです。
let hostport = format!("{}:{}", ip, port);
let sock = UdpSocket::bind(hostport).unwrap();
let mut buf = [0; 0xffff];
if let Ok((rsize, saddr)) = sock.recv_from(&mut buf) {
// saddrから送られてきたデータがbufにrsizeバイト分格納されている
}
これをうまいことコードに組み込んでいきます。
文字列に変換
RFC3261のPage 26にSIP is a text-based protocol and uses the UTF-8 charset
と書いてあるので、受信したデータを文字列に変換します。
先ほどのコードを利用しつつProxyという構造体(struct)を作り、その中でUDPで受信した後にUTF-8に変換するコードを追加しておきます。self.proc()はあとで実装します。
// プロキシ
struct Proxy {
ip: String,
port: u16,
sock: UdpSocket,
locs: HashMap<String, String>,
}
impl Proxy {
fn new(ip: &str, port: u16) -> Self {
let hostport = format!("{}:{}", ip, port);
Self {
ip: String::from(ip),
port: port,
sock: UdpSocket::bind(hostport).unwrap(),
locs: HashMap::new(),
}
}
fn run(&mut self) {
let mut counter = 0;
let mut buf = [0; 0xffff];
loop {
if let Ok((rsize, saddr)) = self.sock.recv_from(&mut buf) {
if let Ok(s) = std::str::from_utf8(&buf[..rsize]) {
self.proc(s, saddr);
counter += 1;
println!("{}", counter);
}
}
}
}
fn proc(&mut self, s: &str, saddr: SocketAddr) -> Option<()> {
// あとで実装
}
}
なお、SIPメッセージにはmessage-bodyと呼ばれる部分があるのですが、RFC3261のPage 33に以下のような記述があり、message-bodyはバイナリでもよいとされているので、本気でSIPスタックを実装するのであれば文字列に変換する前にmessage-bodyを抽出しておくべきでしょう。
SIP messages MAY contain binary bodies or body parts.
SIPメッセージをざっくり解析
Proxyとして信号を中継するにあたり、のちに受信した信号の中身を一部編集したり、中身をみて中継先を判定したりすることになるのですが、都度信号を解析するとコードが複雑になったり処理時間が長くなったりしそうな気がするので、まずは大まかに解析しておくことにします。
SIP信号のフォーマットはRFC3261のPage 27に以下のような構成になっていると書いてあります。
A SIP message is either a request from a client to a server, or a
response from a server to a client.
Both Request (section 7.1) and Response (section 7.2) messages use
the basic format of RFC 2822 [3], even though the syntax differs in
character set and syntax specifics. (SIP allows header fields that
would not be valid RFC 2822 header fields, for example.) Both types
of messages consist of a start-line, one or more header fields, an
empty line indicating the end of the header fields, and an optional
message-body.
generic-message = start-line
*message-header
CRLF
[ message-body ]
start-line = Request-Line / Status-Line
The start-line, each message-header line, and the empty line MUST be
terminated by a carriage-return line-feed sequence (CRLF). Note that
the empty line MUST be present even if the message-body is not.
これに相当する説明+αをRFC3261のPage 219から始まる「25 Augmented BNF for the SIP Protocol」に定義されているBNFから抜き出してまとめてみました(以降、BNFに関して記載するときはいずれもこのセクションからの引用です)。※中略※
と書いた箇所は、RFC3261でそれぞれの意味が言及されているヘッダのフォーマットが書かれているのですが、現段階で実行する解析に必要なフォーマット情報としてはextension-headerに包含されるように見えたので、extension-headerだけを転記しました。
SIP-message = Request / Response
Request = Request-Line
*( message-header )
CRLF
[ message-body ]
Request-Line = Method SP Request-URI SP SIP-Version CRLF
Response = Status-Line
*( message-header )
CRLF
[ message-body ]
Status-Line = SIP-Version SP Status-Code SP Reason-Phrase CRLF
message-header = (
※中略※
/ extension-header) CRLF
extension-header = header-name HCOLON header-value
header-name = token
header-value = *(TEXT-UTF8char / UTF8-CONT / LWS)
message-body = *OCTET
これらを踏まえ、解析完了時にRustの以下のような構造体の中身が埋まっている状態を目指すことにします。
struct Message {
method: String, // start-line(Request-Line)のMethod
requri: String, // start-line(Request-Line)のRequest-URI
stcode: String, // start-line(Status-Line)のStatus Code
reason: String, // start-line(Status-Line)のReason Phrease
hdrs: Vec<Header>, // message-headers
body: String, // message-body
}
struct Header {
name: String, // header-name
vals: Vec<String>, // header-value
}
前述のBNFからもわかりますが、RFC3261のPage 27に以下のように書かれていて、Request-LineもStatus-Lineもmessage-headerもCRLFで終端していて、message-headerのあとにはさらにCRLFが続いていることがわかります。
The start-line, each message-header line, and the empty line MUST be
terminated by a carriage-return line-feed sequence (CRLF). Note that
the empty line MUST be present even if the message-body is not.
CRLFとは、RFC2234のPage 11に書いてある通り、CRとLFが連続している文字列です。CRは同じくRFC2234のPage 11に書いてある通りアスキーコードの0x2D、いわゆる\rのことで、LFもRFC2234のPage 11に書いてある通りアスキーコードの0x0A、いわゆる\nのことを指しています。したがってCRLFは\r\nのことになります。上の文章にもa carriage-return line-feed sequence
と書いてありますし、まぁ、そりゃそうだという感じですね。
ちなみにRFC3261のPage 257に以下のように書かれており、SIPの古い仕様書にあたるRFC2543ではCRLFはCR単体やLF単体も許容されていたのだけれども、RFC3261ではCRLFでないとダメということになっています。逆にいうと、RFC2543にしか対応していない相手とも通信したい場合は単体のCRや単体のLFにも対応する必要がありますが、このsotafugy8では相手がRFC3261準拠であることを期待し、CRLFのみを許容することにします。
o In RFC 2543, lines in a message could be terminated with CR, LF,
or CRLF. This specification only allows CRLF.
SIPのBNFを熱心に読み込んでいくと、message-headerにはそれらを終端するCRLF以外にもCRLFが登場する場合がありますが、その場合はLWSというものに限定されていているようです。LWSはRFC3261のPage 220に以下のように書いてあります。
LWS = [*WSP CRLF] 1*WSP ; linear whitespace
WSPの定義はRFC3261には見当たらないのですが、RFC3261のPage 219に
Section 6.1 of RFC 2234 defines a set of core rules that
are used by this specification, and not repeated here.
と書いてあるので、RFC2234をみてみると、そのPage 12に以下のようにかいてあり、
WSP = SP / HTAB
SPはRFC2234のPage 11に以下のようにかいてあるとおりいわゆる半角スペースのことで、
SP = %x20
SPはRFC2234のPage 11に以下のようにかいてあるとおりいわゆる水平タブのことであるというのがわかりました。
HTAB = %x09
以上から、start-lineやmessage-headerの区切りとなるCRLF以外にCRLFが登場する場合は、その後に必ず半角スペースか水平タブが登場することがわかります。
そこで文字列をその後に半角スペースか水平タブが続いていないCRLFごとに区切り、そのひとつ目をstart-line、ふたつ目以降をmessage-headerたちとして抽出するメソッドをMessage構造体に作ってみました。同時にmessage-bodyも取り出します。途中でエラーによる条件分岐などを書かずに?
で処理しているところがありますが、この程度の解析に失敗する場合はエラーレスポンスを生成することもできないので、さくっとエラーにして終わらせています。ここでは入力を&strにして解析していますが、もしmessage-bodyとしてバイナリを許容するのであればこのメソッドを改造する必要があります。
// SIPメッセージ全体をstart-line、message-header、bodyへの分割を想定
fn split(buf: &str) -> Option<(&str, Vec<&str>, &str)> {
let mut pos = 0;
let mut margin = 0;
let mut lines: Vec<&str> = vec![];
static CRLF: &str = "\r\n";
loop {
let tmp = buf[pos+margin..].find(CRLF)?;
if tmp == 0 {
// empty-line
break
}
let next_line: &str = &buf[pos+margin+tmp+CRLF.len()..];
if next_line.starts_with(" ") || next_line.starts_with("\t") {
margin += tmp + CRLF.len();
} else {
lines.push(&buf[pos..pos+margin+tmp]);
pos += margin + tmp + CRLF.len();
margin = 0;
}
}
let sl = lines.remove(0); // start-lineを抜き出す
Some((sl, lines, &buf[pos + 2..])) // 2は"\r\n"の長さ
}
なお、RFC3261のPage 34に
Implementations processing SIP messages over stream-oriented
transports MUST ignore any CRLF appearing before the start-line
と書いてあり、stream-orientedの場合にはstart-lineの前にCRLFが登場していたら無視しろと言われていますが、sotafugy8ではUDPにしか対応しないので気にしないことにします。
続いてstart-lineを解析します。
start-lineはRequest-lineもしくはStatus-lineのことであり、それらはRFC3261の12481行目あたりであったり、
Request-Line = Method SP Request-URI SP SIP-Version CRLF
RFC3261の12581行目あたりに書いてあります。
Status-Line = SIP-Version SP Status-Code SP Reason-Phrase CRLF
ただし、先ほど書いたsplitというメソッドで最後のCRLFは消えた状態になっています。それぞれの定義は当然RFC3261に記載されており、とても長くなるのでここに転記はしませんが、以下のような正規表現を使えば取り出せそうな定義になっていました。
lazy_static! {
static ref RE_SL: Regex = Regex::new(r"^(([^ ]+) ([^ ]+) )?SIP/2\.0( (\d\d\d) ([^\r\n]+))?").expect("failed to build Regex for Start-Line");
}
なお、lazy_staticを使って定義しておくと、この正規表現の解釈を一度しか実行しないようになるのだそうです。正規表現を定義する場所と使う場所が離れるのは個人的には好きではないのですが、この処理は重そうな予感がするので、一度しか実行しないほうを選びました。
これを使ってstart-lineを解析するメソッドをMessage構造体に作ってみました。
fn parse_start_line(&mut self, buf: &str) -> Option<()> {
let cap = RE_SL.captures(buf)?;
if let Some(m) = cap.get(2) {
// requestの場合
self.method = m.as_str().to_string();
self.requri = cap.get(3)?.as_str().to_string();
} else if let Some(m) = cap.get(5) {
// responseの場合
self.stcode = m.as_str().to_string();
self.reason = cap.get(6)?.as_str().to_string();
}
Some(())
}
続いてmessage-headerを解析します。message-headerは基本的にはRFC3261の12973行目に書いてあるようなフォーマットになっています。
extension-header = header-name HCOLON header-value
HCOLONはRFC3261の12298行目に以下のように書いてあり、
HCOLON = *( SP / HTAB ) ":" SWS
SWSはRFC3261の12291行目に以下のようの書いてあり、
SWS = [LWS] ; sep whitespace
LWSのフォーマットについてはすでに説明しました。
これを真面目に処理するならば、正規表現で、[ \t]*:(([ \t]*\r\n)?[ \t]+)?
とかにマッチさせてそれより前をheader-name、後ろをheader-valueにすればよいのでしょうが、ここでは:
で区切ってそれより前の文字列のうち後ろにくっついている空白を除いたものをheader-name、後ろの文字列のうち前にくっついている空白を除いたものをmessage-valueにしてみます。Headerという構造体に以下のような関数を作ってみました。
fn split(buf: &str) -> Option<(&str, &str)> {
let vals: Vec<&str> = buf.splitn(2, ":").collect();
if vals.len() == 2 {
return Some((vals[0].trim_end(), vals[1].trim_start()));
}
None
}
さて、header-valueについて、RFC3261のPage 21からPage 22にかけて以下のように書いてあり、comma(,
)で区切られた複数の値が存在する場合があることがわかります。
Header Field: A header field is a component of the SIP message
header. A header field can appear as one or more header field
rows. Header field rows consist of a header field name and zero
or more header field values. Multiple header field values on a
given header field row are separated by commas. Some header
fields can only have a single header field value, and as a
result, always appear as a single header field row.
このsotafugy8でのちに解析をする可能性があるmessage-headerのうち、Via、Contact、Routeの3種類のヘッダについては、カンマで区切られた複数の要素が載ってくる場合があるものになっています。そこでこれらのヘッダについては、この時点でカンマで区切ってVec<String>
型で保持することにします。(それ以外のヘッダもVec<String>
型でデータを保持したいのですが、特に解析などは行わずにheader-valueを丸ごとひとつの要素に格納することにします。)
では、それらヘッダのフォーマットをもう少し詳しく確認します。
まずViaから。RFC3261とRFC2234のいろいろな場所からとってきて整理するとこんな具合になります。
Via = ( "Via" / "v" ) HCOLON via-parm *(COMMA via-parm)
via-parm = sent-protocol LWS sent-by *( SEMI via-params )
via-params = via-ttl / via-maddr / via-received / via-branch / via-extension
via-ttl = "ttl" EQUAL ttl
via-maddr = "maddr" EQUAL host
via-received = "received" EQUAL (IPv4address / IPv6address)
via-branch = "branch" EQUAL token
via-extension = generic-param
generic-param = token [ EQUAL gen-value ]
gen-value = token / host / quoted-string
sent-protocol = protocol-name SLASH protocol-version SLASH transport
protocol-name = "SIP" / token
protocol-version = token
transport = "UDP" / "TCP" / "TLS" / "SCTP" / other-transport
sent-by = host [ COLON port ]
ttl = 1*3DIGIT ; 0 to 255
quoted-string = SWS DQUOTE *(qdtext / quoted-pair ) DQUOTE
qdtext = LWS / %x21 / %x23-5B / %x5D-7E / UTF8-NONASCII
quoted-pair = "\" (%x00-09 / %x0B-0C / %x0E-7F)
COMMA = SWS "," SWS ; comma
DQUOTE = %x22
じーっと見ると、mesasge-valueに該当する場所は、via-paramがひとつ以上あって、ふたつ以上ある場合はCOMMAで区切られていることがわかります。また、gen-valueはquoted-stringの場合があり、COMMAに必ず現れる,
のアスキーコードは0x2cに該当するので、qdtextの定義のなかにある%x23-5B
の範囲に含まれていて、quoted-string内に現れる場合にはDQUOTEとDQUOTEでくくられているということがわかります。
次にContactとRouteについてもRFC3261とRFC2234のいろいろな場所からとってきて整理するとこんな具合になります。
Contact = ("Contact" / "m" ) HCOLON
( STAR / (contact-param *(COMMA contact-param)))
contact-param = (name-addr / addr-spec) *(SEMI contact-params)
contact-params = c-p-q / c-p-expires
/ contact-extension
c-p-q = "q" EQUAL qvalue
c-p-expires = "expires" EQUAL delta-seconds
contact-extension = generic-param
delta-seconds = 1*DIGIT
Route = "Route" HCOLON route-param *(COMMA route-param)
route-param = name-addr *( SEMI rr-param )
name-addr = [ display-name ] LAQUOT addr-spec RAQUOT
addr-spec = SIP-URI / SIPS-URI / absoluteURI
display-name = *(token LWS)/ quoted-string
こちらもじーっと見ると、message-valueに該当する箇所は、contact-param、rec-route、route-paramがひとつ以上あり、複数ある場合はCOMMAで区切られていて、いずれもgeneric-paramかdisplay-nameがあり、そのなかにquoted-stringがあります。Viaの場合同様にCOMMAに必ず現れる,
のアスキーコードは0x2cに該当するので、qdtextの定義のなかにある%x23-5B
の範囲に含まれていて、quoted-string内にある場合にはDQUOTEとDQUOTEでくくられているということがわかります。
以上から、Via, Contact, Routeの3種類のmessage-headerについて、そのheader-valueの部分はDQUOTE("
)でくくられていない場所にあるCOMMAで区切ることにします。ちょっと楽をして正規表現で解析することにします。
fn parse_hvalue(buf: &str) -> Option<Vec<String>> {
let mut vals: Vec<String> = vec![];
let mut pos: usize = 0;
while buf[pos..].len() > 0 {
let caps = RE_HVALUE.captures(&buf[pos..])?;
vals.push(caps.get(1)?.as_str().to_string());
pos += caps.get(0)?.as_str().len();
}
Some(vals)
}
ここで、RE_HVALUEというのは、Regex
型の変数で、header-valueを取り出すための正規表現です。こんな感じ。
lazy_static! {
static ref RE_HVALUE: Regex = Regex::new(r#"^(([^",]|("([^"]|(\"))*"))+)(\s*,\s*)?"#)
.expect("failed to build Regex for HVALUE");
}
ダブルクォーテーションとカンマではない文字か、ダブルクォーテーションのペアで括られているゼロ個以上のダブルクォーテーションではない文字かエスケープされたダブルクォーテーションがひとつ以上続き、そのあとカンマがあってもよく、そのカンマの前後には空白があるかもしれない、ということを表現したつもりです。
この正規表現は厳密なRFC3261の定義に比べるとかなりゆる~くしているので意図的に誤ったフォーマットの文字列を含めると正しいものと誤解させることが可能です。ご注意ください。
では今作ったふたつの関数を使ってmessage-headerを解析するメソッドを書いてみます。
RFC3261のPage 32に以下のように書いてあるように、ヘッダ名はcase-insensitiveなので、まず全体を小文字に変換してから小文字同士で比べるようにします。
When comparing header fields, field names are always case-
insensitive.
また、前述のBNFのとおり、ContactヘッダとViaヘッダにはそれぞれm(RFC3261の12749行目)とv(RFC3261の12943行目)という省略形があるので、それらでもマッチするようにしないといけません。
それらを踏まえ、さきほど示したHeaderという構造体にいくつかのメソッドを定義しました。ついでにデータの設定や取得を行うメソッドも定義しています。
impl Header {
fn new() -> Self {
Self {
name: "".to_string(),
vals: vec![],
}
}
// 解析する
fn parse(&mut self, buf: &str) -> Option<()> {
let (name, value) = Self::split(buf)?;
self.name = name.to_string();
if vec!["via", "v", "contact", "m", "route"].contains(&name.to_lowercase().as_str()) {
self.vals = Self::parse_hvalue(value)?;
} else {
self.vals = vec![value.to_string()];
}
Some(())
}
// header-nameとheader-valueに分割する
fn split(buf: &str) -> Option<(&str, &str)> {
let vals: Vec<&str> = buf.splitn(2, ":").collect();
if vals.len() == 2 {
return Some((vals[0].trim_end(), vals[1].trim_start()));
}
None
}
// header-valueをCOMMAで分割して配列にする
fn parse_hvalue(buf: &str) -> Option<Vec<String>> {
let mut vals: Vec<String> = vec![];
let mut pos: usize = 0;
while buf[pos..].len() > 0 {
let caps = RE_HVALUE.captures(&buf[pos..])?;
vals.push(caps.get(1)?.as_str().to_string());
pos += caps.get(0)?.as_str().len();
}
Some(vals)
}
// このヘッダの名前と値を設定する
fn set(&mut self, name: &str, vals: &Vec<String>) {
self.name = name.to_string();
self.vals = vec![];
for val in vals.iter() {
self.vals.push(val.to_string());
}
}
}
最後に、ここまで書いた処理を用いて解析処理全体を実行するメソッドを作っておきます。
impl Message {
fn new() -> Self {
Self {
method: "".to_string(),
requri: "".to_string(),
stcode: "".to_string(),
reason: "".to_string(),
hdrs: vec![],
body: "".to_string(),
}
}
// 解析
fn parse(&mut self, buf: &str) -> Option<()> {
// 全体を分割
let (sl, hdrs, body) = Self::split(&buf)?;
// Start-Lineの解析
self.parse_start_line(sl)?;
// ヘッダの解析
for h in hdrs.iter() {
let mut hdr = Header::new();
hdr.parse(&h)?;
self.hdrs.push(hdr);
}
// ボディの保持
self.body = body.to_string();
Some(())
}
// 続く
先ほど信号受信のところで呼び出したprocというメソッドの中身を書いておきます。
解析した後に、リクエストの場合とレスポンスの場合で異なる処理を実行したいので、そこを呼び出すところも追加しておきます。
fn proc(&mut self, s: &str, saddr: SocketAddr) -> Option<()> {
println!(">>> {} >>>>>>>>\r\n{}", saddr.to_string(), s);
let mut msg = Message::new();
if let Some(_) = msg.parse(s) {
if msg.method != "" && msg.requri != "" {
self.proc_request(&mut msg, &saddr);
} else if msg.stcode != "" && msg.reason != "" {
self.proc_response(&mut msg);
}
}
Some(())
}
fn proc_request(&mut self, msg: &mut Message, saddr: &SocketAddr) -> Option<()> {
// あとで作る
}
fn proc_response(&self, msg: &mut Message) -> Option<()> {
// あとで作る
}
レスポンスの処理
レスポンスの処理の実装の中で作るメソッドをリクエストの処理でも使用したいので、先にレスポンスの処理から作っていきます。
レスポンス処理については、RFC3261の5943行目から始まる「16.7 Response Processing」に説明が書いてあるかと思いきや、「16.11 Stateless Proxy」にこんなことが書いてあります。
Response processing as described in Section 16.7 does not apply to a
proxy behaving statelessly. When a response arrives at a stateless
proxy, the proxy MUST inspect the sent-by value in the first
(topmost) Via header field value. If that address matches the proxy,
(it equals a value this proxy has inserted into previous requests)
the proxy MUST remove that header field value from the response and
forward the result to the location indicated in the next Via header
field value. The proxy MUST NOT add to, modify, or remove the
message body. Unless specified otherwise, the proxy MUST NOT remove
any other header field values. If the address does not match the
proxy, the message MUST be silently discarded.
さらにsotafugy8では思いっきり手を抜くことにして、無条件に先頭のViaの値を削除すること、新たに先頭になったViaの値が示す宛先に中継することにします。
RFC3261の12943行目あたりにViaヘッダのフォーマットが書いてあります。
Via = ( "Via" / "v" ) HCOLON via-parm *(COMMA via-parm)
受信したメッセージを解析する場所ですでに個別のvia-parmが取り出せているので、先頭のvia-parmの削除は簡単です。
まずは指定されたヘッダを探すためのメソッドをMessageという構造体に実装します。
// 指定されたヘッダを探して配列の何番目かを返す。ヘッダ名は小文字で渡すこと。
fn search(&self, hname1: &str, hname2: &str) -> Option<usize> {
for (i, hn) in self.hdrs.iter().enumerate() {
let hname = hn.name.to_lowercase();
if hname == hname1 || hname == hname2 {
return Some(i);
}
}
None // 見つからなかった
}
これを使ってViaヘッダを探し、その先頭の値を消します。
fn proc_response(&self, msg: &mut Message) -> Option<()> {
// 先頭のViaを取り除く(リクエスト中継時に自分が挿入したViaのはず)
match msg.search("via", "v") {
None => {
return Some(()); // Viaがないレスポンスは中継できないのでおしまい
}
Some(viapos) => {
// 消す
msg.hdrs[viapos].vals.remove(0);
// 消した結果、そのヘッダに値がなくなってしまったら、そのヘッダ自体を消す
if msg.hdrs[viapos].vals.len() == 0 {
msg.hdrs.remove(viapos);
}
}
}
// 続く
新たに先頭になったvia-parmを解析していきます。
Viaヘッダはリクエストを送信する時に自分自身がSIPメッセージを受け取ることができるアドレスとポートの情報を(すでにViaヘッダがある場合は既存のViaの前に)書くことになっていて、リクエストからレスポンスを生成する時にはViaヘッダを丸ごとコピーすることになっているので、先ほどの先頭のViaを削除したことにより、このレスポンスを送るべき宛先がわかるようになっています。
via-parmのフォーマットはこんな感じ。
via-parm = sent-protocol LWS sent-by *( SEMI via-params )
sent-by = host [ COLON port ]
via-params = via-ttl / via-maddr
/ via-received / via-branch
/ via-extension
via-ttl = "ttl" EQUAL ttl
via-maddr = "maddr" EQUAL host
via-received = "received" EQUAL (IPv4address / IPv6address)
via-branch = "branch" EQUAL token
via-extension = generic-param
generic-param = token [ EQUAL gen-value ]
gen-value = token / host / quoted-string
quoted-string = SWS DQUOTE *(qdtext / quoted-pair ) DQUOTE
qdtext = LWS / %x21 / %x23-5B / %x5D-7E / UTF8-NONASCII
quoted-pair = "\" (%x00-09 / %x0B-0C / %x0E-7F)
COMMA = SWS "," SWS ; comma
DQUOTE = %x22 ; " (Double Quote)
この中からこのレスポンスの宛先を取り出します。
RFC3261のPage 146にこんなことが書いてあります。
o Otherwise (for unreliable unicast transports), if the top Via
has a "received" parameter, the response MUST be sent to the
address in the "received" parameter, using the port indicated
in the "sent-by" value, or using port 5060 if none is specified
explicitly.
具体的には、まずIPアドレスはvia-receivedがある場合はそこに書かれたアドレスとsent-byに書かれたポート番号、sent-byにポートが書いていない場合はSIPのデフォルトのポート番号である5060になる。
またRFC3261のPage 147にその続きが書いてあり、
o Otherwise, if it is not receiver-tagged, the response MUST be
sent to the address indicated by the "sent-by" value, using the
procedures in Section 5 of [4].
receivedパラメータがない場合はsent-byに書かれているアドレス&ポートが宛先になります。(rportというパラメータを定義した拡張もありますがsotafugy8は対応しないことにします。)
via-parmの実例として冒頭に記したシーケンスのF9のメッセージから取り出すと、こんなやつです。
SIP/2.0/UDP bigbox3.site3.atlanta.com;branch=z9hG4bK77ef4c2312983.1
;received=192.0.2.2
さきほどのBNFをみると、via-params
の部分を構成するvia-ttl
、via-maddr
、via-received
、via-branch
、via-extension
はいずれもgeneric-param
で表現できそうです。generic-paramという名前だけあって他の場所でも使えるかもしれないので、viaとは独立して、複数のgeneric-paramsの値を管理するための構造体を作ることにします。解析するメソッド、指定された名前のパラメータの値を取り出すメソッド、指定された名前のパラメータの値を設定するメソッド、文字列化するメソッドもつくっておきます。
lazy_static! {
static ref RE_PRMS: Regex =
Regex::new(r#"^;\s*(?<name>[^\s=;]+)\s*(=\s*(?<value>([^\s";]+|"([^"]|(\"))*")))?\s*"#)
.expect("faield to build Regex for PRMS");
}
struct GenericParams {
elms: Vec<(String, Option<String>)>,
}
impl std::fmt::Display for GenericParams {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut s = "".to_string();
for prm in self.elms.iter() {
if let Some(v) = &prm.1 {
s.push_str(format!(";{}={}", prm.0, v).as_str());
} else {
s.push_str(format!(";{}", prm.0).as_str());
}
}
write!(f, "{}", s)
}
}
impl GenericParams {
fn new() -> Self {
Self { elms: vec![] }
}
// 解析
fn parse(&mut self, src: &str) -> Option<()> {
self.elms = vec![];
let mut pos = 0;
while src[pos..].len() > 0 {
// 正規表現にマッチさせて値を抽出する
let caps = RE_PRMS.captures(&src[pos..])?;
// パラメータ名を抽出
let name = caps.name("name")?.as_str().to_string();
// パラメータの値を抽出
if let Some(value) = caps.name("value") {
self.elms.push((name, Some(value.as_str().to_string())));
} else {
self.elms.push((name, None));
}
pos += caps.get(0)?.as_str().len();
}
Some(())
}
// 取得
fn get(&self, name: &str) -> Option<String> {
// 値がない要素がある場合と、その名前の要素がない場合の区別がつかない問題あり
// ただsotafugyでは前者を使うことがないので今は気にしない
for elm in self.elms.iter() {
if elm.0 == name {
return elm.1.clone();
}
}
None
}
// 設定
fn set(&mut self, name: &str, value: Option<String>) {
// 名前がnameのパラメータがあったらvalueで上書き。
for elm in self.elms.iter_mut() {
if elm.0 == name {
elm.1 = value;
return;
}
}
// 名前がnameのパラメータがなかったら新規挿入
self.elms.push((name.to_string(), value));
}
}
via-params
の部分としてこのGenericParams
を利用する、via-parmを扱う構造体とそのデータを扱うメソッドを作ります。
lazy_static! {
static ref RE_VIA: Regex =
Regex::new(r"^SIP\s*/\s*2\.0\s*/\s*UDP\s+(?<host>[^ \s:;]+)(\s*:\s*(?<port>\d+))")
.expect("failed to build Regex for VIA");
}
// Viaヘッダの解析&編集用
struct Via {
host: String,
port: u16,
prms: GenericParams,
}
impl Via {
fn new() -> Self {
Self {
host: "".to_string(),
port: 5060,
prms: GenericParams::new(),
}
}
// 解析
fn parse(&mut self, buf: &str) -> Option<()> {
let caps = RE_VIA.captures(buf)?;
// hostを取り出す
self.host = caps.name("host")?.as_str().to_string();
// portがあったらその値を取り出し、なければデフォルトの5060を使う
self.port = 5060;
if let Some(m) = caps.name("port") {
match m.as_str().parse::<u16>() {
Ok(port) => {
self.port = port;
}
Err(_) => {
return None;
}
}
}
// パラメータ部分を解析
self.prms.parse(&buf[caps.get(0)?.as_str().len()..])?;
Some(())
}
// レスポンスの宛先とbranchパラメータを取得
fn get(&self) -> Option<(String, u16, String)> {
// 一旦hostを宛先アドレスとして、
let mut host = self.host.to_string();
if let Some(r) = self.prms.get("received") {
// receivedパラメータがある場合はその値で宛先アドレスを上書き
host = r.to_string();
}
// branchパラメータを取り出す
let mut branch = "".to_string();
if let Some(b) = self.prms.get("branch") {
branch = b;
}
Some((host, self.port, branch))
}
}
これでレスポンスの宛先を決めることができるようになりました。あとはバイト列に戻して送信するだけです。
先ほどの先頭のviaを削除したメソッドにまず処理を追加します。
// レスポンスを処理
fn proc_response(&self, msg: &mut Message) -> Option<()> {
// 先頭のViaを取り除く(リクエスト中継時に自分が挿入したViaのはず)
match msg.search("via", "v") {
None => {
return Some(()); // Viaがないレスポンスは中継できないのでおしまい
}
Some(viapos) => {
// 消す
msg.hdrs[viapos].vals.remove(0);
// 消した結果、そのヘッダに値がなくなってしまったら、そのヘッダ自体を消す
if msg.hdrs[viapos].vals.len() == 0 {
msg.hdrs.remove(viapos);
}
}
}
- // 続く
+ // 新たに先頭になったViaを調べる
+ match msg.search("via", "v") {
+ None => {
+ return Some(()); // Viaがないレスポンスは中継できないのでおしまい
+ }
+ Some(viapos) => {
+ let mut via = Via::new();
+ via.parse(&msg.hdrs[viapos].vals[0])?;
+ let (host, port, _) = via.get()?;
+ self.send_message(host, port, &msg.to_string());
+ }
+ }
+ Some(())
+ }
そして送信するメソッドはこんな感じです。
// 送信
fn send_message(&self, host: String, port: u16, buf: &str) -> Option<()> {
// 自分宛の場合は送信しない
if self.ip == host && self.port == port {
return None;
}
// 送信先アドレスとメッセージの内容を標準出力にはく
println!(">>>>>>>>>> {}:{} >>>>>", host, port);
println!("{}", buf);
// 文字列化しながら送信
let _ = self
.sock
.send_to(buf.as_bytes(), format!("{}:{}", host, port));
Some(())
}
Messageという構造体で管理しているデータを文字列化する関数も必要です。これはリクエストにも対応しておきます。
impl std::fmt::Display for Message {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
static CRLF: &str = "\r\n";
let mut s = String::new();
for h in self.hdrs.iter() {
s.push_str(h.to_string().as_str());
}
if self.stcode != "" && self.reason != "" {
// レスポンスの場合
return write!(
f,
"SIP/2.0 {} {}{}{}{}{}",
self.stcode, self.reason, CRLF, s, CRLF, self.body
);
}
// リクエストの場合
write!(
f,
"{} {} SIP/2.0{}{}{}{}",
self.method, self.requri, CRLF, s, CRLF, self.body
)
}
}
この中でHeaderという構造体で管理しているデータも文字列化したいので、この関数も実装しておきます。
impl std::fmt::Display for Header {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
static CRLF: &str = "\r\n";
write!(f, "{}: {}{}", self.name, self.vals.join(", "), CRLF)
}
}
これでレスポンスの処理はおしまいです。
リクエストの処理
Proxyとしてのリクエストの処理はRFC3261のPage 94から始まる「16.3 Request Validation」から「16.6 Request Forwarding」にかけて書いてあり、さらにStatlessの場合はPage 116から始まる「16.11 Stateless Proxy」にも説明があります。また、sotafugy8はregistrarを内包するので、自身宛のREGISTERについては他のリクエストとは異なる処理を実行します。
RFC3261のプロキシの説明は実行すべき順に説明が書いてありますが、sotafugy8では実装上の都合から、それとは異なる順序で処理を実行していきます。
Viaの編集
このリクエストを中継した後に誰かが返送してきたレスポンスが自分自身に届くように、自分のアドレスをViaヘッダに追加します。RFC3261に記述されている順序とは異なりますが、リクエストの処理中に何らかの理由でレスポンスを生成する可能性があり、その際に先にViaを追加してからレスポンスを生成するようにすれば、そのレスポンスをあたかも受信したレスポンスかのようにみなして先ほど作ったproc_responseメソッドに渡すだけで処理させることができるように真っ先に実行することにしました。
RFC3261のPage 145に以下のような記述があります。
When the server transport receives a request over any transport, it
MUST examine the value of the "sent-by" parameter in the top Via
header field value. If the host portion of the "sent-by" parameter
contains a domain name, or if it contains an IP address that differs
from the packet source address, the server MUST add a "received"
parameter to that Via header field value. This parameter MUST
contain the source address from which the packet was received. This
is to assist the server transport layer in sending the response,
since it must be sent to the source IP address from which the request
came.
sent-byのhost部がドメイン名になっていたり送信元IPアドレスが異なっていた場合にはreceivedパラメータを追加してね、と言っています。sotafugy8はhost部が何であろうと無条件にreceivedパラメータを追加することにします。
先ほどレスポンスを処理する際にViaヘッダを取り扱う処理を実装しているので、これを使って解析、receivedパラメータを追加してから文字列に戻します。
まず文字列化する関数を作っておきます
impl std::fmt::Display for Via {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"SIP/2.0/UDP {}:{}{}",
self.host,
self.port,
self.prms.to_string()
)
}
}
そして、viaヘッダを探し、解析し、receivedパラメータを追加し、文字列にして戻す処理を作ります
// Top-Viaからbranchを取り出しつつreceivedを追加。branchのハッシュを計算して新たに自アドレスをTopViaとして挿入
fn update_via(&self, msg: &mut Message, saddr: &SocketAddr) -> Option<()> {
// viaヘッダを解析(ない場合はエラー)
let viapos = msg.search("via", "v")?;
let mut via = Via::new();
via.parse(&msg.hdrs[viapos].vals[0])?;
// receivedパラメータを追加
via.prms.set("received", Some(saddr.ip().to_string()));
msg.hdrs[viapos].vals = vec![via.to_string()];
// 続く
}
続いて、自身のアドレスを追加します。これによりこのリクエストに対応するレスポンスが自身に戻ってくるようになります。Viaヘッダの値にはbranchというパラメータが必要で、ユニークな値にしなくてはならないのですが、Stateless Proxyは受け取った信号をいじってから送信するだけのものであり、ユニークにするのが難しいので、 RFC3261のPage 116にこんなことが書いてあります。
The stateless proxy MAY use any technique it likes to guarantee
uniqueness of its branch IDs across transactions. However, the
following procedure is RECOMMENDED. The proxy examines the
branch ID in the topmost Via header field of the received
request. If it begins with the magic cookie, the first
component of the branch ID of the outgoing request is computed
as a hash of the received branch ID.
このように、受信したメッセージの先頭のViaにあるbranchパラメータを取り出して、そのハッシュを計算して使うことを推奨されています。(If it begins with the magic cookie,
というのは、RFC3261準拠なら、といっているのと同義で、sotafugy8は通信相手がRFC3261準拠であることを前提にしているのでこの方法で良いということになります)
従って、まずは受け取ったリクエストの先頭のViaからbranchパラメータの値を取り出したい、ということになります。実はサラッと流して誤魔化しましたが、このbranchパラメータの値を取り出す処理はレスポンスを処理する際にその宛先を判定するために作ったメソッドで取得できるようにしておきました。
先ほどのupdate_viaというメソッドの続きを実装します。
// Top-Viaからbranchを取り出しつつreceivedを追加。branchのハッシュを計算して新たに自アドレスをTopViaとして挿入
fn update_via(&self, msg: &mut Message, saddr: &SocketAddr) -> Option<()> {
// viaヘッダを解析(ない場合はエラー)
let viapos = msg.search("via", "v")?;
let mut via = Via::new();
via.parse(&msg.hdrs[viapos].vals[0])?;
// receivedパラメータを追加
via.prms.set("received", Some(saddr.ip().to_string()));
msg.hdrs[viapos].vals = vec![via.to_string()];
- // 続く
+ // 自分のアドレスを追加する上で必要な情報(branch)を取得
+ let (_, _, mut branch) = via.get()?;
+ // 自身の情報をViaヘッダの先頭に追加
+ branch = format!("{:x}", md5::compute(format!("xxx{}", branch).as_bytes()));
+ let tmp = format!(
+ "SIP/2.0/UDP {}:{};branch=z9hG4bK{}",
+ self.ip, self.port, branch
+ );
+ msg.hdrs[viapos].vals.insert(0, tmp);
+ Some(())
}
Proxy-Requireヘッダの処理
Proxyに何らかの拡張への対応を求める場合に、Proxy-Requireというヘッダにその拡張の名前が列挙されたリクエストが送られてくる場合があります。RFC3261のPage 95から96にかけてこんな感じで書いてあります。
5. Proxy-Require check
Future extensions to this protocol may introduce features that
require special handling by proxies. Endpoints will include a
Proxy-Require header field in requests that use these features,
telling the proxy not to process the request unless the feature is
understood.
If the request contains a Proxy-Require header field (Section
20.29) with one or more option-tags this element does not
understand, the element MUST return a 420 (Bad Extension)
response. The response MUST include an Unsupported (Section
20.40) header field listing those option-tags the element did not
understand.
sotafugy8は拡張の類には対応しないことにしたので、上記の通り、420エラーレスポンスを返すことにします。
まずMessageという構造体にリクエストからレスポンスを生成するメソッドを実装します。
RFC3261のPage 50に以下のような説明があります。
8.2.6.2 Headers and Tags
The From field of the response MUST equal the From header field of
the request. The Call-ID header field of the response MUST equal the
Call-ID header field of the request. The CSeq header field of the
response MUST equal the CSeq field of the request. The Via header
field values in the response MUST equal the Via header field values
in the request and MUST maintain the same ordering.
If a request contained a To tag in the request, the To header field
in the response MUST equal that of the request. However, if the To
header field in the request did not contain a tag, the URI in the To
header field in the response MUST equal the URI in the To header
field; additionally, the UAS MUST add a tag to the To header field in
the response (with the exception of the 100 (Trying) response, in
which a tag MAY be present). This serves to identify the UAS that is
responding, possibly resulting in a component of a dialog ID. The
same tag MUST be used for all responses to that request, both final
and provisional (again excepting the 100 (Trying)). Procedures for
the generation of tags are defined in Section 19.3.
From、Call-ID、CSeq、Viaはコピー。Toヘッダはtagがある場合はコピー、無い場合はtagを生成してToにくっつけてからコピー。Statelessでタグを生成する方法についてはPage 51に
o To header tags MUST be generated for responses in a stateless
manner - in a manner that will generate the same tag for the
same request consistently. For information on tag construction
see Section 19.3.
と、書いてあります。しかし、Section 19.3を見てみると、、
Besides the requirement for global uniqueness, the algorithm for
generating a tag is implementation-specific.
ということで具体的なことは書いてありませんでした。
他の箇所も含め、tagの具体的な生成方法については見つけることができませんでした。branchの生成方法は具体的な説明があったので、見落としているだけかもしれませんが。。
しょうがないのでsotafugy8ではCall-IDのハッシュを計算してtagに使うことにします。(前段にproxyがいてforkingなどしていたらまずいかもしれませんが、ちょっとよくわかりませんでした。いずれ勉強しておきます。)
// この信号(リクエスト)からレスポンスを生成する
fn gen_resp(&self, stcode: &str, reason: &str) -> Option<Message> {
if self.method == "ACK" {
return None;
}
let resp_hdrs: Vec<&str> = vec![
"call-id",
"i",
"from",
"f",
"to",
"t",
"via",
"v",
"cseq",
"record-route",
];
let mut resp = Message::new();
(resp.stcode, resp.reason) = (stcode.to_string(), reason.to_string());
for hdr in self.hdrs.iter() {
if resp_hdrs.contains(&hdr.name.to_lowercase().as_str()) {
let mut h = Header::new();
h.set(&hdr.name, &hdr.vals);
resp.hdrs.push(h);
}
}
let mut content_length = Header::new();
content_length.set("l", &vec!["0".to_string()]);
resp.hdrs.push(content_length);
let topos = resp.search("to", "t")?;
let mut toaddr = NameAddr::new();
toaddr.parse(&resp.hdrs[topos].vals[0])?;
if None == toaddr.prms.get("tag") {
let cidpos = resp.search("call-id", "i")?;
let cid = &resp.hdrs[cidpos].vals[0];
let totag = format!("{:x}", md5::compute(format!("yyy{}", cid).as_bytes()));
toaddr.prms.set("tag", Some(totag));
}
resp.hdrs[topos].vals = vec![toaddr.to_string()];
Some(resp)
}
この中で使っているNameAddrについては後ほど説明します(先に別の構造体の説明をしたほうがわかりやすいと思うので)。
これを使って、Proxy-Requireヘッダがあったら420レスポンスを返すメソッドを実装します。このときProxy-Requireヘッダに載っていた情報は丸ごとレスポンスのUnsupportedヘッダにコピーします。
fn proc_proxy_require(&self, msg: &mut Message) -> Option<()> {
// proxy-requireヘッダを探して、、
if let Some(prpos) = msg.search("proxy-require", "") {
// あったら420レスポンスを生成
let mut resp = msg.gen_resp("420", "Bad Extension")?;
// proxy-requireにのっていた文字列をunsupportedにコピー
let mut unsupported = Header::new();
unsupported.set("Unsupported", &msg.hdrs[prpos].vals);
resp.hdrs.push(unsupported);
self.proc_response(&mut resp);
return None;
}
Some(())
}
宛先は自分か?チェック
Request-URIを解析して、このメッセージがこのプロキシの管理下にあるユーザに宛てたものであるかを調べます。
sotafugy8はSIP-URIにしか対応しないので、RFC3261に書いてあるSIP-URIのフォーマットを以下に示します。
SIP-URI = "sip:" [ userinfo ] hostport
uri-parameters [ headers ]
userinfo = ( user / telephone-subscriber ) [ ":" password ] "@"
user = 1*( unreserved / escaped / user-unreserved )
user-unreserved = "&" / "=" / "+" / "$" / "," / ";" / "?" / "/"
password = *( unreserved / escaped /
"&" / "=" / "+" / "$" / "," )
hostport = host [ ":" port ]
host = hostname / IPv4address / IPv6reference
hostname = *( domainlabel "." ) toplabel [ "." ]
domainlabel = alphanum
/ alphanum *( alphanum / "-" ) alphanum
toplabel = ALPHA / ALPHA *( alphanum / "-" ) alphanum
IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT
uri-parameters = *( ";" uri-parameter)
uri-parameter = transport-param / user-param / method-param
/ ttl-param / maddr-param / lr-param / other-param
transport-param = "transport="
( "udp" / "tcp" / "sctp" / "tls"
/ other-transport)
other-transport = token
user-param = "user=" ( "phone" / "ip" / other-user)
other-user = token
method-param = "method=" Method
ttl-param = "ttl=" ttl
maddr-param = "maddr=" host
lr-param = "lr"
other-param = pname [ "=" pvalue ]
pname = 1*paramchar
pvalue = 1*paramchar
paramchar = param-unreserved / unreserved / escaped
param-unreserved = "[" / "]" / "/" / ":" / "&" / "+" / "$"
headers = "?" header *( "&" header )
header = hname "=" hvalue
hname = 1*( hnv-unreserved / unreserved / escaped )
hvalue = *( hnv-unreserved / unreserved / escaped )
hnv-unreserved = "[" / "]" / "/" / "?" / ":" / "+" / "$"
alphanum = ALPHA / DIGIT
unreserved = alphanum / mark
mark = "-" / "_" / "." / "!" / "~" / "*" / "'"
/ "(" / ")"
escaped = "%" HEXDIG HEXDIG
ではSIP-URIを解析しデータを保持するための構造体を定義することにします。解析処理については本気でやると面倒なので、またまたざっくりな解析になっています。
lazy_static! {
static ref RE_SIPURI: Regex =
Regex::new(r"^sip:(((?<user>[^:@]+)(:([^@]+))?)@)?(?<host>[^:;]+)(:(?<port>\d+))?")
.expect("failed to build Regex for SIPURI");
}
struct SIPURI {
user: String,
host: String,
port: u16,
prms: String, // 中身を解釈しないので丸ごと文字列のまま保持
}
impl std::fmt::Display for SIPURI {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let s;
if self.user != "" {
s = format!("sip:{}@{}:{}", self.user, self.host, self.port);
} else {
s = format!("sip:{}:{}", self.host, self.port);
}
write!(f, "{}{}", s, self.prms)
}
}
impl SIPURI {
fn new() -> Self {
Self {
user: "".to_string(),
host: "".to_string(),
port: 5060,
prms: "".to_string(),
}
}
// 解析
fn parse(&mut self, buf: &str) -> Option<()> {
let caps = RE_SIPURI.captures(&buf)?;
if let Some(u) = caps.name("user") {
self.user = u.as_str().to_string();
} else {
self.user = "".to_string();
}
self.host = caps.name("host")?.as_str().to_string();
self.port = 5060; // デフォルトポートは5060
if let Some(tmp) = caps.name("port") {
if let Ok(p) = tmp.as_str().to_string().parse::<u16>() {
self.port = p; // あったら上書き
} else {
return None;
}
}
self.prms = buf[caps.get(0)?.len()..].to_string();
Some(())
}
}
これでRequest-URIを解析してhostとportを取り出す準備が整いました。こんな感じで使います。
let mut requri = SIPURI::new();
requri.parse(&msg.requri)?;
if requri.host == self.ip && requri.port == self.port {
// 自分宛だった場合の処理をここにかく
}
Registrarとしての動作
このあとで中継処理を見ていきますが、そのなかで送信されてきた信号を投げる先を判断する必要が生じますが、クライアントはREGISTERというリクエストをRegistrarに送信してクライアント自身のアドレスを登録します。RFC3261の1295行目あたりに以下のようにRegistrarが説明されています。
Registrar: A registrar is a server that accepts REGISTER requests
and places the information it receives in those requests into
the location service for the domain it handles.
より詳細についてはRFC3261のPage 63から始まる「10.3 Processing REGISTER Requests」に書いてあります。sotafugy8は(proxyとして動作する場合と同様に)そのごく一部のみを実装することにします。具体的には、以下のような感じです。
- REGISTERからToヘッダのuser部と、Contactヘッダの値(複数ある場合はその先頭にある値)を抽出する
- Contactヘッダの値が「*」だった場合は現在保持しているuser部に対応するContact AddressをContactヘッダに載せて応答する
- Contactヘッダの値がcontact-paramだった場合はそのSIP-URIをContact Addressとして扱う
これを実現するにはToとContactを解析する必要があるのでそのフォーマットを見てみます。RFC3261のBNFから関連の箇所を拾い集めてみました。
To = ( "To" / "t" ) HCOLON ( name-addr
/ addr-spec ) *( SEMI to-param )
to-param = tag-param / generic-param
tag-param = "tag" EQUAL token
Contact = ("Contact" / "m" ) HCOLON
( STAR / (contact-param *(COMMA contact-param)))
contact-param = (name-addr / addr-spec) *(SEMI contact-params)
name-addr = [ display-name ] LAQUOT addr-spec RAQUOT
addr-spec = SIP-URI / SIPS-URI / absoluteURI
display-name = *(token LWS)/ quoted-string
contact-params = c-p-q / c-p-expires
/ contact-extension
c-p-q = "q" EQUAL qvalue
c-p-expires = "expires" EQUAL delta-seconds
contact-extension = generic-param
delta-seconds = 1*DIGIT
generic-param = token [ EQUAL gen-value ]
gen-value = token / host / quoted-string
ContactがSTARの場合と、Contactに複数のcontact-paramが載りうることを除いては、ToもContactもname-addrにパラメータがくっつく場合があるものであることが分かります。name-addrに含まれるSIP-URIの部分については、Request-URIを解析する際に実装したSIPURIという構造体を利用し、またパラメータ部分はGenericParamsという構造体を使うことにします。
lazy_static! {
static ref RE_NAMEADDR: Regex =
Regex::new(r#"^(([^"<]+|("([^"]|(\"))*")))?\s*<(?<sipuri>[^>]+)>\s*"#)
.expect("failed to build Regex for NAMEADDR");
}
struct NameAddr {
display_name: String,
sipuri: SIPURI,
prms: GenericParams,
}
impl std::fmt::Display for NameAddr {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let s: String;
if self.display_name == "" {
s = format!("<{}>", self.sipuri);
} else {
s = format!("\"{}\" <{}>", self.display_name, self.sipuri);
}
write!(f, "{}{}", s, self.prms)
}
}
impl NameAddr {
fn new() -> Self {
Self {
display_name: "".to_string(),
sipuri: SIPURI::new(),
prms: GenericParams::new(),
}
}
fn parse(&mut self, buf: &str) -> Option<()> {
if let Some(caps) = RE_NAMEADDR.captures(buf) {
// name-addrの正規表現にマッチした場合
if let Some(m) = caps.get(1) {
// display-nameがあった場合
self.display_name = m.as_str().trim().to_string();
} else {
self.display_name = "".to_string();
}
self.sipuri.parse(caps.name("sipuri")?.as_str());
self.prms.parse(&buf[caps.get(0)?.len()..])?;
return Some(());
}
// name-addrの正規表現にマッチしなかった場合
if let Some(l) = buf.find(";") {
// セミコロンがあったらそこまでがSIP-URI、それより後ろはパラメータ
self.sipuri.parse(&buf[..l]);
self.prms.parse(&buf[l..])?;
return Some(());
}
// セミコロンがないなら全てSIP-URI
self.sipuri.parse(buf);
Some(())
}
}
これを使ってRegistrarとしての処理を実装していきます。
fn proc_register(&mut self, msg: &Message) -> Option<()> {
// Toヘッダの値を解析
let topos = msg.search("to", "t")?;
let mut to = NameAddr::new();
to.parse(&msg.hdrs[topos].vals[0])?;
// Contactヘッダの値を取得
let mpos = msg.search("contact", "m")?;
if msg.hdrs[mpos].vals[0] == "*" {
// 登録されているContactアドレスを取得する
match self.locs.get(&to.sipuri.user) {
None => {
// いないので404エラーを返す
let mut resp = msg.gen_resp("404", "Not Found")?;
return self.proc_response(&mut resp);
}
Some(addr) => {
// 登録されていたので200を生成
let mut resp = msg.gen_resp("200", "OK")?;
// 登録されていたアドレスをContactヘッダに載せる
let mut contact = Header::new();
contact.set("m", &vec![addr.to_string()]);
resp.hdrs.push(contact);
return self.proc_response(&mut resp);
}
}
} else {
// REGISTERにContactアドレスが載っていたので解析して保存
let mut addr = NameAddr::new();
addr.parse(&msg.hdrs[mpos].vals[0])?;
self.locs.insert(to.sipuri.user, addr.sipuri.to_string());
// 200レスポンスを生成
let mut resp = msg.gen_resp("200", "OK")?;
// Contactアドレスを200レスポンスに載せる
let mut contact = Header::new();
contact.set("m", &vec![addr.sipuri.to_string()]);
resp.hdrs.push(contact);
self.proc_response(&mut resp);
}
None
}
Request-URIの更新
自分宛だけれどREGISTERではないリクエストについては、Request-URIが指しているユーザをLocation Serviceから探し出してRequest-URIをそのアドレスで書き換えます。Location Serviceに登録されていないユーザの場合は404レスポンスを返します。
こんな感じです。
if requri.host == self.ip && requri.port == self.port {
if msg.method == "REGISTER" {
return self.proc_register(&msg); // 自身宛のREGISTERはregistrarとして処理
} else {
// ユーザを探す
match self.locs.get(&requri.user) {
None => {
// 通信相手が見つからなかったので404レスポンスを返す
let mut resp = msg.gen_resp("404", "Not Found")?;
return self.proc_response(&mut resp);
}
Some(addr) => {
msg.requri = addr.clone();
}
}
}
}
Max-Forwardsの減算
RFC3261のPage 95にこんなことが書いてあります。
3. Max-Forwards check
The Max-Forwards header field (Section 20.22) is used to limit the
number of elements a SIP request can traverse.
If the request does not contain a Max-Forwards header field, this
check is passed.
If the request contains a Max-Forwards header field with a field
value greater than zero, the check is passed.
If the request contains a Max-Forwards header field with a field
value of zero (0), the element MUST NOT forward the request. If
the request was for OPTIONS, the element MAY act as the final
recipient and respond per Section 11. Otherwise, the element MUST
return a 483 (Too many hops) response.
Max-Forwardsヘッダがあって、もしその値が0だったら483エラーを返します。
さらにRFC3261のPage 100にこんなことが書いてあります。
3. Max-Forwards
If the copy contains a Max-Forwards header field, the proxy
MUST decrement its value by one (1).
If the copy does not contain a Max-Forwards header field, the
proxy MUST add one with a field value, which SHOULD be 70.
Some existing UAs will not provide a Max-Forwards header field
in a request.
Max-Forwardsヘッダの値を1減らす、なかったらその値が70であるものとみなす、ということです。
これを実施するメソッドを実装してみます。
fn update_max_forwards(&self, msg: &mut Message) -> Option<()> {
// max-forwardsヘッダを探す
match msg.search("max-forwards", "") {
None => {
// Max-Forwardsヘッダがない場合は70を設定しておしまい
let mut mf = Header::new();
mf.set("Max-Forwards", &vec!["70".to_string()]);
msg.hdrs.push(mf);
}
Some(mfpos) => {
// Max-Forwardsヘッダがあった場合、、
if let Ok(n) = msg.hdrs[mfpos].vals[0].parse::<u16>() {
if n <= 0 {
// 0だったら420エラーを返す
let mut resp = msg.gen_resp("420", "Too Many Hops")?;
self.proc_response(&mut resp);
return None;
}
// 1以上だったら1減算する
msg.hdrs[mfpos].vals = vec![(n - 1).to_string()];
}
}
}
Some(())
}
Routeの処理
RFC3261のPage 97に以下の記載があります。
If the first value in the Route header field indicates this proxy,
the proxy MUST remove that value from the request.
この前に、Request-URIが以前Record-Routeに挿入した値だった場合などの説明がありますが、sotafugy8はいずれも該当しないのでこれだけです。
アドレス解析処理をすでに実装してあるので簡単です。
fn update_route(&self, msg: &mut Message) -> Option<()> {
// routeヘッダを探す
if let Some(rpos) = msg.search("route", "") {
// 先頭のアドレスを解析
let mut addr = NameAddr::new();
addr.parse(&msg.hdrs[rpos].vals[0])?;
// 自身のアドレスと比較
if self.ip == addr.sipuri.host && self.port == addr.sipuri.port {
// 一致していたら削除
msg.pop("route", "");
}
}
Some(())
}
中継先の決定
RFC3261のPage 104に「7. Determine Next-Hop Address, Port, and Transport」というアイテムがあり、リクエストの送信先を決める方法が説明されています。sotafugy8はその説明のほとんどが当てはまらず、この記述に従えばよさそうです。
the proxy MUST apply
the procedures to the first value in the Route header field, if
present, else the Request-URI.
Routeヘッダがあればその先頭のアドレス、なければRequest-URIが示すアドレスを中継先t
fn get_target(&self, msg: &mut Message) -> Option<(String, u16)> {
// routeヘッダがある場合はその先頭のアドレスが宛先
if let Some(rpos) = msg.search("route", "") {
let mut addr = NameAddr::new();
addr.parse(&msg.hdrs[rpos].vals[0])?;
return Some((addr.sipuri.host, addr.sipuri.port));
}
// routeヘッダがない場合はrequest-uriが宛先
let mut target = SIPURI::new();
target.parse(&msg.requri)?;
Some((target.host, target.port))
}
送信
編集が終わり宛先も決まったので送信します。送信する処理はレスポンスの送信の時にリクエストへの対応もしてあるので、そのメソッドを呼ぶだけです。
ここまでに実装した関数も含めて呼び出してリクエストの処理を完成させます。
fn proc_request(&mut self, msg: &mut Message, saddr: &SocketAddr) -> Option<()> {
self.update_via(msg, &saddr)?;
self.proc_proxy_require(msg)?;
let mut requri = SIPURI::new();
requri.parse(&msg.requri)?;
if requri.host == self.ip && requri.port == self.port {
if msg.method == "REGISTER" {
return self.proc_register(&msg); // 自身宛のREGISTERはregistrarとして処理
} else {
// ユーザを探す
match self.locs.get(&requri.user) {
None => {
// 通信相手が見つからなかったので404レスポンスを返す
let mut resp = msg.gen_resp("404", "Not Found")?;
return self.proc_response(&mut resp);
}
Some(addr) => {
msg.requri = addr.clone();
}
}
}
}
self.update_max_forwards(msg)?;
self.update_route(msg)?;
let (host, port) = self.get_target(msg)?;
return self.send_message(host, port, &msg.to_string());
}
起動
起動パラメータを使ってbindするIPアドレスとポートを受けとるようにします。IPアドレスはこのsotafugy8が管理するドメインとしても使用します。
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() >=3 {
if let Ok(port) = args[2].parse::<u16>() {
let mut px = Proxy::new(&args[1], port);
px.run();
}
}
}
これで完成です。
AppStoreでSIPフォンを探して手持ちのiPhoneとiPadの双方にインストール、動かしてみたら、無事通話できました。よかった。
まとめ
SIPの仕様をどうにか思い出しながら、いろいろ機能を限定しつつRustでstateless proxyを実装、適当にみつけたSIPフォン間での通話を成功させることができました。
どうにか動く物は作れましたが、手を抜きまくっているし間違えているところもあるでしょうし、いまもよくわかっていない点も多々ありますので、今後も学習を継続していきたいと思います。
ほぼ私の趣味のプログラミングのトレースでしかない記事となりましたが、どなたかの参考あるいは暇つぶしになれば幸いです。
最後に、今回作ったソースコード全体を記しておきます。クリックすると展開されます。
完成したソースコード
use lazy_static::lazy_static;
use regex::Regex;
use std::collections::HashMap;
use std::env;
use std::net::SocketAddr;
use std::net::UdpSocket;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() >= 3 {
if let Ok(port) = args[2].parse::<u16>() {
let mut px = Proxy::new(&args[1], port);
px.run();
}
}
}
// 各種解析に使っている正規表現たち
lazy_static! {
static ref RE_SL: Regex = Regex::new(r"^(([^ ]+) ([^ ]+) )?SIP/2\.0( (\d\d\d) ([^\r\n]+))?")
.expect("failed to build Regex for Start-Line");
static ref RE_HVALUE: Regex = Regex::new(r#"^(([^",]|("([^"]|(\"))*"))+)(\s*,\s*)?"#)
.expect("failed to build Regex for HVALUE");
static ref RE_VIA: Regex =
Regex::new(r"^SIP\s*/\s*2\.0\s*/\s*UDP\s+(?<host>[^ \s:;]+)(\s*:\s*(?<port>\d+))")
.expect("failed to build Regex for VIA");
static ref RE_NAMEADDR: Regex =
Regex::new(r#"^(([^"<]+|("([^"]|(\"))*")))?\s*<(?<sipuri>[^>]+)>\s*"#)
.expect("failed to build Regex for NAMEADDR");
static ref RE_SIPURI: Regex =
Regex::new(r"^sip:(((?<user>[^:@]+)(:([^@]+))?)@)?(?<host>[^:;]+)(:(?<port>\d+))?")
.expect("failed to build Regex for SIPURI");
static ref RE_PRMS: Regex =
Regex::new(r#"^;\s*(?<name>[^\s=;]+)\s*(=\s*(?<value>([^\s";]+|"([^"]|(\"))*")))?\s*"#)
.expect("faield to build Regex for PRMS");
}
// プロキシ
struct Proxy {
ip: String,
port: u16,
sock: UdpSocket,
locs: HashMap<String, String>,
}
impl Proxy {
fn new(ip: &str, port: u16) -> Self {
let hostport = format!("{}:{}", ip, port);
Self {
ip: String::from(ip),
port: port,
sock: UdpSocket::bind(hostport).unwrap(),
locs: HashMap::new(),
}
}
fn run(&mut self) {
let mut counter = 0;
let mut buf = [0; 0xffff];
loop {
if let Ok((rsize, saddr)) = self.sock.recv_from(&mut buf) {
if let Ok(s) = std::str::from_utf8(&buf[..rsize]) {
self.proc(s, saddr);
counter += 1;
println!("{}", counter);
}
}
}
}
fn proc(&mut self, s: &str, saddr: SocketAddr) -> Option<()> {
println!(">>> {} >>>>>>>>\r\n{}", saddr.to_string(), s);
let mut msg = Message::new();
if let Some(_) = msg.parse(s) {
if msg.method != "" && msg.requri != "" {
self.proc_request(&mut msg, &saddr);
} else if msg.stcode != "" && msg.reason != "" {
self.proc_response(&mut msg);
}
}
Some(())
}
// リクエストの処理
fn proc_request(&mut self, msg: &mut Message, saddr: &SocketAddr) -> Option<()> {
self.update_via(msg, &saddr)?;
self.proc_proxy_require(msg)?;
let mut requri = SIPURI::new();
requri.parse(&msg.requri)?;
if requri.host == self.ip && requri.port == self.port {
if msg.method == "REGISTER" {
return self.proc_register(&msg); // 自身宛のREGISTERはregistrarとして処理
} else {
// ユーザを探す
match self.locs.get(&requri.user) {
None => {
// 通信相手が見つからなかったので404レスポンスを返す
let mut resp = msg.gen_resp("404", "Not Found")?;
return self.proc_response(&mut resp);
}
Some(addr) => {
msg.requri = addr.clone();
}
}
}
}
self.update_max_forwards(msg)?;
self.update_route(msg)?;
let (host, port) = self.get_target(msg)?;
return self.send_message(host, port, &msg.to_string());
}
// Proxy-Requireが載っていたら420レスポンスを返す
fn proc_proxy_require(&self, msg: &mut Message) -> Option<()> {
// proxy-requireヘッダを探して、、
if let Some(prpos) = msg.search("proxy-require", "") {
// あったら420レスポンスを生成
let mut resp = msg.gen_resp("420", "Bad Extension")?;
// proxy-requireにのっていた文字列をunsupportedにコピー
let mut unsupported = Header::new();
unsupported.set("Unsupported", &msg.hdrs[prpos].vals);
resp.hdrs.push(unsupported);
self.proc_response(&mut resp);
return None;
}
Some(())
}
// REGISTRARとしての処理
fn proc_register(&mut self, msg: &Message) -> Option<()> {
// Toヘッダの値を解析
let topos = msg.search("to", "t")?;
let mut to = NameAddr::new();
to.parse(&msg.hdrs[topos].vals[0])?;
// Contactヘッダの値を取得
let mpos = msg.search("contact", "m")?;
if msg.hdrs[mpos].vals[0] == "*" {
// 登録されているContactアドレスを取得する
match self.locs.get(&to.sipuri.user) {
None => {
// いないので404エラーを返す
let mut resp = msg.gen_resp("404", "Not Found")?;
return self.proc_response(&mut resp);
}
Some(addr) => {
// 登録されていたので200を生成
let mut resp = msg.gen_resp("200", "OK")?;
// 登録されていたアドレスをContactヘッダに載せる
let mut contact = Header::new();
contact.set("m", &vec![addr.to_string()]);
resp.hdrs.push(contact);
return self.proc_response(&mut resp);
}
}
} else {
// REGISTERにContactアドレスが載っていたので解析して保存
let mut addr = NameAddr::new();
addr.parse(&msg.hdrs[mpos].vals[0])?;
self.locs.insert(to.sipuri.user, addr.sipuri.to_string());
// 200レスポンスを生成
let mut resp = msg.gen_resp("200", "OK")?;
// Contactアドレスを200レスポンスに載せる
let mut contact = Header::new();
contact.set("m", &vec![addr.sipuri.to_string()]);
resp.hdrs.push(contact);
self.proc_response(&mut resp);
}
None
}
// Max-Forwardsの減算処理
fn update_max_forwards(&self, msg: &mut Message) -> Option<()> {
// max-forwardsヘッダを探す
match msg.search("max-forwards", "") {
None => {
// Max-Forwardsヘッダがない場合は70を設定しておしまい
let mut mf = Header::new();
mf.set("Max-Forwards", &vec!["70".to_string()]);
msg.hdrs.push(mf);
}
Some(mfpos) => {
// Max-Forwardsヘッダがあった場合、、
if let Ok(n) = msg.hdrs[mfpos].vals[0].parse::<u16>() {
if n <= 0 {
// 0だったら420エラーを返す
let mut resp = msg.gen_resp("420", "Too Many Hops")?;
self.proc_response(&mut resp);
return None;
}
// 1以上だったら1減算する
msg.hdrs[mfpos].vals = vec![(n - 1).to_string()];
}
}
}
Some(())
}
// routeヘッダの更新
fn update_route(&self, msg: &mut Message) -> Option<()> {
// routeヘッダを探す
if let Some(rpos) = msg.search("route", "") {
// 先頭のアドレスを解析
let mut addr = NameAddr::new();
addr.parse(&msg.hdrs[rpos].vals[0])?;
// 自身のアドレスと比較
if self.ip == addr.sipuri.host && self.port == addr.sipuri.port {
// 一致していたら削除
msg.pop("route", "");
}
}
Some(())
}
// リクエストの宛先の決定
fn get_target(&self, msg: &mut Message) -> Option<(String, u16)> {
// routeヘッダがある場合はその先頭のアドレスが宛先
if let Some(rpos) = msg.search("route", "") {
let mut addr = NameAddr::new();
addr.parse(&msg.hdrs[rpos].vals[0])?;
return Some((addr.sipuri.host, addr.sipuri.port));
}
// routeヘッダがない場合はrequest-uriが宛先
let mut target = SIPURI::new();
target.parse(&msg.requri)?;
Some((target.host, target.port))
}
// レスポンスを処理
fn proc_response(&self, msg: &mut Message) -> Option<()> {
// 先頭のViaを取り除く(リクエスト中継時に自分が挿入したViaのはず)
match msg.search("via", "v") {
None => {
return Some(()); // Viaがないレスポンスは中継できないのでおしまい
}
Some(viapos) => {
// 消す
msg.hdrs[viapos].vals.remove(0);
// 消した結果、そのヘッダに値がなくなってしまったら、そのヘッダ自体を消す
if msg.hdrs[viapos].vals.len() == 0 {
msg.hdrs.remove(viapos);
}
}
}
// 新たに先頭になったViaを調べる
match msg.search("via", "v") {
None => {
return Some(()); // Viaがないレスポンスは中継できないのでおしまい
}
Some(viapos) => {
let mut via = Via::new();
via.parse(&msg.hdrs[viapos].vals[0])?;
let (host, port, _) = via.get()?;
self.send_message(host, port, &msg.to_string());
}
}
Some(())
}
// 送信
fn send_message(&self, host: String, port: u16, buf: &str) -> Option<()> {
// 自分宛の場合は送信しない
if self.ip == host && self.port == port {
return None;
}
// 送信先アドレスとメッセージの内容を標準出力にはく
println!(">>>>>>>>>> {}:{} >>>>>", host, port);
println!("{}", buf);
// 文字列化しながら送信
let _ = self
.sock
.send_to(buf.as_bytes(), format!("{}:{}", host, port));
Some(())
}
// Top-Viaからbranchを取り出しつつreceivedを追加。branchのハッシュを計算して新たに自アドレスをTopViaとして挿入
fn update_via(&self, msg: &mut Message, saddr: &SocketAddr) -> Option<()> {
// viaヘッダを解析(ない場合はエラー)
let viapos = msg.search("via", "v")?;
let mut via = Via::new();
via.parse(&msg.hdrs[viapos].vals[0])?;
// receivedパラメータを追加
via.prms.set("received", Some(saddr.ip().to_string()));
msg.hdrs[viapos].vals = vec![via.to_string()];
// 自分のアドレスを追加する上で必要な情報(branch)を取得
let (_, _, mut branch) = via.get()?;
// 自身の情報をViaヘッダの先頭に追加
branch = format!("{:x}", md5::compute(format!("xxx{}", branch).as_bytes()));
let tmp = format!(
"SIP/2.0/UDP {}:{};branch=z9hG4bK{}",
self.ip, self.port, branch
);
msg.hdrs[viapos].vals.insert(0, tmp);
Some(())
}
}
// SIPメッセージ
struct Message {
method: String, // start-line(Request-Line)のMethod
requri: String, // start-line(Request-Line)のRequest-URI
stcode: String, // start-line(Status-Line)のStatus Code
reason: String, // start-line(Status-Line)のReason Phrease
hdrs: Vec<Header>, // message-headers
body: String, // message-body
}
impl std::fmt::Display for Message {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
static CRLF: &str = "\r\n";
let mut s = String::new();
for h in self.hdrs.iter() {
s.push_str(h.to_string().as_str());
}
if self.stcode != "" && self.reason != "" {
// レスポンスの場合
return write!(
f,
"SIP/2.0 {} {}{}{}{}{}",
self.stcode, self.reason, CRLF, s, CRLF, self.body
);
}
// リクエストの場合
write!(
f,
"{} {} SIP/2.0{}{}{}{}",
self.method, self.requri, CRLF, s, CRLF, self.body
)
}
}
impl Message {
fn new() -> Self {
Self {
method: "".to_string(),
requri: "".to_string(),
stcode: "".to_string(),
reason: "".to_string(),
hdrs: vec![],
body: "".to_string(),
}
}
// 解析
fn parse(&mut self, buf: &str) -> Option<()> {
// 全体を分割
let (sl, hdrs, body) = Self::split(&buf)?;
// Start-Lineの解析
self.parse_start_line(sl)?;
// ヘッダの解析
for h in hdrs.iter() {
let mut hdr = Header::new();
hdr.parse(&h)?;
self.hdrs.push(hdr);
}
// ボディの保持
self.body = body.to_string();
Some(())
}
// SIPメッセージ全体をstart-line、message-header、bodyへの分割を想定
fn split(buf: &str) -> Option<(&str, Vec<&str>, &str)> {
let mut pos = 0;
let mut margin = 0;
let mut lines: Vec<&str> = vec![];
static CRLF: &str = "\r\n";
loop {
let tmp = buf[pos+margin..].find(CRLF)?;
if tmp == 0 {
// empty-line
break
}
let next_line: &str = &buf[pos+margin+tmp+CRLF.len()..];
if next_line.starts_with(" ") || next_line.starts_with("\t") {
margin += tmp + CRLF.len();
} else {
lines.push(&buf[pos..pos+margin+tmp]);
pos += margin + tmp + CRLF.len();
margin = 0;
}
}
let sl = lines.remove(0); // start-lineを抜き出す
Some((sl, lines, &buf[pos + 2..])) // 2は"\r\n"の長さ
}
// start-lineの解析
fn parse_start_line(&mut self, buf: &str) -> Option<()> {
let cap = RE_SL.captures(buf)?;
if let Some(m) = cap.get(2) {
// requestの場合
self.method = m.as_str().to_string();
self.requri = cap.get(3)?.as_str().to_string();
} else if let Some(m) = cap.get(5) {
// responseの場合
self.stcode = m.as_str().to_string();
self.reason = cap.get(6)?.as_str().to_string();
}
Some(())
}
// 指定されたヘッダを探して配列の何番目かを返す。ヘッダ名は小文字で渡すこと。
fn search(&self, hname1: &str, hname2: &str) -> Option<usize> {
for (i, hn) in self.hdrs.iter().enumerate() {
let hname = hn.name.to_lowercase();
if hname == hname1 || hname == hname2 {
return Some(i);
}
}
None // 見つからなかった
}
// 指定されたヘッダの先頭の要素を取り除きながら返す
fn pop(&mut self, hname1: &str, hname2: &str) -> Option<String> {
let i = self.search(hname1, hname2)?;
let v = self.hdrs[i].vals[0].clone();
self.hdrs[i].vals.remove(0);
if self.hdrs[i].vals.len() == 0 {
self.hdrs.remove(i);
}
return Some(v);
}
// この信号(リクエスト)からレスポンスを生成する
fn gen_resp(&self, stcode: &str, reason: &str) -> Option<Message> {
if self.method == "ACK" {
return None;
}
let resp_hdrs: Vec<&str> = vec![
"call-id",
"i",
"from",
"f",
"to",
"t",
"via",
"v",
"cseq",
"record-route",
];
let mut resp = Message::new();
(resp.stcode, resp.reason) = (stcode.to_string(), reason.to_string());
for hdr in self.hdrs.iter() {
if resp_hdrs.contains(&hdr.name.to_lowercase().as_str()) {
let mut h = Header::new();
h.set(&hdr.name, &hdr.vals);
resp.hdrs.push(h);
}
}
let mut content_length = Header::new();
content_length.set("l", &vec!["0".to_string()]);
resp.hdrs.push(content_length);
let topos = resp.search("to", "t")?;
let mut toaddr = NameAddr::new();
toaddr.parse(&resp.hdrs[topos].vals[0])?;
if None == toaddr.prms.get("tag") {
let cidpos = resp.search("call-id", "i")?;
let cid = &resp.hdrs[cidpos].vals[0];
let totag = format!("{:x}", md5::compute(format!("yyy{}", cid).as_bytes()));
toaddr.prms.set("tag", Some(totag));
}
resp.hdrs[topos].vals = vec![toaddr.to_string()];
Some(resp)
}
}
// message-headerの解析&編集用
struct Header {
name: String, // header-name
vals: Vec<String>, // header-value
}
impl std::fmt::Display for Header {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
static CRLF: &str = "\r\n";
write!(f, "{}: {}{}", self.name, self.vals.join(", "), CRLF)
}
}
impl Header {
fn new() -> Self {
Self {
name: "".to_string(),
vals: vec![],
}
}
// 解析する
fn parse(&mut self, buf: &str) -> Option<()> {
let (name, value) = Self::split(buf)?;
self.name = name.to_string();
if vec!["via", "v", "contact", "m", "route"].contains(&name.to_lowercase().as_str()) {
self.vals = Self::parse_hvalue(value)?;
} else {
self.vals = vec![value.to_string()];
}
Some(())
}
// header-nameとheader-valueに分割する
fn split(buf: &str) -> Option<(&str, &str)> {
let vals: Vec<&str> = buf.splitn(2, ":").collect();
if vals.len() == 2 {
return Some((vals[0].trim_end(), vals[1].trim_start()));
}
None
}
// header-valueをCOMMAで分割して配列にする
fn parse_hvalue(buf: &str) -> Option<Vec<String>> {
let mut vals: Vec<String> = vec![];
let mut pos: usize = 0;
while buf[pos..].len() > 0 {
let caps = RE_HVALUE.captures(&buf[pos..])?;
vals.push(caps.get(1)?.as_str().to_string());
pos += caps.get(0)?.as_str().len();
}
Some(vals)
}
// このヘッダの名前と値を設定する
fn set(&mut self, name: &str, vals: &Vec<String>) {
self.name = name.to_string();
self.vals = vec![];
for val in vals.iter() {
self.vals.push(val.to_string());
}
}
}
// Viaヘッダの解析&編集用
struct Via {
host: String,
port: u16,
prms: GenericParams,
}
impl std::fmt::Display for Via {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"SIP/2.0/UDP {}:{}{}",
self.host,
self.port,
self.prms.to_string()
)
}
}
impl Via {
fn new() -> Self {
Self {
host: "".to_string(),
port: 5060,
prms: GenericParams::new(),
}
}
// 解析
fn parse(&mut self, buf: &str) -> Option<()> {
let caps = RE_VIA.captures(buf)?;
// hostを取り出す
self.host = caps.name("host")?.as_str().to_string();
// portがあったらその値を取り出し、なければデフォルトの5060を使う
self.port = 5060;
if let Some(m) = caps.name("port") {
match m.as_str().parse::<u16>() {
Ok(port) => {
self.port = port;
}
Err(_) => {
return None;
}
}
}
// パラメータ部分を解析
self.prms.parse(&buf[caps.get(0)?.as_str().len()..])?;
Some(())
}
// レスポンスの宛先とbranchパラメータを取得
fn get(&self) -> Option<(String, u16, String)> {
// 一旦hostを宛先アドレスとして、
let mut host = self.host.to_string();
if let Some(r) = self.prms.get("received") {
// receivedパラメータがある場合はその値で宛先アドレスを上書き
host = r.to_string();
}
// branchパラメータを取り出す
let mut branch = "".to_string();
if let Some(b) = self.prms.get("branch") {
branch = b;
}
Some((host, self.port, branch))
}
}
// ContactヘッダとToヘッダとRouteヘッダの解析&編集用
struct NameAddr {
display_name: String,
sipuri: SIPURI,
prms: GenericParams,
}
impl std::fmt::Display for NameAddr {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let s: String;
if self.display_name == "" {
s = format!("<{}>", self.sipuri);
} else {
s = format!("\"{}\" <{}>", self.display_name, self.sipuri);
}
write!(f, "{}{}", s, self.prms)
}
}
impl NameAddr {
fn new() -> Self {
Self {
display_name: "".to_string(),
sipuri: SIPURI::new(),
prms: GenericParams::new(),
}
}
fn parse(&mut self, buf: &str) -> Option<()> {
if let Some(caps) = RE_NAMEADDR.captures(buf) {
// name-addrの正規表現にマッチした場合
if let Some(m) = caps.get(1) {
// display-nameがあった場合
self.display_name = m.as_str().trim().to_string();
} else {
self.display_name = "".to_string();
}
self.sipuri.parse(caps.name("sipuri")?.as_str());
self.prms.parse(&buf[caps.get(0)?.len()..])?;
return Some(());
}
// name-addrの正規表現にマッチしなかった場合
if let Some(l) = buf.find(";") {
// セミコロンがあったらそこまでがSIP-URI、それより後ろはパラメータ
self.sipuri.parse(&buf[..l]);
self.prms.parse(&buf[l..])?;
return Some(());
}
// セミコロンがないなら全てSIP-URI
self.sipuri.parse(buf);
Some(())
}
}
// Request-URIとcontact-paramとToヘッダのname-addrとroute-paramで使う
struct SIPURI {
user: String,
host: String,
port: u16,
prms: String, // 中身を解釈しないので丸ごと文字列のまま保持
}
impl std::fmt::Display for SIPURI {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let s;
if self.user != "" {
s = format!("sip:{}@{}:{}", self.user, self.host, self.port);
} else {
s = format!("sip:{}:{}", self.host, self.port);
}
write!(f, "{}{}", s, self.prms)
}
}
impl SIPURI {
fn new() -> Self {
Self {
user: "".to_string(),
host: "".to_string(),
port: 5060,
prms: "".to_string(),
}
}
// 解析
fn parse(&mut self, buf: &str) -> Option<()> {
let caps = RE_SIPURI.captures(&buf)?;
if let Some(u) = caps.name("user") {
self.user = u.as_str().to_string();
} else {
self.user = "".to_string();
}
self.host = caps.name("host")?.as_str().to_string();
self.port = 5060; // デフォルトポートは5060
if let Some(tmp) = caps.name("port") {
if let Ok(p) = tmp.as_str().to_string().parse::<u16>() {
self.port = p; // あったら上書き
} else {
return None;
}
}
self.prms = buf[caps.get(0)?.len()..].to_string();
Some(())
}
}
// 各種パラメータの管理用
struct GenericParams {
elms: Vec<(String, Option<String>)>,
}
impl std::fmt::Display for GenericParams {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut s = "".to_string();
for prm in self.elms.iter() {
if let Some(v) = &prm.1 {
s.push_str(format!(";{}={}", prm.0, v).as_str());
} else {
s.push_str(format!(";{}", prm.0).as_str());
}
}
write!(f, "{}", s)
}
}
impl GenericParams {
fn new() -> Self {
Self { elms: vec![] }
}
// 解析
fn parse(&mut self, src: &str) -> Option<()> {
self.elms = vec![];
let mut pos = 0;
while src[pos..].len() > 0 {
// 正規表現にマッチさせて値を抽出する
let caps = RE_PRMS.captures(&src[pos..])?;
// パラメータ名を抽出
let name = caps.name("name")?.as_str().to_string();
// パラメータの値を抽出
if let Some(value) = caps.name("value") {
self.elms.push((name, Some(value.as_str().to_string())));
} else {
self.elms.push((name, None));
}
pos += caps.get(0)?.as_str().len();
}
Some(())
}
// 取得
fn get(&self, name: &str) -> Option<String> {
// 値がない要素がある場合と、その名前の要素がない場合の区別がつかない問題あり
// ただsotafugyでは前者を使うことがないので今は気にしない
for elm in self.elms.iter() {
if elm.0 == name {
return elm.1.clone();
}
}
None
}
// 設定
fn set(&mut self, name: &str, value: Option<String>) {
// 名前がnameのパラメータがあったらvalueで上書き。
for elm in self.elms.iter_mut() {
if elm.0 == name {
elm.1 = value;
return;
}
}
// 名前がnameのパラメータがなかったら新規挿入
self.elms.push((name.to_string(), value));
}
}
私はこのソースを基にトランザクションの実装やマルチスレッド化、RDBMS利用などの改造を加えて楽しい年末年始を過ごそうと思います!よいお年を!
出典
- RFC2234: The Internet Society (1997)
- RFC3261: The Internet Society (2002)
- RFC3665: The Internet Society (2003)
※ 記載されている会社名、製品名、サービス名は、各社の商標または登録商標です。