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()) + } + } +}