diff --git a/.dockerignore b/.dockerignore index e45021a..349f3ea 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ .git smscsim +.idea diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index ce726b0..5b60f6e 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -3,10 +3,10 @@ name: run-tests on: push: branches: - - main + - master pull_request: branches: - - main + - master jobs: build: diff --git a/.gitignore b/.gitignore index fe329ea..1557eff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ smscsim *.swp +.idea/ diff --git a/README.md b/README.md index e80a654..a0a8e05 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/main.go b/main.go index 1df446c..d1ec368 100644 --- a/main.go +++ b/main.go @@ -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() } diff --git a/smsc.go b/smsc.go index 1f344f0..2f0b146 100644 --- a/smsc.go +++ b/smsc.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "log" + "math" "math/rand" "net" "strconv" @@ -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)) @@ -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/ @@ -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 @@ -221,9 +226,9 @@ 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 @@ -231,23 +236,25 @@ func handleSmppConnection(smsc *Smsc, conn net.Conn) { 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 @@ -255,7 +262,7 @@ func handleSmppConnection(smsc *Smsc, conn net.Conn) { 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 @@ -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 @@ -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 @@ -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)) @@ -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)) @@ -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) @@ -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 +} diff --git a/smsc_test.go b/smsc_test.go index caf8271..f8269ee 100644 --- a/smsc_test.go +++ b/smsc_test.go @@ -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 @@ -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") diff --git a/web.go b/web.go index 52bbfd3..48b2d71 100644 --- a/web.go +++ b/web.go @@ -106,7 +106,7 @@ const WEB_PAGE_TPL = `
- +
@@ -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))