aboutsummaryrefslogtreecommitdiff
path: root/sip
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-11-19 02:08:52 +0100
committermetamuffin <metamuffin@disroot.org>2024-11-19 02:08:52 +0100
commitcbc111f90b5facc1f2a9dd79ced216279d6260af (patch)
treefa5a1d2d67874413d8e66673825c6789e8cc0945 /sip
parent2d9a31244eab6d3a9871369d3148de253e902d36 (diff)
downloadsip-rs-cbc111f90b5facc1f2a9dd79ced216279d6260af.tar
sip-rs-cbc111f90b5facc1f2a9dd79ced216279d6260af.tar.bz2
sip-rs-cbc111f90b5facc1f2a9dd79ced216279d6260af.tar.zst
move files + rtp parser
Diffstat (limited to 'sip')
-rw-r--r--sip/Cargo.toml19
-rw-r--r--sip/examples/Cargo.lock687
-rw-r--r--sip/examples/server.rs83
-rw-r--r--sip/src/encoding/headermap.rs60
-rw-r--r--sip/src/encoding/headers.rs182
-rw-r--r--sip/src/encoding/method.rs39
-rw-r--r--sip/src/encoding/mod.rs46
-rw-r--r--sip/src/encoding/request.rs57
-rw-r--r--sip/src/encoding/response.rs53
-rw-r--r--sip/src/encoding/status.rs63
-rw-r--r--sip/src/encoding/uri.rs55
-rw-r--r--sip/src/lib.rs4
-rw-r--r--sip/src/transaction/auth.rs51
-rw-r--r--sip/src/transaction/mod.rs87
-rw-r--r--sip/src/transport/mod.rs11
-rw-r--r--sip/src/transport/tcp.rs48
-rw-r--r--sip/src/transport/udp.rs35
17 files changed, 1580 insertions, 0 deletions
diff --git a/sip/Cargo.toml b/sip/Cargo.toml
new file mode 100644
index 0000000..c91634b
--- /dev/null
+++ b/sip/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "sip"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+tokio = { version = "1.38.0", features = ["full"] }
+anyhow = "1.0.86"
+log = "0.4.22"
+rand = "0.9.0-alpha.1"
+base64 = "0.22.1"
+md5 = "0.7.0"
+hex = "0.4.3"
+
+sdp = { workspace = true }
+rtp = { workspace = true }
+
+[dev-dependencies]
+env_logger = "0.11.3"
diff --git a/sip/examples/Cargo.lock b/sip/examples/Cargo.lock
new file mode 100644
index 0000000..9d21de3
--- /dev/null
+++ b/sip/examples/Cargo.lock
@@ -0,0 +1,687 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
+
+[[package]]
+name = "autocfg"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
+
+[[package]]
+name = "backtrace"
+version = "0.3.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+
+[[package]]
+name = "bytes"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
+
+[[package]]
+name = "cc"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
+
+[[package]]
+name = "env_filter"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "humantime",
+ "log",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gimli"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
+
+[[package]]
+name = "libc"
+version = "0.2.155"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
+
+[[package]]
+name = "lock_api"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+
+[[package]]
+name = "md5"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec 1.13.2",
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.0-alpha.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d31e63ea85be51c423e52ba8f2e68a3efd53eed30203ee029dd09947333693e"
+dependencies = [
+ "rand_chacha",
+ "rand_core",
+ "zerocopy",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0-alpha.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78674ef918c19451dbd250f8201f8619b494f64c9aa6f3adb28fd8a0f1f6da46"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.0-alpha.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc89dffba8377c5ec847d12bb41492bda235dba31a25e8b695cd0fe6589eb8c9"
+dependencies = [
+ "getrandom",
+ "zerocopy",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
+
+[[package]]
+name = "rtp"
+version = "0.1.0"
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "sdp"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "sip"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "base64",
+ "env_logger",
+ "hex",
+ "log",
+ "md5",
+ "rand",
+ "rtp",
+ "sdp",
+ "smallvec 2.0.0-alpha.7",
+ "tokio",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+
+[[package]]
+name = "smallvec"
+version = "2.0.0-alpha.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6a96cb564e12be8458b004bd829787ebe887de197d94516d2ba5a2a32235a0c"
+
+[[package]]
+name = "socket2"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tokio"
+version = "1.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "num_cpus",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.5",
+ "windows_aarch64_msvc 0.52.5",
+ "windows_i686_gnu 0.52.5",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.5",
+ "windows_x86_64_gnu 0.52.5",
+ "windows_x86_64_gnullvm 0.52.5",
+ "windows_x86_64_msvc 0.52.5",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
+
+[[package]]
+name = "zerocopy"
+version = "0.8.0-alpha.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db678a6ee512bd06adf35c35be471cae2f9c82a5aed2b5d15e03628c98bddd57"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.0-alpha.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "201585ea96d37ee69f2ac769925ca57160cef31acb137c16f38b02b76f4c1e62"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/sip/examples/server.rs b/sip/examples/server.rs
new file mode 100644
index 0000000..02c06b4
--- /dev/null
+++ b/sip/examples/server.rs
@@ -0,0 +1,83 @@
+use anyhow::Result;
+use log::{info, warn};
+use sip::{
+ encoding::{
+ headermap::HeaderMap,
+ headers::{Contact, From, To, UserAgent, Via},
+ method::Method,
+ response::Response,
+ status::Status,
+ },
+ transaction::TransactionUser,
+ transport::tcp::TcpTransport,
+};
+use std::net::SocketAddr;
+use tokio::{
+ net::{TcpListener, TcpStream},
+ spawn,
+};
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ env_logger::init_from_env("LOG");
+ let listener = TcpListener::bind("0.0.0.0:5060").await?;
+ info!("tcp listener bound to {}", listener.local_addr().unwrap());
+
+ loop {
+ let (stream, addr) = listener.accept().await?;
+ info!("connect {addr}");
+
+ spawn(async move {
+ if let Err(e) = handle_client(stream, addr).await {
+ warn!("client error: {e}")
+ }
+ info!("disconnect {addr}")
+ });
+ }
+}
+
+async fn handle_client(stream: TcpStream, addr: SocketAddr) -> Result<()> {
+ let transport = TcpTransport::new(stream).await?;
+ let tu = TransactionUser::new(transport);
+ loop {
+ let req = tu.process_incoming().await?;
+
+ if req.method == Method::Register {
+ let from: From = req.headers.get_res()?;
+ let to: To = req.headers.get_res()?;
+ let via: Via = req.headers.get_res()?;
+ let contact: Contact = req.headers.get_res()?;
+ info!("Registered {}", contact.uri.localpart.as_ref().unwrap());
+
+ tu.respond(
+ &req,
+ Response {
+ status: Status::Ok,
+ headers: HeaderMap::new()
+ .add(contact)
+ .add(via)
+ .add(to)
+ .add(from)
+ .add(UserAgent("siptest v0.1.0".to_string())),
+ body: String::new(),
+ },
+ )
+ .await?;
+ }
+ if req.method == Method::Invite {}
+ }
+}
+
+/*
+[2024-07-05T22:22:40Z DEBUG sip::transport::udp] SIP/2.0 200 OK
+ Via: SIP/2.0/UDP 198.18.1.135:52125;branch=Uz7r7ysvrS91q9j9;rport=52125
+ Contact: <sip:metatest-0x5e11f89f7900@198.18.1.135:40708>;expires=379;+sip.instance="<urn:uuid:9b2421ef-43ed-3b3b-9181-607ba5c13fb9>"
+ Contact: <sip:metatest-0x5e11f89f7900@198.18.1.135:47412>;expires=411
+ Contact: <sip:metatest-0x5e11f89f7900@198.18.1.135:52125>;expires=600
+ To: <sip:metatest@198.18.0.220>;tag=81756f4e
+ From: <sip:metatest@198.18.0.220>;tag=-AK4OkphTFVZv50h
+ Call-ID: U3Fyb6vT1BhWxiKG
+ CSeq: 1 REGISTER
+ User-Agent: AGFEO SIP V3.00.15 n (MAC=00094070069C)
+ Content-Length: 0
+*/
diff --git a/sip/src/encoding/headermap.rs b/sip/src/encoding/headermap.rs
new file mode 100644
index 0000000..01e1962
--- /dev/null
+++ b/sip/src/encoding/headermap.rs
@@ -0,0 +1,60 @@
+use super::headers::Header;
+use anyhow::{anyhow, Result};
+use std::fmt::Display;
+
+#[derive(Debug, Clone)]
+pub struct HeaderMap(pub Vec<(String, String)>);
+
+impl HeaderMap {
+ pub fn new() -> Self {
+ Self(vec![])
+ }
+ pub fn add<H: Header>(mut self, h: H) -> Self {
+ self.0.push((H::NAME.to_string(), format!("{h}")));
+ self
+ }
+ pub fn insert<H: Header>(&mut self, h: H) {
+ self.0.push((H::NAME.to_string(), format!("{h}")));
+ }
+ pub fn get_raw(&self, name: &str) -> Option<&str> {
+ self.0
+ .iter()
+ .find(|(k, _)| k.eq_ignore_ascii_case(name))
+ .map(|(_, v)| v.as_str())
+ }
+ pub fn get<H: Header>(&self) -> Option<Result<H>> {
+ self.get_raw(H::NAME).map(H::from_str)
+ }
+ pub fn get_res<H: Header>(&self) -> Result<H> {
+ self.get().ok_or(anyhow!("{} header missing", H::NAME))?
+ }
+ pub fn insert_raw(&mut self, key: String, value: String) {
+ self.0.push((key, value))
+ }
+}
+
+impl Display for HeaderMap {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ for (k, v) in &self.0 {
+ write!(f, "{k}: {v}\r\n")?;
+ }
+ Ok(())
+ }
+}
+impl HeaderMap {
+ pub fn parse<'a>(lines: &mut impl Iterator<Item = &'a str>) -> Result<Self> {
+ let mut headers = HeaderMap::new();
+ for line in lines {
+ // TODO multiline values
+ let (key, value) = line.split_once(":").ok_or(anyhow!("header malformed"))?;
+ headers.insert_raw(key.trim().to_string(), value.trim().to_string())
+ }
+ Ok(headers)
+ }
+}
+
+impl FromIterator<(String, String)> for HeaderMap {
+ fn from_iter<T: IntoIterator<Item = (String, String)>>(iter: T) -> Self {
+ Self(Vec::from_iter(iter))
+ }
+}
diff --git a/sip/src/encoding/headers.rs b/sip/src/encoding/headers.rs
new file mode 100644
index 0000000..e880739
--- /dev/null
+++ b/sip/src/encoding/headers.rs
@@ -0,0 +1,182 @@
+use super::{headermap::HeaderMap, method::Method, uri::Uri};
+use anyhow::{anyhow, bail, Result};
+use std::{fmt::Display, str::FromStr};
+
+macro_rules! header {
+ ($hname:literal, struct $name:ident($type:ty)) => {
+ #[derive(Debug)]
+ pub struct $name(pub $type);
+ impl Header for $name {
+ const NAME: &'static str = $hname;
+ }
+ impl Display for $name {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0)
+ }
+ }
+ impl FromStr for $name {
+ type Err = anyhow::Error;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Ok($name(<$type>::from_str(s)?))
+ }
+ }
+ };
+}
+
+pub trait Header: FromStr<Err = anyhow::Error> + Display {
+ const NAME: &'static str;
+}
+
+header!("Content-Length", struct ContentLength(usize));
+header!("Content-Type", struct ContentType(String));
+header!("Call-ID", struct CallID(String));
+header!("Via", struct Via(String));
+header!("Max-Forwards", struct MaxForwards(usize));
+header!("From", struct From(String));
+header!("To", struct To(String));
+header!("User-Agent", struct UserAgent(String));
+header!("Allow", struct Allow(String));
+
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+pub struct CSeq(pub u32, pub Method);
+
+impl Header for CSeq {
+ const NAME: &'static str = "CSeq";
+}
+impl Display for CSeq {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{} {}", self.0, self.1)
+ }
+}
+impl FromStr for CSeq {
+ type Err = anyhow::Error;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let (seq, method) = s.split_once(" ").ok_or(anyhow!("method missing"))?;
+ Ok(CSeq(seq.parse()?, method.parse()?))
+ }
+}
+
+#[derive(Debug)]
+pub struct WWWAuthenticate {
+ pub realm: String,
+ pub nonce: String,
+}
+impl Header for WWWAuthenticate {
+ const NAME: &'static str = "WWW-Authenticate";
+}
+impl Display for WWWAuthenticate {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "Digest realm={:?}, nonce={:?}", self.realm, self.nonce)
+ }
+}
+impl FromStr for WWWAuthenticate {
+ type Err = anyhow::Error;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ // TODO this is totally wrong
+ let kvs = s
+ .strip_prefix("Digest ")
+ .ok_or(anyhow!("type not digest"))?
+ .split(",")
+ .map(|e| {
+ let Some((k, v)) = e.split_once("=") else {
+ bail!("not a KV-pair")
+ };
+ Ok((k.trim().to_string(), v.trim().to_string()))
+ })
+ .try_collect::<HeaderMap>()?;
+ Ok(WWWAuthenticate {
+ realm: unquote(
+ &kvs.get_raw("realm")
+ .ok_or(anyhow!("realm missing"))?
+ .to_string(),
+ )?,
+ nonce: unquote(
+ &kvs.get_raw("nonce")
+ .ok_or(anyhow!("nonce missing"))?
+ .to_string(),
+ )?,
+ })
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct Authorization {
+ pub username: String,
+ pub realm: String,
+ pub uri: String,
+ pub nonce: String,
+ pub response: String,
+}
+impl Header for Authorization {
+ const NAME: &'static str = "Authorization";
+}
+impl Display for Authorization {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let Self {
+ username,
+ realm,
+ nonce,
+ uri,
+ response,
+ } = self;
+ write!(
+ f,
+ "Digest username={username:?},\r\n realm={realm:?},\r\n nonce={nonce:?},\r\n uri={uri:?},\r\n response={response:?},\r\n algorithm=MD5"
+ )
+ }
+}
+impl FromStr for Authorization {
+ type Err = anyhow::Error;
+ fn from_str(_s: &str) -> Result<Self, Self::Err> {
+ todo!()
+ }
+}
+
+pub fn unquote(v: &str) -> Result<String> {
+ Ok(v.strip_prefix("\"")
+ .ok_or(anyhow!("start quote missing"))?
+ .strip_suffix("\"")
+ .ok_or(anyhow!("end quote missing"))?
+ .to_string())
+}
+
+#[derive(Debug)]
+pub struct Contact {
+ pub display_name: Option<String>,
+ pub uri: Uri,
+ pub params: String,
+}
+
+impl Header for Contact {
+ const NAME: &'static str = "Contact";
+}
+impl Display for Contact {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let Self {
+ display_name,
+ uri,
+ params,
+ } = self;
+ if let Some(display_name) = display_name {
+ write!(f, "{display_name} <{uri}>{params}")
+ } else {
+ write!(f, "<{uri}>{params}")
+ }
+ }
+}
+impl FromStr for Contact {
+ type Err = anyhow::Error;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let (display_name, rest) = s.split_once("<").ok_or(anyhow!("malformed contact"))?;
+ let (uri, params) = rest.split_once(">;").ok_or(anyhow!("malformed contact"))?;
+ Ok(Self {
+ display_name: if display_name.is_empty() {
+ None
+ } else {
+ Some(display_name.to_string())
+ },
+ params: params.to_string(),
+ uri: Uri::from_str(uri)?,
+ })
+ }
+}
diff --git a/sip/src/encoding/method.rs b/sip/src/encoding/method.rs
new file mode 100644
index 0000000..6d38cab
--- /dev/null
+++ b/sip/src/encoding/method.rs
@@ -0,0 +1,39 @@
+use anyhow::bail;
+use std::{fmt::Display, str::FromStr};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum Method {
+ Register,
+ Invite,
+ Ack,
+ Options,
+ Cancel,
+ Bye,
+}
+
+impl Display for Method {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str(match self {
+ Method::Register => "REGISTER",
+ Method::Invite => "INVITE",
+ Method::Ack => "ACK",
+ Method::Options => "OPTIONS",
+ Method::Cancel => "CANCEL",
+ Method::Bye => "BYE",
+ })
+ }
+}
+impl FromStr for Method {
+ type Err = anyhow::Error;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Ok(match s {
+ "REGISTER" => Method::Register,
+ "INVITE" => Method::Invite,
+ "ACK" => Method::Ack,
+ "OPTIONS" => Method::Options,
+ "CANCEL" => Method::Cancel,
+ "BYE" => Method::Bye,
+ _ => bail!("unknown method"),
+ })
+ }
+}
diff --git a/sip/src/encoding/mod.rs b/sip/src/encoding/mod.rs
new file mode 100644
index 0000000..816aa01
--- /dev/null
+++ b/sip/src/encoding/mod.rs
@@ -0,0 +1,46 @@
+use std::{fmt::Display, str::FromStr};
+
+use request::Request;
+use response::Response;
+
+pub mod headermap;
+pub mod headers;
+pub mod method;
+pub mod request;
+pub mod response;
+pub mod status;
+pub mod uri;
+
+#[derive(Debug, Clone)]
+pub enum Message {
+ Request(Request),
+ Response(Response),
+}
+
+impl Display for Message {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Message::Request(r) => write!(f, "{r}"),
+ Message::Response(r) => write!(f, "{r}"),
+ }
+ }
+}
+impl FromStr for Message {
+ type Err = anyhow::Error;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if s.starts_with("SIP/") {
+ Response::from_str(s).map(Message::Response)
+ } else {
+ Request::from_str(s).map(Message::Request)
+ }
+ }
+}
+
+impl Message {
+ pub fn body_mut(&mut self) -> &mut String {
+ match self {
+ Message::Request(r) => &mut r.body,
+ Message::Response(r) => &mut r.body,
+ }
+ }
+}
diff --git a/sip/src/encoding/request.rs b/sip/src/encoding/request.rs
new file mode 100644
index 0000000..ab41b7c
--- /dev/null
+++ b/sip/src/encoding/request.rs
@@ -0,0 +1,57 @@
+use super::{headermap::HeaderMap, method::Method, uri::Uri};
+use anyhow::{anyhow, bail};
+use std::{fmt::Display, str::FromStr};
+
+#[derive(Debug, Clone)]
+pub struct Request {
+ pub method: Method,
+ pub uri: Uri,
+ pub headers: HeaderMap,
+ pub body: String,
+}
+
+impl Display for Request {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let Self {
+ headers,
+ method,
+ uri,
+ ..
+ } = self;
+ write!(f, "{method} {uri} SIP/2.0\r\n")?;
+ write!(f, "{headers}\r\n")?;
+ Ok(())
+ }
+}
+impl FromStr for Request {
+ type Err = anyhow::Error;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut lines = s.lines();
+ let statusline = lines.next().ok_or(anyhow!("status line missing"))?;
+ let (method, rest) = statusline
+ .split_once(" ")
+ .ok_or(anyhow!("status line malformed"))?;
+ let (uri, sipver) = rest
+ .split_once(" ")
+ .ok_or(anyhow!("status line malformed"))?;
+
+ let Some(ver) = sipver.strip_prefix("SIP/") else {
+ bail!("sip version malformed");
+ };
+ if ver != "2.0" {
+ bail!("sip version {ver:?} is not supported");
+ }
+
+ let uri = Uri::from_str(uri)?;
+
+ let headers = HeaderMap::parse(&mut lines)?;
+ let method = Method::from_str(method)?;
+
+ Ok(Self {
+ body: String::new(),
+ headers,
+ method,
+ uri,
+ })
+ }
+}
diff --git a/sip/src/encoding/response.rs b/sip/src/encoding/response.rs
new file mode 100644
index 0000000..0b7996c
--- /dev/null
+++ b/sip/src/encoding/response.rs
@@ -0,0 +1,53 @@
+use super::{headermap::HeaderMap, status::Status};
+use anyhow::{anyhow, bail, Context};
+use std::{fmt::Display, str::FromStr};
+
+#[derive(Debug, Clone)]
+pub struct Response {
+ pub status: Status,
+ pub headers: HeaderMap,
+ pub body: String,
+}
+
+impl FromStr for Response {
+ type Err = anyhow::Error;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut lines = s.lines();
+ let statusline = lines.next().ok_or(anyhow!("status line missing"))?;
+ let (sipver, rest) = statusline
+ .split_once(" ")
+ .ok_or(anyhow!("status line malformed"))?;
+ let (code, _status_str) = rest
+ .split_once(" ")
+ .ok_or(anyhow!("status line malformed"))?;
+ let code = u16::from_str(code).context("status code")?;
+
+ let Some(ver) = sipver.strip_prefix("SIP/") else {
+ bail!("sip version malformed");
+ };
+ if ver != "2.0" {
+ bail!("sip version {ver:?} is not supported");
+ }
+
+ let headers = HeaderMap::parse(&mut lines)?;
+
+ let status = Status::from_code(code);
+ Ok(Self {
+ status,
+ headers,
+ body: String::new(),
+ })
+ }
+}
+impl Display for Response {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let Self {
+ status,
+ headers,
+ body,
+ } = self;
+ write!(f, "SIP/2.0 {} {status:?}\r\n", status.to_code())?;
+ write!(f, "{headers}\r\n{body}")?;
+ Ok(())
+ }
+}
diff --git a/sip/src/encoding/status.rs b/sip/src/encoding/status.rs
new file mode 100644
index 0000000..61b2d2a
--- /dev/null
+++ b/sip/src/encoding/status.rs
@@ -0,0 +1,63 @@
+macro_rules! status_enum {
+ ($v:vis enum $name:ident { $($variant:ident = $value:literal),*, }) => {
+ #[derive(Debug, Clone, Eq, PartialEq, Hash)]
+ $v enum $name { $($variant),*, Other(u16) }
+ impl $name { pub fn from_code(c: u16) -> Self { match c { $($value => Self::$variant),*, x => Self::Other(x) } } }
+ impl $name { pub fn to_code(&self) -> u16 { match self { $(Self::$variant => $value),*, Self::Other(x) => *x } } }
+ };
+}
+
+status_enum!(
+ pub enum Status {
+ Trying = 100,
+ Ringing = 180,
+ CallIsBeingForwarded = 181,
+ Queued = 182,
+ SessionProgress = 183,
+ Ok = 200,
+ MultipleChoices = 300,
+ MovedPermanently = 301,
+ MovedTemporarily = 302,
+ UseProxy = 305,
+ AlternativeService = 380,
+ BadRequest = 400,
+ Unauthorized = 401,
+ PaymentRequired = 402,
+ Forbidden = 403,
+ NotFound = 404,
+ MethodNotAllowed = 405,
+ NotAcceptable = 406,
+ ProxyAuthenticationRequired = 407,
+ RequestTimeout = 408,
+ Gone = 410,
+ RequestEntityTooLarge = 413,
+ RequestURITooLarge = 414,
+ UnsupportedMediaType = 415,
+ UnsupportedURIScheme = 416,
+ BadExtension = 420,
+ ExtensionRequired = 421,
+ IntervalTooBrief = 423,
+ TemporarilyNotAvailable = 480,
+ CallLegTransactionDoesNotExist = 481,
+ LoopDetected = 482,
+ TooManyHops = 483,
+ AddressIncomplete = 484,
+ Ambiguous = 485,
+ BusyHere = 486,
+ RequestTerminated = 487,
+ NotAcceptableHere = 488,
+ RequestPending = 491,
+ Undecipherable = 493,
+ InternalServerError = 500,
+ NotImplemented = 501,
+ BadGateway = 502,
+ ServiceUnavailable = 503,
+ ServerTimeout = 504,
+ SIPVersionNotSupported = 505,
+ MessageTooLarge = 513,
+ BusyEverywhere = 600,
+ Decline = 603,
+ DoesNotExistAnywhere = 604,
+ GlobalNotAcceptable = 606,
+ }
+);
diff --git a/sip/src/encoding/uri.rs b/sip/src/encoding/uri.rs
new file mode 100644
index 0000000..b1a1282
--- /dev/null
+++ b/sip/src/encoding/uri.rs
@@ -0,0 +1,55 @@
+use anyhow::anyhow;
+use std::{fmt::Display, str::FromStr};
+
+#[derive(Debug, Clone)]
+pub struct Uri {
+ pub protocol: String,
+ pub localpart: Option<String>,
+ pub addr: String,
+ pub params: String,
+}
+
+impl Display for Uri {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let Self {
+ protocol,
+ localpart,
+ addr,
+ params,
+ } = self;
+ write!(
+ f,
+ "{protocol}:{}{addr}{}",
+ if let Some(localpart) = localpart {
+ format!("{localpart}@")
+ } else {
+ "".to_string()
+ },
+ if params.is_empty() {
+ "".to_string()
+ } else {
+ format!(";{params}")
+ }
+ )?;
+ Ok(())
+ }
+}
+impl FromStr for Uri {
+ type Err = anyhow::Error;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let (pr, s) = s.split_once(":").ok_or(anyhow!("protocol sep"))?;
+ let (lp, s) = s.split_once("@").unwrap_or(("", s));
+ let (addr, params) = s.split_once(";").unwrap_or((s, ""));
+
+ Ok(Self {
+ addr: addr.to_owned(),
+ localpart: if lp.is_empty() {
+ None
+ } else {
+ Some(lp.to_string())
+ },
+ params: params.to_string(),
+ protocol: pr.to_string(),
+ })
+ }
+}
diff --git a/sip/src/lib.rs b/sip/src/lib.rs
new file mode 100644
index 0000000..6c6cc3b
--- /dev/null
+++ b/sip/src/lib.rs
@@ -0,0 +1,4 @@
+#![feature(iterator_try_collect)]
+pub mod encoding;
+pub mod transport;
+pub mod transaction;
diff --git a/sip/src/transaction/auth.rs b/sip/src/transaction/auth.rs
new file mode 100644
index 0000000..4c46f9b
--- /dev/null
+++ b/sip/src/transaction/auth.rs
@@ -0,0 +1,51 @@
+use crate::encoding::{
+ headers::{Authorization, WWWAuthenticate},
+ method::Method,
+ request::Request,
+ response::Response,
+ uri::Uri,
+};
+use anyhow::Result;
+
+impl Authorization {
+ pub fn construct(
+ request: &Request,
+ failed_response: &Response,
+ username: &str,
+ password: &str,
+ ) -> Result<Authorization> {
+ let challenge = failed_response.headers.get_res::<WWWAuthenticate>()?;
+
+ Ok(Authorization {
+ response: response_digest(
+ username.to_string(),
+ challenge.realm.clone(),
+ password.to_string(),
+ request.method,
+ challenge.nonce.clone(),
+ request.uri.clone(),
+ ),
+ nonce: challenge.nonce,
+ realm: challenge.realm,
+ uri: request.uri.to_string(),
+ username: username.to_string(),
+ })
+ }
+}
+
+fn response_digest(
+ username: String,
+ realm: String,
+ password: String,
+ method: Method,
+ nonce: String,
+ uri: Uri,
+) -> String {
+ let h = |s: String| hex::encode(md5::compute(s.as_bytes()).0);
+ let kd = |secret, data| h(format!("{secret}:{data}"));
+
+ let a1 = format!("{username}:{realm}:{password}");
+ let a2 = format!("{method}:{uri}");
+ let response_digest = kd(h(a1), format!("{nonce}:{}", h(a2)));
+ return response_digest;
+}
diff --git a/sip/src/transaction/mod.rs b/sip/src/transaction/mod.rs
new file mode 100644
index 0000000..3368c47
--- /dev/null
+++ b/sip/src/transaction/mod.rs
@@ -0,0 +1,87 @@
+pub mod auth;
+
+use crate::{
+ encoding::{
+ headers::{CSeq, CallID, ContentLength},
+ request::Request,
+ response::Response,
+ Message,
+ },
+ transport::Transport,
+};
+use anyhow::{anyhow, Result};
+use std::{
+ collections::HashMap,
+ sync::atomic::{AtomicU32, Ordering},
+};
+use tokio::sync::{
+ mpsc::{self, channel},
+ RwLock,
+};
+
+pub struct TransactionUser<T> {
+ transport: T,
+ sequence: AtomicU32,
+ pending_requests: RwLock<HashMap<CSeq, mpsc::Sender<Response>>>,
+}
+
+impl<T: Transport> TransactionUser<T> {
+ pub fn new(transport: T) -> Self {
+ Self {
+ sequence: 0.into(),
+ pending_requests: Default::default(),
+ transport,
+ }
+ }
+
+ pub async fn process_incoming(&self) -> Result<Request> {
+ loop {
+ let mesg = self.transport.recv().await?;
+ match mesg {
+ Message::Request(req) => break Ok(req),
+ Message::Response(resp) => {
+ let cseq = resp
+ .headers
+ .get()
+ .ok_or(anyhow!("response cseq missing"))??;
+ self.pending_requests
+ .write()
+ .await
+ .get_mut(&cseq)
+ .ok_or(anyhow!("message was not requested"))?
+ .send(resp)
+ .await?;
+ }
+ }
+ }
+ }
+ pub async fn respond(&self, req: &Request, mut resp: Response) -> Result<()> {
+ resp.headers.insert(
+ req.headers
+ .get::<CSeq>()
+ .ok_or(anyhow!("cseq is mandatory"))??,
+ );
+ resp.headers.insert(
+ req.headers
+ .get::<CallID>()
+ .ok_or(anyhow!("call-id is mandatory"))??,
+ );
+ resp.headers.insert(ContentLength(resp.body.len()));
+ self.transport.send(Message::Response(resp)).await?;
+ Ok(())
+ }
+
+ pub async fn transact(&self, mut request: Request) -> Result<mpsc::Receiver<Response>> {
+ let seq = self.sequence.fetch_add(1, Ordering::Relaxed);
+ let cseq = CSeq(seq, request.method);
+ request.headers.insert(cseq);
+ request.headers.insert(ContentLength(request.body.len()));
+
+ let (tx, rx) = channel(4);
+
+ self.transport.send(Message::Request(request)).await?;
+ self.pending_requests.write().await.insert(cseq, tx);
+
+ Ok(rx)
+ }
+}
diff --git a/sip/src/transport/mod.rs b/sip/src/transport/mod.rs
new file mode 100644
index 0000000..b4c512b
--- /dev/null
+++ b/sip/src/transport/mod.rs
@@ -0,0 +1,11 @@
+use crate::encoding::Message;
+use anyhow::Result;
+
+pub mod tcp;
+pub mod udp;
+
+#[allow(async_fn_in_trait)]
+pub trait Transport {
+ async fn recv(&self) -> Result<Message>;
+ async fn send(&self, message: Message) -> Result<()>;
+}
diff --git a/sip/src/transport/tcp.rs b/sip/src/transport/tcp.rs
new file mode 100644
index 0000000..efe433d
--- /dev/null
+++ b/sip/src/transport/tcp.rs
@@ -0,0 +1,48 @@
+use super::Transport;
+use crate::encoding::Message;
+use anyhow::Result;
+use log::debug;
+use std::str::FromStr;
+use tokio::{
+ io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter},
+ net::{
+ tcp::{OwnedReadHalf, OwnedWriteHalf},
+ TcpStream,
+ },
+ sync::Mutex,
+};
+
+pub struct TcpTransport {
+ write: Mutex<BufWriter<OwnedWriteHalf>>,
+ read: Mutex<BufReader<OwnedReadHalf>>,
+}
+
+impl TcpTransport {
+ pub async fn new(stream: TcpStream) -> Result<Self> {
+ let (read, write) = stream.into_split();
+ Ok(Self {
+ write: BufWriter::new(write).into(),
+ read: BufReader::new(read).into(),
+ })
+ }
+}
+
+impl Transport for TcpTransport {
+ async fn recv(&self) -> Result<Message> {
+ let mut g = self.read.lock().await;
+ let mut message = String::new();
+ while !message.ends_with("\r\n\r\n") {
+ g.read_line(&mut message).await?;
+ }
+ let mesg = Message::from_str(message.trim())?;
+ debug!("<- {mesg}");
+ Ok(mesg)
+ }
+ async fn send(&self, request: Message) -> Result<()> {
+ debug!("-> {request}");
+ let mut g = self.write.lock().await;
+ g.write_all(format!("{request}").as_bytes()).await?;
+ g.flush().await?;
+ Ok(())
+ }
+}
diff --git a/sip/src/transport/udp.rs b/sip/src/transport/udp.rs
new file mode 100644
index 0000000..c0d7829
--- /dev/null
+++ b/sip/src/transport/udp.rs
@@ -0,0 +1,35 @@
+use super::Transport;
+use crate::encoding::Message;
+use anyhow::{anyhow, Result};
+use log::debug;
+use std::str::FromStr;
+use tokio::net::UdpSocket;
+
+pub struct UdpTransport {
+ sock: UdpSocket,
+}
+
+impl UdpTransport {
+ pub async fn new(sock: UdpSocket) -> Result<Self> {
+ Ok(Self { sock })
+ }
+}
+impl Transport for UdpTransport {
+ async fn recv(&self) -> Result<Message> {
+ let mut buf = [0; 1024];
+ let size = self.sock.recv(&mut buf).await?;
+ let message = String::from_utf8(buf[..size].to_vec())?;
+ let (head, body) = message
+ .split_once("\r\n\r\n")
+ .ok_or(anyhow!("header end missing"))?;
+ debug!("<- {head}\n\n{body}");
+ let mut mesg = Message::from_str(head.trim_end())?;
+ *mesg.body_mut() = body.to_string();
+ Ok(mesg)
+ }
+ async fn send(&self, request: Message) -> Result<()> {
+ debug!("-> {request}");
+ self.sock.send(format!("{request}").as_bytes()).await?;
+ Ok(())
+ }
+}