| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372 |
- package ims
- import (
- "context"
- "encoding/base64"
- "errors"
- "fmt"
- "net"
- "strings"
- "testing"
- "time"
- "vocat/internal/vowifi"
- )
- type smsTestAKA struct{ *recordingAKA }
- func (smsTestAKA) ReadSMSCenter(context.Context, string) (string, error) {
- return "+447785016005", nil
- }
- func TestSessionReceivesAndAcknowledgesSMSOverIMS(t *testing.T) {
- listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")})
- if err != nil {
- t.Fatal(err)
- }
- defer listener.Close()
- _ = listener.SetDeadline(time.Now().Add(10 * time.Second))
- received := make(chan ReceivedSMS, 1)
- serverDone := make(chan error, 1)
- nonce := base64.StdEncoding.EncodeToString(make([]byte, 32))
- go func() { serverDone <- serveInboundSMS(listener, nonce) }()
- provider, err := NewProvider(
- smsTestAKA{&recordingAKA{result: vowifi.AKAResult{RES: []byte{1, 2, 3, 4}}}},
- Config{
- PCSCF: listener.LocalAddr().String(), LocalAddress: "127.0.0.1",
- Transport: "udp", TransactionTimeout: 3 * time.Second, SecurityMode: SecurityDisabled,
- OnSMS: func(_ context.Context, message ReceivedSMS) error {
- received <- message
- return nil
- },
- },
- )
- if err != nil {
- t.Fatal(err)
- }
- session, err := provider.Start(context.Background(), vowifi.IMSRequest{
- DeviceID: "ec20",
- Identity: vowifi.SIMIdentity{IMSI: "001010123456789", HomeMCC: "001", HomeMNC: "01"},
- Tunnel: evidenceTunnel{evidence: vowifi.TunnelEvidence{
- Established: true, LocalIPv4: "127.0.0.1", PCSCF: []string{listener.LocalAddr().String()},
- }},
- })
- if err != nil {
- t.Fatal(err)
- }
- select {
- case message := <-received:
- if message.From != "+12345" || message.Text != "HELLO" ||
- message.MessageID != "ims:network-deliver-1:42" ||
- message.ServiceCenterTimestamp == nil || message.Timestamp.IsZero() {
- t.Fatalf("received = %#v", message)
- }
- case <-time.After(5 * time.Second):
- t.Fatal("timed out waiting for inbound SMS")
- }
- if err := session.Close(context.Background()); err != nil {
- t.Fatal(err)
- }
- if err := <-serverDone; err != nil {
- t.Fatal(err)
- }
- }
- func TestRuntimeSecurityHeaders(t *testing.T) {
- verify := "ipsec-3gpp;alg=hmac-sha-1-96;prot=esp;mod=trans"
- headers := runtimeSecurityHeaders(true, verify)
- want := []string{
- "Security-Verify: " + verify,
- "Require: sec-agree",
- "Proxy-Require: sec-agree",
- }
- if len(headers) != len(want) {
- t.Fatalf("security header count = %d, want %d", len(headers), len(want))
- }
- for index := range want {
- if headers[index] != want[index] {
- t.Fatalf("security header %d = %q, want %q", index, headers[index], want[index])
- }
- }
- if headers := runtimeSecurityHeaders(false, verify); len(headers) != 0 {
- t.Fatalf("disabled security headers = %#v", headers)
- }
- }
- func TestSessionSendsSMSOverIMS(t *testing.T) {
- listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")})
- if err != nil {
- t.Fatal(err)
- }
- defer listener.Close()
- _ = listener.SetDeadline(time.Now().Add(10 * time.Second))
- serverDone := make(chan error, 1)
- statusReceived := make(chan ReceivedSMSStatus, 1)
- nonce := base64.StdEncoding.EncodeToString(make([]byte, 32))
- go func() { serverDone <- serveOutboundSMS(listener, nonce) }()
- provider, err := NewProvider(
- smsTestAKA{&recordingAKA{result: vowifi.AKAResult{RES: []byte{1, 2, 3, 4}}}},
- Config{
- PCSCF: listener.LocalAddr().String(), LocalAddress: "127.0.0.1",
- Transport: "udp", TransactionTimeout: 3 * time.Second, SecurityMode: SecurityDisabled,
- OnSMSStatus: func(_ context.Context, status ReceivedSMSStatus) error {
- statusReceived <- status
- return nil
- },
- },
- )
- if err != nil {
- t.Fatal(err)
- }
- session, err := provider.Start(context.Background(), vowifi.IMSRequest{
- DeviceID: "ec20",
- Identity: vowifi.SIMIdentity{IMSI: "001010123456789", HomeMCC: "001", HomeMNC: "01"},
- Tunnel: evidenceTunnel{evidence: vowifi.TunnelEvidence{
- Established: true, LocalIPv4: "127.0.0.1", PCSCF: []string{listener.LocalAddr().String()},
- }},
- })
- if err != nil {
- t.Fatal(err)
- }
- result, err := session.(vowifi.SMSSender).SendSMS(context.Background(), vowifi.SMSSubmitRequest{
- Recipient: "+12345", Text: "HELLO",
- })
- if err != nil || !result.AllPartsAccepted || result.PartsAccepted != 1 || result.PartResults[0].SIPCode != 202 {
- t.Fatalf("SendSMS = (%#v, %v)", result, err)
- }
- select {
- case status := <-statusReceived:
- if status.To != "+12345" || status.MessageReference != result.PartResults[0].Reference ||
- status.StatusCode != 0 || status.DeliveryStatus != "delivered" ||
- status.ServiceCenterTimestamp == nil || status.DischargeTimestamp == nil {
- t.Fatalf("SMS status = %#v", status)
- }
- case <-time.After(5 * time.Second):
- t.Fatal("timed out waiting for SMS delivery status")
- }
- if err := session.Close(context.Background()); err != nil {
- t.Fatal(err)
- }
- if err := <-serverDone; err != nil {
- t.Fatal(err)
- }
- }
- func serveInboundSMS(listener *net.UDPConn, nonce string) error {
- packet := make([]byte, 65535)
- count, remote, err := listener.ReadFromUDP(packet)
- if err != nil {
- return err
- }
- _, headers, err := parseTestRequest(packet[:count])
- if err != nil {
- return err
- }
- callID := headers["call-id"]
- if _, err = listener.WriteToUDP(testResponse(401, "Unauthorized", callID, headers["cseq"], []string{
- `WWW-Authenticate: Digest realm="ims.mnc001.mcc001.3gppnetwork.org", nonce="` + nonce + `", algorithm=AKAv1-MD5, qop="auth"`,
- }), remote); err != nil {
- return err
- }
- count, remote, err = listener.ReadFromUDP(packet)
- if err != nil {
- return err
- }
- _, headers, err = parseTestRequest(packet[:count])
- if err != nil {
- return err
- }
- if _, err = listener.WriteToUDP(testResponse(200, "OK", callID, headers["cseq"], []string{
- "Contact: " + headers["contact"] + ";expires=600",
- }), remote); err != nil {
- return err
- }
- tpdu := []byte{
- 0x04, 0x05, 0x91, 0x21, 0x43, 0xf5, 0x00, 0x00,
- 0x42, 0x10, 0x20, 0x30, 0x40, 0x50, 0x00, 0x05,
- 0xc8, 0x22, 0x93, 0xf9, 0x04,
- }
- rpdu := []byte{0x01, 0x2a, 0x00, 0x00, byte(len(tpdu))}
- rpdu = append(rpdu, tpdu...)
- request := []byte(strings.Join([]string{
- "MESSAGE sip:[email protected] SIP/2.0",
- "Via: SIP/2.0/UDP " + listener.LocalAddr().String() + ";branch=z9hG4bKdeliver",
- "From: <sip:[email protected]>;tag=gw",
- "To: <sip:[email protected]>",
- "P-Asserted-Identity: <sip:[email protected]>",
- "Call-ID: network-deliver-1",
- "CSeq: 1 MESSAGE",
- "Content-Type: application/vnd.3gpp.sms",
- fmt.Sprintf("Content-Length: %d", len(rpdu)), "", "",
- }, "\r\n"))
- request = append(request, rpdu...)
- if _, err = listener.WriteToUDP(request, remote); err != nil {
- return err
- }
- count, remote, err = listener.ReadFromUDP(packet)
- if err != nil {
- return err
- }
- response, err := parseSIPResponse(packet[:count])
- if err != nil || response.StatusCode != 200 {
- return fmt.Errorf("delivery SIP response = (%#v, %v)", response, err)
- }
- count, remote, err = listener.ReadFromUDP(packet)
- if err != nil {
- return err
- }
- report, err := parseSIPPacket(packet[:count])
- if err != nil || report.Request == nil {
- return fmt.Errorf("delivery report parse: %v", err)
- }
- if report.Request.Method != "MESSAGE" || report.Request.value("In-Reply-To") != "network-deliver-1" ||
- len(report.Request.Body) != 2 || report.Request.Body[0] != 0x02 || report.Request.Body[1] != 0x2a {
- return fmt.Errorf("unexpected delivery report %#v", report.Request)
- }
- if _, err = listener.WriteToUDP(testResponse(200, "OK", report.Request.value("Call-ID"), report.Request.value("CSeq"), nil), remote); err != nil {
- return err
- }
- count, remote, err = listener.ReadFromUDP(packet)
- if err != nil {
- return err
- }
- _, headers, err = parseTestRequest(packet[:count])
- if err != nil {
- return err
- }
- if headers["expires"] != "0" {
- return errors.New("expected deregistration")
- }
- _, err = listener.WriteToUDP(testResponse(200, "OK", callID, headers["cseq"], nil), remote)
- return err
- }
- func serveOutboundSMS(listener *net.UDPConn, nonce string) error {
- packet := make([]byte, 65535)
- count, remote, err := listener.ReadFromUDP(packet)
- if err != nil {
- return err
- }
- _, headers, err := parseTestRequest(packet[:count])
- if err != nil {
- return err
- }
- registerCallID := headers["call-id"]
- if _, err = listener.WriteToUDP(testResponse(401, "Unauthorized", registerCallID, headers["cseq"], []string{
- `WWW-Authenticate: Digest realm="ims.mnc001.mcc001.3gppnetwork.org", nonce="` + nonce + `", algorithm=AKAv1-MD5, qop="auth"`,
- }), remote); err != nil {
- return err
- }
- count, remote, err = listener.ReadFromUDP(packet)
- if err != nil {
- return err
- }
- _, headers, err = parseTestRequest(packet[:count])
- if err != nil {
- return err
- }
- if _, err = listener.WriteToUDP(testResponse(200, "OK", registerCallID, headers["cseq"], []string{
- "Contact: " + headers["contact"] + ";expires=600",
- }), remote); err != nil {
- return err
- }
- count, remote, err = listener.ReadFromUDP(packet)
- if err != nil {
- return err
- }
- message, err := parseSIPPacket(packet[:count])
- if err != nil || message.Request == nil {
- return fmt.Errorf("outbound MESSAGE parse: %v", err)
- }
- if message.Request.Method != "MESSAGE" || message.Request.URI != "tel:+447785016005" ||
- strings.ToLower(message.Request.value("Content-Type")) != smsContentType {
- return fmt.Errorf("unexpected outbound MESSAGE %#v", message.Request)
- }
- rpdu, err := parseRPDU(message.Request.Body)
- if err != nil || rpdu.messageType != 0 || len(rpdu.tpdu) != 0 {
- // parseRPDU intentionally decodes only network-to-MS RP-DATA; inspect
- // the mandatory MO prefix and TPDU length directly below.
- if err != nil {
- return err
- }
- }
- body := message.Request.Body
- if len(body) < 8 || body[0] != 0x00 || body[2] != 0x00 {
- return fmt.Errorf("invalid MO RP-DATA %x", body)
- }
- destinationLength := int(body[3])
- userLengthIndex := 4 + destinationLength
- if userLengthIndex >= len(body) || int(body[userLengthIndex]) != len(body)-userLengthIndex-1 {
- return fmt.Errorf("invalid MO RP-DATA lengths %x", body)
- }
- tpdu := body[userLengthIndex+1:]
- if len(tpdu) < 2 || tpdu[0]&0x03 != 1 || tpdu[0]&0x20 == 0 || tpdu[1] != body[1] {
- return fmt.Errorf("SMS-SUBMIT did not request a trackable status report: %x", tpdu)
- }
- if _, err = listener.WriteToUDP(testResponse(202, "Accepted", message.Request.value("Call-ID"), message.Request.value("CSeq"), nil), remote); err != nil {
- return err
- }
- statusTPDU := []byte{
- 0x02, tpdu[1], 0x05, 0x91, 0x21, 0x43, 0xf5,
- 0x42, 0x10, 0x20, 0x30, 0x40, 0x50, 0x00,
- 0x42, 0x10, 0x20, 0x30, 0x50, 0x50, 0x00,
- 0x00,
- }
- statusRPDU := []byte{0x01, 0x2b, 0x00, 0x00, byte(len(statusTPDU))}
- statusRPDU = append(statusRPDU, statusTPDU...)
- statusRequest := []byte(strings.Join([]string{
- "MESSAGE sip:[email protected] SIP/2.0",
- "Via: SIP/2.0/UDP " + listener.LocalAddr().String() + ";branch=z9hG4bKstatus",
- "From: <sip:[email protected]>;tag=gw",
- "To: <sip:[email protected]>",
- "P-Asserted-Identity: <sip:[email protected]>",
- "Call-ID: network-status-1",
- "CSeq: 2 MESSAGE",
- "Content-Type: application/vnd.3gpp.sms",
- fmt.Sprintf("Content-Length: %d", len(statusRPDU)), "", "",
- }, "\r\n"))
- statusRequest = append(statusRequest, statusRPDU...)
- if _, err = listener.WriteToUDP(statusRequest, remote); err != nil {
- return err
- }
- count, remote, err = listener.ReadFromUDP(packet)
- if err != nil {
- return err
- }
- statusResponse, err := parseSIPResponse(packet[:count])
- if err != nil || statusResponse.StatusCode != 200 {
- return fmt.Errorf("status SIP response = (%#v, %v)", statusResponse, err)
- }
- count, remote, err = listener.ReadFromUDP(packet)
- if err != nil {
- return err
- }
- statusACK, err := parseSIPPacket(packet[:count])
- if err != nil || statusACK.Request == nil || statusACK.Request.value("In-Reply-To") != "network-status-1" ||
- len(statusACK.Request.Body) != 2 || statusACK.Request.Body[0] != 0x02 || statusACK.Request.Body[1] != 0x2b {
- return fmt.Errorf("unexpected status RP-ACK %#v (%v)", statusACK.Request, err)
- }
- if _, err = listener.WriteToUDP(testResponse(200, "OK", statusACK.Request.value("Call-ID"), statusACK.Request.value("CSeq"), nil), remote); err != nil {
- return err
- }
- count, remote, err = listener.ReadFromUDP(packet)
- if err != nil {
- return err
- }
- _, headers, err = parseTestRequest(packet[:count])
- if err != nil {
- return err
- }
- if headers["expires"] != "0" {
- return errors.New("expected deregistration")
- }
- _, err = listener.WriteToUDP(testResponse(200, "OK", registerCallID, headers["cseq"], nil), remote)
- return err
- }
|