Skip to content

Commit

Permalink
Concatenated MO sms implemented. esm_class, src_addr and dest_addr fi…
Browse files Browse the repository at this point in the history
…elds fixed for delivery report pdu
  • Loading branch information
ukarim committed Aug 10, 2024
1 parent 67dfdfc commit a2c646d
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 73 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.git
smscsim
.idea

4 changes: 2 additions & 2 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ name: run-tests
on:
push:
branches:
- main
- master
pull_request:
branches:
- main
- master

jobs:
build:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
smscsim
*.swp
.idea/

23 changes: 5 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,17 @@ Lightweight, zero-dependency and stupid SMSc simulator.

### Usage

1) Build from sources (need golang compiler)
1) Use prebuild docker image (from hub.docker.com)

```
go build
./smscsim
```

2) or build docker image

```
docker build -t smscsim .
docker run -p 2775:2775 -p 12775:12775 smscsim
```

3) or build and run with docker-compose

```
docker-compose up
docker run -p 2775:2775 -p 12775:12775 ukarim/smscsim
```

4) or use prebuild docker image (from hub.docker.com)
2) Build from sources (need golang compiler)

```
docker run -p 2775:2775 -p 12775:12775 ukarim/smscsim
go build
./smscsim
```

then, just configure your smpp client to connect to `localhost:2775`
Expand Down
4 changes: 2 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ func main() {

// start smpp server
smsc := NewSmsc(failedSubmits)
go smsc.Start(smscPort, wg)
go smsc.Start(smscPort, &wg)

// start web server
webServer := NewWebServer(smsc)
go webServer.Start(webPort, wg)
go webServer.Start(webPort, &wg)

wg.Wait()
}
Expand Down
101 changes: 63 additions & 38 deletions smsc.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"log"
"math"
"math/rand"
"net"
"strconv"
Expand Down Expand Up @@ -76,7 +77,7 @@ func NewSmsc(failedSubmits bool) Smsc {
return Smsc{sessions, failedSubmits}
}

