diff --git a/.golangci.yml b/.golangci.yml
index 7d2cff6..3130a06 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -101,6 +101,11 @@ linters:
- staticcheck
path: cli/commandline_test.go
text: SA1019
+ - linters:
+ - gocognit
+ - gocyclo
+ - cyclop
+ path: protocol/sip/helper_test.go
paths:
- protocol/mikrotik/msg.go
- third_party$
diff --git a/go.mod b/go.mod
index c10cc2c..2f6d823 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.24.1
require (
github.com/Masterminds/semver v1.5.0
github.com/antchfx/htmlquery v1.3.4
+ github.com/emiago/sipgo v0.33.0
github.com/lor00x/goldap v0.0.0-20240304151906-8d785c64d1c8
github.com/vjeantet/ldapserver v1.0.2-0.20240305064909-a417792e2906
golang.org/x/crypto v0.40.0
@@ -16,8 +17,12 @@ require (
require (
github.com/antchfx/xpath v1.3.3 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/gobwas/httphead v0.1.0 // indirect
+ github.com/gobwas/pool v0.2.1 // indirect
+ github.com/gobwas/ws v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/uuid v1.6.0 // indirect
+ github.com/icholy/digest v1.1.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
diff --git a/go.sum b/go.sum
index e9118c4..24b48a6 100644
--- a/go.sum
+++ b/go.sum
@@ -4,15 +4,28 @@ github.com/antchfx/htmlquery v1.3.4 h1:Isd0srPkni2iNTWCwVj/72t7uCphFeor5Q8nCzj1j
github.com/antchfx/htmlquery v1.3.4/go.mod h1:K9os0BwIEmLAvTqaNSua8tXLWRWZpocZIH73OzWQbwM=
github.com/antchfx/xpath v1.3.3 h1:tmuPQa1Uye0Ym1Zn65vxPgfltWb/Lxu2jeqIGteJSRs=
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/emiago/sipgo v0.33.0 h1:UxPKCoPREffSjrRE6oesG/RPz5/ZSp8tA8Jc6YvYUsk=
+github.com/emiago/sipgo v0.33.0/go.mod h1:gbOLw/kZHZ3wS/5PIa9qVjpdil/IKLdigbZFIYFpHTs=
+github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
+github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
+github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
+github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q=
+github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
+github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3/go.mod h1:37YR9jabpiIxsb8X9VCIx8qFOjTDIIrIHHODa8C4gz0=
github.com/lor00x/goldap v0.0.0-20240304151906-8d785c64d1c8 h1:z9RDOBcFcf3f2hSfKuoM3/FmJpt8M+w0fOy4wKneBmc=
github.com/lor00x/goldap v0.0.0-20240304151906-8d785c64d1c8/go.mod h1:37YR9jabpiIxsb8X9VCIx8qFOjTDIIrIHHODa8C4gz0=
@@ -20,8 +33,12 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/vjeantet/ldapserver v1.0.2-0.20240305064909-a417792e2906 h1:qHFp1iRg6qE8xYel3bQT9x70pyxsdPLbJnM40HG3Oig=
github.com/vjeantet/ldapserver v1.0.2-0.20240305064909-a417792e2906/go.mod h1:YvUqhu5vYhmbcLReMLrm/Tq3S7Yj43kSVFvvol6Lh6k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -105,6 +122,10 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
+gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
diff --git a/protocol/sip/examples/README.md b/protocol/sip/examples/README.md
new file mode 100644
index 0000000..0390014
--- /dev/null
+++ b/protocol/sip/examples/README.md
@@ -0,0 +1,13 @@
+# SIP examples
+
+## Usage
+
+Start the local Asterisk server and run a example.
+
+```sh
+cd protocol/sip/examples
+docker compose up -d
+go run ping/main.go
+# go run tcp/main.go
+# go run call/main.go
+```
diff --git a/protocol/sip/examples/call/main.go b/protocol/sip/examples/call/main.go
new file mode 100644
index 0000000..1fe06f0
--- /dev/null
+++ b/protocol/sip/examples/call/main.go
@@ -0,0 +1,111 @@
+// This example registers an endpoint (user authentication) in a server
+// and starts a call (only the SIP related part).
+package main
+
+import (
+ sipgo "github.com/emiago/sipgo/sip"
+ "github.com/icholy/digest"
+ "github.com/vulncheck-oss/go-exploit/output"
+ "github.com/vulncheck-oss/go-exploit/protocol"
+ "github.com/vulncheck-oss/go-exploit/protocol/sip"
+)
+
+const (
+ host = "127.0.0.1"
+ fromUser = "john.doe"
+ pass = "password"
+ toUser = "dembele"
+)
+
+func main() {
+ port := sip.DefaultPorts[sip.UDP]
+ conn, ok := protocol.UDPConnect(host, port)
+ if !ok {
+ output.PrintfFrameworkError("Connecting to server %s:%d", host, port)
+
+ return
+ }
+ defer conn.Close()
+ reqOpts := sip.NewSipRequestOpts{
+ Port: port,
+ User: fromUser,
+ ToUser: fromUser,
+ }
+ req, ok := sip.NewSipRequest(sipgo.REGISTER, host, &reqOpts)
+ if !ok {
+ output.PrintfFrameworkError("Creating REGISTER request to %s with options %+v", host, reqOpts)
+
+ return
+ }
+ resp, ok := sip.SendAndReceiveUDP(conn, req)
+ if !ok {
+ output.PrintfFrameworkError("Sending REGISTER request %s", req.String())
+
+ return
+ }
+ if resp.StatusCode != sipgo.StatusUnauthorized {
+ output.PrintfFrameworkError("Unexpected response %d (%s) to REGISTER request", resp.StatusCode, resp.Reason)
+
+ return
+ }
+ wwwAuth := resp.GetHeader("WWW-Authenticate")
+ chal, err := digest.ParseChallenge(wwwAuth.Value())
+ if err != nil {
+ output.PrintfFrameworkError("Parsing WWW-Authenticate header: %s", err)
+
+ return
+ }
+ cred, _ := digest.Digest(chal, digest.Options{
+ Method: req.Method.String(),
+ URI: host,
+ Username: fromUser,
+ Password: pass,
+ })
+ newReq := req.Clone()
+ authHeader := sipgo.NewHeader("Authorization", cred.String())
+ newReq.AppendHeader(authHeader)
+ cseqHeader := sipgo.CSeqHeader{SeqNo: uint32(2), MethodName: sipgo.REGISTER}
+ newReq.ReplaceHeader(&cseqHeader)
+ resp, ok = sip.SendAndReceiveUDP(conn, newReq)
+ if !ok {
+ output.PrintfFrameworkError("Sending REGISTER request with Authorization header %s", newReq.String())
+
+ return
+ }
+ if resp.StatusCode != sipgo.StatusOK {
+ output.PrintfFrameworkError("Unexpected response %d (%s) to REGISTER request with Authorization header", resp.StatusCode, resp.Reason)
+
+ return
+ }
+ reqOpts = sip.NewSipRequestOpts{
+ Port: port,
+ ToUser: toUser,
+ User: fromUser,
+ }
+ req, ok = sip.NewSipRequest(sipgo.INVITE, host, &reqOpts)
+ if !ok {
+ output.PrintfFrameworkError("Creating INVITE request to %s with options %+v", host, reqOpts)
+
+ return
+ }
+ cred, _ = digest.Digest(chal, digest.Options{
+ Method: sipgo.INVITE.String(),
+ URI: host,
+ Username: fromUser,
+ Password: pass,
+ })
+ authHeader = sipgo.NewHeader("Authorization", cred.String())
+ req.AppendHeader(authHeader)
+ resp, ok = sip.SendAndReceiveUDP(conn, req)
+ if !ok {
+ output.PrintfFrameworkError("Sending INVITE request %s", req.String())
+
+ return
+ }
+ // Not found is expected here, as we are not actually calling a real user.
+ if resp.StatusCode == sipgo.StatusNotFound {
+ output.PrintfFrameworkSuccess("Response (INVITE): %s", resp.String())
+ } else {
+ output.PrintfFrameworkError("Unexpected response (INVITE): %s", resp.String())
+ }
+}
diff --git a/protocol/sip/examples/docker-compose.yml b/protocol/sip/examples/docker-compose.yml
new file mode 100644
index 0000000..9d3829d
--- /dev/null
+++ b/protocol/sip/examples/docker-compose.yml
@@ -0,0 +1,15 @@
+name: go-exploit-examples
+
+services:
+ asterisk:
+ image: mlan/asterisk
+ network_mode: bridge # For testing
+ cap_add:
+ - sys_ptrace # For testing
+ ports:
+ - "5060:5060/udp" # SIP UDP
+ - "5060:5060" # SIP TCP
+ - "5061:5061" # SIP TLS
+ - "10000-10099:10000-10099/udp" # RTP
+ environment:
+ - HOSTNAME=asterisk.${DOMAIN-docker.localhost}
diff --git a/protocol/sip/examples/ping/main.go b/protocol/sip/examples/ping/main.go
new file mode 100644
index 0000000..a6a3727
--- /dev/null
+++ b/protocol/sip/examples/ping/main.go
@@ -0,0 +1,47 @@
+// This example checks if a UDP server is up.
+//
+// The OPTION method is used to discover the server capabilities:
+// - https://datatracker.ietf.org/doc/html/rfc3261#section-11
+package main
+
+import (
+ sipgo "github.com/emiago/sipgo/sip"
+ "github.com/vulncheck-oss/go-exploit/output"
+ "github.com/vulncheck-oss/go-exploit/protocol"
+ "github.com/vulncheck-oss/go-exploit/protocol/sip"
+)
+
+const (
+ host = "127.0.0.1"
+ // Most of the times the servers answer without them.
+ fromUser = "bob"
+ toUser = "alice"
+)
+
+func main() {
+ port := sip.DefaultPorts[sip.UDP]
+ conn, ok := protocol.UDPConnect(host, port)
+ if !ok {
+ output.PrintfFrameworkError("Connecting to server %s:%d", host, port)
+
+ return
+ }
+ defer conn.Close()
+ reqOpts := sip.NewSipRequestOpts{
+ Port: port,
+ ToUser: toUser,
+ User: fromUser,
+ }
+ req, ok := sip.NewSipRequest(sipgo.OPTIONS, host, &reqOpts)
+ if !ok {
+ output.PrintfFrameworkError("Creating request to %s with options %+v", host, reqOpts)
+
+ return
+ }
+ resp, ok := sip.SendAndReceiveUDP(conn, req)
+ if ok {
+ output.PrintfFrameworkSuccess("Server is up, response: %s", resp.String())
+ } else {
+ output.PrintfFrameworkError("Sending request %s", req.String())
+ }
+}
diff --git a/protocol/sip/examples/tcp/main.go b/protocol/sip/examples/tcp/main.go
new file mode 100644
index 0000000..754f03c
--- /dev/null
+++ b/protocol/sip/examples/tcp/main.go
@@ -0,0 +1,45 @@
+// This example sends an OPTIONS request over TCP (or TLS).
+package main
+
+import (
+ sipgo "github.com/emiago/sipgo/sip"
+ "github.com/vulncheck-oss/go-exploit/output"
+ "github.com/vulncheck-oss/go-exploit/protocol"
+ "github.com/vulncheck-oss/go-exploit/protocol/sip"
+)
+
+const (
+ host = "127.0.0.1"
+ useTLS = true
+)
+
+func main() {
+ transport := sip.TCP
+ if useTLS {
+ transport = sip.TLS
+ }
+ port := sip.DefaultPorts[transport]
+ conn, ok := protocol.MixedConnect(host, port, useTLS)
+ if !ok {
+ output.PrintfFrameworkError("Connecting to server %s:%d", host, port)
+
+ return
+ }
+ defer conn.Close()
+ reqOpts := sip.NewSipRequestOpts{
+ Port: port,
+ Transport: transport,
+ }
+ req, ok := sip.NewSipRequest(sipgo.OPTIONS, host, &reqOpts)
+ if !ok {
+ output.PrintfFrameworkError("Creating request to %s with options %+v", host, reqOpts)
+
+ return
+ }
+ resp, ok := sip.SendAndReceiveTCP(conn, req)
+ if ok {
+ output.PrintfFrameworkSuccess("Response: %s", resp.String())
+ } else {
+ output.PrintfFrameworkError("Sending request %s", req.String())
+ }
+}
diff --git a/protocol/sip/helper.go b/protocol/sip/helper.go
new file mode 100644
index 0000000..cadd657
--- /dev/null
+++ b/protocol/sip/helper.go
@@ -0,0 +1,568 @@
+// Package sip is a very basic (and incomplete) implementation of SIP messaging protocol.
+//
+// Based on the sipgo library, we are only adding some helpers useful in the context of exploitation.
+// For example, we prefer to avoid the complexity of concepts like transactions or dialogs.
+//
+// References:
+// - https://github.com/emiago/sipgo
+// - https://datatracker.ietf.org/doc/html/rfc3261
+// - https://datatracker.ietf.org/doc/html/rfc3311
+// - https://datatracker.ietf.org/doc/html/rfc3581
+// - https://datatracker.ietf.org/doc/html/rfc3265
+// - https://datatracker.ietf.org/doc/html/rfc7118
+package sip
+
+import (
+ "bufio"
+ _ "embed"
+ "fmt"
+ "io"
+ "net"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/emiago/sipgo/sip"
+ "github.com/google/uuid"
+ "github.com/vulncheck-oss/go-exploit/output"
+ "github.com/vulncheck-oss/go-exploit/protocol"
+ "github.com/vulncheck-oss/go-exploit/random"
+)
+
+const (
+ // SIP over UDP message size should be lower than 1300 bytes.
+ // https://datatracker.ietf.org/doc/html/rfc3261#section-18.1.1
+ UDPMessageLength = 1300
+ errMsgRequiredParam = "Required parameter: %s"
+ DefaultMaxForwards = 70
+ DefaultCSeq = 1
+ DefaultInviteContentType = "application/sdp"
+ DefaultMessageContentType = "text/plain"
+ DefaultMessageBodyContent = "Hello, this is a test message."
+ DefaultInfoContentType = "application/dtmf-relay"
+ DefaultInfoBodyContent = "Signal=1Signal=1\nDuration=100"
+ DefaultExpiresHeader = 3600
+ DefaultRackCSeq = 1
+ DefaultRackInviteCSeq = 314159
+ ContentTypePidf = "application/pidf+xml"
+ // Even though the specification requires "\r\n", it is common to see
+ // implementations using "\n" as a line ending.
+ // https://datatracker.ietf.org/doc/html/rfc3261#section-7.5
+ sipLineEnd = "\r\n"
+ sipLineEndAlt = "\n"
+)
+
+// GlobalUA is the default User-Agent for all SIP go-exploit comms.
+//
+//go:embed user-agent.txt
+var GlobalUA string
+
+// Supported transport protocol.
+type TransportType int
+
+const (
+ UNKNOWN TransportType = iota
+ UDP
+ TCP
+ TLS
+ WS
+ WSS
+)
+
+func (t TransportType) String() string {
+ return [...]string{"UNKNOWN", "UDP", "TCP", "TLS", "WS", "WSS"}[t]
+}
+
+// Default server ports for each transport protocol.
+var DefaultPorts = map[TransportType]int{
+ UDP: sip.DefaultUdpPort,
+ TCP: sip.DefaultTcpPort,
+ TLS: sip.DefaultTlsPort,
+ WS: sip.DefaultWsPort,
+ WSS: sip.DefaultWssPort,
+}
+
+// Sends a TCP/TLS message and returns the response.
+func SendAndReceiveTCP(conn net.Conn, req sip.Message) (*sip.Response, bool) {
+ if conn == nil {
+ output.PrintfFrameworkError(errMsgRequiredParam, "conn")
+
+ return nil, false
+ }
+ if req == nil {
+ output.PrintfFrameworkError(errMsgRequiredParam, "msg")
+
+ return nil, false
+ }
+ ok := protocol.TCPWrite(conn, []byte(req.String()))
+ if !ok {
+ output.PrintfFrameworkError("Writing message %s to the socket", req.String())
+
+ return nil, false
+ }
+ // To discard requests coming from the server, like OPTIONS or NOTIFY.
+ for {
+ respMsg, ok := ReadMessageTCP(conn)
+ if !ok {
+ output.PrintfFrameworkError("Reading response from the socket")
+
+ return nil, false
+ }
+ resp, ok := respMsg.(*sip.Response)
+ if !ok {
+ output.PrintfFrameworkDebug("Response is not a valid: %+v", respMsg)
+
+ continue
+ }
+
+ return resp, true
+ }
+}
+
+// Returns a SIP message from a TCP connection.
+func ReadMessageTCP(conn net.Conn) (sip.Message, bool) {
+ reader := bufio.NewReader(conn)
+ var resp string
+ for {
+ line, err := reader.ReadString('\n')
+ if err != nil {
+ output.PrintfFrameworkError("Reading response from the socket: %s", err.Error())
+
+ return nil, false
+ }
+ resp += line
+ if line == sipLineEnd || line == sipLineEndAlt {
+ break
+ }
+ }
+ // First message parse to get the length of the body.
+ msg, err := sip.ParseMessage([]byte(resp))
+ if err != nil {
+ output.PrintfFrameworkError("Parsing response %+v: %s", resp, err.Error())
+
+ return nil, false
+ }
+ bodyLen, err := strconv.ParseInt(msg.ContentLength().Value(), 10, 64)
+ if err != nil {
+ output.PrintfFrameworkError("Parsing Content-Length header: %s", err.Error())
+ }
+ if bodyLen > 0 {
+ body := make([]byte, bodyLen)
+ count, err := io.ReadFull(reader, body)
+ if err != nil {
+ output.PrintfFrameworkError("Reading message body: %s", err.Error())
+
+ return nil, false
+ }
+ output.PrintfFrameworkDebug("Read %d bytes of body", count)
+ resp := resp + string(body)
+ // Second parse, now with the body included.
+ msg, err := sip.ParseMessage([]byte(resp))
+ if err != nil {
+ output.PrintFrameworkError("Parsing response %+v with body: %s", msg, err.Error())
+
+ return nil, false
+ }
+ }
+
+ return msg, true
+}
+
+// Sends a UDP message and returns the response.
+//
+// If 'amount' is set to 0, it will use the recommended size for UDP messages (1300 bytes).
+func SendAndReceiveUDP(
+ conn *net.UDPConn, req sip.Message,
+) (*sip.Response, bool) {
+ if conn == nil {
+ output.PrintfFrameworkError(errMsgRequiredParam, "conn")
+
+ return nil, false
+ }
+ if req == nil {
+ output.PrintfFrameworkError(errMsgRequiredParam, "req")
+
+ return nil, false
+ }
+ ok := protocol.UDPWrite(conn, []byte(req.String()))
+ if !ok {
+ output.PrintfFrameworkError("Writing message %s to the socket", req.String())
+
+ return nil, false
+ }
+ // To discard requests coming from the server, like OPTIONS or NOTIFY.
+ for {
+ respMsg, ok := ReadMessageUDP(conn)
+ if !ok {
+ output.PrintFrameworkError("Reading response from the socket")
+
+ return nil, false
+ }
+ resp, ok := respMsg.(*sip.Response)
+ if !ok {
+ output.PrintfFrameworkDebug("Response is not a valid: %+v", respMsg)
+
+ continue
+ }
+
+ return resp, true
+ }
+}
+
+// Returns a SIP message from a UDP connection using the message length
+// defined in the RFC 3261.
+// https://datatracker.ietf.org/doc/html/rfc3261#section-18.1.1
+func ReadMessageUDP(conn *net.UDPConn) (sip.Message, bool) {
+ resp := make([]byte, UDPMessageLength)
+ count, err := conn.Read(resp)
+ if err != nil {
+ output.PrintFrameworkError("Failed to read from the socket: " + err.Error())
+
+ return nil, false
+ }
+ resp = resp[:count]
+ msg, err := sip.ParseMessage(resp)
+ if err != nil {
+ output.PrintfFrameworkError("Parsing response %+v: %s", resp, err.Error())
+
+ return nil, false
+ }
+
+ return msg, true
+}
+
+// Returns a generic well-formed request. Useful to start the communication.
+//
+// Depending on the method (if not set in 'opts') required headers and body content
+// is automatically added.
+func NewSipRequest(
+ method sip.RequestMethod, host string, opts *NewSipRequestOpts,
+) (*sip.Request, bool) {
+ if method == "" {
+ output.PrintfFrameworkError(errMsgRequiredParam, "method")
+
+ return nil, false
+ }
+ if host == "" {
+ output.PrintfFrameworkError(errMsgRequiredParam, "host")
+
+ return nil, false
+ }
+ if opts == nil {
+ opts = &NewSipRequestOpts{}
+ }
+ lHost := host
+ if opts.LocalHost != "" {
+ lHost = opts.LocalHost
+ }
+ lPort := opts.Port
+ if opts.LocalPort != 0 {
+ lPort = opts.LocalPort
+ }
+ toURI := sip.Uri{
+ User: opts.ToUser,
+ Host: host,
+ Port: opts.Port,
+ }
+ fromURI := sip.Uri{
+ User: opts.User,
+ Password: opts.Password,
+ Host: lHost,
+ Port: lPort,
+ }
+ req := sip.NewRequest(method, toURI)
+ if opts.Transport != 0 {
+ req.SetTransport(opts.Transport.String())
+ }
+ callID, err := uuid.NewRandom()
+ if err != nil {
+ output.PrintfFrameworkError("Could not generate UUID: %s", err.Error())
+
+ return nil, false
+ }
+ callIDHeader := sip.CallIDHeader(callID.String())
+ viaOpts := NewViaOpts{
+ Host: lHost,
+ Port: lPort,
+ Transport: req.Transport(),
+ }
+ viaHeader, ok := NewViaHeader(&viaOpts)
+ if !ok {
+ output.PrintfFrameworkError("Creating Via header with options %+v", viaOpts)
+
+ return nil, false
+ }
+ fromHeader := &sip.FromHeader{
+ Address: fromURI,
+ Params: map[string]string{"tag": sip.GenerateTagN(10)},
+ }
+ toHeader := &sip.ToHeader{Address: toURI}
+ maxForwardsHeader := sip.MaxForwardsHeader(70)
+ req.PrependHeader(
+ viaHeader,
+ &maxForwardsHeader,
+ fromHeader,
+ toHeader,
+ &callIDHeader,
+ &sip.CSeqHeader{SeqNo: uint32(DefaultCSeq), MethodName: method},
+ sip.NewHeader("User-Agent", strings.TrimSpace(GlobalUA)),
+ )
+ if IsContactRequired(method) {
+ req.AppendHeader(&sip.ContactHeader{
+ Address: fromURI,
+ Params: sip.HeaderParams{"transport": req.Transport()},
+ })
+ }
+ AddMethodHeaders(req, method)
+ body, contentType := NewRequestBody(method)
+ bodyLen := len(body)
+ if bodyLen > 0 {
+ contentTypeHeader := sip.ContentTypeHeader(contentType)
+ req.AppendHeader(&contentTypeHeader)
+ req.SetBody([]byte(body))
+ // The Content-Length header is automatically added by sipgo.
+ } else {
+ contentLengthHeader := sip.ContentLengthHeader(0)
+ req.AppendHeader(&contentLengthHeader)
+ }
+
+ return req, true
+}
+
+// Optional parameters to create a request.
+type NewSipRequestOpts struct {
+ // Default: 0. The host could be a domain name.
+ Port int
+ // Default: 'host' function parameter.
+ LocalHost string
+ // Default: 'Port' property.
+ LocalPort int
+ // Default: No user set in the request (Via and To headers).
+ ToUser string
+ // Default: No user set in the request (From header).
+ User string
+ // Default: No password set in the request (From header).
+ Password string
+ // Default: UDP.
+ Transport TransportType
+}
+
+// Returns a 'Via' header for a SIP request.
+func NewViaHeader(opts *NewViaOpts) (*sip.ViaHeader, bool) {
+ host := "localhost"
+ transport := sip.TransportUDP
+ protocolName := "SIP"
+ protocolVersion := "2.0"
+ if opts == nil {
+ opts = &NewViaOpts{}
+ }
+ if opts.ProtocolName != "" {
+ protocolName = opts.ProtocolName
+ }
+ if opts.ProtocolVersion != "" {
+ protocolVersion = opts.ProtocolVersion
+ }
+ if opts.Transport != "" {
+ transport = opts.Transport
+ }
+ if opts.Host != "" {
+ host = opts.Host
+ }
+
+ // Port could be zero. For example if host is a domain name.
+ return &sip.ViaHeader{
+ ProtocolName: protocolName,
+ ProtocolVersion: protocolVersion,
+ Transport: transport,
+ Host: host,
+ Port: opts.Port,
+ Params: sip.HeaderParams{
+ "branch": sip.GenerateBranchN(16),
+ "rport": "",
+ },
+ }, true
+}
+
+// Optional parameters to create a Via header.
+type NewViaOpts struct {
+ // Default: "SIP"
+ ProtocolName string
+ // Default: "2.0"
+ ProtocolVersion string
+ // Default: "UDP"
+ Transport string
+ // Default: "localhost"
+ Host string
+ // Default: 0
+ Port int
+}
+
+// Checks if contact header is required for a method.
+func IsContactRequired(method sip.RequestMethod) bool {
+ switch method {
+ case sip.REGISTER, sip.INVITE, sip.SUBSCRIBE, sip.REFER, sip.PUBLISH, sip.NOTIFY:
+
+ return true
+ case sip.ACK, sip.CANCEL, sip.BYE, sip.OPTIONS, sip.INFO, sip.PRACK, sip.UPDATE, sip.MESSAGE:
+
+ return false
+ default:
+
+ return false
+ }
+}
+
+// Adds generic method-specific headers to a request.
+//
+// INVITE and MESSAGE ones are handled in the function 'NewRequestBody'.
+func AddMethodHeaders(req *sip.Request, method sip.RequestMethod) bool {
+ if req == nil {
+ output.PrintFrameworkError(errMsgRequiredParam, "param", "req")
+
+ return false
+ }
+ switch method {
+ case sip.OPTIONS:
+ req.AppendHeader(sip.NewHeader("Accept", "application/sdp"))
+ case sip.REGISTER:
+ req.AppendHeader(sip.NewHeader("Expires", strconv.Itoa(DefaultExpiresHeader)))
+ case sip.PRACK:
+ req.AppendHeader(sip.NewHeader("RAck", fmt.Sprintf("%d %d INVITE", DefaultRackCSeq, DefaultRackInviteCSeq)))
+ case sip.SUBSCRIBE:
+ req.AppendHeader(sip.NewHeader("Event", "presence"))
+ req.AppendHeader(sip.NewHeader("Accept", ContentTypePidf))
+ req.AppendHeader(sip.NewHeader("Supported", "eventlist"))
+ case sip.NOTIFY:
+ req.AppendHeader(sip.NewHeader("Subscription-State", "active;expires=3500"))
+ req.AppendHeader(sip.NewHeader("Event", "presence"))
+ case sip.REFER:
+ if req.CallID() == nil {
+ output.PrintFrameworkError("CallID header is required for REFER")
+
+ return false
+ }
+ req.AppendHeader(&sip.ReferToHeader{
+ Address: sip.Uri{
+ Host: req.Recipient.Host,
+ Port: req.Recipient.Port,
+ User: "carol",
+ UriParams: sip.HeaderParams{
+ "Replaces": req.CallID().Value() + `%3Bto-tag%3D54321%3Bfrom-tag%3D12345`,
+ },
+ },
+ })
+ req.AppendHeader(sip.NewHeader("Refer-Sub", "true"))
+ req.AppendHeader(sip.NewHeader("Supported", "replaces"))
+ case sip.PUBLISH:
+ req.AppendHeader(sip.NewHeader("Expires", strconv.Itoa(DefaultExpiresHeader)))
+ req.AppendHeader(sip.NewHeader("Event", "presence"))
+ case sip.INVITE, sip.ACK, sip.CANCEL, sip.BYE, sip.INFO, sip.MESSAGE, sip.UPDATE:
+ }
+
+ return true
+}
+
+// Returns a valid body and content type header for the given method.
+func NewRequestBody(method sip.RequestMethod) (string, string) {
+ switch method {
+ case sip.INVITE:
+ return NewDefaultInviteBody("", "", ""), DefaultInviteContentType
+ case sip.MESSAGE:
+ return DefaultMessageBodyContent, DefaultMessageContentType
+ case sip.INFO:
+ return DefaultInfoBodyContent, DefaultInfoContentType
+ case sip.PUBLISH:
+ return NewDefaultPublishBody("", "", ""), ContentTypePidf
+ case sip.NOTIFY:
+ return NewDefaultNotifyBody("", "", "", ""), ContentTypePidf
+ case sip.ACK, sip.CANCEL, sip.BYE, sip.REGISTER, sip.OPTIONS, sip.SUBSCRIBE, sip.REFER, sip.PRACK, sip.UPDATE:
+
+ return "", ""
+ }
+
+ return "", ""
+}
+
+// Returns a default body for an INVITE request.
+//
+// Default parameters:
+// - host: "randomIPv4()".
+// - sessid: random digits.
+// - sessver: random digits.
+func NewDefaultInviteBody(host, sessid, sessver string) string {
+ if host == "" {
+ host = random.RandIPv4().String()
+ }
+ if sessid == "" {
+ sessid = random.RandDigits(6)
+ }
+ if sessver == "" {
+ sessver = random.RandDigits(6)
+ }
+ return fmt.Sprintf(`v=0
+o=caller %s %s IN IP4 %s
+s=-
+c=IN IP4 %s
+t=0 0
+m=audio 5004 RTP/AVP 0
+a=rtpmap:0 PCMU/8000`, sessid, sessver, host, host)
+}
+
+// Returns a default body for a PUBLISH request.
+//
+// Default parameters:
+// - id: random letters.
+// - status: "open".
+// - entity: "sip:bob@randomIPv4():5060".
+func NewDefaultPublishBody(id, status, entity string) string {
+ if id == "" {
+ id = random.RandLetters(6)
+ }
+ if status == "" {
+ status = "open"
+ }
+ if entity == "" {
+ entity = fmt.Sprintf("sip:bob@%s:5060", random.RandIPv4())
+ }
+ return fmt.Sprintf(`
+
+
+
+ %s
+
+
+`, entity, id, status)
+}
+
+// Returns a default body for a NOTIFY request.
+//
+// Default parameters:
+// - id: random letters.
+// - status: "open".
+// - contact: "sip:bob@randomIPv4():5060".
+// - ts: current UTC time in RFC 3339 format.
+func NewDefaultNotifyBody(id, status, contact, ts string) string {
+ if id == "" {
+ id = random.RandLetters(6)
+ }
+ if status == "" {
+ status = "open"
+ }
+ if contact == "" {
+ contact = fmt.Sprintf("sip:bob@%s:5060", random.RandIPv4())
+ }
+ if ts == "" {
+ ts = time.Now().UTC().Format(time.RFC3339)
+ }
+ return fmt.Sprintf(`
+
+
+
+ %s
+
+ %s
+ %s
+
+`, id, status, contact, ts)
+}
diff --git a/protocol/sip/helper_test.go b/protocol/sip/helper_test.go
new file mode 100644
index 0000000..532d204
--- /dev/null
+++ b/protocol/sip/helper_test.go
@@ -0,0 +1,811 @@
+package sip
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/emiago/sipgo/sip"
+)
+
+const (
+ host = "localhost"
+ expectedMethod = sip.OPTIONS
+ expectedMaxForwardsHeader = "Max-Forwards: 70"
+ expectedUserAgent = "Jitsi2.10.5550Mac OS X"
+ expectedAccept = "application/sdp"
+ expectedProtocolName = "SIP"
+ expectedProtocolVersion = "2.0"
+ expectedTransport = "UDP"
+ expectedCSeqHeader = "CSeq: 1 OPTIONS"
+ expectedPresenceHeaderValue = "presence"
+)
+
+func TestNewSipRequest(t *testing.T) {
+ t.Run("returns ok to false if method is zero", func(t *testing.T) {
+ req, ok := NewSipRequest("", "", nil)
+ if ok {
+ t.Fatal("got true ok")
+ }
+ if req != nil {
+ t.Fatal("got non 'nil' request")
+ }
+ })
+ t.Run("returns ok to false if host is zero", func(t *testing.T) {
+ req, ok := NewSipRequest(sip.OPTIONS, "", nil)
+ if ok {
+ t.Fatal("got true ok")
+ }
+ if req != nil {
+ t.Fatal("got non 'nil' request")
+ }
+ })
+ t.Run("returns the request with default options", func(t *testing.T) {
+ req, ok := NewSipRequest(sip.OPTIONS, host, nil)
+ if !ok {
+ t.Fatal("got false ok")
+ }
+ if req == nil {
+ t.Fatal("got 'nil' request")
+ }
+ if req.Method != expectedMethod {
+ t.Fatalf("got method %s, want %s", req.Method, expectedMethod)
+ }
+ if req.Recipient.Host != host {
+ t.Fatalf("got recipient host %s, want %s", req.Recipient.Host, host)
+ }
+ if req.Recipient.Port != 0 {
+ t.Fatalf("got recipient port %d, want 0", req.Recipient.Port)
+ }
+ transport := req.Transport()
+ if transport != "UDP" {
+ t.Fatalf("got recipient transport %s, wants UDP", transport)
+ }
+ startLine := req.StartLine()
+ expectedStartLine := "OPTIONS sip:localhost SIP/2.0"
+ if startLine != expectedStartLine {
+ t.Fatalf("got start line %s, want %s", startLine, expectedStartLine)
+ }
+ if len(req.Headers()) != 9 {
+ t.Fatalf("got %d headers, want 9", len(req.Headers()))
+ }
+ viaHeader := req.Via()
+ if viaHeader == nil {
+ t.Fatal("got 'nil' Via header")
+ }
+ if viaHeader.ProtocolName != expectedProtocolName {
+ t.Fatalf("got protocol name %s, want %s", viaHeader.ProtocolName, expectedProtocolName)
+ }
+ if viaHeader.ProtocolVersion != expectedProtocolVersion {
+ t.Fatalf("got protocol version %s, want %s", viaHeader.ProtocolVersion, expectedProtocolVersion)
+ }
+ expectedTransport := expectedTransport
+ if viaHeader.Transport != expectedTransport {
+ t.Fatalf("got transport %s, want %s", viaHeader.Transport, expectedTransport)
+ }
+ if viaHeader.Host != host {
+ t.Fatalf("got host %s, want %s", viaHeader.Host, host)
+ }
+ expectedPort := 0
+ if viaHeader.Port != expectedPort {
+ t.Fatalf("got port %d, want %d", viaHeader.Port, expectedPort)
+ }
+ if len(viaHeader.Params) != 2 {
+ t.Fatalf("got %d parameters, want 2", len(viaHeader.Params))
+ }
+ if viaHeader.Params["branch"] == "" {
+ t.Fatal("got zero branch parameter")
+ }
+ if viaHeader.Params["rport"] != "" {
+ t.Fatal("got non-zero rport parameter")
+ }
+ maxForwardsHeader := req.MaxForwards().String()
+ if maxForwardsHeader != expectedMaxForwardsHeader {
+ t.Fatalf("got Max-Forwards header %s, want %s", maxForwardsHeader, expectedMaxForwardsHeader)
+ }
+ fromHeader := req.From().String()
+ expectedFromHeader := "From: ;tag="
+ if strings.Contains(fromHeader, expectedFromHeader) == false {
+ t.Fatalf("got From header %s, want %s", fromHeader, expectedFromHeader)
+ }
+ toHeader := req.To().String()
+ expectedToHeader := "To: "
+ if toHeader != expectedToHeader {
+ t.Fatalf("got To header %s, want %s", toHeader, expectedToHeader)
+ }
+ ciHeader := req.CallID().String()
+ if ciHeader == "" {
+ t.Fatal("got zero Call-ID header")
+ }
+ cseqHeader := req.CSeq().String()
+ if cseqHeader != expectedCSeqHeader {
+ t.Fatalf("got CSeq header %s, want %s", cseqHeader, expectedCSeqHeader)
+ }
+ userAgentHeader := req.GetHeader("user-agent")
+ if userAgentHeader == nil {
+ t.Fatal("got 'nil' User-Agent header")
+ }
+ if userAgentHeader.Value() != expectedUserAgent {
+ t.Fatalf("got User-Agent header %s, want %s", userAgentHeader.Value(), expectedUserAgent)
+ }
+ acceptHeader := req.GetHeader("accept")
+ if acceptHeader == nil {
+ t.Fatal("got 'nil' Accept header")
+ }
+ if acceptHeader.Value() != expectedAccept {
+ t.Fatalf("got Accept header %s, want %s", acceptHeader.Value(), expectedAccept)
+ }
+ if req.Contact() != nil {
+ t.Fatal("got non-nil Contact header")
+ }
+ if req.ContentType() != nil {
+ t.Fatal("got non-nil Content-Type header")
+ }
+ if req.ContentLength() == nil {
+ t.Fatal("got nil Content-Length header")
+ }
+ contentLenValue := req.ContentLength().Value()
+ if contentLenValue != "0" {
+ t.Fatalf("got Content-Length header %s, want 0", contentLenValue)
+ }
+ if req.Body() != nil {
+ t.Fatal("got non-nil body")
+ }
+ })
+ t.Run("returns the request with custom options", func(t *testing.T) {
+ opts := NewSipRequestOpts{
+ Port: 5061,
+ LocalHost: "192.168.1.1",
+ LocalPort: 5065,
+ ToUser: "dembele",
+ User: "fabian",
+ Password: "pass-0",
+ Transport: TCP,
+ }
+ req, ok := NewSipRequest(sip.OPTIONS, host, &opts)
+ if !ok {
+ t.Fatal("got false ok")
+ }
+ if req == nil {
+ t.Fatal("got 'nil' request")
+ }
+ if req.Method != expectedMethod {
+ t.Fatalf("got method %s, want %s", req.Method, expectedMethod)
+ }
+ if req.Recipient.Host != host {
+ t.Fatalf("got recipient host %s, want %s", req.Recipient.Host, host)
+ }
+ if req.Recipient.Port != opts.Port {
+ t.Fatalf("got recipient port %d, want %d", req.Recipient.Port, opts.Port)
+ }
+ transport := req.Transport()
+ if transport != TCP.String() {
+ t.Fatalf("got recipient transport %s, wants %s", transport, TCP.String())
+ }
+ startLine := req.StartLine()
+ expectedStartLine := "OPTIONS sip:dembele@localhost:5061 SIP/2.0"
+ if startLine != expectedStartLine {
+ t.Fatalf("got start line %s, want %s", startLine, expectedStartLine)
+ }
+ if len(req.Headers()) != 9 {
+ t.Fatalf("got %d headers, want 9", len(req.Headers()))
+ }
+ viaHeader := req.Via()
+ if viaHeader == nil {
+ t.Fatal("got 'nil' Via header")
+ }
+ if viaHeader.ProtocolName != expectedProtocolName {
+ t.Fatalf("got protocol name %s, want %s", viaHeader.ProtocolName, expectedProtocolName)
+ }
+ if viaHeader.ProtocolVersion != expectedProtocolVersion {
+ t.Fatalf("got protocol version %s, want %s", viaHeader.ProtocolVersion, expectedProtocolVersion)
+ }
+ expectedTransport := "TCP"
+ if viaHeader.Transport != expectedTransport {
+ t.Fatalf("got transport %s, want %s", viaHeader.Transport, expectedTransport)
+ }
+ if viaHeader.Host != opts.LocalHost {
+ t.Fatalf("got host %s, want %s", viaHeader.Host, opts.LocalHost)
+ }
+ if viaHeader.Port != opts.LocalPort {
+ t.Fatalf("got port %d, want %d", viaHeader.Port, opts.LocalPort)
+ }
+ if len(viaHeader.Params) != 2 {
+ t.Fatalf("got %d parameters, want 2", len(viaHeader.Params))
+ }
+ if viaHeader.Params["branch"] == "" {
+ t.Fatal("got zero branch parameter")
+ }
+ if viaHeader.Params["rport"] != "" {
+ t.Fatal("got non-zero rport parameter")
+ }
+ maxForwardsHeader := req.MaxForwards().String()
+ if maxForwardsHeader != expectedMaxForwardsHeader {
+ t.Fatalf("got Max-Forwards header %s, want %s", maxForwardsHeader, expectedMaxForwardsHeader)
+ }
+ fromHeader := req.From().String()
+ expectedFromHeader := "From: ;tag="
+ if !strings.Contains(fromHeader, expectedFromHeader) {
+ t.Fatalf("got From header %s, want %s", fromHeader, expectedFromHeader)
+ }
+ toHeader := req.To().String()
+ expectedToHeader := "To: "
+ if toHeader != expectedToHeader {
+ t.Fatalf("got To header %s, want %s", toHeader, expectedToHeader)
+ }
+ ciHeader := req.CallID().String()
+ if ciHeader == "" {
+ t.Fatal("got zero Call-ID header")
+ }
+ cseqHeader := req.CSeq().String()
+ if cseqHeader != expectedCSeqHeader {
+ t.Fatalf("got CSeq header %s, want %s", cseqHeader, expectedCSeqHeader)
+ }
+ userAgentHeader := req.GetHeader("user-agent")
+ if userAgentHeader == nil {
+ t.Fatal("got 'nil' User-Agent header")
+ }
+ if userAgentHeader.Value() != expectedUserAgent {
+ t.Fatalf("got User-Agent header %s, want %s", userAgentHeader.Value(), expectedUserAgent)
+ }
+ acceptHeader := req.GetHeader("accept")
+ if acceptHeader == nil {
+ t.Fatal("got 'nil' Accept header")
+ }
+ if acceptHeader.Value() != expectedAccept {
+ t.Fatalf("got Accept header %s, want %s", acceptHeader.Value(), expectedAccept)
+ }
+ if req.Contact() != nil {
+ t.Fatal("got non-nil Contact header")
+ }
+ if req.ContentType() != nil {
+ t.Fatal("got non-nil Content-Type header")
+ }
+ if req.ContentLength() == nil {
+ t.Fatal("got nil Content-Length header")
+ }
+ contentLenValue := req.ContentLength().Value()
+ if contentLenValue != "0" {
+ t.Fatalf("got Content-Length header %s, want 0", contentLenValue)
+ }
+ if req.Body() != nil {
+ t.Fatal("got non-nil body")
+ }
+ })
+ t.Run("adds contact header for methods that require it", func(t *testing.T) {
+ req, ok := NewSipRequest(sip.REGISTER, "localhost", nil)
+ if !ok {
+ t.Fatal("got false ok")
+ }
+ if req == nil {
+ t.Fatal("got 'nil' request")
+ }
+ if req.Contact() == nil {
+ t.Fatal("got 'nil' Contact header")
+ }
+ expectedContact := ";transport=UDP"
+ if req.Contact().Value() != expectedContact {
+ t.Fatalf("got Contact header %s, want %s", req.Contact().Value(), expectedContact)
+ }
+ })
+ t.Run("adds content type and body for methods that require them", func(t *testing.T) {
+ req, ok := NewSipRequest(sip.INVITE, "localhost", nil)
+ if !ok {
+ t.Fatal("got false ok")
+ }
+ if req == nil {
+ t.Fatal("got 'nil' request")
+ }
+ if req.ContentType() == nil {
+ t.Fatal("got 'nil' Content-Type header")
+ }
+ expectedContentType := "application/sdp"
+ if req.ContentType().Value() != expectedContentType {
+ t.Fatalf("got Content-Type header %s, want %s", req.ContentType().Value(), expectedContentType)
+ }
+ if req.Body() == nil {
+ t.Fatal("got 'nil' body")
+ }
+ expectedBody := "o=caller"
+ if !strings.Contains(string(req.Body()), expectedBody) {
+ t.Fatalf("got body %s, want %s", req.Body(), expectedBody)
+ }
+ })
+}
+
+//gocognit:ignore
+func TestNewViaHeader(t *testing.T) {
+ t.Run("returns the header with default options", func(t *testing.T) {
+ header, ok := NewViaHeader(nil)
+ if !ok {
+ t.Fatal("got false ok")
+ }
+ if header == nil {
+ t.Fatal("got 'nil' Via header")
+ }
+ expectedProtocolName := "SIP"
+ if header.ProtocolName != expectedProtocolName {
+ t.Fatalf("got protocol name %s, want %s", header.ProtocolName, expectedProtocolName)
+ }
+ expectedProtocolVersion := "2.0"
+ if header.ProtocolVersion != expectedProtocolVersion {
+ t.Fatalf("got protocol version %s, want %s", header.ProtocolVersion, expectedProtocolVersion)
+ }
+ expectedTransport := "UDP"
+ if header.Transport != expectedTransport {
+ t.Fatalf("got transport %s, want %s", header.Transport, expectedTransport)
+ }
+ if header.Host != host {
+ t.Fatalf("got host %s, want %s", header.Host, host)
+ }
+ expectedPort := 0
+ if header.Port != expectedPort {
+ t.Fatalf("got port %d, want %d", header.Port, expectedPort)
+ }
+ if len(header.Params) != 2 {
+ t.Fatalf("got %d parameters, want 2", len(header.Params))
+ }
+ if header.Params["branch"] == "" {
+ t.Fatal("got zero branch parameter")
+ }
+ if header.Params["rport"] != "" {
+ t.Fatal("got non-zero rport parameter")
+ }
+ })
+ t.Run("returns the header with custom options", func(t *testing.T) {
+ opts := NewViaOpts{
+ ProtocolName: "PNAME",
+ ProtocolVersion: "1.0",
+ Transport: "TCP",
+ Host: "localhost",
+ Port: 5061,
+ }
+ header, ok := NewViaHeader(&opts)
+ if !ok {
+ t.Fatal("got false ok")
+ }
+ if header == nil {
+ t.Fatal("got 'nil' header")
+ }
+ if header.ProtocolName != opts.ProtocolName {
+ t.Fatalf("got protocol name %s, want %s", header.ProtocolName, opts.ProtocolName)
+ }
+ if header.ProtocolVersion != opts.ProtocolVersion {
+ t.Fatalf("got protocol version %s, want %s", header.ProtocolVersion, opts.ProtocolVersion)
+ }
+ if header.Transport != "TCP" {
+ t.Fatalf("got transport %s, want %s", header.Transport, "TCP")
+ }
+ if header.Host != opts.Host {
+ t.Fatalf("got host %s, want %s", header.Host, opts.Host)
+ }
+ if header.Port != opts.Port {
+ t.Fatalf("got port %d, want %d", header.Port, opts.Port)
+ }
+ if len(header.Params) != 2 {
+ t.Fatalf("got %d parameters, want 2", len(header.Params))
+ }
+ if header.Params["branch"] == "" {
+ t.Fatal("got zero branch parameter")
+ }
+ if header.Params["rport"] != "" {
+ t.Fatal("got non-zero rport parameter")
+ }
+ })
+}
+
+func TestIsContactRequired(t *testing.T) {
+ tests := []struct {
+ name string
+ method sip.RequestMethod
+ expected bool
+ }{
+ {sip.INVITE.String(), sip.INVITE, true},
+ {sip.ACK.String(), sip.ACK, false},
+ {sip.CANCEL.String(), sip.CANCEL, false},
+ {sip.BYE.String(), sip.BYE, false},
+ {sip.REGISTER.String(), sip.REGISTER, true},
+ {sip.OPTIONS.String(), sip.OPTIONS, false},
+ {sip.SUBSCRIBE.String(), sip.SUBSCRIBE, true},
+ {sip.NOTIFY.String(), sip.NOTIFY, true},
+ {sip.REFER.String(), sip.REFER, true},
+ {sip.INFO.String(), sip.INFO, false},
+ {sip.MESSAGE.String(), sip.MESSAGE, false},
+ {sip.PRACK.String(), sip.PRACK, false},
+ {sip.UPDATE.String(), sip.UPDATE, false},
+ {sip.PUBLISH.String(), sip.PUBLISH, true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := IsContactRequired(tt.method); got != tt.expected {
+ t.Fatalf("got %v, want %v", got, tt.expected)
+ }
+ })
+ }
+}
+
+func TestAddMethodHeaders(t *testing.T) {
+ t.Run("returns ok to false if request is nil", func(t *testing.T) {
+ ok := AddMethodHeaders(nil, sip.OPTIONS)
+ if ok {
+ t.Fatal("got true ok")
+ }
+ })
+ t.Run("OPTIONS", func(t *testing.T) {
+ req := sip.Request{}
+ ok := AddMethodHeaders(&req, sip.OPTIONS)
+ if !ok {
+ t.Fatal("got false ok")
+ }
+ headers := req.Headers()
+ if len(headers) != 1 {
+ t.Fatalf("got %d headers, want 1", len(headers))
+ }
+ header := req.GetHeader("accept")
+ if header == nil {
+ t.Fatal("got 'nil' accept header")
+ }
+ value := header.Value()
+ if value != "application/sdp" {
+ t.Fatalf("got accept header value %v, want application/sdp", value)
+ }
+ })
+ t.Run("REGISTER", func(t *testing.T) {
+ req := sip.Request{}
+ ok := AddMethodHeaders(&req, sip.REGISTER)
+ if !ok {
+ t.Fatal("got false ok")
+ }
+ headers := req.Headers()
+ if len(headers) != 1 {
+ t.Fatalf("got %d headers, want 1", len(headers))
+ }
+ header := req.GetHeader("expires")
+ if header == nil {
+ t.Fatal("got 'nil' expires header")
+ }
+ value := header.Value()
+ if value != "3600" {
+ t.Fatalf("got expires header value %v, want 3600", value)
+ }
+ })
+ t.Run("PRACK", func(t *testing.T) {
+ req := sip.Request{}
+ ok := AddMethodHeaders(&req, sip.PRACK)
+ if !ok {
+ t.Fatal("got false ok")
+ }
+ headers := req.Headers()
+ if len(headers) != 1 {
+ t.Fatalf("got %d headers, want 1", len(headers))
+ }
+ header := req.GetHeader("RAck")
+ if header == nil {
+ t.Fatal("got 'nil' rack header")
+ }
+ value := header.Value()
+ if value != "1 314159 INVITE" {
+ t.Fatalf("got rack header value %v, want 1 314159 INVITE", value)
+ }
+ })
+ t.Run("SUBSCRIBE", func(t *testing.T) {
+ req := sip.Request{}
+ ok := AddMethodHeaders(&req, sip.SUBSCRIBE)
+ if !ok {
+ t.Fatal("got false ok")
+ }
+ headers := req.Headers()
+ if len(headers) != 3 {
+ t.Fatalf("got %d headers, want 3", len(headers))
+ }
+ header := req.GetHeader("event")
+ if header == nil {
+ t.Fatal("got 'nil' event header")
+ }
+ value := header.Value()
+ if value != expectedPresenceHeaderValue {
+ t.Fatalf("got event header value %v, want presence", value)
+ }
+ header = req.GetHeader("accept")
+ if header == nil {
+ t.Fatal("got 'nil' accept header")
+ }
+ value = header.Value()
+ if value != "application/pidf+xml" {
+ t.Fatalf("got accept header value %v, want application/pidf+xml", value)
+ }
+ header = req.GetHeader("supported")
+ if header == nil {
+ t.Fatal("got 'nil' supported header")
+ }
+ value = header.Value()
+ if value != "eventlist" {
+ t.Fatalf("got supported header value %v, want eventlist", value)
+ }
+ })
+ t.Run("NOTIFY", func(t *testing.T) {
+ req := sip.Request{}
+ ok := AddMethodHeaders(&req, sip.NOTIFY)
+ if !ok {
+ t.Fatal("got false ok")
+ }
+ headers := req.Headers()
+ if len(headers) != 2 {
+ t.Fatalf("got %d headers, want 2", len(headers))
+ }
+ header := req.GetHeader("event")
+ if header == nil {
+ t.Fatal("got 'nil' event header")
+ }
+ value := header.Value()
+ if value != expectedPresenceHeaderValue {
+ t.Fatalf("got event header value %v, want presence", value)
+ }
+ header = req.GetHeader("subscription-state")
+ if header == nil {
+ t.Fatal("got 'nil' subscription-state header")
+ }
+ value = header.Value()
+ if value != "active;expires=3500" {
+ t.Fatalf("got subscription-state header value %v, want active;expires=3500", value)
+ }
+ })
+ t.Run("REFER returns error if call id is not set", func(t *testing.T) {
+ req := sip.Request{}
+ ok := AddMethodHeaders(&req, sip.REFER)
+ if ok {
+ t.Fatal("got true ok")
+ }
+ headers := req.Headers()
+ if len(headers) != 0 {
+ t.Fatalf("got %d headers, want 0", len(headers))
+ }
+ })
+ t.Run("REFER", func(t *testing.T) {
+ req := sip.Request{
+ Recipient: sip.Uri{
+ Host: "127.0.0.1",
+ },
+ }
+ ciHeader := sip.CallIDHeader("123456")
+ req.AppendHeader(&ciHeader)
+ ok := AddMethodHeaders(&req, sip.REFER)
+ if !ok {
+ t.Fatal("got false ok")
+ }
+ headers := req.Headers()
+ if len(headers) != 4 {
+ t.Fatalf("got %d headers, want 3", len(headers))
+ }
+ header := req.GetHeader("refer-to")
+ if header == nil {
+ t.Fatal("got 'nil' refer-to header")
+ }
+ value := header.Value()
+ expectedValue := ""
+ if value != expectedValue {
+ t.Fatalf("got refer-to header value %s, want %s", value, expectedValue)
+ }
+ header = req.GetHeader("refer-sub")
+ if header == nil {
+ t.Fatal("got 'nil' refer-sub header")
+ }
+ value = header.Value()
+ if value != "true" {
+ t.Fatalf("got refer-sub header value %v, want true", value)
+ }
+ header = req.GetHeader("supported")
+ if header == nil {
+ t.Fatal("got 'nil' supported header")
+ }
+ value = header.Value()
+ if value != "replaces" {
+ t.Fatalf("got supported header value %v, want replaces", value)
+ }
+ })
+ t.Run("PUBLISH", func(t *testing.T) {
+ req := sip.Request{}
+ ok := AddMethodHeaders(&req, sip.PUBLISH)
+ if !ok {
+ t.Fatal("got false ok")
+ }
+ headers := req.Headers()
+ if len(headers) != 2 {
+ t.Fatalf("got %d headers, want 2", len(headers))
+ }
+ header := req.GetHeader("event")
+ if header == nil {
+ t.Fatal("got 'nil' event header")
+ }
+ value := header.Value()
+ if value != expectedPresenceHeaderValue {
+ t.Fatalf("got event header value %v, want presence", value)
+ }
+ header = req.GetHeader("expires")
+ if header == nil {
+ t.Fatal("got 'nil' expires header")
+ }
+ value = header.Value()
+ if value != "3600" {
+ t.Fatalf("got expires header value %v, want 3600", value)
+ }
+ })
+ t.Run("MESSAGE", func(t *testing.T) {
+ req := sip.Request{}
+ ok := AddMethodHeaders(&req, sip.MESSAGE)
+ if !ok {
+ t.Fatal("got false ok")
+ }
+ headers := req.Headers()
+ if len(headers) != 0 {
+ t.Fatalf("got %d headers, want 0", len(headers))
+ }
+ })
+}
+
+func TestNewRequestBody(t *testing.T) {
+ tests := []struct {
+ name string
+ method sip.RequestMethod
+ expectedBodyInclude string
+ expectedHeader string
+ }{
+ {
+ "INVITE",
+ sip.INVITE,
+ "o=caller",
+ "application/sdp",
+ },
+ {
+ "MESSAGE",
+ sip.MESSAGE,
+ "Hello, this is a test message",
+ "text/plain",
+ },
+ {
+ "INFO",
+ sip.INFO,
+ "Signal=1Signal=1\nDuration=10",
+ "application/dtmf-relay",
+ },
+ {"ACK", sip.ACK, "", ""},
+ {"CANCEL", sip.CANCEL, "", ""},
+ {"BYE", sip.BYE, "", ""},
+ {"REGISTER", sip.REGISTER, "", ""},
+ {"OPTIONS", sip.OPTIONS, "", ""},
+ {"SUBSCRIBE", sip.SUBSCRIBE, "", ""},
+ {
+ "NOTIFY",
+ sip.NOTIFY,
+ "",
+ "application/pidf+xml",
+ },
+ {"REFER", sip.REFER, "", ""},
+ {
+ "PUBLISH",
+ sip.PUBLISH,
+ `presence xmlns="urn:ietf:params:xml:ns:pidf`,
+ "application/pidf+xml",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ body, header := NewRequestBody(tt.method)
+ if !strings.Contains(body, tt.expectedBodyInclude) {
+ t.Fatalf("got body %v, want %v", body, tt.expectedBodyInclude)
+ }
+ if header != tt.expectedHeader {
+ t.Fatalf("got header %v, want %v", header, tt.expectedHeader)
+ }
+ })
+ }
+}
+
+func TestNewDefaultInviteBody(t *testing.T) {
+ t.Run("returns a valid body", func(t *testing.T) {
+ body := NewDefaultInviteBody("", "", "")
+ if body == "" {
+ t.Fatal("got empty")
+ }
+ if !strings.Contains(body, "o=caller") {
+ t.Fatalf("got %s, want o=caller", body)
+ }
+ if !strings.Contains(body, "s=-") {
+ t.Fatalf("got %s, want s=-", body)
+ }
+ if !strings.Contains(body, "c=IN IP4") {
+ t.Fatalf("got %s, want c=IN IP4", body)
+ }
+ expected := `t=0 0
+m=audio 5004 RTP/AVP 0
+a=rtpmap:0 PCMU/8000`
+ if !strings.Contains(body, expected) {
+ t.Fatalf("got %s, want %s", body, expected)
+ }
+ })
+ t.Run("returns a valid body with custom parameters", func(t *testing.T) {
+ body := NewDefaultInviteBody("host-0", "sess-0", "sessver-0")
+ expected := `v=0
+o=caller sess-0 sessver-0 IN IP4 host-0
+s=-
+c=IN IP4 host-0
+t=0 0
+m=audio 5004 RTP/AVP 0
+a=rtpmap:0 PCMU/8000`
+ if body != expected {
+ t.Fatalf("got %s, want %s", body, expected)
+ }
+ })
+}
+
+func TestNewDefaultPublishBody(t *testing.T) {
+ t.Run("returns a valid body", func(t *testing.T) {
+ body := NewDefaultPublishBody("", "", "")
+ if body == "" {
+ t.Fatal("got empty")
+ }
+ expected := `
+
+
+
+
+ status-0
+
+
+`
+ if body != expected {
+ t.Fatalf("got %s, want %s", body, expected)
+ }
+ })
+}
+
+func TestNewDefaultNotifyBody(t *testing.T) {
+ t.Run("returns a valid body", func(t *testing.T) {
+ body := NewDefaultNotifyBody("", "", "", "")
+ if body == "" {
+ t.Fatal("got empty")
+ }
+ expected := `
+
+
+ open
+
+ sip:bob@`
+ if !strings.Contains(body, expected) {
+ t.Fatalf("got %s, want %s", body, expected)
+ }
+ expected = `
+ `
+ if !strings.Contains(body, expected) {
+ t.Fatalf("got %s, want %s", body, expected)
+ }
+ expected = `
+
+`
+ if !strings.Contains(body, expected) {
+ t.Fatalf("got %s, want %s", body, expected)
+ }
+ })
+}
diff --git a/protocol/sip/user-agent.txt b/protocol/sip/user-agent.txt
new file mode 100644
index 0000000..28ebe4e
--- /dev/null
+++ b/protocol/sip/user-agent.txt
@@ -0,0 +1 @@
+Jitsi2.10.5550Mac OS X
diff --git a/random/random.go b/random/random.go
index 7c49ba5..692476a 100644
--- a/random/random.go
+++ b/random/random.go
@@ -3,6 +3,7 @@ package random
import (
"crypto/rand"
"math/big"
+ "net"
"strings"
"github.com/vulncheck-oss/go-exploit/output"
@@ -140,3 +141,22 @@ var CommonTLDs = []string{
"org",
"net",
}
+
+// RandIPv4 generates a random IPv4 address.
+func RandIPv4() net.IP {
+ return net.IPv4(
+ byte(RandIntRange(1, 256)),
+ byte(RandIntRange(1, 256)),
+ byte(RandIntRange(1, 256)),
+ byte(RandIntRange(1, 256)),
+ )
+}
+
+// RandIPv6 generates a random IPv6 address.
+func RandIPv6() net.IP {
+ ip := make(net.IP, net.IPv6len)
+ for i := range net.IPv6len {
+ ip[i] = byte(RandIntRange(1, 256))
+ }
+ return ip
+}
diff --git a/random/random_test.go b/random/random_test.go
index 98d9674..971708e 100644
--- a/random/random_test.go
+++ b/random/random_test.go
@@ -1,6 +1,7 @@
package random
import (
+ "net"
"strings"
"testing"
)
@@ -165,3 +166,29 @@ func Test_RandEmail(t *testing.T) {
}
}
}
+
+func TestRandIPv4(t *testing.T) {
+ for range 100 {
+ r := RandIPv4()
+ parsed := net.ParseIP(r.String())
+ if parsed == nil {
+ t.Error("Generated IP is nil: " + r.String())
+ }
+ if parsed.To4() == nil {
+ t.Error("Generated IP is not a valid IPv4 address: " + r.String())
+ }
+ }
+}
+
+func TestRandIPv6(t *testing.T) {
+ for range 100 {
+ r := RandIPv6()
+ parsed := net.ParseIP(r.String())
+ if parsed == nil {
+ t.Error("Generated IP is nil: " + r.String())
+ }
+ if parsed.To4() != nil {
+ t.Error("Generated IP is not a valid IPv6 address: " + r.String())
+ }
+ }
+}