func (smsc *Smsc) Start(port int, wg sync.WaitGroup) {
func (smsc *Smsc) Start(port int, wg *sync.WaitGroup) {
defer wg.Done()

ln, err := net.Listen("tcp", fmt.Sprint(":", port))
Expand Down Expand Up @@ -124,17 +125,21 @@ func (smsc *Smsc) SendMoMessage(sender, recipient, message, systemId string) err
return fmt.Errorf("Only RECEIVER and TRANSCEIVER sessions could receive MO messages")
}

// TODO implement UDH for large messages
shortMsg := truncateString(message, 70) // just truncate to 70 symbols
udhParts := toUdhParts(toUcs2Coding(message))
esmClass := byte(0x00)
if len(udhParts) > 1 {
esmClass = 0x40
}
var tlvs []Tlv
moMessage := deliverSmPDU(sender, recipient, toUcs2Coding(shortMsg), CODING_UCS2, rand.Int(), tlvs)
if _, err := session.Conn.Write(moMessage); err != nil {
log.Printf("Cannot send MO message to systemId: [%s]. Network error [%v]", systemId, err)
return fmt.Errorf("Cannot send MO message. Network error")
} else {
log.Printf("MO message to systemId: [%s] was successfully sent. Sender: [%s], recipient: [%s]", systemId, sender, recipient)
return nil
for i := range udhParts {
pdu := deliverSmPDU(sender, recipient, udhParts[i], CODING_UCS2, rand.Int(), esmClass, tlvs)
if _, err := session.Conn.Write(pdu); err != nil {
log.Printf("Cannot send MO message to systemId: [%s]. Network error [%v]", systemId, err)
return fmt.Errorf("Cannot send MO message. Network error")
}
}
log.Printf("MO message to systemId: [%s] was successfully sent. Sender: [%s], recipient: [%s]", systemId, sender, recipient)
return nil
}

// how to convert ints to and from bytes https://golang.org/pkg/encoding/binary/
Expand Down Expand Up @@ -172,7 +177,7 @@ func handleSmppConnection(smsc *Smsc, conn net.Conn) {
}

// find first null terminator
idx := bytes.Index(pduBody, []byte("\x00"))
idx := bytes.IndexByte(pduBody, byte(0))
if idx == -1 {
log.Printf("invalid pdu_body. cannot find system_id. closing connection")
return
Expand Down Expand Up @@ -221,41 +226,43 @@ func handleSmppConnection(smsc *Smsc, conn net.Conn) {
}

idxCounter := 0
nullTerm := []byte("\x00")
nullTerm := byte(0)

srvTypeEndIdx := bytes.Index(pduBody, nullTerm)
srvTypeEndIdx := bytes.IndexByte(pduBody, nullTerm)
if srvTypeEndIdx == -1 {
respBytes = headerPDU(GENERIC_NACK, STS_INVALID_CMD, seqNum)
break
}
idxCounter = idxCounter + srvTypeEndIdx
idxCounter = idxCounter + 3 // skip src ton and npi

srcAddrEndIdx := bytes.Index(pduBody[idxCounter:], nullTerm)
srcAddrEndIdx := bytes.IndexByte(pduBody[idxCounter:], nullTerm)
if srcAddrEndIdx == -1 {
respBytes = headerPDU(GENERIC_NACK, STS_INVALID_CMD, seqNum)
break
}
srcAddr := string(pduBody[idxCounter : idxCounter+srcAddrEndIdx])
idxCounter = idxCounter + srcAddrEndIdx
idxCounter = idxCounter + 3 // skip dest ton and npi

destAddrEndIdx := bytes.Index(pduBody[idxCounter:], nullTerm)
destAddrEndIdx := bytes.IndexByte(pduBody[idxCounter:], nullTerm)
if destAddrEndIdx == -1 {
respBytes = headerPDU(GENERIC_NACK, STS_INVALID_CMD, seqNum)
break
}
destAddr := string(pduBody[idxCounter : idxCounter+destAddrEndIdx])
idxCounter = idxCounter + destAddrEndIdx
idxCounter = idxCounter + 4 // skip esm_class, protocol_id, priority_flag

schedEndIdx := bytes.Index(pduBody[idxCounter:], nullTerm)
schedEndIdx := bytes.IndexByte(pduBody[idxCounter:], nullTerm)
if schedEndIdx == -1 {
respBytes = headerPDU(GENERIC_NACK, STS_INVALID_CMD, seqNum)
break
}
idxCounter = idxCounter + schedEndIdx
idxCounter = idxCounter + 1 // next is validity period

validityEndIdx := bytes.Index(pduBody[idxCounter:], nullTerm)
validityEndIdx := bytes.IndexByte(pduBody[idxCounter:], nullTerm)
if validityEndIdx == -1 {
respBytes = headerPDU(GENERIC_NACK, STS_INVALID_CMD, seqNum)
break
Expand All @@ -276,7 +283,7 @@ func handleSmppConnection(smsc *Smsc, conn net.Conn) {
go func() {
time.Sleep(2000 * time.Millisecond)
now := time.Now()
dlr := deliveryReceiptPDU(msgId, now, now, smsc.FailedSubmits)
dlr := deliveryReceiptPDU(destAddr, srcAddr, msgId, now, now, smsc.FailedSubmits)
if _, err := conn.Write(dlr); err != nil {
log.Printf("error sending delivery receipt to system_id[%s] due %v.", systemId, err)
return
Expand Down Expand Up @@ -344,7 +351,7 @@ func stringBodyPDU(cmdId, cmdSts, seqNum uint32, body string) []byte {
const DLR_RECEIPT_FORMAT = "id:%s sub:001 dlvrd:001 submit date:%s done date:%s stat:DELIVRD err:000 Text:..."
const DLR_RECEIPT_FORMAT_FAILED = "id:%s sub:001 dlvrd:000 submit date:%s done date:%s stat:UNDELIV err:069 Text:..."

func deliveryReceiptPDU(msgId string, submitDate, doneDate time.Time, failedDeliv bool) []byte {
func deliveryReceiptPDU(src, dst, msgId string, submitDate, doneDate time.Time, failedDeliv bool) []byte {
sbtDateFrmt := submitDate.Format("0601021504")
doneDateFrmt := doneDate.Format("0601021504")
dlrFmt := DLR_RECEIPT_FORMAT
Expand All @@ -367,10 +374,10 @@ func deliveryReceiptPDU(msgId string, submitDate, doneDate time.Time, failedDeli
msgStateTlv := Tlv{TLV_MESSAGE_STATE, 1, msgState}
tlvs = append(tlvs, msgStateTlv)

return deliverSmPDU("", "", []byte(deliveryReceipt), CODING_DEFAULT, rand.Int(), tlvs)
return deliverSmPDU(src, dst, []byte(deliveryReceipt), CODING_DEFAULT, rand.Int(), 0x04, tlvs)
}

func deliverSmPDU(sender, recipient string, shortMessage []byte, coding byte, seqNum int, tlvs []Tlv) []byte {
func deliverSmPDU(sender, recipient string, shortMessage []byte, coding byte, seqNum int, esmClass byte, tlvs []Tlv) []byte {
// header without cmd_len
header := make([]byte, 12)
binary.BigEndian.PutUint32(header[0:], uint32(DELIVER_SM))
Expand Down Expand Up @@ -402,15 +409,15 @@ func deliverSmPDU(sender, recipient string, shortMessage []byte, coding byte, se
buf.WriteByte(0)
}

buf.WriteByte(0) // esm class
buf.WriteByte(0) // protocol id
buf.WriteByte(0) // priority flag
buf.WriteByte(0) // sched delivery time
buf.WriteByte(0) // validity period
buf.WriteByte(0) // registered delivery
buf.WriteByte(0) // replace if present
buf.WriteByte(coding) // data coding
buf.WriteByte(0) // def msg id
buf.WriteByte(esmClass) // esm class
buf.WriteByte(0) // protocol id
buf.WriteByte(0) // priority flag
buf.WriteByte(0) // sched delivery time
buf.WriteByte(0) // validity period
buf.WriteByte(0) // registered delivery
buf.WriteByte(0) // replace if present
buf.WriteByte(coding) // data coding
buf.WriteByte(0) // def msg id

smLen := len(shortMessage)
buf.WriteByte(byte(smLen))
Expand All @@ -436,14 +443,6 @@ func deliverSmPDU(sender, recipient string, shortMessage []byte, coding byte, se
return deliverSm.Bytes()
}

func truncateString(input string, maxLen int) string {
result := input
if len(input) > maxLen {
result = input[0:maxLen]
}
return result
}

func toUcs2Coding(input string) []byte {
// not most elegant implementation, but ok for testing purposes
l := utf8.RuneCountInString(input)
Expand All @@ -461,3 +460,29 @@ func toUcs2Coding(input string) []byte {
}
return buf
}

func toUdhParts(longMsg []byte) [][]byte {
msgLen := len(longMsg)
if msgLen <= 140 { // max len for message field in pdu
// one part is enough
return [][]byte{longMsg}
}
maxUdhContentLen := 134
c := int(math.Ceil(float64(msgLen) / float64(maxUdhContentLen)))
parts := make([][]byte, c)
for i := 0; i < c; i++ {
si := i * maxUdhContentLen
ei := int(math.Min(float64(si+maxUdhContentLen), float64(msgLen)))
partLen := ei - si
part := make([]byte, partLen+6) // plus 6 for udh headers
part[0] = 0x05
part[1] = 0x00
part[2] = 0x03
part[3] = 0x01 // maybe accept id as method argument?
part[4] = byte(c)
part[5] = byte(i + 1)
copy(part[6:], longMsg[si:ei])
parts[i] = part
}
return parts
}
22 changes: 11 additions & 11 deletions smsc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,16 @@ func TestDeliverSmPduBytes(t *testing.T) {
0x73, 0x6d, 0x73, 0x63, 0x73, 0x69, 0x6d, 0x00, // service_type
0x00, 0x00, 0x37, 0x37, 0x30, 0x31, 0x32, 0x31, 0x31, 0x30, 0x30, 0x30, 0x30, 0x00, // source_addr_ton, source_addr_npi, source_addr
0x00, 0x00, 0x31, 0x30, 0x30, 0x31, 0x00, // dest_addr_ton, dest_addr_npi, destination_addr
0x00, // esm class
0x00, // protocol_id
0x00, // priority_flag
0x00, // schedule_delivery_time
0x00, // validity_period
0x00, // registered_delivery
0x00, // replace_if_present_flag
0x00, // data_coding
0x00, // sm_default_msg_id
0x04, // sm_length
0x40, // esm class
0x00, // protocol_id
0x00, // priority_flag
0x00, // schedule_delivery_time
0x00, // validity_period
0x00, // registered_delivery
0x00, // replace_if_present_flag
0x00, // data_coding
0x00, // sm_default_msg_id
0x04, // sm_length
0x54, 0x65, 0x73, 0x74, // short_message
0x02, 0x01, 0x00, 0x01, 0x03, // privacy_indicator tlv
0x12, 0x04, 0x00, 0x01, 0x02, // ms_validity tlv
Expand All @@ -68,7 +68,7 @@ func TestDeliverSmPduBytes(t *testing.T) {
ms_validity_tlv := Tlv{0x1204, 1, []byte{2}}
tlv_list := []Tlv{privacy_indicator_tlv, ms_validity_tlv}

actualBytes := deliverSmPDU(source_addr, destination_addr, []byte(short_message), 0, sequence_number, tlv_list)
actualBytes := deliverSmPDU(source_addr, destination_addr, []byte(short_message), 0, sequence_number, 0x40, tlv_list)
if !reflect.DeepEqual(expectedBytes, actualBytes) {
fmt.Printf("expected: [%s]\nactual: [%s]\n\n", hex.EncodeToString(expectedBytes), hex.EncodeToString(actualBytes))
t.Errorf("PDU with string body incorrectly encoded")
Expand Down
4 changes: 2 additions & 2 deletions web.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const WEB_PAGE_TPL = `
</p>
<p>
<label for="short_message">Short message</label>
<textarea id="short_message" name="message" maxlength="70" placeholder="Short message..."></textarea>
<textarea id="short_message" name="message" placeholder="Short message..."></textarea>
</p>
<p>
<input type="submit" value="Submit">
Expand Down Expand Up @@ -139,7 +139,7 @@ func NewWebServer(smsc Smsc) WebServer {
return WebServer{smsc}
}

func (webServer *WebServer) Start(port int, wg sync.WaitGroup) {
func (webServer *WebServer) Start(port int, wg *sync.WaitGroup) {
defer wg.Done()

http.HandleFunc("/", webHandler(&webServer.Smsc))
Expand Down

0 comments on commit a2c646d

Please sign in to comment.