├── .github ├── build.sh └── workflows │ └── build.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── cmd ├── smpp-receiver │ ├── .gitignore │ ├── README.md │ ├── main.go │ ├── schema.json │ └── types.go └── smpp-repl │ ├── README.md │ ├── client-side.go │ ├── main.go │ └── utils.go ├── coding ├── best_coding.go ├── best_coding_test.go ├── data_coding.go ├── gsm7bit │ ├── decoder.go │ ├── encoder.go │ ├── encoding.go │ ├── encoding_test.go │ ├── errors.go │ └── table.go ├── range_table.go ├── semioctet │ ├── semi_octet.go │ └── semi_octet_test.go └── splitter.go ├── docs ├── SMPP_v3_3.pdf ├── SMPP_v3_4_Issue1_2.pdf ├── SMPP_v5.pdf ├── device-specific-caveats.md └── wireshark.md ├── errors.go ├── go.mod ├── go.sum ├── pdu ├── address.go ├── address_test.go ├── constants.go ├── errors.go ├── esm_class.go ├── esm_class_test.go ├── factory.go ├── factory_test.go ├── header.go ├── header_kit.go ├── header_names.go ├── header_test.go ├── interface_version.go ├── interface_version_test.go ├── internal.go ├── marshal.go ├── marshal_test.go ├── message.go ├── message_multipart.go ├── message_state.go ├── message_test.go ├── packet.go ├── packet_test.go ├── pdu.go ├── pdu_test.go ├── registered_delivery.go ├── registered_delivery_test.go ├── tag.go ├── tag_test.go ├── time.go ├── time_test.go ├── udh.go ├── udh_element.go └── udh_test.go ├── serve.go ├── session.go └── sms ├── README.md ├── address.go ├── address_test.go ├── bridge ├── deliver.go └── submit.go ├── indicator.go ├── indicator_test.go ├── marshal.go ├── marshal_test.go ├── message.go ├── message_test.go ├── message_type.go ├── message_type_test.go ├── time.go ├── time_test.go └── types.go /.github/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuo pipefail 3 | 4 | mkdir -p dist 5 | 6 | function build() { 7 | local IFS='/' 8 | local NAME="smpp-$1" 9 | for i in "${@:2}"; do 10 | read -r -a platform <<<"$i" 11 | local GOOS="${platform[0]}" 12 | local GOARCH="${platform[1]}" 13 | env GOOS="$GOOS" GOARCH="$GOARCH" \ 14 | go build \ 15 | -ldflags '-s -w' \ 16 | -o "dist/$NAME-$GOOS-$GOARCH" \ 17 | "./cmd/$NAME" 18 | done 19 | } 20 | 21 | build receiver linux/amd64 linux/arm linux/arm64 22 | build repl linux/amd64 linux/arm linux/arm64 darwin/amd64 23 | 24 | upx dist/* 25 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ubuntu-20.04 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-go@v3 18 | with: 19 | go-version: 1.19 20 | - run: go test -v -race -cover ./... 21 | - run: .github/build.sh 22 | - uses: actions/upload-artifact@v3 23 | with: 24 | path: dist/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *DS_Store 2 | /.idea/ 3 | /smpp-* 4 | .smpp_repl_history -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 NiceLabs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-smpp 2 | 3 | A complete implementation of SMPP v5 protocol, written in golang. 4 | 5 | ## Key features 6 | 7 | - Message encoding auto-detection 8 | - Multipart SMS automatic splitting and concatenating 9 | - Supported encodings: 10 | 11 | ```plain 12 | UCS-2 GSM 7bit ASCII Latin-1 13 | Cyrillic Hebrew Shift-JIS ISO-2022-JP 14 | EUC-JP EUC-KR 15 | ``` 16 | 17 | ## Caveats 18 | 19 | - Please read [the SMPP Specification Version 5](docs/SMPP_v5.pdf) first. [pdu](pdu) is not limited to any value range. 20 | - If you do not like the default [session.go](session.go) implementation, you can easily replace it. 21 | - [Device-specific Caveats](docs/device-specific-caveats.md) 22 | 23 | ## Command line tools 24 | 25 | 1. [smpp-receiver](cmd/smpp-receiver) 26 | 27 | SMPP Simple Receiver tool 28 | 29 | 2. [smpp-repl](cmd/smpp-repl) 30 | 31 | SMPP Simple Test tool 32 | 33 | ## LICENSE 34 | 35 | This piece of software is released under [the MIT license](LICENSE). 36 | -------------------------------------------------------------------------------- /cmd/smpp-receiver/.gitignore: -------------------------------------------------------------------------------- 1 | configure.json 2 | *.py 3 | -------------------------------------------------------------------------------- /cmd/smpp-receiver/README.md: -------------------------------------------------------------------------------- 1 | # SMPP Receiver 2 | 3 | This simple program receive `deliver_sm` message and call a local hook function 4 | 5 | ## Configuration 6 | 7 | Write to `configure.json` file: 8 | 9 | ```json 10 | { 11 | "hook": "./send-email.py", 12 | "devices": [ 13 | { 14 | "smsc": "target ip:target port", 15 | "password": "your password", 16 | "system_id": "tenant-1" 17 | }, 18 | { "system_id": "tenant-2" } 19 | ] 20 | } 21 | ``` 22 | 23 | Sample hook script: 24 | 25 | ```python 26 | #!/usr/bin/env python3 27 | import json 28 | 29 | import requests 30 | 31 | payload = json.load(sys.stdin) 32 | """ 33 | { 34 | "smsc": "[login smsc address]", 35 | "system_id": "[login system id]", 36 | "system_type": "[login system type]", 37 | "source": "[source phone number]", 38 | "target": "[target phone number]", 39 | "message": "[merged message content]", 40 | "deliver_time": "[iso8601 formatted]" 41 | } 42 | """ 43 | 44 | to_addresses = { 45 | "tenant-1": "tenant one addresses", 46 | "tenant-2": "tenant two addresses", 47 | } 48 | 49 | data = { 50 | "to": to_addresses[payload["system_id"]], 51 | "from": "%(target)s <[your from address]>" % payload, 52 | "subject": payload["source"], 53 | "text": "%(message)s\n\nDate: %(deliver_time)s" % payload 54 | } 55 | 56 | requests.post( 57 | "https://api.mailgun.net/v3/[your api domain]/messages", 58 | auth=("api", "your api token"), 59 | data=data, 60 | ) 61 | ``` 62 | 63 | Sample systemd service file: 64 | 65 | ```ini 66 | [Unit] 67 | Description=SMPP Receiver 68 | 69 | [Service] 70 | Type=simple 71 | WorkingDirectory=/opt/smpp-receiver 72 | ExecStart=/opt/smpp-receiver/smpp-receiver-linux-arm 73 | Restart=on-failure 74 | RestartSec=1m 75 | Environment="TZ=Asia/Shanghai" 76 | 77 | [Install] 78 | WantedBy=multi-user.target 79 | ``` 80 | -------------------------------------------------------------------------------- /cmd/smpp-receiver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "encoding/json" 7 | "flag" 8 | "fmt" 9 | "log" 10 | "net" 11 | "os" 12 | "os/exec" 13 | "sync" 14 | "time" 15 | 16 | "github.com/M2MGateway/go-smpp" 17 | "github.com/M2MGateway/go-smpp/pdu" 18 | "github.com/imdario/mergo" 19 | . "github.com/xeipuuv/gojsonschema" 20 | ) 21 | 22 | var configure Configuration 23 | var mutex sync.Mutex 24 | 25 | //go:embed schema.json 26 | var schemaFile []byte 27 | 28 | func init() { 29 | var confPath string 30 | flag.StringVar(&confPath, "c", "configure.json", "configure file-path") 31 | 32 | if data, err := os.ReadFile(confPath); err != nil { 33 | log.Fatalln(err) 34 | } else if result, _ := Validate(NewBytesLoader(schemaFile), NewBytesLoader(data)); !result.Valid() { 35 | for _, desc := range result.Errors() { 36 | log.Println(desc) 37 | } 38 | log.Fatalln("invalid configuration") 39 | } else { 40 | _ = json.Unmarshal(data, &configure) 41 | } 42 | _ = mergo.Merge(configure.Devices[0], &Device{ 43 | Version: pdu.SMPPVersion34, 44 | BindMode: "receiver", 45 | KeepAliveTick: time.Millisecond * 500, 46 | KeepAliveTimeout: time.Second, 47 | }) 48 | for i := 1; i < len(configure.Devices); i++ { 49 | _ = mergo.Merge(configure.Devices[i], configure.Devices[i-1]) 50 | } 51 | } 52 | 53 | func main() { 54 | hook := runProgramWithEvent 55 | if configure.HookMode == "ndjson" { 56 | hook = runProgramWithStream() 57 | } 58 | for _, device := range configure.Devices { 59 | go connect(device, hook) 60 | } 61 | select {} 62 | } 63 | 64 | //goland:noinspection GoUnhandledErrorResult 65 | func connect(device *Device, hook func(*Payload)) { 66 | ctx := context.Background() 67 | parent, err := net.Dial("tcp", device.SMSC) 68 | if err != nil { 69 | log.Fatalln(err) 70 | } 71 | session := smpp.NewSession(ctx, parent) 72 | session.ReadTimeout = time.Second 73 | session.WriteTimeout = time.Second 74 | defer session.Close(ctx) 75 | if resp, err := session.Submit(ctx, device.Binder()); err != nil { 76 | log.Fatalln(device, err) 77 | } else if status := pdu.ReadCommandStatus(resp); status != 0 { 78 | log.Fatalln(device, status) 79 | } else { 80 | log.Println(device, "Connected") 81 | go session.EnquireLink(ctx, device.KeepAliveTick, device.KeepAliveTimeout) 82 | } 83 | addDeliverSM := makeCombineMultipartDeliverSM(device, hook) 84 | for { 85 | select { 86 | case <-ctx.Done(): 87 | log.Println(device, "Disconnected") 88 | time.Sleep(time.Second) 89 | go connect(device, hook) 90 | return 91 | case packet := <-session.PDU(): 92 | switch p := packet.(type) { 93 | case *pdu.DeliverSM: 94 | addDeliverSM(p) 95 | _ = session.Send(p.Resp()) 96 | case pdu.Responsable: 97 | _ = session.Send(p.Resp()) 98 | } 99 | } 100 | } 101 | } 102 | 103 | //goland:noinspection GoUnhandledErrorResult 104 | func runProgramWithEvent(message *Payload) { 105 | mutex.Lock() 106 | defer mutex.Unlock() 107 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute*15) 108 | defer cancel() 109 | cmd := exec.CommandContext(ctx, configure.Hook) 110 | cmd.Stdout = os.Stdout 111 | cmd.Stderr = os.Stderr 112 | if stdin, err := cmd.StdinPipe(); err != nil { 113 | log.Fatalln(err) 114 | } else { 115 | go func() { 116 | defer stdin.Close() 117 | _ = json.NewEncoder(stdin).Encode(message) 118 | }() 119 | } 120 | if err := cmd.Run(); err != nil { 121 | log.Fatalln(err) 122 | } 123 | } 124 | 125 | //goland:noinspection GoUnhandledErrorResult 126 | func runProgramWithStream() func(*Payload) { 127 | cmd := exec.Command(configure.Hook) 128 | stdin, err := cmd.StdinPipe() 129 | if err != nil { 130 | log.Fatalln(err) 131 | } 132 | cmd.Stdout = os.Stdout 133 | cmd.Stderr = os.Stderr 134 | go func() { 135 | if err = cmd.Run(); err != nil { 136 | log.Fatalln(err) 137 | } 138 | }() 139 | return func(message *Payload) { 140 | mutex.Lock() 141 | defer mutex.Unlock() 142 | _ = json.NewEncoder(stdin).Encode(message) 143 | _, _ = fmt.Fprintln(stdin) 144 | } 145 | } 146 | 147 | func makeCombineMultipartDeliverSM(device *Device, hook func(*Payload)) func(*pdu.DeliverSM) { 148 | return pdu.CombineMultipartDeliverSM(func(delivers []*pdu.DeliverSM) { 149 | var mergedMessage string 150 | for _, sm := range delivers { 151 | if sm.Message.DataCoding == 0x00 && device.Workaround == "SMG4000" { 152 | mergedMessage += string(sm.Message.Message) 153 | } else if message, err := sm.Message.Parse(); err == nil { 154 | mergedMessage += message 155 | } 156 | } 157 | source := delivers[0].SourceAddr 158 | target := delivers[0].DestAddr 159 | log.Println(device, source, "->", target) 160 | go hook(&Payload{ 161 | SMSC: device.SMSC, 162 | SystemID: device.SystemID, 163 | SystemType: device.SystemType, 164 | Owner: device.Owner, 165 | Phone: device.Phone, 166 | Extra: device.Extra, 167 | Source: source.String(), 168 | Target: target.String(), 169 | Message: mergedMessage, 170 | DeliverTime: time.Now(), 171 | }) 172 | }) 173 | } 174 | -------------------------------------------------------------------------------- /cmd/smpp-receiver/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "type": "object", 4 | "properties": { 5 | "$schema": { 6 | "type": "string" 7 | }, 8 | "hook": { 9 | "type": "string" 10 | }, 11 | "hook_mode": { 12 | "enum": [ 13 | "event", 14 | "ndjson" 15 | ] 16 | }, 17 | "devices": { 18 | "type": "array", 19 | "items": { 20 | "$ref": "#/definitions/device" 21 | }, 22 | "minItems": 1 23 | } 24 | }, 25 | "definitions": { 26 | "device": { 27 | "type": "object", 28 | "properties": { 29 | "smsc": { 30 | "type": "string" 31 | }, 32 | "system_id": { 33 | "type": "string", 34 | "maxLength": 16 35 | }, 36 | "password": { 37 | "type": "string", 38 | "maxLength": 9 39 | }, 40 | "system_type": { 41 | "type": "string", 42 | "maxLength": 13 43 | }, 44 | "version": { 45 | "enum": [ 46 | "3.3", 47 | "3.4", 48 | "5.0" 49 | ] 50 | }, 51 | "bind_mode": { 52 | "enum": [ 53 | "transceiver", 54 | "receiver" 55 | ] 56 | }, 57 | "keepalive_tick": { 58 | "type": "string" 59 | }, 60 | "keepalive_timeout": { 61 | "type": "string" 62 | }, 63 | "workaround": { 64 | "enum": [ 65 | "SMG4000" 66 | ] 67 | }, 68 | "owner": { 69 | "type": "string" 70 | }, 71 | "phone": { 72 | "type": "string" 73 | }, 74 | "extra": { 75 | "type": [ 76 | "string", 77 | "object", 78 | "array" 79 | ] 80 | } 81 | }, 82 | "additionalProperties": false 83 | } 84 | }, 85 | "additionalProperties": false, 86 | "required": [ 87 | "hook", 88 | "hook_mode", 89 | "devices" 90 | ] 91 | } 92 | -------------------------------------------------------------------------------- /cmd/smpp-receiver/types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/M2MGateway/go-smpp/pdu" 9 | ) 10 | 11 | type Configuration struct { 12 | Hook string `json:"hook"` 13 | HookMode string `json:"hook_mode"` 14 | Devices []*Device `json:"devices"` 15 | } 16 | 17 | //goland:noinspection ALL 18 | type Device struct { 19 | SMSC string `json:"smsc"` 20 | SystemID string `json:"system_id"` 21 | Password string `json:"password"` 22 | SystemType string `json:"system_type"` 23 | Version pdu.InterfaceVersion `json:"version"` 24 | BindMode string `json:"bind_mode"` 25 | Owner string `json:"owner"` 26 | Phone string `json:"phone"` 27 | Extra json.RawMessage `json:"extra"` 28 | Workaround string `json:"workaround"` 29 | KeepAliveTick time.Duration `json:"keepalive_tick"` 30 | KeepAliveTimeout time.Duration `json:"keepalive_timeout"` 31 | } 32 | 33 | func (d *Device) String() string { 34 | return fmt.Sprintf("%s @ %s", d.SMSC, d.SystemID) 35 | } 36 | 37 | func (d *Device) Binder() pdu.Responsable { 38 | switch d.BindMode { 39 | case "receiver": 40 | return &pdu.BindReceiver{ 41 | SystemID: d.SystemID, 42 | Password: d.Password, 43 | SystemType: d.SystemType, 44 | Version: d.Version, 45 | } 46 | case "transceiver": 47 | return &pdu.BindTransceiver{ 48 | SystemID: d.SystemID, 49 | Password: d.Password, 50 | SystemType: d.SystemType, 51 | Version: d.Version, 52 | } 53 | } 54 | return nil 55 | } 56 | 57 | //goland:noinspection ALL 58 | type Payload struct { 59 | SMSC string `json:"smsc"` 60 | SystemID string `json:"system_id"` 61 | SystemType string `json:"system_type"` 62 | Source string `json:"source,omitempty"` 63 | Target string `json:"target,omitempty"` 64 | Message string `json:"message"` 65 | DeliverTime time.Time `json:"deliver_time"` 66 | Owner string `json:"owner,omitempty"` 67 | Phone string `json:"phone,omitempty"` 68 | Extra json.RawMessage `json:"extra,omitempty"` 69 | } 70 | -------------------------------------------------------------------------------- /cmd/smpp-repl/README.md: -------------------------------------------------------------------------------- 1 | # SMPP REPL 2 | 3 | ## Command 4 | 5 | ### Client feature 6 | 7 | - [x] `connect` to target smpp server 8 | - [x] `disconnect` 9 | - [x] `send-message`, send a `submit_sm` to remote 10 | - [x] `send-ussd`, send a `submit_sm` with `ussd_service_op` to remote 11 | - [x] `query`, send a `query_sm` or `query_broadcast_sm` to remote 12 | - [ ] `load-test`, Load testing to server 13 | 14 | ### Server feature 15 | 16 | - [ ] `start-service`, Start SMPP Server 17 | - [ ] `stop-service`, Stop SMPP Server 18 | - [ ] `send-message`, Send a `deliver_sm` to client-side 19 | - [ ] `load-test`, Load testing to client 20 | -------------------------------------------------------------------------------- /cmd/smpp-repl/client-side.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "flag" 7 | "fmt" 8 | "math/rand" 9 | "net" 10 | "time" 11 | 12 | "github.com/M2MGateway/go-smpp" 13 | "github.com/M2MGateway/go-smpp/coding" 14 | "github.com/M2MGateway/go-smpp/pdu" 15 | "github.com/abiosoft/ishell" 16 | "github.com/davecgh/go-spew/spew" 17 | ) 18 | 19 | var clientCommands []*ishell.Cmd 20 | 21 | func init() { 22 | clientCommands = []*ishell.Cmd{ 23 | {Name: "send-message", Help: "send message", Func: onSendMessageToServer}, 24 | {Name: "send-ussd", Help: "send ussd", Func: onSendUSSDCommandToServer}, 25 | {Name: "query", Help: "query status", Func: onSendQueryCommandToServer}, 26 | {Name: "disconnect", Help: "disconnect", Func: onDisconnectToServer}, 27 | } 28 | } 29 | 30 | func onAddClientCommands() { 31 | for _, command := range clientCommands { 32 | shell.AddCmd(command) 33 | } 34 | } 35 | 36 | func onRemoveClientCommands() { 37 | for _, command := range clientCommands { 38 | shell.DeleteCmd(command.Name) 39 | } 40 | } 41 | 42 | func onConnectToServer(c *ishell.Context) { 43 | c.ShowPrompt(false) 44 | defer c.ShowPrompt(true) 45 | if session != nil { 46 | fmt.Println("connected") 47 | fmt.Println("use `disconnect` command, disconnect") 48 | return 49 | } 50 | var host, port, systemId, password, systemType string 51 | var enableTLS bool 52 | flags := makeFlags(func(flags *flag.FlagSet) { 53 | flags.StringVar(&host, "host", "", "Host") 54 | flags.StringVar(&port, "port", "2775", "Port") 55 | flags.StringVar(&systemId, "system-id", "", "System ID") 56 | flags.StringVar(&password, "password", "", "Password") 57 | flags.StringVar(&systemType, "system-type", "", "System Type") 58 | flags.BoolVar(&enableTLS, "tls", false, "Use TLS Mode") 59 | }) 60 | if err := flags.Parse(c.Args); err != nil { 61 | fmt.Println("Error:", err.Error()) 62 | return 63 | } else if flags.NFlag() < 3 { 64 | flags.Usage() 65 | return 66 | } 67 | address := net.JoinHostPort(host, port) 68 | var parent net.Conn 69 | var err error 70 | if enableTLS { 71 | parent, err = tls.Dial("tcp", address, &tls.Config{InsecureSkipVerify: true}) 72 | } else { 73 | parent, err = net.Dial("tcp", address) 74 | } 75 | if err != nil { 76 | fmt.Println("Error:", err.Error()) 77 | return 78 | } 79 | session = smpp.NewSession(context.Background(), parent) 80 | session.WriteTimeout = time.Minute 81 | session.ReadTimeout = time.Minute 82 | go onWatchInboundMessages(session) 83 | defer onAddClientCommands() 84 | fmt.Printf("Connect %q successfully\n", address) 85 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 86 | defer cancel() 87 | resp, err := session.Submit(ctx, &pdu.BindReceiver{ 88 | SystemID: systemId, 89 | Password: password, 90 | SystemType: systemType, 91 | Version: pdu.SMPPVersion50, 92 | }) 93 | if err != nil { 94 | fmt.Println("Error:", err.Error()) 95 | return 96 | } 97 | spew.Dump(resp) 98 | if status := pdu.ReadCommandStatus(resp); status == 0 { 99 | go session.EnquireLink(context.Background(), time.Minute, time.Minute) 100 | fmt.Println("Bind successfully") 101 | } 102 | } 103 | 104 | func onDisconnectToServer(c *ishell.Context) { 105 | c.ShowPrompt(false) 106 | defer c.ShowPrompt(true) 107 | defer onRemoveClientCommands() 108 | if err := session.Close(context.Background()); err != nil { 109 | fmt.Println(err) 110 | } 111 | session = nil 112 | } 113 | 114 | func onSendMessageToServer(c *ishell.Context) { 115 | c.ShowPrompt(false) 116 | defer c.ShowPrompt(true) 117 | var source, dest, message string 118 | flags := makeFlags(func(flags *flag.FlagSet) { 119 | flags.StringVar(&source, "source", "", "Source address") 120 | flags.StringVar(&dest, "dest", "", "Destination address") 121 | flags.StringVar(&message, "message", "Test", "Message Content") 122 | }) 123 | if err := flags.Parse(c.Args); err != nil { 124 | fmt.Println("Error:", err.Error()) 125 | return 126 | } else if flags.NFlag() < 1 { 127 | flags.Usage() 128 | return 129 | } 130 | reference := uint16(rand.Intn(0xFFFF)) 131 | parts, err := pdu.ComposeMultipartShortMessage(message, coding.BestCoding(message), reference) 132 | if err != nil { 133 | fmt.Println("Error:", err.Error()) 134 | return 135 | } 136 | for _, message := range parts { 137 | packet := &pdu.SubmitSM{ 138 | SourceAddr: pdu.Address{TON: 1, NPI: 1, No: source}, 139 | DestAddr: pdu.Address{TON: 1, NPI: 1, No: dest}, 140 | ESMClass: pdu.ESMClass{UDHIndicator: true}, 141 | Message: message, 142 | } 143 | spew.Dump(packet) 144 | resp, err := session.Submit(context.Background(), packet) 145 | if err != nil { 146 | fmt.Println("Error:", err.Error()) 147 | break 148 | } 149 | spew.Dump(resp) 150 | } 151 | } 152 | 153 | func onSendUSSDCommandToServer(c *ishell.Context) { 154 | c.ShowPrompt(false) 155 | defer c.ShowPrompt(true) 156 | var source, dest, message string 157 | flags := makeFlags(func(flags *flag.FlagSet) { 158 | flags.StringVar(&source, "source", "", "Source address") 159 | flags.StringVar(&dest, "dest", "", "Destination address") 160 | flags.StringVar(&message, "ussd", "*100#", "USSD command") 161 | }) 162 | if err := flags.Parse(c.Args); err != nil { 163 | fmt.Println("Error:", err.Error()) 164 | return 165 | } else if flags.NFlag() < 1 { 166 | flags.Usage() 167 | return 168 | } 169 | packet := &pdu.SubmitSM{ 170 | ServiceType: "USSD", 171 | SourceAddr: pdu.Address{TON: 1, NPI: 1, No: source}, 172 | DestAddr: pdu.Address{TON: 1, NPI: 1, No: dest}, 173 | Tags: pdu.Tags{0x5010: []byte{0x02}}, 174 | } 175 | err := packet.Message.Compose(message) 176 | if err != nil { 177 | fmt.Println("Error:", err.Error()) 178 | return 179 | } 180 | spew.Dump(packet) 181 | resp, err := session.Submit(context.Background(), packet) 182 | if err != nil { 183 | fmt.Println("Error:", err.Error()) 184 | return 185 | } 186 | spew.Dump(resp) 187 | } 188 | 189 | func onSendQueryCommandToServer(c *ishell.Context) { 190 | var id, source string 191 | var broadcast bool 192 | flags := makeFlags(func(flags *flag.FlagSet) { 193 | flags.StringVar(&id, "id", "", "Message ID") 194 | flags.StringVar(&source, "source", "", "Source address") 195 | flags.BoolVar(&broadcast, "broadcast", false, "Query Broadcast") 196 | }) 197 | if err := flags.Parse(c.Args); err != nil { 198 | fmt.Println("Error:", err.Error()) 199 | return 200 | } else if flags.NFlag() < 2 { 201 | flags.Usage() 202 | return 203 | } 204 | var packet pdu.Responsable 205 | if !broadcast { 206 | packet = &pdu.QuerySM{ 207 | MessageID: id, 208 | SourceAddr: pdu.Address{TON: 1, NPI: 1, No: source}, 209 | } 210 | } else { 211 | packet = &pdu.QueryBroadcastSM{ 212 | MessageID: id, 213 | SourceAddr: pdu.Address{TON: 1, NPI: 1, No: source}, 214 | } 215 | } 216 | spew.Dump(packet) 217 | resp, err := session.Submit(context.Background(), packet) 218 | if err != nil { 219 | fmt.Println("Error:", err.Error()) 220 | return 221 | } 222 | spew.Dump(resp) 223 | } 224 | 225 | func onWatchInboundMessages(session *smpp.Session) { 226 | var err error 227 | for { 228 | packet := <-session.PDU() 229 | if packet == nil { 230 | return 231 | } 232 | shell.ShowPrompt(false) 233 | shell.Println() 234 | spew.Dump(packet) 235 | if p, ok := packet.(pdu.Responsable); ok { 236 | resp := p.Resp() 237 | spew.Dump(resp) 238 | if err = session.Send(resp); err != nil { 239 | fmt.Println(err) 240 | } 241 | } 242 | shell.ShowPrompt(true) 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /cmd/smpp-repl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/M2MGateway/go-smpp" 5 | "github.com/abiosoft/ishell" 6 | ) 7 | 8 | var session *smpp.Session 9 | 10 | var shell = ishell.New() 11 | 12 | func init() { 13 | shell.AutoHelp(true) 14 | shell.SetHistoryPath(".smpp_repl_history") 15 | shell.AddCmd(&ishell.Cmd{Name: "connect", Help: "connect to server", Func: onConnectToServer}) 16 | } 17 | 18 | func main() { 19 | shell.Println("Short Message Peer-to-Peer interactive shell") 20 | shell.Run() 21 | } 22 | -------------------------------------------------------------------------------- /cmd/smpp-repl/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | ) 6 | 7 | func makeFlags(on func(flags *flag.FlagSet)) *flag.FlagSet { 8 | flags := flag.NewFlagSet("", flag.ContinueOnError) 9 | on(flags) 10 | return flags 11 | } 12 | -------------------------------------------------------------------------------- /coding/best_coding.go: -------------------------------------------------------------------------------- 1 | package coding 2 | 3 | func BestCoding(input string) DataCoding { 4 | codings := []DataCoding{ 5 | GSM7BitCoding, ASCIICoding, Latin1Coding, 6 | CyrillicCoding, HebrewCoding, ShiftJISCoding, 7 | EUCKRCoding, 8 | } 9 | for _, coding := range codings { 10 | if coding.Validate(input) { 11 | return coding 12 | } 13 | } 14 | return UCS2Coding 15 | } 16 | 17 | func BestSafeCoding(input string) DataCoding { 18 | if GSM7BitCoding.Validate(input) { 19 | return GSM7BitCoding 20 | } 21 | return UCS2Coding 22 | } 23 | -------------------------------------------------------------------------------- /coding/best_coding_test.go: -------------------------------------------------------------------------------- 1 | package coding 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | //goland:noinspection SpellCheckingInspection 11 | var multipartList = [][]string{ 12 | { // 7 Bits 13 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent molestie eros ut ex dapibus sollicitudin in ut eros. Pellentesque venenatis vitae est e", 14 | "u porttitor. Nullam facilisis euismod felis, consectetur vehicula lorem aliquet at. Sed sit amet auctor lorem. Pellentesque euismod, orci non iaculis ull", 15 | "amcorper, massa ligula commodo sem, ac dictum lorem nulla vel tortor.", 16 | }, 17 | { // 1 byte 18 | "Лорем ипсум долор сит амет, еа яуи ностер елигенди. Перпетуа ассентиор ех нам. Ан молестие торяуатос вис, яуи виси тота трацтатос те. ", 19 | "Алии дебитис ин усу. Алии ерат тимеам дуо цу, пурто ерос иус ид, ет малис инвенире иус. Агам солет семпер яуо цу, граецо аперири витуп", 20 | "ерата еа цум. Ат сед дебет вениам сигниферумяуе, пер но миним фацете интеллегат, волумус демоцритум либерависсе еам цу.", 21 | }, 22 | { // 2 byte - Shift JIS 23 | "田ルマエ不効最ミラ報重マウタイ政身ロ朝連ドスど施康ルフオム象62訓誘30価ぽイ問需ニネキヲ指能トょそら問界亭ユムコス月投う続読ッ夕催ぶぼよ", 24 | "ょ京品状票変でッゅ。中執ぐでむ禁紀購ナアヒ中汁ノヘミオ行活リ和金ヒサテカ大情りイすち気康研ン瞬因ノ誕全げれフ働明えんざい最急僧かあ。入", 25 | "聴ほらゆで鉄42首ロ力録フタ都題月ロヨ真度づ見光ヨキ追5章否ご目3聞ぴぽ氏光ぽど価南べり回挑め。", 26 | }, 27 | { // 2 byte - EUC-KR 28 | "국회는 회계연도 개시 30일전까지 이를 의결하여야 한다, 동일한 범죄에 대하여 거듭 처벌받지 아니한다, 감사위원은 원장의 제청으로 대통령이", 29 | " 임명하고, 모든 국민은 신속한 재판을 받을 권리를 가진다.", 30 | }, 31 | { // UTF-16 32 | "👋🌌🎁🍛👵🐉🌁🐅🌜👰💧🕑👣🎤🌶🌐👺🍯🌶🌍 🎈🔶🐹👎🐁🔬🍌🔥💒🔴🌄🌁🕙", 33 | "📑🎒👤💫🔓🍜📎🎢🏭🐗🔊📶🌱🍣👞👫🔭📻📙🏢🍕🐻👼📍🎴🔪🌎🏫📭🍦🎃🐀🏮", 34 | "🕓🔐🏪🐈💐💧👷🏧🐸🍇💦🔧🔜👃📋💃🐒🔽🌃🔪🎲📇🍸", 35 | }, 36 | } 37 | 38 | func TestSplit(t *testing.T) { 39 | limit := 134 40 | for _, multipart := range multipartList { 41 | expected := strings.Join(multipart, "") 42 | coding := BestCoding(expected) 43 | splitter := coding.Splitter() 44 | segments := splitter.Split(expected, limit) 45 | require.Equal(t, multipart, segments) 46 | encoder := coding.Encoding().NewEncoder() 47 | for _, segment := range segments { 48 | require.LessOrEqual(t, splitter.Len(segment), limit) 49 | encoded, err := encoder.Bytes([]byte(segment)) 50 | require.NoError(t, err) 51 | require.Equal(t, splitter.Len(segment), len(encoded), segment) 52 | } 53 | } 54 | } 55 | 56 | func TestDataCoding(t *testing.T) { 57 | coding, class := DataCoding(0b11111111).MessageClass() 58 | require.Equal(t, UCS2Coding, coding) 59 | require.Equal(t, 3, class) 60 | 61 | { 62 | dataCoding := DataCoding(0b11000000) 63 | coding, active, kind := dataCoding.MessageWaitingInfo() 64 | require.Equal(t, NoCoding, coding) 65 | require.False(t, active) 66 | require.Equal(t, 0, kind) 67 | require.Nil(t, dataCoding.Encoding()) 68 | require.Nil(t, dataCoding.Splitter()) 69 | } 70 | { 71 | dataCoding := DataCoding(0b11010000) 72 | coding, active, kind := dataCoding.MessageWaitingInfo() 73 | require.Equal(t, GSM7BitCoding, coding) 74 | require.False(t, active) 75 | require.Equal(t, 0, kind) 76 | require.NotNil(t, dataCoding.Encoding()) 77 | require.NotNil(t, dataCoding.Splitter()) 78 | } 79 | { 80 | dataCoding := DataCoding(0b11100000) 81 | coding, active, kind := dataCoding.MessageWaitingInfo() 82 | require.Equal(t, UCS2Coding, coding) 83 | require.False(t, active) 84 | require.Equal(t, 0, kind) 85 | require.NotNil(t, dataCoding.Encoding()) 86 | require.NotNil(t, dataCoding.Splitter()) 87 | } 88 | { 89 | dataCoding := DataCoding(0b11110000) 90 | coding, active, kind := dataCoding.MessageWaitingInfo() 91 | require.Equal(t, NoCoding, coding) 92 | require.False(t, active) 93 | require.Equal(t, -1, kind) 94 | require.NotNil(t, dataCoding.Encoding()) 95 | require.NotNil(t, dataCoding.Splitter()) 96 | } 97 | require.Nil(t, NoCoding.Encoding()) 98 | require.Equal(t, DataCoding(0b11111111).GoString(), "11111111") 99 | require.NotEmpty(t, UCS2Coding.GoString()) 100 | require.NotNil(t, UCS2Coding.Encoding()) 101 | } 102 | 103 | //goland:noinspection SpellCheckingInspection 104 | func TestBestCoding(t *testing.T) { 105 | mapping := map[DataCoding]string{ 106 | GSM7BitCoding: "ΨΠΦ", 107 | ASCIICoding: "\x00abc", 108 | Latin1Coding: "\u0100", 109 | ShiftJISCoding: "日本に行きたい。", 110 | CyrillicCoding: "\u0410", 111 | HebrewCoding: "\u05B0", 112 | EUCKRCoding: "안녕", 113 | UCS2Coding: "💊", 114 | } 115 | for coding, input := range mapping { 116 | require.Equal(t, coding.String(), BestCoding(input).String()) 117 | } 118 | } 119 | 120 | func TestBestSplitter(t *testing.T) { 121 | require.Nil(t, NoCoding.Splitter()) 122 | } 123 | -------------------------------------------------------------------------------- /coding/data_coding.go: -------------------------------------------------------------------------------- 1 | package coding 2 | 3 | import ( 4 | "fmt" 5 | . "unicode" 6 | 7 | "github.com/M2MGateway/go-smpp/coding/gsm7bit" 8 | . "golang.org/x/text/encoding" 9 | "golang.org/x/text/encoding/charmap" 10 | "golang.org/x/text/encoding/japanese" 11 | "golang.org/x/text/encoding/korean" 12 | "golang.org/x/text/encoding/unicode" 13 | "golang.org/x/text/unicode/rangetable" 14 | ) 15 | 16 | // DataCoding see SMPP v5, section 4.7.7 (123p) 17 | 18 | type DataCoding byte 19 | 20 | func (c DataCoding) GoString() string { 21 | return c.String() 22 | } 23 | 24 | func (c DataCoding) String() string { 25 | return fmt.Sprintf("%08b", byte(c)) 26 | } 27 | 28 | func (c DataCoding) MessageWaitingInfo() (coding DataCoding, active bool, kind int) { 29 | kind = -1 30 | coding = NoCoding 31 | switch c >> 4 & 0b1111 { 32 | case 0b1100: 33 | case 0b1101: 34 | coding = GSM7BitCoding 35 | case 0b1110: 36 | coding = UCS2Coding 37 | default: 38 | return 39 | } 40 | active = c>>3 == 1 41 | kind = int(c & 0b11) 42 | return 43 | } 44 | 45 | func (c DataCoding) MessageClass() (coding DataCoding, class int) { 46 | class = int(c & 0b11) 47 | coding = GSM7BitCoding 48 | if c>>4&0b1111 != 0b1111 { 49 | coding = NoCoding 50 | class = -1 51 | } else if c>>2&0b1 == 1 { 52 | coding = UCS2Coding 53 | } 54 | return 55 | } 56 | 57 | func (c DataCoding) Encoding() Encoding { 58 | if coding, _, kind := c.MessageWaitingInfo(); kind != -1 { 59 | return encodingMap[coding] 60 | } else if coding, class := c.MessageClass(); class != -1 { 61 | return encodingMap[coding] 62 | } 63 | return encodingMap[c] 64 | } 65 | 66 | func (c DataCoding) Splitter() Splitter { 67 | if coding, _, kind := c.MessageWaitingInfo(); kind != -1 { 68 | return splitterMap[coding] 69 | } else if coding, class := c.MessageClass(); class != -1 { 70 | return splitterMap[coding] 71 | } 72 | return splitterMap[c] 73 | } 74 | 75 | func (c DataCoding) Validate(input string) bool { 76 | if c == UCS2Coding { 77 | return true 78 | } 79 | for _, r := range input { 80 | if !Is(alphabetMap[c], r) { 81 | return false 82 | } 83 | } 84 | return true 85 | } 86 | 87 | const ( 88 | GSM7BitCoding DataCoding = 0b00000000 // GSM 7Bit 89 | ASCIICoding DataCoding = 0b00000001 // ASCII 90 | Latin1Coding DataCoding = 0b00000011 // ISO-8859-1 (Latin-1) 91 | ShiftJISCoding DataCoding = 0b00000101 // Shift-JIS 92 | CyrillicCoding DataCoding = 0b00000110 // ISO-8859-5 (Cyrillic) 93 | HebrewCoding DataCoding = 0b00000111 // ISO-8859-8 (Hebrew) 94 | UCS2Coding DataCoding = 0b00001000 // UCS-2 95 | ISO2022JPCoding DataCoding = 0b00001010 // ISO-2022-JP 96 | EUCJPCoding DataCoding = 0b00001101 // Extended Kanji JIS (X 0212-1990) 97 | EUCKRCoding DataCoding = 0b00001110 // KS X 1001 (KS C 5601) 98 | NoCoding DataCoding = 0b10111111 // Reserved (Non-specification definition) 99 | ) 100 | 101 | var encodingMap = map[DataCoding]Encoding{ 102 | GSM7BitCoding: gsm7bit.Packed, 103 | ASCIICoding: charmap.ISO8859_1, 104 | Latin1Coding: charmap.ISO8859_1, 105 | ShiftJISCoding: japanese.ShiftJIS, 106 | CyrillicCoding: charmap.ISO8859_5, 107 | HebrewCoding: charmap.ISO8859_8, 108 | UCS2Coding: unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM), 109 | ISO2022JPCoding: japanese.ISO2022JP, 110 | EUCJPCoding: japanese.EUCJP, 111 | EUCKRCoding: korean.EUCKR, 112 | } 113 | 114 | var alphabetMap = map[DataCoding]*RangeTable{ 115 | GSM7BitCoding: gsm7bit.DefaultAlphabet, 116 | ASCIICoding: _ASCII, 117 | Latin1Coding: rangetable.Merge(_ASCII, Latin), 118 | CyrillicCoding: rangetable.Merge(_ASCII, Cyrillic), 119 | HebrewCoding: rangetable.Merge(_ASCII, Hebrew), 120 | ShiftJISCoding: rangetable.Merge(_ASCII, _Shift_JIS_Definition), 121 | EUCKRCoding: rangetable.Merge(_ASCII, _EUC_KR_Definition), 122 | } 123 | 124 | var splitterMap = map[DataCoding]Splitter{ 125 | GSM7BitCoding: _7BitSplitter, 126 | ASCIICoding: _1ByteSplitter, 127 | HebrewCoding: _1ByteSplitter, 128 | CyrillicCoding: _1ByteSplitter, 129 | Latin1Coding: _1ByteSplitter, 130 | ShiftJISCoding: _MultibyteSplitter, 131 | ISO2022JPCoding: _MultibyteSplitter, 132 | EUCJPCoding: _MultibyteSplitter, 133 | EUCKRCoding: _MultibyteSplitter, 134 | UCS2Coding: _UTF16Splitter, 135 | } 136 | -------------------------------------------------------------------------------- /coding/gsm7bit/decoder.go: -------------------------------------------------------------------------------- 1 | package gsm7bit 2 | 3 | import ( 4 | "bytes" 5 | 6 | "golang.org/x/text/transform" 7 | ) 8 | 9 | type gsm7Decoder struct{} 10 | 11 | func (d gsm7Decoder) Reset() { /* no needed */ } 12 | 13 | func (d gsm7Decoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { 14 | if len(src) == 0 { 15 | return 16 | } 17 | var buf bytes.Buffer 18 | septets := unpackSeptets(src) 19 | err = ErrInvalidByte 20 | for i, septet := 0, byte(0); i < len(septets); i++ { 21 | septet = septets[i] 22 | if septet <= 0x7F && septet != esc { 23 | buf.WriteRune(reverseLookup[septet]) 24 | } else { 25 | i++ 26 | if i >= len(septets) { 27 | return 28 | } 29 | r, ok := reverseEscapes[septets[i]] 30 | if !ok { 31 | return 32 | } 33 | buf.WriteRune(r) 34 | } 35 | } 36 | err = nil 37 | nDst = buf.Len() 38 | if len(dst) < nDst { 39 | nDst = 0 40 | err = transform.ErrShortDst 41 | } else { 42 | decoded := buf.Bytes() 43 | if n := len(decoded); n > 2 && (decoded[n-1] == cr || decoded[n-2] == cr) { 44 | nDst-- 45 | } 46 | copy(dst, decoded) 47 | } 48 | return 49 | } 50 | 51 | func unpackSeptets(septets []byte) []byte { 52 | var septet, bit byte = 0, 0 53 | var buf bytes.Buffer 54 | buf.Grow(len(septets)) 55 | for _, octet := range septets { 56 | for i := 0; i < 8; i++ { 57 | septet |= octet >> i & 1 << bit 58 | bit++ 59 | if bit == 7 { 60 | buf.WriteByte(septet) 61 | septet = 0 62 | bit = 0 63 | } 64 | } 65 | } 66 | return buf.Bytes() 67 | } 68 | -------------------------------------------------------------------------------- /coding/gsm7bit/encoder.go: -------------------------------------------------------------------------------- 1 | package gsm7bit 2 | 3 | import ( 4 | "bytes" 5 | 6 | "golang.org/x/text/transform" 7 | ) 8 | 9 | type gsm7Encoder struct{} 10 | 11 | func (e gsm7Encoder) Reset() { /* no needed */ } 12 | 13 | func (e gsm7Encoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { 14 | if len(src) == 0 { 15 | return 16 | } 17 | septets, err := toSeptets(string(src)) 18 | if err != nil { 19 | return 20 | } 21 | nDst = blocks(len(septets) * 7) 22 | if len(dst) < nDst { 23 | nDst = 0 24 | err = transform.ErrShortDst 25 | return 26 | } 27 | packSeptets(dst, septets) 28 | return 29 | } 30 | 31 | func packSeptets(dst []byte, septets []byte) { 32 | var index int 33 | var bit, item byte 34 | pack := func(c byte) { 35 | for i := 0; i < 7; i++ { 36 | dst[index] |= c >> i & 1 << bit 37 | bit++ 38 | if bit == 8 { 39 | index++ 40 | bit = 0 41 | } 42 | } 43 | } 44 | for _, c := range septets { 45 | item = c 46 | pack(c) 47 | } 48 | if 8-bit == 7 { 49 | pack(cr) 50 | } else if bit == 0 && item == cr { 51 | dst[index] = 0x00 52 | pack(cr) 53 | } 54 | } 55 | 56 | func toSeptets(input string) (septets []byte, err error) { 57 | var buf bytes.Buffer 58 | for _, r := range input { 59 | if v, ok := forwardLookup[r]; ok { 60 | buf.WriteByte(v) 61 | } else if v, ok := forwardEscapes[r]; ok { 62 | buf.WriteByte(esc) 63 | buf.WriteByte(v) 64 | } else { 65 | err = ErrInvalidCharacter 66 | return 67 | } 68 | } 69 | septets = buf.Bytes() 70 | return 71 | } 72 | 73 | func blocks(n int) (length int) { 74 | length = n / 8 75 | if n%8 != 0 { 76 | length += 1 77 | } 78 | return 79 | } 80 | -------------------------------------------------------------------------------- /coding/gsm7bit/encoding.go: -------------------------------------------------------------------------------- 1 | package gsm7bit 2 | 3 | import ( 4 | "golang.org/x/text/encoding" 5 | "golang.org/x/text/transform" 6 | ) 7 | 8 | var Packed = gsm7Encoding{ 9 | encoder: new(gsm7Encoder), 10 | decoder: new(gsm7Decoder), 11 | } 12 | 13 | type gsm7Encoding struct{ encoder, decoder transform.Transformer } 14 | 15 | func (e gsm7Encoding) NewDecoder() *encoding.Decoder { 16 | return &encoding.Decoder{Transformer: e.decoder} 17 | } 18 | 19 | func (e gsm7Encoding) NewEncoder() *encoding.Encoder { 20 | return &encoding.Encoder{Transformer: e.encoder} 21 | } 22 | -------------------------------------------------------------------------------- /coding/gsm7bit/encoding_test.go: -------------------------------------------------------------------------------- 1 | package gsm7bit 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | //goland:noinspection SpellCheckingInspection 11 | var mapping = map[string]string{ 12 | "": "", 13 | "1": "31", 14 | "12": "3119", 15 | "123": "31D90C", 16 | "1234": "31D98C06", 17 | "12345": "31D98C5603", 18 | "123456": "31D98C56B301", 19 | "1234567": "31D98C56B3DD1A", 20 | "12345678": "31D98C56B3DD70", 21 | "123456789": "31D98C56B3DD7039", 22 | "12345[6]": "31D98C56DBF06C1B1F", 23 | "^{}\\[~]|€": "1bca06b5496d5e1bdea6b7f16d809b32", 24 | "of the printing and typesetting": "6F33888E2E83E0F2B49B9E769F4161371944CFC3CBF3329D9E769F1B", 25 | "industry. Lorem Ipsum has been": "6937B93EA7CBF32E10F32D2FB74149F8BCDE06A1C37390B85C7603", 26 | "the industry's standard dummy": "747419947693EB73BA3C7F9A83E6F4B09B1C969341E47ABB9D07", 27 | } 28 | 29 | var invalidEncoder = [][]byte{ 30 | {0xFF}, 31 | } 32 | 33 | var invalidDecoder = [][]byte{ 34 | {0x1B}, 35 | {0x1B, 0x80}, 36 | } 37 | 38 | func TestGSM7Encoding(t *testing.T) { 39 | encoder := Packed.NewEncoder() 40 | decoder := Packed.NewDecoder() 41 | for decodedText, encodedHex := range mapping { 42 | decoded, err := hex.DecodeString(encodedHex) 43 | require.NoError(t, err) 44 | 45 | encoded, err := encoder.Bytes([]byte(decodedText)) 46 | require.NoError(t, err) 47 | require.Equal(t, decoded, encoded, hex.EncodeToString(encoded)) 48 | 49 | decoded, err = decoder.Bytes(encoded) 50 | require.NoError(t, err) 51 | require.Equal(t, decodedText, string(decoded), hex.EncodeToString(encoded)) 52 | } 53 | for _, input := range invalidEncoder { 54 | _, err := encoder.Bytes(input) 55 | require.Error(t, err) 56 | } 57 | for _, encoded := range invalidDecoder { 58 | _, err := decoder.Bytes(encoded) 59 | require.Error(t, err) 60 | } 61 | _, _ = encoder.Bytes([]byte("1234567\r")) 62 | } 63 | -------------------------------------------------------------------------------- /coding/gsm7bit/errors.go: -------------------------------------------------------------------------------- 1 | package gsm7bit 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrInvalidCharacter = errors.New("gsm7bit: invalid gsm7 character") 7 | ErrInvalidByte = errors.New("gsm7bit: invalid gsm7 byte") 8 | ) 9 | -------------------------------------------------------------------------------- /coding/gsm7bit/table.go: -------------------------------------------------------------------------------- 1 | package gsm7bit 2 | 3 | import "unicode" 4 | 5 | func init() { 6 | for index, r := range reverseLookup { 7 | forwardLookup[r] = byte(index) 8 | } 9 | for r, b := range forwardEscapes { 10 | reverseEscapes[b] = r 11 | } 12 | } 13 | 14 | const esc, cr byte = 0x1B, 0x0D 15 | 16 | var forwardLookup = map[rune]byte{} 17 | 18 | var reverseLookup = [256]rune{ 19 | 0x40, 0xA3, 0x24, 0xA5, 0xE8, 0xE9, 0xF9, 0xEC, 0xF2, 0xC7, 0x0A, 0xD8, 0xF8, 0x0D, 0xC5, 0xE5, 20 | 0x0394, 0x005F, 0x03A6, 0x0393, 0x039B, 0x03A9, 0x03A0, 0x03A8, 0x03A3, 0x0398, 0x039E, 0x00A0, 21 | 0xC6, 0xE6, 0xDF, 0xC9, 0x20, 0x21, 0x22, 0x23, 0xA4, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 22 | 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 23 | 0x3C, 0x3D, 0x3E, 0x3F, 0xA1, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 24 | 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0xC4, 25 | 0xD6, 0xD1, 0xDC, 0xA7, 0xBF, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 26 | 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xE4, 27 | 0xF6, 0xF1, 0xFC, 0xE0, 28 | } 29 | 30 | var forwardEscapes = map[rune]byte{ 31 | 0x0C: 0x0A, 0x5B: 0x3C, 0x5C: 0x2F, 0x5D: 0x3E, 0x5E: 0x14, 0x7B: 0x28, 0x7C: 0x40, 0x7D: 0x29, 0x7E: 0x3D, 32 | 0x20AC: 0x65, 33 | } 34 | 35 | var reverseEscapes = map[byte]rune{} 36 | 37 | var DefaultAlphabet = &unicode.RangeTable{R16: []unicode.Range16{ 38 | {0x000A, 0x000A, 1}, {0x000C, 0x000D, 1}, {0x0020, 0x005F, 1}, {0x0061, 0x007E, 1}, {0x00A0, 0x00A1, 1}, 39 | {0x00A3, 0x00A5, 1}, {0x00A7, 0x00A7, 1}, {0x00BF, 0x00BF, 1}, {0x00C4, 0x00C6, 1}, {0x00C9, 0x00C9, 1}, 40 | {0x00D1, 0x00D1, 1}, {0x00D6, 0x00D6, 1}, {0x00D8, 0x00D8, 1}, {0x00DC, 0x00DC, 1}, {0x00DF, 0x00E0, 1}, 41 | {0x00E4, 0x00E9, 1}, {0x00EC, 0x00EC, 1}, {0x00F1, 0x00F2, 1}, {0x00F6, 0x00F6, 1}, {0x00F8, 0x00F9, 1}, 42 | {0x00FC, 0x00FC, 1}, {0x0393, 0x0394, 1}, {0x0398, 0x0398, 1}, {0x039B, 0x039B, 1}, {0x039E, 0x039E, 1}, 43 | {0x03A0, 0x03A0, 1}, {0x03A3, 0x03A3, 1}, {0x03A6, 0x03A6, 1}, {0x03A8, 0x03A9, 1}, {0x20AC, 0x20AC, 1}, 44 | }} 45 | -------------------------------------------------------------------------------- /coding/range_table.go: -------------------------------------------------------------------------------- 1 | package coding 2 | 3 | import . "unicode" 4 | 5 | //goland:noinspection GoSnakeCaseUsage 6 | var ( 7 | _ASCII = &RangeTable{R16: []Range16{ 8 | {0x00, 0x7F, 1}, 9 | }} 10 | _Shift_JIS_Definition = &RangeTable{R16: []Range16{ 11 | {0x00A1, 0x0460, 1}, 12 | {0x2010, 0x2670, 1}, 13 | {0x3000, 0x33CE, 1}, 14 | {0x4E00, 0x9FA6, 1}, 15 | {0xF929, 0xFA2E, 1}, 16 | {0xFF01, 0xFFE6, 1}, 17 | }} 18 | _EUC_KR_Definition = &RangeTable{R16: []Range16{ 19 | {0x00A1, 0x0452, 1}, 20 | {0x2015, 0x266E, 1}, 21 | {0x3000, 0x33DE, 1}, 22 | {0x4E00, 0x9F9D, 1}, 23 | {0xAC00, 0xD7A4, 1}, 24 | {0xF900, 0xFA0C, 1}, 25 | {0xFF01, 0xFFE7, 1}, 26 | }} 27 | ) 28 | -------------------------------------------------------------------------------- /coding/semioctet/semi_octet.go: -------------------------------------------------------------------------------- 1 | package semioctet 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "strconv" 7 | ) 8 | 9 | func EncodeSemi(w io.Writer, chunks ...int) (n int64, err error) { 10 | digits := toDigits(chunks) 11 | var buf bytes.Buffer 12 | buf.Grow(len(digits) / 2) 13 | i, remain := 0, len(digits) 14 | for remain > 1 { 15 | buf.WriteByte(digits[i+1]<<4 | digits[i]) 16 | i += 2 17 | remain -= 2 18 | } 19 | if remain > 0 { 20 | buf.WriteByte(0b11110000 | digits[i]) 21 | } 22 | return buf.WriteTo(w) 23 | } 24 | 25 | func DecodeSemi(encoded []byte) (chunks []int) { 26 | var half byte 27 | for _, item := range encoded { 28 | half = item >> 4 29 | if half == 0b1111 { 30 | return append(chunks, int(item&0b1111)) 31 | } 32 | chunks = append(chunks, int(item&0b1111*10+half)) 33 | } 34 | return 35 | } 36 | 37 | func EncodeSemiAddress(w io.Writer, input string) (n int64, err error) { 38 | parsed, err := strconv.ParseUint(input, 10, 64) 39 | if err != nil { 40 | return 41 | } 42 | return EncodeSemi(w, int(parsed)) 43 | } 44 | 45 | func DecodeSemiAddress(encoded []byte) (output string) { 46 | var buf bytes.Buffer 47 | var half byte 48 | for _, item := range encoded { 49 | half = item & 0b1111 50 | buf.WriteByte('0' + half) 51 | if half = item >> 4; half != 0b1111 { 52 | buf.WriteByte('0' + half) 53 | } 54 | } 55 | return buf.String() 56 | } 57 | 58 | func toDigits(chunks []int) (digits []byte) { 59 | for _, chunk := range chunks { 60 | if chunk < 10 { 61 | digits = append(digits, 0) 62 | } 63 | for _, r := range strconv.Itoa(chunk) { 64 | digits = append(digits, byte(r-'0')) 65 | } 66 | } 67 | return 68 | } 69 | -------------------------------------------------------------------------------- /coding/semioctet/semi_octet_test.go: -------------------------------------------------------------------------------- 1 | package semioctet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestSemiOctet(t *testing.T) { 12 | tests := map[string][]int{ 13 | "1020304050": {1, 2, 3, 4, 5}, 14 | "1122334455": {11, 22, 33, 44, 55}, 15 | } 16 | for expected, input := range tests { 17 | decoded, err := hex.DecodeString(expected) 18 | require.NoError(t, err) 19 | var buf bytes.Buffer 20 | _, err = EncodeSemi(&buf, input...) 21 | require.NoError(t, err) 22 | require.Equal(t, decoded, buf.Bytes()) 23 | require.Equal(t, input, DecodeSemi(decoded)) 24 | } 25 | { 26 | require.Equal(t, []int{65, 53, 5}, DecodeSemi([]byte{0x56, 0x35, 0xF5})) 27 | } 28 | } 29 | 30 | func TestSemiOctetAddress(t *testing.T) { 31 | tests := map[string]string{ 32 | "2143658709": "1234567890", 33 | "2143658709F0": "12345678900", 34 | } 35 | for expected, input := range tests { 36 | decoded, err := hex.DecodeString(expected) 37 | require.NoError(t, err) 38 | var buf bytes.Buffer 39 | _, err = EncodeSemiAddress(&buf, input) 40 | require.NoError(t, err) 41 | require.Equal(t, decoded, buf.Bytes()) 42 | require.Equal(t, input, DecodeSemiAddress(decoded)) 43 | } 44 | { 45 | _, err := EncodeSemiAddress(nil, "ABC") 46 | require.Error(t, err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /coding/splitter.go: -------------------------------------------------------------------------------- 1 | package coding 2 | 3 | type Splitter func(rune) int 4 | 5 | var ( 6 | _7BitSplitter Splitter = func(rune) int { return 7 } 7 | _1ByteSplitter Splitter = func(rune) int { return 8 } 8 | _MultibyteSplitter Splitter = func(r rune) int { 9 | if r < 0x7F { 10 | return 8 11 | } 12 | return 16 13 | } 14 | _UTF16Splitter Splitter = func(r rune) int { 15 | if (r <= 0xD7FF) || ((r >= 0xE000) && (r <= 0xFFFF)) { 16 | return 16 17 | } 18 | return 32 19 | } 20 | ) 21 | 22 | func (fn Splitter) Len(input string) (n int) { 23 | for _, point := range input { 24 | n += fn(point) 25 | } 26 | if n%8 != 0 { 27 | n += 8 - n%8 28 | } 29 | return n / 8 30 | } 31 | 32 | func (fn Splitter) Split(input string, limit int) (segments []string) { 33 | limit *= 8 34 | points := []rune(input) 35 | var start, length int 36 | for i := 0; i < len(points); i++ { 37 | length += fn(points[i]) 38 | if length > limit { 39 | segments = append(segments, string(points[start:i])) 40 | start, length = i, 0 41 | i-- 42 | } 43 | } 44 | if length > 0 { 45 | segments = append(segments, string(points[start:])) 46 | } 47 | return 48 | } 49 | -------------------------------------------------------------------------------- /docs/SMPP_v3_3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CursedHardware/go-smpp/92d023664ef07c975c3ec989fc8901b7e767ad60/docs/SMPP_v3_3.pdf -------------------------------------------------------------------------------- /docs/SMPP_v3_4_Issue1_2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CursedHardware/go-smpp/92d023664ef07c975c3ec989fc8901b7e767ad60/docs/SMPP_v3_4_Issue1_2.pdf -------------------------------------------------------------------------------- /docs/SMPP_v5.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CursedHardware/go-smpp/92d023664ef07c975c3ec989fc8901b7e767ad60/docs/SMPP_v5.pdf -------------------------------------------------------------------------------- /docs/device-specific-caveats.md: -------------------------------------------------------------------------------- 1 | # Device-specific Caveats 2 | 3 | ## Synway SMG4000 Series 4 | 5 | - Only **SMPP v3.4** is implemented. 6 | 7 | - `enquire_link` need to be invoked every 0.5 seconds. 8 | 9 | - Firmware versions before 09/25/2020, `deliver_sm` - `dest_addr` field returns garbage. 10 | 11 | - The use of `bind_receiver` and `bind_transmitter` is not supported. 12 | 13 | ## DBLTek GoIP Series 14 | 15 | - Only **SMPP v3.4** is implemented. 16 | 17 | - `enquire_link` need to be invoked every 0.5 seconds. 18 | 19 | - System ID pattern\ 20 | e.q: if set `goip` then every slot is `goip01` ... `goip48`,\ 21 | but if use `goip` login, then use **all slots** sent sms. 22 | 23 | - Multipart SMS, 24 | Only support `TLV 0424, 4.8.4.36 message_payload` 25 | 26 | - Only supports some Command IDs: 27 | 28 | ```plaintext 29 | bind_receiver 30 | bind_transmitter 31 | bind_transceiver 32 | submit_sm 33 | deliver_sm 34 | enquire_link 35 | unbind 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/wireshark.md: -------------------------------------------------------------------------------- 1 | # Wireshark 2 | 3 | 1. Discard `enquire_link` and `enquire_link_resp` packets 4 | 5 | ```plain 6 | smpp and !(smpp.command_id in {0x00000015 0x80000015}) 7 | ``` 8 | 9 | 2. Capturing 10 | 11 | ```shell 12 | tcpdump -w smpp.pcap port 2775 13 | # or 14 | tshark -w smpp.pcap port 2775 15 | ``` 16 | 17 | ## References 18 | 19 | - 20 | - 21 | - 22 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package smpp 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrConnectionClosed = errors.New("smpp: connection closed") 7 | ) 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/M2MGateway/go-smpp 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/abiosoft/ishell v2.0.0+incompatible 7 | github.com/davecgh/go-spew v1.1.1 8 | github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible 9 | github.com/imdario/mergo v0.3.11 10 | github.com/stretchr/testify v1.6.1 11 | github.com/xeipuuv/gojsonschema v1.2.0 12 | golang.org/x/text v0.3.6 13 | ) 14 | 15 | require ( 16 | github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect 17 | github.com/chzyer/logex v1.1.10 // indirect 18 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect 19 | github.com/fatih/color v1.9.0 // indirect 20 | github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect 21 | github.com/mattn/go-colorable v0.1.4 // indirect 22 | github.com/mattn/go-isatty v0.0.11 // indirect 23 | github.com/pmezard/go-difflib v1.0.0 // indirect 24 | github.com/technoweenie/multipartstreamer v1.0.1 // indirect 25 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect 26 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 27 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect 28 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw= 2 | github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg= 3 | github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8= 4 | github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530= 5 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 6 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 7 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 8 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= 13 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 14 | github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI= 15 | github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M= 16 | github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU= 17 | github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= 18 | github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= 19 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 20 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 21 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 22 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 23 | github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= 24 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 25 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 26 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 27 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 28 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 29 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 30 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 31 | github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= 32 | github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= 33 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= 34 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 35 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 36 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 37 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 38 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 39 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 40 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= 41 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 42 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 43 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 44 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 48 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 49 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 50 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 51 | -------------------------------------------------------------------------------- /pdu/address.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "fmt" 8 | "io" 9 | ) 10 | 11 | type Address struct { 12 | TON byte // see SMPP v5, section 4.7.1 (113p) 13 | NPI byte // see SMPP v5, section 4.7.2 (113p) 14 | No string 15 | } 16 | 17 | func (p *Address) ReadFrom(r io.Reader) (n int64, err error) { 18 | buf := bufio.NewReader(r) 19 | p.TON, err = buf.ReadByte() 20 | if err == nil { 21 | p.NPI, err = buf.ReadByte() 22 | } 23 | if err == nil { 24 | p.No, err = readCString(buf) 25 | } 26 | return 27 | } 28 | 29 | func (p Address) WriteTo(w io.Writer) (n int64, err error) { 30 | var buf bytes.Buffer 31 | buf.WriteByte(p.TON) 32 | buf.WriteByte(p.NPI) 33 | writeCString(&buf, p.No) 34 | return buf.WriteTo(w) 35 | } 36 | 37 | func (p Address) String() string { 38 | if p.TON == 1 && p.NPI == 1 && len(p.No) > 0 && p.No[0] != '+' { 39 | return "+" + p.No 40 | } 41 | return p.No 42 | } 43 | 44 | type DestinationAddresses struct { 45 | Addresses []Address 46 | DistributionList []string 47 | } 48 | 49 | func (p *DestinationAddresses) ReadFrom(r io.Reader) (n int64, err error) { 50 | buf := bufio.NewReader(r) 51 | count, err := buf.ReadByte() 52 | if err != nil { 53 | err = ErrInvalidCommandLength 54 | return 55 | } 56 | *p = DestinationAddresses{} 57 | var destFlag byte 58 | var value string 59 | var address Address 60 | for i := byte(0); i < count; i++ { 61 | switch destFlag, _ = buf.ReadByte(); destFlag { 62 | case 1: 63 | if _, err = address.ReadFrom(buf); err == nil { 64 | p.Addresses = append(p.Addresses, address) 65 | } 66 | case 2: 67 | if value, err = readCString(buf); err == nil { 68 | p.DistributionList = append(p.DistributionList, value) 69 | } 70 | default: 71 | err = ErrInvalidDestFlag 72 | return 73 | } 74 | if err != nil { 75 | err = ErrInvalidCommandLength 76 | return 77 | } 78 | } 79 | return 80 | } 81 | 82 | func (p DestinationAddresses) WriteTo(w io.Writer) (n int64, err error) { 83 | length := len(p.Addresses) + len(p.DistributionList) 84 | if length > 0xFF { 85 | err = ErrInvalidDestCount 86 | return 87 | } 88 | var buf bytes.Buffer 89 | buf.WriteByte(byte(length)) 90 | for _, address := range p.Addresses { 91 | buf.WriteByte(1) 92 | _, _ = address.WriteTo(&buf) 93 | } 94 | for _, distribution := range p.DistributionList { 95 | buf.WriteByte(2) 96 | writeCString(&buf, distribution) 97 | } 98 | return buf.WriteTo(w) 99 | } 100 | 101 | type UnsuccessfulRecords []UnsuccessfulRecord 102 | 103 | type UnsuccessfulRecord struct { 104 | DestAddr Address 105 | ErrorStatusCode CommandStatus 106 | } 107 | 108 | func (i UnsuccessfulRecord) String() string { 109 | return fmt.Sprintf("%s#%s", i.DestAddr, i.ErrorStatusCode) 110 | } 111 | 112 | func (p *UnsuccessfulRecords) ReadFrom(r io.Reader) (n int64, err error) { 113 | buf := bufio.NewReader(r) 114 | count, err := buf.ReadByte() 115 | if err != nil { 116 | err = ErrInvalidCommandLength 117 | return 118 | } 119 | items := UnsuccessfulRecords{} 120 | var item UnsuccessfulRecord 121 | for i := byte(0); i < count; i++ { 122 | _, err = item.DestAddr.ReadFrom(buf) 123 | if err == nil { 124 | err = binary.Read(buf, binary.BigEndian, &item.ErrorStatusCode) 125 | } 126 | if err != nil { 127 | err = ErrInvalidCommandLength 128 | return 129 | } 130 | items = append(items, item) 131 | } 132 | *p = items 133 | return 134 | } 135 | 136 | func (p UnsuccessfulRecords) WriteTo(w io.Writer) (n int64, err error) { 137 | if len(p) > 0xFF { 138 | err = ErrItemTooMany 139 | return 140 | } 141 | var buf bytes.Buffer 142 | buf.WriteByte(byte(len(p))) 143 | for _, item := range p { 144 | _, _ = item.DestAddr.WriteTo(&buf) 145 | _ = binary.Write(&buf, binary.BigEndian, item.ErrorStatusCode) 146 | } 147 | return buf.WriteTo(w) 148 | } 149 | -------------------------------------------------------------------------------- /pdu/address_test.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestAddress_ReadFrom(t *testing.T) { 13 | mapping := map[string]Address{ 14 | "315B7068616E746F6D537472696B6500": {TON: 0x31, NPI: 0x5B, No: "phantomStrike"}, 15 | "5F0D7068616E746F6D4F7065726100": {TON: 0x5F, NPI: 0x0D, No: "phantomOpera"}, 16 | } 17 | for packet, expected := range mapping { 18 | var address Address 19 | decoded, err := hex.DecodeString(packet) 20 | require.NoError(t, err) 21 | 22 | _, err = address.ReadFrom(bytes.NewReader(decoded)) 23 | require.NoError(t, err) 24 | require.Equal(t, expected, address) 25 | require.Equal(t, expected.No, address.String()) 26 | 27 | var buf bytes.Buffer 28 | _, err = address.WriteTo(&buf) 29 | require.NoError(t, err) 30 | require.Equal(t, decoded, buf.Bytes()) 31 | } 32 | } 33 | 34 | func TestAddress_String(t *testing.T) { 35 | mapping := map[string]Address{ 36 | "+15417543010": {TON: 1, NPI: 1, No: "15417543010"}, 37 | } 38 | for expected, address := range mapping { 39 | require.Equal(t, expected, address.String()) 40 | } 41 | } 42 | 43 | func TestDestinationAddresses(t *testing.T) { 44 | mapping := map[string]DestinationAddresses{ 45 | "03010000426F623100024C6973743100024C6973743200": { 46 | Addresses: []Address{{No: "Bob1"}}, 47 | DistributionList: []string{"List1", "List2"}, 48 | }, 49 | } 50 | for packet, expected := range mapping { 51 | addresses := DestinationAddresses{} 52 | decoded, err := hex.DecodeString(packet) 53 | require.NoError(t, err) 54 | 55 | _, err = addresses.ReadFrom(bytes.NewReader(decoded)) 56 | require.NoError(t, err) 57 | require.Equal(t, expected, addresses) 58 | 59 | var buf bytes.Buffer 60 | _, err = addresses.WriteTo(&buf) 61 | require.NoError(t, err) 62 | require.Equal(t, decoded, buf.Bytes()) 63 | } 64 | } 65 | 66 | func TestDestinationAddresses_ReadFrom(t *testing.T) { 67 | var addresses DestinationAddresses 68 | _, err := addresses.ReadFrom(bytes.NewReader([]byte{0xFF, 0x02})) 69 | require.Error(t, err) 70 | _, err = addresses.ReadFrom(bytes.NewReader([]byte{0xFF, 0x03})) 71 | require.Error(t, err) 72 | _, err = addresses.ReadFrom(bytes.NewReader(nil)) 73 | require.Error(t, err) 74 | addresses.DistributionList = make([]string, 0x100) 75 | var buf bytes.Buffer 76 | _, err = addresses.WriteTo(&buf) 77 | require.Error(t, err) 78 | } 79 | 80 | func TestUnsuccessfulRecords(t *testing.T) { 81 | packet := "022621426F623100000000130000426F62320000000014" 82 | parsed := UnsuccessfulRecords{ 83 | UnsuccessfulRecord{ErrorStatusCode: 19, DestAddr: Address{TON: 38, NPI: 33, No: "Bob1"}}, 84 | UnsuccessfulRecord{ErrorStatusCode: 20, DestAddr: Address{No: "Bob2"}}, 85 | } 86 | stringify := "[Bob1#ESME_RREPLACEFAIL Bob2#ESME_RMSGQFUL]" 87 | smes := UnsuccessfulRecords{} 88 | decoded, err := hex.DecodeString(packet) 89 | require.NoError(t, err) 90 | 91 | _, err = smes.ReadFrom(bytes.NewReader(decoded)) 92 | require.NoError(t, err) 93 | require.Equal(t, parsed, smes) 94 | require.Equal(t, stringify, fmt.Sprint(smes)) 95 | 96 | var buf bytes.Buffer 97 | _, err = smes.WriteTo(&buf) 98 | require.NoError(t, err) 99 | require.Equal(t, decoded, buf.Bytes()) 100 | 101 | smes = make([]UnsuccessfulRecord, 0x100) 102 | _, err = smes.WriteTo(&buf) 103 | require.Error(t, err) 104 | } 105 | 106 | func TestUnsuccessfulRecords_ReadFrom(t *testing.T) { 107 | sme := UnsuccessfulRecords{} 108 | _, err := sme.ReadFrom(bytes.NewReader([]byte{0xFF})) 109 | require.Error(t, err) 110 | _, err = sme.ReadFrom(bytes.NewReader(nil)) 111 | require.Error(t, err) 112 | } 113 | -------------------------------------------------------------------------------- /pdu/constants.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | const ( 4 | MaxShortMessageLength = 140 5 | ) 6 | -------------------------------------------------------------------------------- /pdu/errors.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | //goland:noinspection ALL 8 | var ( 9 | ErrUnmarshalPDUFailed = errors.New("pdu: unmarshal pdu failed") 10 | ErrUnknownDataCoding = errors.New("pdu: unknown data coding") 11 | ErrInvalidSequence = errors.New("pdu: invalid sequence (should be 31 bit integer)") 12 | ErrItemTooMany = errors.New("pdu: item too many") 13 | ErrDataTooLarge = errors.New("pdu: data too large") 14 | ErrUnparseableTime = errors.New("pdu: unparseable time") 15 | ErrShortMessageTooLarge = errors.New("pdu: encoded short message data exceeds size of 140 bytes") 16 | ErrMultipartTooMuch = errors.New("pdu: multipart sms too much (max 254 segments)") 17 | ) 18 | 19 | const ( 20 | ErrInvalidCommandLength CommandStatus = 0x002 21 | ErrInvalidCommandID CommandStatus = 0x003 22 | ErrInvalidDestCount CommandStatus = 0x033 23 | ErrInvalidDestFlag CommandStatus = 0x040 24 | ErrInvalidTagLength CommandStatus = 0x0C2 25 | ErrUnknownError CommandStatus = 0x0FF 26 | ) 27 | -------------------------------------------------------------------------------- /pdu/esm_class.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import "fmt" 4 | 5 | // ESMClass see SMPP v5, section 4.7.12 (125p) 6 | type ESMClass struct { 7 | MessageMode byte // __ ____ ** 8 | MessageType byte // __ **** __ 9 | UDHIndicator bool // _* ____ __ 10 | ReplyPath bool // *_ ____ __ 11 | } 12 | 13 | func (e ESMClass) ReadByte() (c byte, err error) { 14 | c |= e.MessageMode & 0b11 15 | c |= e.MessageType & 0b1111 << 2 16 | c |= getBool(e.UDHIndicator) << 6 17 | c |= getBool(e.ReplyPath) << 7 18 | return 19 | } 20 | 21 | func (e *ESMClass) WriteByte(c byte) error { 22 | e.MessageMode = c & 0b11 23 | e.MessageType = c >> 2 & 0b1111 24 | e.UDHIndicator = c>>6&0b1 == 1 25 | e.ReplyPath = c>>7&0b1 == 1 26 | return nil 27 | } 28 | 29 | func (e ESMClass) String() string { 30 | c, _ := e.ReadByte() 31 | return fmt.Sprintf("%08b", c) 32 | } 33 | -------------------------------------------------------------------------------- /pdu/esm_class_test.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestESMClass(t *testing.T) { 10 | expected := byte(0b11001101) 11 | var esm ESMClass 12 | _ = esm.WriteByte(expected) 13 | c, _ := esm.ReadByte() 14 | require.Equal(t, esm, ESMClass{ 15 | MessageMode: 1, 16 | MessageType: 3, 17 | UDHIndicator: true, 18 | ReplyPath: true, 19 | }) 20 | require.Equal(t, expected, c) 21 | require.Equal(t, "11001101", esm.String()) 22 | } 23 | -------------------------------------------------------------------------------- /pdu/factory.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | var types = map[CommandID]reflect.Type{ 11 | 0x00000002: reflect.TypeOf(BindTransmitter{}), // see SMPP v5, section 4.1.1.1 (56p) 12 | 0x80000002: reflect.TypeOf(BindTransmitterResp{}), // see SMPP v5, section 4.1.1.2 (57p) 13 | 0x00000001: reflect.TypeOf(BindReceiver{}), // see SMPP v5, section 4.1.1.3 (58p) 14 | 0x80000001: reflect.TypeOf(BindReceiverResp{}), // see SMPP v5, section 4.1.1.4 (59p) 15 | 0x00000009: reflect.TypeOf(BindTransceiver{}), // see SMPP v5, section 4.1.1.5 (59p) 16 | 0x80000009: reflect.TypeOf(BindTransceiverResp{}), // see SMPP v5, section 4.1.1.6 (60p) 17 | 0x0000000B: reflect.TypeOf(Outbind{}), // see SMPP v5, section 4.1.1.7 (61p) 18 | 0x00000006: reflect.TypeOf(Unbind{}), // see SMPP v5, section 4.1.1.8 (61p) 19 | 0x80000006: reflect.TypeOf(UnbindResp{}), // see SMPP v5, section 4.1.1.9 (62p) 20 | 0x00000015: reflect.TypeOf(EnquireLink{}), // see SMPP v5, section 4.1.2.1 (63p) 21 | 0x80000015: reflect.TypeOf(EnquireLinkResp{}), // see SMPP v5, section 4.1.2.2 (63p) 22 | 0x00000102: reflect.TypeOf(AlertNotification{}), // see SMPP v5, section 4.1.3.1 (64p) 23 | 0x80000000: reflect.TypeOf(GenericNACK{}), // see SMPP v5, section 4.1.4.1 (65p) 24 | 0x00000004: reflect.TypeOf(SubmitSM{}), // see SMPP v5, section 4.2.1.1 (66p) 25 | 0x80000004: reflect.TypeOf(SubmitSMResp{}), // see SMPP v5, section 4.2.1.2 (68p) 26 | 0x00000103: reflect.TypeOf(DataSM{}), // see SMPP v5, section 4.2.2.1 (69p) 27 | 0x80000103: reflect.TypeOf(DataSMResp{}), // see SMPP v5, section 4.2.2.2 (70p) 28 | 0x00000021: reflect.TypeOf(SubmitMulti{}), // see SMPP v5, section 4.2.3.1 (71p) 29 | 0x80000021: reflect.TypeOf(SubmitMultiResp{}), // see SMPP v5, section 4.2.3.2 (74p) 30 | 0x00000005: reflect.TypeOf(DeliverSM{}), // see SMPP v5, section 4.3.1.1 (85p) 31 | 0x80000005: reflect.TypeOf(DeliverSMResp{}), // see SMPP v5, section 4.3.1.1 (87p) 32 | 0x00000112: reflect.TypeOf(BroadcastSM{}), // see SMPP v5, section 4.4.1.1 (92p) 33 | 0x80000112: reflect.TypeOf(BroadcastSMResp{}), // see SMPP v5, section 4.4.1.2 (96p) 34 | 0x00000008: reflect.TypeOf(CancelSM{}), // see SMPP v5, section 4.5.1.1 (100p) 35 | 0x80000008: reflect.TypeOf(CancelSMResp{}), // see SMPP v5, section 4.5.1.2 (101p) 36 | 0x00000003: reflect.TypeOf(QuerySM{}), // see SMPP v5, section 4.5.2.1 (101p) 37 | 0x80000003: reflect.TypeOf(QuerySMResp{}), // see SMPP v5, section 4.5.2.2 (103p) 38 | 0x00000007: reflect.TypeOf(ReplaceSM{}), // see SMPP v5, section 4.5.3.1 (104p) 39 | 0x80000007: reflect.TypeOf(ReplaceSMResp{}), // see SMPP v5, section 4.5.3.2 (106p) 40 | 0x00000111: reflect.TypeOf(QueryBroadcastSM{}), // see SMPP v5, section 4.6.1.1 (107p) 41 | 0x80000111: reflect.TypeOf(QueryBroadcastSMResp{}), // see SMPP v5, section 4.6.1.3 (108p) 42 | 0x00000113: reflect.TypeOf(CancelBroadcastSM{}), // see SMPP v5, section 4.6.2.1 (110p) 43 | 0x80000113: reflect.TypeOf(CancelBroadcastSMResp{}), // see SMPP v5, section 4.6.2.3 (112p) 44 | } 45 | 46 | func toCommandIDName(name string) string { 47 | isUpper := unicode.IsUpper 48 | toLower := unicode.ToLower 49 | var b strings.Builder 50 | for i, r := range strings.ReplaceAll(name, "SM", "Sm") { 51 | if i > 0 && isUpper(r) { 52 | b.WriteRune('_') 53 | } 54 | b.WriteRune(toLower(r)) 55 | } 56 | return b.String() 57 | } 58 | 59 | func (c CommandID) String() string { 60 | if t, ok := types[c]; ok { 61 | return toCommandIDName(t.Name()) 62 | } 63 | return fmt.Sprintf("%08X", uint32(c)) 64 | } 65 | -------------------------------------------------------------------------------- /pdu/factory_test.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | //goland:noinspection SpellCheckingInspection 10 | func TestCommandID(t *testing.T) { 11 | mapping := map[string]string{ 12 | "SubmitSM": "submit_sm", 13 | "SubmitSMResp": "submit_sm_resp", 14 | } 15 | for input, output := range mapping { 16 | require.Equal(t, output, toCommandIDName(input)) 17 | } 18 | require.Equal(t, CommandID(0x00000004).String(), "submit_sm") 19 | require.Equal(t, CommandID(0xFFFFFFFF).String(), "FFFFFFFF") 20 | } 21 | 22 | //goland:noinspection SpellCheckingInspection 23 | func TestCommandStatus(t *testing.T) { 24 | require.Equal(t, CommandStatus(0x00000000).String(), "ESME_ROK") 25 | require.Equal(t, CommandStatus(0xFFFFFFFF).String(), "FFFFFFFF") 26 | } 27 | -------------------------------------------------------------------------------- /pdu/header.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | ) 7 | 8 | // CommandID see SMPP v5, section 4.7.5 (115p) 9 | type CommandID uint32 10 | 11 | // CommandStatus see SMPP v5, section 4.7.6 (116p) 12 | type CommandStatus uint32 13 | 14 | type Header struct { 15 | CommandLength uint32 16 | CommandID CommandID 17 | CommandStatus CommandStatus 18 | Sequence int32 19 | } 20 | 21 | func readHeaderFrom(r io.Reader, header *Header) (err error) { 22 | err = binary.Read(r, binary.BigEndian, header) 23 | if err == nil && (header.CommandLength < 16 || header.CommandLength > 0x10000) { 24 | err = ErrInvalidCommandLength 25 | } 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /pdu/header_kit.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func ReadSequence(packet any) int32 { 8 | if h := getHeader(packet); h != nil { 9 | return h.Sequence 10 | } 11 | return 0 12 | } 13 | 14 | func WriteSequence(packet any, sequence int32) { 15 | if h := getHeader(packet); h != nil { 16 | h.Sequence = sequence 17 | } 18 | } 19 | 20 | func ReadCommandStatus(packet any) CommandStatus { 21 | if h := getHeader(packet); h != nil { 22 | return h.CommandStatus 23 | } 24 | return 0 25 | } 26 | 27 | func getHeader(packet any) *Header { 28 | p := reflect.ValueOf(packet) 29 | if p.Kind() == reflect.Ptr { 30 | p = p.Elem() 31 | } 32 | for i := 0; i < p.NumField(); i++ { 33 | field := p.Field(i) 34 | if h, ok := field.Addr().Interface().(*Header); ok { 35 | return h 36 | } 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /pdu/header_names.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | //goland:noinspection SpellCheckingInspection 9 | var commandStatusNames = map[CommandStatus]string{ 10 | 0x000: "ok", 11 | 0x001: "invmsglen", 12 | 0x002: "invcmdlen", 13 | 0x003: "invcmdid", 14 | 0x004: "invbndsts", 15 | 0x005: "alybnd", 16 | 0x006: "invprtflg", 17 | 0x007: "invregdlvflg", 18 | 0x008: "syserr", 19 | 0x00A: "invsrcadr", 20 | 0x00B: "invdstadr", 21 | 0x00C: "invmsgid", 22 | 0x00D: "bindfail", 23 | 0x00E: "invpaswd", 24 | 0x00F: "invsysid", 25 | 0x011: "cancelfail", 26 | 0x013: "replacefail", 27 | 0x014: "msgqful", 28 | 0x015: "invsertyp", 29 | 0x033: "invnumdests", 30 | 0x034: "invdlname", 31 | 0x040: "invdestflag", 32 | 0x042: "invsubrep", 33 | 0x043: "invesmclass", 34 | 0x044: "cntsubdl", 35 | 0x045: "submitfail", 36 | 0x048: "invsrcton", 37 | 0x049: "invsrcnpi", 38 | 0x050: "invdstton", 39 | 0x051: "invdstnpi", 40 | 0x053: "invsystyp", 41 | 0x054: "invrepflag", 42 | 0x055: "invnummsgs", 43 | 0x058: "throttled", 44 | 0x061: "invsched", 45 | 0x062: "invexpiry", 46 | 0x063: "invdftmsgid", 47 | 0x064: "x_t_appn", 48 | 0x065: "x_p_appn", 49 | 0x066: "x_r_appn", 50 | 0x067: "queryfail", 51 | 0x0C0: "invtlvstream", 52 | 0x0C1: "tlvnotallwd", 53 | 0x0C2: "invtlvlen", 54 | 0x0C3: "missingtlv", 55 | 0x0C4: "invtlvval", 56 | 0x0FE: "deliveryfailure", 57 | 0x0FF: "unknownerr", 58 | 0x100: "sertypunauth", 59 | 0x101: "prohibited", 60 | 0x102: "sertypunavail", 61 | 0x103: "sertypdenied", 62 | 0x104: "invdcs", 63 | 0x105: "invsrcaddrsubunit", 64 | 0x106: "invdstaddrsubunit", 65 | 0x107: "invbcastfreqint", 66 | 0x108: "invbcastalias_name", 67 | 0x109: "invbcastareafmt", 68 | 0x10A: "invnumbcast_areas", 69 | 0x10B: "invbcastcnttype", 70 | 0x10C: "invbcastmsgclass", 71 | 0x10D: "bcastfail", 72 | 0x10E: "bcastqueryfail", 73 | 0x10F: "bcastcancelfail", 74 | 0x110: "invbcast_rep", 75 | 0x111: "invbcastsrvgrp", 76 | 0x112: "invbcastchanind", 77 | } 78 | 79 | func (c CommandStatus) String() string { 80 | if name, ok := commandStatusNames[c]; ok { 81 | return fmt.Sprintf("ESME_R%s", strings.ToUpper(name)) 82 | } 83 | return fmt.Sprintf("%08X", uint32(c)) 84 | } 85 | 86 | func (c CommandStatus) Error() string { 87 | return c.String() 88 | } 89 | -------------------------------------------------------------------------------- /pdu/header_test.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | //goland:noinspection SpellCheckingInspection 12 | func TestHeader(t *testing.T) { 13 | expectedList := map[string]Header{ 14 | "00000010000000150000000000000007": { 15 | CommandLength: 16, 16 | CommandID: 0x00000015, 17 | Sequence: 7, 18 | }, 19 | "000000100000FFFF0000FFFF0000FFFF": { 20 | CommandLength: 16, 21 | CommandID: 0x0000FFFF, 22 | CommandStatus: 0x0000FFFF, 23 | Sequence: 0x0000FFFF, 24 | }, 25 | } 26 | var header Header 27 | for packet, expected := range expectedList { 28 | decoded, _ := hex.DecodeString(packet) 29 | err := readHeaderFrom(bytes.NewReader(decoded), &header) 30 | require.NoError(t, err) 31 | require.Equal(t, expected, header) 32 | } 33 | errorList := []string{ 34 | "00000000000000000000000000000000", 35 | "00010001000000000000000000000000", 36 | } 37 | for _, packet := range errorList { 38 | decoded, _ := hex.DecodeString(packet) 39 | err := readHeaderFrom(bytes.NewReader(decoded), &header) 40 | require.Error(t, err) 41 | } 42 | } 43 | 44 | func TestSequence(t *testing.T) { 45 | ReadSequence(new(struct{})) 46 | ReadSequence(&DeliverSM{}) 47 | _ = ReadCommandStatus(new(struct{})) 48 | _ = ReadCommandStatus(&DeliverSM{}) 49 | WriteSequence(&DeliverSM{}, 0) 50 | } 51 | -------------------------------------------------------------------------------- /pdu/interface_version.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // InterfaceVersion see SMPP v5, section 4.7.13 (126p) 9 | type InterfaceVersion byte 10 | 11 | const ( 12 | SMPPVersion33 InterfaceVersion = 0x33 13 | SMPPVersion34 InterfaceVersion = 0x34 14 | SMPPVersion50 InterfaceVersion = 0x50 15 | ) 16 | 17 | func (v InterfaceVersion) String() string { 18 | major := (v >> 4) & 0b1111 19 | minor := v & 0b1111 20 | return fmt.Sprintf("%d.%d", major, minor) 21 | } 22 | 23 | func (v InterfaceVersion) MarshalJSON() (data []byte, err error) { 24 | return json.Marshal(v.String()) 25 | } 26 | 27 | func (v *InterfaceVersion) UnmarshalJSON(data []byte) (err error) { 28 | var value string 29 | var major, minor InterfaceVersion 30 | if err = json.Unmarshal(data, &value); err != nil { 31 | return 32 | } 33 | if _, err = fmt.Sscanf(value, "%d.%d", &major, &minor); err != nil { 34 | return 35 | } 36 | *v = ((major & 0b1111) << 4) | (minor & 0b1111) 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /pdu/interface_version_test.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestInterfaceVersion(t *testing.T) { 11 | samples := map[InterfaceVersion][]byte{ 12 | SMPPVersion33: []byte(`"3.3"`), 13 | SMPPVersion34: []byte(`"3.4"`), 14 | SMPPVersion50: []byte(`"5.0"`), 15 | } 16 | var err error 17 | var version InterfaceVersion 18 | for expected, expectedEncoded := range samples { 19 | err = json.Unmarshal(expectedEncoded, &version) 20 | assert.NoError(t, err) 21 | assert.Equal(t, expected, version) 22 | 23 | encoded, err := json.Marshal(expected) 24 | assert.NoError(t, err) 25 | assert.Equal(t, expectedEncoded, encoded) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /pdu/internal.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | ) 7 | 8 | func readCString(buf *bufio.Reader) (value string, err error) { 9 | value, err = buf.ReadString(0) 10 | if err == nil { 11 | value = value[0 : len(value)-1] 12 | } 13 | return 14 | } 15 | 16 | func writeCString(buf *bytes.Buffer, value string) { 17 | buf.WriteString(value) 18 | buf.WriteByte(0) 19 | } 20 | 21 | func getBool(v bool) byte { 22 | if v { 23 | return 1 24 | } 25 | return 0 26 | } 27 | -------------------------------------------------------------------------------- /pdu/marshal.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "io" 8 | "reflect" 9 | ) 10 | 11 | func unmarshal(r io.Reader, packet any) (n int64, err error) { 12 | buf := bufio.NewReader(r) 13 | v := reflect.ValueOf(packet) 14 | if v.Kind() == reflect.Ptr { 15 | v = v.Elem() 16 | } 17 | for i := 0; i < v.NumField(); i++ { 18 | switch field := v.Field(i); field.Kind() { 19 | case reflect.String: 20 | var value string 21 | if value, err = readCString(buf); err == nil { 22 | field.SetString(value) 23 | } 24 | case reflect.Uint8: 25 | var value byte 26 | if value, err = buf.ReadByte(); err == nil { 27 | field.SetUint(uint64(value)) 28 | } 29 | case reflect.Bool: 30 | var value byte 31 | if value, err = buf.ReadByte(); err == nil { 32 | field.SetBool(value == 1) 33 | } 34 | case reflect.Array, reflect.Map, reflect.Slice, reflect.Struct: 35 | switch v := (field.Addr().Interface()).(type) { 36 | case *Header: 37 | err = readHeaderFrom(buf, v) 38 | if v.CommandStatus != 0 { 39 | return 40 | } 41 | case io.ByteWriter: 42 | var value byte 43 | if value, err = buf.ReadByte(); err == nil { 44 | err = v.WriteByte(value) 45 | } 46 | case io.ReaderFrom: 47 | if m, ok := v.(*ShortMessage); ok { 48 | m.Prepare(packet) 49 | } 50 | _, err = v.ReadFrom(buf) 51 | } 52 | } 53 | n = int64(buf.Size()) 54 | if err != nil { 55 | err = ErrUnmarshalPDUFailed 56 | return 57 | } 58 | } 59 | return 60 | } 61 | 62 | func Marshal(w io.Writer, packet any) (n int64, err error) { 63 | var buf bytes.Buffer 64 | p := reflect.ValueOf(packet) 65 | if p.Kind() == reflect.Ptr { 66 | p = p.Elem() 67 | } 68 | for i := 0; i < p.NumField(); i++ { 69 | field := p.Field(i) 70 | switch field.Kind() { 71 | case reflect.String: 72 | writeCString(&buf, field.String()) 73 | case reflect.Uint8: 74 | buf.WriteByte(byte(field.Uint())) 75 | case reflect.Bool: 76 | var value byte 77 | if field.Bool() { 78 | value = 1 79 | } 80 | buf.WriteByte(value) 81 | case reflect.Array, reflect.Map, reflect.Slice, reflect.Struct: 82 | switch v := field.Addr().Interface().(type) { 83 | case *Header: 84 | v.CommandID = findCommandID(p.Type()) 85 | if err == nil && v.Sequence > 0 { 86 | _ = binary.Write(&buf, binary.BigEndian, v) 87 | } else { 88 | err = ErrInvalidSequence 89 | } 90 | if v.CommandStatus != 0 { 91 | goto write 92 | } 93 | case io.ByteReader: 94 | var value byte 95 | value, err = v.ReadByte() 96 | buf.WriteByte(value) 97 | case io.WriterTo: 98 | if m, ok := v.(*ShortMessage); ok { 99 | m.Prepare(packet) 100 | } 101 | _, err = v.WriteTo(&buf) 102 | } 103 | } 104 | if err != nil { 105 | return 106 | } 107 | } 108 | write: 109 | if p.Field(0).Type() == reflect.TypeOf(Header{}) { 110 | data := buf.Bytes() 111 | binary.BigEndian.PutUint32(data[0:4], uint32(buf.Len())) 112 | } 113 | return buf.WriteTo(w) 114 | } 115 | 116 | func findCommandID(target reflect.Type) CommandID { 117 | for id, t := range types { 118 | if target == t { 119 | return id 120 | } 121 | } 122 | panic("unregistered command id") 123 | } 124 | -------------------------------------------------------------------------------- /pdu/marshal_test.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestMarshal(t *testing.T) { 12 | var pdu any 13 | { 14 | pdu = new(DeliverSM) 15 | _, err := unmarshal(bytes.NewReader(nil), pdu) 16 | require.Error(t, err) 17 | } 18 | { 19 | pdu = new(SubmitSMResp) 20 | decoded, err := hex.DecodeString("00000010800000040000000b55104dc7") 21 | require.NoError(t, err) 22 | _, err = unmarshal(bytes.NewReader(decoded), pdu) 23 | require.NoError(t, err) 24 | var buf bytes.Buffer 25 | _, err = Marshal(&buf, pdu) 26 | require.NoError(t, err) 27 | require.Equal(t, decoded, buf.Bytes()) 28 | } 29 | { 30 | pdu = &SubmitMulti{DestAddrList: DestinationAddresses{DistributionList: make([]string, 0x100)}} 31 | var buf bytes.Buffer 32 | _, err := Marshal(&buf, pdu) 33 | require.Error(t, err) 34 | } 35 | { 36 | var buf bytes.Buffer 37 | pdu = &SubmitMulti{Header: Header{Sequence: -1}} 38 | _, err := Marshal(&buf, pdu) 39 | require.Error(t, err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pdu/message.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/hex" 7 | "io" 8 | "reflect" 9 | 10 | . "github.com/M2MGateway/go-smpp/coding" 11 | ) 12 | 13 | type ShortMessage struct { 14 | DefaultMessageID byte // see SMPP v5, section 4.7.27 (134p) 15 | DataCoding DataCoding 16 | UDHeader UserDataHeader 17 | Message []byte 18 | } 19 | 20 | func (p *ShortMessage) ReadFrom(r io.Reader) (n int64, err error) { 21 | buf := bufio.NewReader(r) 22 | if p.DataCoding != NoCoding { 23 | coding, _ := buf.ReadByte() 24 | p.DataCoding = DataCoding(coding) 25 | } 26 | p.DefaultMessageID, err = buf.ReadByte() 27 | if err == nil { 28 | var length byte 29 | if length, err = buf.ReadByte(); err == nil && p.UDHeader != nil { 30 | _, err = p.UDHeader.ReadFrom(buf) 31 | } 32 | if err == nil { 33 | p.Message = make([]byte, length-byte(p.UDHeader.Len())) 34 | _, err = buf.Read(p.Message) 35 | } 36 | } 37 | return 38 | } 39 | 40 | func (p ShortMessage) WriteTo(w io.Writer) (n int64, err error) { 41 | if len(p.Message) > MaxShortMessageLength { 42 | err = ErrShortMessageTooLarge 43 | return 44 | } 45 | var buf bytes.Buffer 46 | if p.DataCoding != NoCoding { 47 | buf.WriteByte(byte(p.DataCoding)) 48 | } 49 | buf.WriteByte(p.DefaultMessageID) 50 | start := buf.Len() 51 | buf.WriteByte(0) 52 | _, err = p.UDHeader.WriteTo(&buf) 53 | if err != nil { 54 | return 55 | } 56 | buf.Write(p.Message) 57 | data := buf.Bytes() 58 | data[start] = byte(len(data) - 1 - start) 59 | return buf.WriteTo(w) 60 | } 61 | 62 | func (p *ShortMessage) Prepare(pdu any) { 63 | if _, ok := pdu.(*ReplaceSM); ok { 64 | p.DataCoding = NoCoding 65 | } else if p.UDHeader == nil { 66 | t := reflect.ValueOf(pdu).Elem() 67 | target := reflect.TypeOf(ESMClass{}) 68 | v := t.FieldByNameFunc(func(name string) bool { return t.FieldByName(name).Type() == target }) 69 | if v.IsValid() && v.Interface().(ESMClass).UDHIndicator { 70 | p.UDHeader = UserDataHeader{} 71 | } 72 | } 73 | } 74 | 75 | func (p *ShortMessage) Parse() (message string, err error) { 76 | encoder := p.DataCoding.Encoding() 77 | if encoder == nil { 78 | message = hex.EncodeToString(p.Message) 79 | return 80 | } 81 | decoded, err := encoder.NewDecoder().Bytes(p.Message) 82 | message = string(decoded) 83 | return 84 | } 85 | 86 | func (p *ShortMessage) Compose(input string) (err error) { 87 | coding := BestCoding(input) 88 | if coding.Splitter().Len(input) > MaxShortMessageLength { 89 | return ErrShortMessageTooLarge 90 | } 91 | message, err := coding.Encoding().NewEncoder().Bytes([]byte(input)) 92 | if err == nil { 93 | p.DataCoding = coding 94 | p.Message = message 95 | } 96 | return 97 | } 98 | -------------------------------------------------------------------------------- /pdu/message_multipart.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "fmt" 5 | 6 | . "github.com/M2MGateway/go-smpp/coding" 7 | ) 8 | 9 | func ComposeMultipartShortMessage(input string, coding DataCoding, reference uint16) (parts []ShortMessage, err error) { 10 | if coding.Splitter() == nil || coding.Encoding() == nil { 11 | err = ErrUnknownDataCoding 12 | return 13 | } else if coding.Splitter().Len(input) <= MaxShortMessageLength { 14 | var m ShortMessage 15 | m.DataCoding = coding 16 | m.Message, err = coding.Encoding().NewEncoder().Bytes([]byte(input)) 17 | parts = []ShortMessage{m} 18 | return 19 | } 20 | header := ConcatenatedHeader{Reference: reference} 21 | segments := coding.Splitter().Split(input, MaxShortMessageLength-1-header.Len()) 22 | if len(segments) > 0xFE { 23 | err = ErrMultipartTooMuch 24 | return 25 | } 26 | header.TotalParts = byte(len(segments)) 27 | encoder := coding.Encoding().NewEncoder() 28 | part := ShortMessage{DataCoding: coding} 29 | for _, segment := range segments { 30 | encoder.Reset() 31 | part.UDHeader = make(UserDataHeader) 32 | if part.Message, err = encoder.Bytes([]byte(segment)); err != nil { 33 | return 34 | } 35 | header.Sequence++ 36 | header.Set(part.UDHeader) 37 | parts = append(parts, part) 38 | } 39 | return 40 | } 41 | 42 | func CombineMultipartDeliverSM(on func([]*DeliverSM)) func(*DeliverSM) { 43 | registry := make(map[string][]*DeliverSM) 44 | isDone := func(id string, total byte) bool { 45 | for _, sm := range registry[id] { 46 | if sm != nil { 47 | total-- 48 | } 49 | } 50 | return total == 0 51 | } 52 | return func(p *DeliverSM) { 53 | header := p.Message.UDHeader.ConcatenatedHeader() 54 | if header == nil { 55 | on([]*DeliverSM{p}) 56 | } else { 57 | id := fmt.Sprint( 58 | p.SourceAddr.TON, p.SourceAddr.NPI, p.SourceAddr.No, 59 | p.DestAddr.TON, p.DestAddr.NPI, p.DestAddr.No, 60 | header.Reference, 61 | ) 62 | if _, ok := registry[id]; !ok { 63 | registry[id] = make([]*DeliverSM, header.TotalParts) 64 | } 65 | registry[id][header.Sequence-1] = p 66 | if isDone(id, header.TotalParts) { 67 | on(registry[id]) 68 | delete(registry, id) 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pdu/message_state.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | // MessageState see SMPP v5, section 4.7.15 (127p) 9 | type MessageState byte 10 | 11 | //goland:noinspection SpellCheckingInspection 12 | var messageStateMap = []string{ 13 | "scheduled", 14 | "enroute", 15 | "delivered", 16 | "expired", 17 | "deleted", 18 | "undeliverable", 19 | "accepted", 20 | "unknown", 21 | "rejected", 22 | "skipped", 23 | } 24 | 25 | func (m MessageState) String() string { 26 | if int(m) > len(messageStateMap) { 27 | return strconv.Itoa(int(m)) 28 | } 29 | return strings.ToUpper(messageStateMap[m]) 30 | } 31 | -------------------------------------------------------------------------------- /pdu/message_test.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/M2MGateway/go-smpp/coding" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestShortMessage(t *testing.T) { 13 | var buf bytes.Buffer 14 | var message ShortMessage 15 | err := message.Compose("abc") 16 | require.NoError(t, err) 17 | _, err = message.Parse() 18 | require.NoError(t, err) 19 | err = message.Compose(strings.Repeat("abc", 54)) 20 | require.Error(t, err) 21 | 22 | header := ConcatenatedHeader{Reference: 1, TotalParts: 1, Sequence: 1} 23 | 24 | message.Message = make([]byte, 100) 25 | message.UDHeader = make(UserDataHeader) 26 | header.Set(message.UDHeader) 27 | _, err = message.WriteTo(&buf) 28 | require.NoError(t, err) 29 | 30 | message.UDHeader[0x00] = make([]byte, 0x100) 31 | _, err = message.WriteTo(&buf) 32 | require.Error(t, err) 33 | 34 | message.Message = make([]byte, MaxShortMessageLength+1) 35 | message.UDHeader = nil 36 | _, err = message.WriteTo(&buf) 37 | require.Error(t, err) 38 | 39 | message.DataCoding = coding.NoCoding 40 | parsed, err := message.Parse() 41 | require.NoError(t, err) 42 | require.NotEmpty(t, parsed) 43 | } 44 | 45 | func TestMessageState_String(t *testing.T) { 46 | require.Equal(t, "SCHEDULED", MessageState(0).String()) 47 | require.Equal(t, "255", MessageState(0xFF).String()) 48 | } 49 | 50 | //goland:noinspection SpellCheckingInspection 51 | func TestComposeMultipartShortMessage(t *testing.T) { 52 | reference := uint16(0xFFFF) 53 | input := "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmno" + 54 | "pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcde" + 55 | "fghijklmnopqrstuvwxyz1234123456789" 56 | expected := []ShortMessage{ 57 | { 58 | Message: []byte(input[:133]), 59 | UDHeader: UserDataHeader{0x08: []byte{0xFF, 0xFF, 0x02, 0x01}}, 60 | DataCoding: coding.Latin1Coding, 61 | }, 62 | { 63 | Message: []byte(input[133:]), 64 | UDHeader: UserDataHeader{0x08: []byte{0xFF, 0xFF, 0x02, 0x02}}, 65 | DataCoding: coding.Latin1Coding, 66 | }, 67 | } 68 | messages, err := ComposeMultipartShortMessage(input, coding.Latin1Coding, reference) 69 | require.NoError(t, err) 70 | require.Equal(t, expected, messages) 71 | 72 | input = input[:10] 73 | expected = []ShortMessage{ 74 | {Message: []byte(input), DataCoding: coding.Latin1Coding}, 75 | } 76 | messages, err = ComposeMultipartShortMessage(input, coding.Latin1Coding, reference) 77 | require.NoError(t, err) 78 | require.Equal(t, expected, messages) 79 | } 80 | 81 | func TestComposeMultipartShortMessage_Error(t *testing.T) { 82 | input := make([]byte, 134*256) 83 | _, err := ComposeMultipartShortMessage(string(input), coding.NoCoding, 1) 84 | require.Error(t, err) 85 | _, err = ComposeMultipartShortMessage(string(input), coding.ASCIICoding, 1) 86 | require.Error(t, err) 87 | _, err = ComposeMultipartShortMessage(strings.Repeat("\xFF", 1000), coding.ASCIICoding, 1) 88 | require.Error(t, err) 89 | } 90 | 91 | func TestCombineMultipartDeliverSM(t *testing.T) { 92 | addDeliverSM := CombineMultipartDeliverSM(func([]*DeliverSM) {}) 93 | addDeliverSM(&DeliverSM{ 94 | Message: ShortMessage{Message: []byte(""), DataCoding: coding.Latin1Coding}, 95 | }) 96 | for i := 1; i < 3; i++ { 97 | header := UserDataHeader{0x08: []byte{0xFF, 0xFF, 0x02, byte(i)}} 98 | addDeliverSM(&DeliverSM{ 99 | Message: ShortMessage{Message: []byte(""), UDHeader: header, DataCoding: coding.Latin1Coding}, 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pdu/packet.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import . "github.com/M2MGateway/go-smpp/coding" 4 | 5 | type Responsable interface { 6 | Resp() any 7 | } 8 | 9 | // AlertNotification see SMPP v5, section 4.1.3.1 (64p) 10 | type AlertNotification struct { 11 | Header Header 12 | SourceAddr Address 13 | ESMEAddr Address 14 | Tags Tags 15 | } 16 | 17 | // BindReceiver see SMPP v5, section 4.1.1.3 (58p) 18 | type BindReceiver struct { 19 | Header Header 20 | SystemID string 21 | Password string 22 | SystemType string 23 | Version InterfaceVersion 24 | AddressRange Address // see section 4.7.3.1 25 | } 26 | 27 | func (p *BindReceiver) Resp() any { 28 | return &BindReceiverResp{Header: Header{Sequence: p.Header.Sequence}, SystemID: p.SystemID} 29 | } 30 | 31 | // BindReceiverResp see SMPP v5, section 4.1.1.4 (59p) 32 | type BindReceiverResp struct { 33 | Header Header 34 | SystemID string 35 | Tags Tags 36 | } 37 | 38 | // BindTransceiver see SMPP v5, section 4.1.1.5 (59p) 39 | type BindTransceiver struct { 40 | Header Header 41 | SystemID string 42 | Password string 43 | SystemType string 44 | Version InterfaceVersion 45 | AddressRange Address // see section 4.7.3.1 46 | } 47 | 48 | func (p *BindTransceiver) Resp() any { 49 | return &BindTransceiverResp{Header: Header{Sequence: p.Header.Sequence}, SystemID: p.SystemID} 50 | } 51 | 52 | // BindTransceiverResp see SMPP v5, section 4.1.1.6 (60p) 53 | type BindTransceiverResp struct { 54 | Header Header 55 | SystemID string 56 | Tags Tags 57 | } 58 | 59 | // BindTransmitter see SMPP v5, section 4.1.1.1 (56p) 60 | type BindTransmitter struct { 61 | Header Header 62 | SystemID string 63 | Password string 64 | SystemType string 65 | Version InterfaceVersion 66 | AddressRange Address // see section 4.7.3.1 67 | } 68 | 69 | func (p *BindTransmitter) Resp() any { 70 | return &BindTransmitterResp{Header: Header{Sequence: p.Header.Sequence}, SystemID: p.SystemID} 71 | } 72 | 73 | // BindTransmitterResp see SMPP v5, section 4.1.1.2 (57p) 74 | type BindTransmitterResp struct { 75 | Header Header 76 | SystemID string 77 | Tags Tags 78 | } 79 | 80 | // BroadcastSM see SMPP v5, section 4.4.1.1 (92p) 81 | type BroadcastSM struct { 82 | Header Header 83 | ServiceType string 84 | SourceAddr Address 85 | MessageID string 86 | PriorityFlag byte 87 | ScheduleDeliveryTime string 88 | ValidityPeriod string 89 | ReplaceIfPresent bool 90 | DataCoding DataCoding 91 | DefaultMessageID byte 92 | Tags Tags 93 | } 94 | 95 | func (p *BroadcastSM) Resp() any { 96 | return &BroadcastSMResp{Header: Header{Sequence: p.Header.Sequence}, MessageID: p.MessageID} 97 | } 98 | 99 | // BroadcastSMResp see SMPP v5, section 4.4.1.2 (96p) 100 | type BroadcastSMResp struct { 101 | Header Header 102 | MessageID string 103 | Tags Tags 104 | } 105 | 106 | // CancelBroadcastSM see SMPP v5, section 4.6.2.1 (110p) 107 | type CancelBroadcastSM struct { 108 | Header Header 109 | ServiceType string 110 | MessageID string 111 | SourceAddr Address 112 | Tags Tags 113 | } 114 | 115 | func (p *CancelBroadcastSM) Resp() any { 116 | return &CancelBroadcastSMResp{Header: Header{Sequence: p.Header.Sequence}} 117 | } 118 | 119 | // CancelBroadcastSMResp see SMPP v5, section 4.6.2.3 (112p) 120 | type CancelBroadcastSMResp struct { 121 | Header Header 122 | } 123 | 124 | // CancelSM see SMPP v5, section 4.5.1.1 (100p) 125 | type CancelSM struct { 126 | Header Header 127 | ServiceType string 128 | MessageID string 129 | SourceAddr Address 130 | DestAddr Address 131 | } 132 | 133 | func (p *CancelSM) Resp() any { 134 | return &CancelSMResp{Header: Header{Sequence: p.Header.Sequence}} 135 | } 136 | 137 | // CancelSMResp see SMPP v5, section 4.5.1.2 (101p) 138 | type CancelSMResp struct { 139 | Header Header 140 | } 141 | 142 | // DataSM see SMPP v5, section 4.2.2.1 (69p) 143 | type DataSM struct { 144 | Header Header 145 | ServiceType string 146 | SourceAddr Address 147 | DestAddr Address 148 | ESMClass ESMClass 149 | RegisteredDelivery RegisteredDelivery 150 | DataCoding DataCoding 151 | Tags Tags 152 | } 153 | 154 | func (p *DataSM) Resp() any { 155 | return &DataSMResp{Header: Header{Sequence: p.Header.Sequence}} 156 | } 157 | 158 | // DataSMResp see SMPP v5, section 4.2.2.2 (70p) 159 | type DataSMResp struct { 160 | Header Header 161 | MessageID string 162 | Tags Tags 163 | } 164 | 165 | // DeliverSM see SMPP v5, section 4.3.1.1 (85p) 166 | type DeliverSM struct { 167 | Header Header 168 | ServiceType string 169 | SourceAddr Address 170 | DestAddr Address 171 | ESMClass ESMClass 172 | ProtocolID byte 173 | PriorityFlag byte 174 | ScheduleDeliveryTime string 175 | ValidityPeriod string 176 | RegisteredDelivery RegisteredDelivery 177 | ReplaceIfPresent bool 178 | Message ShortMessage 179 | Tags Tags 180 | } 181 | 182 | func (p *DeliverSM) Resp() any { 183 | return &DeliverSMResp{Header: Header{Sequence: p.Header.Sequence}} 184 | } 185 | 186 | // DeliverSMResp see SMPP v5, section 4.3.1.1 (87p) 187 | type DeliverSMResp struct { 188 | Header Header 189 | MessageID string 190 | Tags Tags 191 | } 192 | 193 | // EnquireLink see SMPP v5, section 4.1.2.1 (63p) 194 | type EnquireLink struct { 195 | Header Header 196 | Tags Tags 197 | } 198 | 199 | func (p *EnquireLink) Resp() any { 200 | return &EnquireLinkResp{Header: Header{Sequence: p.Header.Sequence}} 201 | } 202 | 203 | // EnquireLinkResp see SMPP v5, section 4.1.2.2 (63p) 204 | type EnquireLinkResp struct { 205 | Header Header 206 | } 207 | 208 | // GenericNACK see SMPP v5, section 4.1.4.1 (65p) 209 | type GenericNACK struct { 210 | Header Header 211 | Tags Tags 212 | } 213 | 214 | // Outbind see SMPP v5, section 4.1.1.7 (61p) 215 | type Outbind struct { 216 | Header Header 217 | SystemID string 218 | Password string 219 | } 220 | 221 | // QueryBroadcastSM see SMPP v5, section 4.6.1.1 (107p) 222 | type QueryBroadcastSM struct { 223 | Header Header 224 | MessageID string 225 | SourceAddr Address 226 | Tags Tags 227 | } 228 | 229 | func (p *QueryBroadcastSM) Resp() any { 230 | return &QueryBroadcastSMResp{Header: Header{Sequence: p.Header.Sequence}, MessageID: p.MessageID} 231 | } 232 | 233 | // QueryBroadcastSMResp see SMPP v5, section 4.6.1.3 (108p) 234 | type QueryBroadcastSMResp struct { 235 | Header Header 236 | MessageID string 237 | Tags Tags 238 | } 239 | 240 | // QuerySM see SMPP v5, section 4.5.2.1 (101p) 241 | type QuerySM struct { 242 | Header Header 243 | MessageID string 244 | SourceAddr Address 245 | } 246 | 247 | func (p *QuerySM) Resp() any { 248 | return &QuerySMResp{Header: Header{Sequence: p.Header.Sequence}} 249 | } 250 | 251 | // QuerySMResp see SMPP v5, section 4.5.2.2 (103p) 252 | type QuerySMResp struct { 253 | Header Header 254 | MessageID string 255 | FinalDate string 256 | MessageState MessageState 257 | ErrorCode CommandStatus 258 | } 259 | 260 | // ReplaceSM see SMPP v5, section 4.5.3.1 (104p) 261 | type ReplaceSM struct { 262 | Header Header 263 | MessageID string 264 | SourceAddr Address 265 | ScheduleDeliveryTime string 266 | ValidityPeriod string 267 | RegisteredDelivery RegisteredDelivery 268 | Message ShortMessage 269 | Tags Tags 270 | } 271 | 272 | func (p *ReplaceSM) Resp() any { 273 | return &ReplaceSMResp{Header: Header{Sequence: p.Header.Sequence}} 274 | } 275 | 276 | // ReplaceSMResp see SMPP v5, section 4.5.3.2 (106p) 277 | type ReplaceSMResp struct { 278 | Header Header 279 | } 280 | 281 | // SubmitMulti see SMPP v5, section 4.2.3.1 (71p) 282 | type SubmitMulti struct { 283 | Header Header 284 | ServiceType string 285 | SourceAddr Address 286 | DestAddrList DestinationAddresses 287 | ESMClass ESMClass 288 | ProtocolID byte 289 | PriorityFlag byte 290 | ScheduleDeliveryTime string 291 | ValidityPeriod string 292 | RegisteredDelivery RegisteredDelivery 293 | ReplaceIfPresent bool 294 | Message ShortMessage 295 | Tags Tags 296 | } 297 | 298 | func (p *SubmitMulti) Resp() any { 299 | return &SubmitMultiResp{Header: Header{Sequence: p.Header.Sequence}} 300 | } 301 | 302 | // SubmitMultiResp see SMPP v5, section 4.2.3.2 (74p) 303 | type SubmitMultiResp struct { 304 | Header Header 305 | MessageID string 306 | UnsuccessfulSMEs UnsuccessfulRecords 307 | Tags Tags 308 | } 309 | 310 | // SubmitSM see SMPP v5, section 4.2.1.1 (66p) 311 | type SubmitSM struct { 312 | Header Header 313 | ServiceType string 314 | SourceAddr Address 315 | DestAddr Address 316 | ESMClass ESMClass 317 | ProtocolID byte 318 | PriorityFlag byte 319 | ScheduleDeliveryTime string 320 | ValidityPeriod string 321 | RegisteredDelivery RegisteredDelivery 322 | ReplaceIfPresent bool 323 | Message ShortMessage 324 | Tags Tags 325 | } 326 | 327 | func (p *SubmitSM) Resp() any { 328 | return &SubmitSMResp{Header: Header{Sequence: p.Header.Sequence}} 329 | } 330 | 331 | // SubmitSMResp see SMPP v5, section 4.2.1.2 (68p) 332 | type SubmitSMResp struct { 333 | Header Header 334 | MessageID string 335 | } 336 | 337 | // Unbind see SMPP v5, section 4.1.1.8 (61p) 338 | type Unbind struct { 339 | Header Header 340 | } 341 | 342 | func (p *Unbind) Resp() any { 343 | return &UnbindResp{Header: Header{Sequence: p.Header.Sequence}} 344 | } 345 | 346 | // UnbindResp see SMPP v5, section 4.1.1.9 (62p) 347 | type UnbindResp struct { 348 | Header Header 349 | } 350 | -------------------------------------------------------------------------------- /pdu/packet_test.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "testing" 7 | 8 | . "github.com/M2MGateway/go-smpp/coding" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | //goland:noinspection SpellCheckingInspection 13 | var ( 14 | alice = Address{TON: 13, NPI: 15, No: "Alice"} 15 | bob = Address{TON: 19, NPI: 7, No: "Bob"} 16 | empty = Address{TON: 23, NPI: 101, No: "empty"} 17 | ) 18 | 19 | //goland:noinspection SpellCheckingInspection 20 | var mapping = []struct { 21 | Packet string 22 | Expected any 23 | Response any 24 | ResponsePacket string 25 | }{ 26 | { 27 | Packet: "0000003600000001000000000000000D73797374656D5F69645F66616B650070617373776F7264006F6E6C7900500D0F416C69636500", 28 | Expected: &BindReceiver{ 29 | Header: Header{54, 0x00000001, 0, 13}, 30 | SystemID: "system_id_fake", 31 | Password: "password", 32 | SystemType: "only", 33 | Version: SMPPVersion50, 34 | AddressRange: alice, 35 | }, 36 | Response: &BindReceiverResp{ 37 | Header: Header{31, 0x80000001, 0, 13}, 38 | SystemID: "system_id_fake", 39 | }, 40 | ResponsePacket: "0000001F80000001000000000000000D73797374656D5F69645F66616B6500", 41 | }, 42 | { 43 | Packet: "00000024000000090000000000000001706F72742D31006D616E61676564000034000000", 44 | Expected: &BindTransceiver{ 45 | Header: Header{36, 0x00000009, 0, 1}, 46 | SystemID: "port-1", 47 | Password: "managed", 48 | Version: SMPPVersion34, 49 | }, 50 | Response: &BindTransceiverResp{ 51 | Header: Header{23, 0x80000009, 0, 1}, 52 | SystemID: "port-1", 53 | }, 54 | ResponsePacket: "00000017800000090000000000000001706F72742D3100", 55 | }, 56 | { 57 | Packet: "0000003600000002000000000000000D73797374656D5F69645F66616B650070617373776F7264006F6E6C7900501765656D70747900", 58 | Expected: &BindTransmitter{ 59 | Header: Header{54, 0x00000002, 0, 13}, 60 | SystemID: "system_id_fake", 61 | Password: "password", 62 | SystemType: "only", 63 | Version: SMPPVersion50, 64 | AddressRange: empty, 65 | }, 66 | Response: &BindTransmitterResp{ 67 | Header: Header{31, 0x80000002, 0, 13}, 68 | SystemID: "system_id_fake", 69 | }, 70 | ResponsePacket: "0000001F80000002000000000000000D73797374656D5F69645F66616B6500", 71 | }, 72 | { 73 | Packet: "0000003200000112000000000000000D58585800010138363133383030313338303030006578616D706C6500000000010000", 74 | Expected: &BroadcastSM{ 75 | Header: Header{50, 0x00000112, 0, 13}, 76 | ServiceType: "XXX", 77 | SourceAddr: Address{1, 1, "8613800138000"}, 78 | MessageID: "example", 79 | ReplaceIfPresent: true, 80 | DataCoding: GSM7BitCoding, 81 | }, 82 | Response: &BroadcastSMResp{ 83 | Header: Header{24, 0x80000112, 0, 13}, 84 | MessageID: "example", 85 | }, 86 | ResponsePacket: "0000001880000112000000000000000D6578616D706C6500", 87 | }, 88 | { 89 | Packet: "0000002C00000113000000000000000D585858006578616D706C650001013836313338303031333830303000", 90 | Expected: &CancelBroadcastSM{ 91 | Header: Header{44, 0x00000113, 0, 13}, 92 | MessageID: "example", 93 | ServiceType: "XXX", 94 | SourceAddr: Address{1, 1, "8613800138000"}, 95 | }, 96 | Response: &CancelBroadcastSMResp{ 97 | Header: Header{16, 0x80000113, 0, 13}, 98 | }, 99 | ResponsePacket: "0000001080000113000000000000000D", 100 | }, 101 | { 102 | Packet: "0000001E00000102000000000000000D0D0F416C696365001307426F6200", 103 | Expected: &AlertNotification{ 104 | Header: Header{30, 0x00000102, 0, 13}, 105 | SourceAddr: alice, 106 | ESMEAddr: bob, 107 | }, 108 | }, 109 | { 110 | Packet: "0000002300000008000000000000000D58585800000D0F416C696365001307426F6200", 111 | Expected: &CancelSM{ 112 | Header: Header{35, 0x00000008, 0, 13}, 113 | ServiceType: "XXX", 114 | MessageID: "", 115 | SourceAddr: alice, 116 | DestAddr: bob, 117 | }, 118 | Response: &CancelSMResp{ 119 | Header: Header{16, 0x80000008, 0, 13}, 120 | }, 121 | ResponsePacket: "0000001080000008000000000000000D", 122 | }, 123 | { 124 | Packet: "0000001080000008000000000000000D", 125 | Expected: &CancelSMResp{ 126 | Header: Header{16, 0x80000008, 0, 13}, 127 | }, 128 | }, 129 | { 130 | Packet: "0000002A00000103000000000000000D616263000D0F416C696365001307426F62000D135B000700015F", 131 | Expected: &DataSM{ 132 | Header: Header{42, 0x00000103, 0, 13}, 133 | ServiceType: "abc", 134 | SourceAddr: alice, 135 | DestAddr: bob, 136 | ESMClass: ESMClass{MessageType: 3, MessageMode: 1}, 137 | RegisteredDelivery: RegisteredDelivery{MCDeliveryReceipt: 3, IntermediateNotification: true}, 138 | DataCoding: 0b01011011, 139 | Tags: Tags{0x0007: []byte{0x5F}}, 140 | }, 141 | Response: &DataSMResp{Header: Header{17, 0x80000103, 0, 13}}, 142 | ResponsePacket: "0000001180000103000000000000000D00", 143 | }, 144 | { 145 | Packet: "000000BC00000005000000000000000958585800020131303031300002013000400000000000000800920500030503015C0A656C768475286237FF0C60A853EF4EE576F463A556DE590D63074EE48FDB884C4E1A52A167E58BE26216529E7406FF1A007F007F002000300030FF1A624B673A4E0A7F516D4191CF67E58BE2007F007F002000300031FF1A8D2662374F59989D007F007F002000300032FF1A5B9E65F68BDD8D39007F007F002000300033FF1A5E387528529E74064E1A", 146 | Expected: &DeliverSM{ 147 | Header: Header{188, 0x00000005, 0, 9}, 148 | ServiceType: "XXX", 149 | SourceAddr: Address{2, 1, "10010"}, 150 | DestAddr: Address{2, 1, "0"}, 151 | ESMClass: ESMClass{UDHIndicator: true}, 152 | Message: ShortMessage{ 153 | DataCoding: UCS2Coding, 154 | UDHeader: UserDataHeader{0x00: []byte{0x05, 0x03, 0x01}}, 155 | Message: []byte{ 156 | 0x5C, 0x0A, 0x65, 0x6C, 0x76, 0x84, 0x75, 0x28, 0x62, 0x37, 0xFF, 0x0C, 0x60, 0xA8, 0x53, 0xEF, 157 | 0x4E, 0xE5, 0x76, 0xF4, 0x63, 0xA5, 0x56, 0xDE, 0x59, 0x0D, 0x63, 0x07, 0x4E, 0xE4, 0x8F, 0xDB, 158 | 0x88, 0x4C, 0x4E, 0x1A, 0x52, 0xA1, 0x67, 0xE5, 0x8B, 0xE2, 0x62, 0x16, 0x52, 0x9E, 0x74, 0x06, 159 | 0xFF, 0x1A, 0x00, 0x7F, 0x00, 0x7F, 0x00, 0x20, 0x00, 0x30, 0x00, 0x30, 0xFF, 0x1A, 0x62, 0x4B, 160 | 0x67, 0x3A, 0x4E, 0x0A, 0x7F, 0x51, 0x6D, 0x41, 0x91, 0xCF, 0x67, 0xE5, 0x8B, 0xE2, 0x00, 0x7F, 161 | 0x00, 0x7F, 0x00, 0x20, 0x00, 0x30, 0x00, 0x31, 0xFF, 0x1A, 0x8D, 0x26, 0x62, 0x37, 0x4F, 0x59, 162 | 0x98, 0x9D, 0x00, 0x7F, 0x00, 0x7F, 0x00, 0x20, 0x00, 0x30, 0x00, 0x32, 0xFF, 0x1A, 0x5B, 0x9E, 163 | 0x65, 0xF6, 0x8B, 0xDD, 0x8D, 0x39, 0x00, 0x7F, 0x00, 0x7F, 0x00, 0x20, 0x00, 0x30, 0x00, 0x33, 164 | 0xFF, 0x1A, 0x5E, 0x38, 0x75, 0x28, 0x52, 0x9E, 0x74, 0x06, 0x4E, 0x1A, 165 | }, 166 | }, 167 | }, 168 | Response: &DeliverSMResp{Header: Header{17, 0x80000005, 0, 9}}, 169 | ResponsePacket: "0000001180000005000000000000000900", 170 | }, 171 | { 172 | Packet: "00000010000000150000000000000007", 173 | Expected: &EnquireLink{Header: Header{16, 0x00000015, 0, 7}}, 174 | Response: &EnquireLinkResp{Header: Header{16, 0x80000015, 0, 7}}, 175 | ResponsePacket: "00000010800000150000000000000007", 176 | }, 177 | { 178 | Packet: "0000001080000000000000000000000D", 179 | Expected: &GenericNACK{Header: Header{16, 0x80000000, 0, 13}}, 180 | }, 181 | { 182 | Packet: "000000240000000B000000000000000D696E76656E746F7279006970617373776F726400", 183 | Expected: &Outbind{Header: Header{36, 0x0000000B, 0, 13}, SystemID: "inventory", Password: "ipassword"}, 184 | }, 185 | { 186 | Packet: "0000002000000111000000000000000D6578616D706C65000D0F416C69636500", 187 | Expected: &QueryBroadcastSM{ 188 | Header: Header{32, 0x00000111, 0, 13}, 189 | MessageID: "example", 190 | SourceAddr: alice, 191 | }, 192 | Response: &QueryBroadcastSMResp{Header: Header{24, 0x80000111, 0, 13}, MessageID: "example"}, 193 | ResponsePacket: "0000001880000111000000000000000D6578616D706C6500", 194 | }, 195 | { 196 | Packet: "0000001D00000003000000000000000D61776179000D0F416C69636500", 197 | Expected: &QuerySM{Header: Header{29, 0x00000003, 0, 13}, MessageID: "away", SourceAddr: alice}, 198 | Response: &QuerySMResp{Header: Header{19, 0x80000003, 0, 13}}, 199 | ResponsePacket: "0000001380000003000000000000000D000000", 200 | }, 201 | { 202 | Packet: "0000002D00000007000000000000000D49445F486572000D0F416C6963650000001300096E6967687477697368", 203 | Expected: &ReplaceSM{ 204 | Header: Header{45, 0x00000007, 0, 13}, 205 | MessageID: "ID_Her", 206 | SourceAddr: alice, 207 | RegisteredDelivery: RegisteredDelivery{MCDeliveryReceipt: 3, IntermediateNotification: true}, 208 | Message: ShortMessage{Message: []byte("nightwish"), DataCoding: NoCoding}, 209 | }, 210 | Response: &ReplaceSMResp{Header: Header{16, 0x80000007, 0, 13}}, 211 | ResponsePacket: "0000001080000007000000000000000D", 212 | }, 213 | { 214 | Packet: "0000006D00000021000000000000000D585858000D0F416C6963650003010000426F623100024C6973743100024C69737432000D633D00001300080030006E006700681EAF0020006E00670068006900EA006E00670020006E0067006800691EC5006E00670020006E00671EA3", 215 | Expected: &SubmitMulti{ 216 | Header: Header{CommandLength: 109, CommandID: 0x00000021, Sequence: 13}, 217 | ServiceType: "XXX", 218 | SourceAddr: alice, 219 | DestAddrList: DestinationAddresses{Addresses: []Address{{No: "Bob1"}}, DistributionList: []string{"List1", "List2"}}, 220 | ESMClass: ESMClass{MessageType: 3, MessageMode: 1}, 221 | ProtocolID: 99, 222 | PriorityFlag: 61, 223 | RegisteredDelivery: RegisteredDelivery{MCDeliveryReceipt: 3, IntermediateNotification: true}, 224 | Message: ShortMessage{ 225 | DataCoding: UCS2Coding, 226 | Message: []byte{ 227 | 0x00, 0x6E, 0x00, 0x67, 0x00, 0x68, 0x1E, 0xAF, 0x00, 0x20, 0x00, 0x6E, 0x00, 0x67, 0x00, 0x68, 228 | 0x00, 0x69, 0x00, 0xEA, 0x00, 0x6E, 0x00, 0x67, 0x00, 0x20, 0x00, 0x6E, 0x00, 0x67, 0x00, 0x68, 229 | 0x00, 0x69, 0x1E, 0xC5, 0x00, 0x6E, 0x00, 0x67, 0x00, 0x20, 0x00, 0x6E, 0x00, 0x67, 0x1E, 0xA3, 230 | }, 231 | }, 232 | }, 233 | Response: &SubmitMultiResp{Header: Header{18, 0x80000021, 0, 13}, UnsuccessfulSMEs: UnsuccessfulRecords{}}, 234 | ResponsePacket: "0000001280000021000000000000000D0000", 235 | }, 236 | { 237 | Packet: "0000003080000021000000000000000D666F6F7462616C6C00022621426F623100000000130000426F62320000000014", 238 | Expected: &SubmitMultiResp{ 239 | Header: Header{CommandLength: 48, CommandID: 0x80000021, Sequence: 13}, 240 | MessageID: "football", 241 | UnsuccessfulSMEs: UnsuccessfulRecords{ 242 | UnsuccessfulRecord{ 243 | DestAddr: Address{TON: 38, NPI: 33, No: "Bob1"}, 244 | ErrorStatusCode: 19, 245 | }, 246 | UnsuccessfulRecord{ 247 | DestAddr: Address{No: "Bob2"}, 248 | ErrorStatusCode: 20, 249 | }, 250 | }, 251 | }, 252 | }, 253 | { 254 | Packet: "0000005C00000004000000000000000D585858000D0F416C696365001307426F62000D633D00001301080030006E006700681EAF0020006E00670068006900EA006E00670020006E0067006800691EC5006E00670020006E00671EA3", 255 | Expected: &SubmitSM{ 256 | Header: Header{CommandLength: 92, CommandID: 0x00000004, Sequence: 13}, 257 | ServiceType: "XXX", 258 | SourceAddr: alice, 259 | DestAddr: bob, 260 | ESMClass: ESMClass{MessageType: 3, MessageMode: 1}, 261 | ProtocolID: 99, 262 | PriorityFlag: 61, 263 | RegisteredDelivery: RegisteredDelivery{MCDeliveryReceipt: 3, IntermediateNotification: true}, 264 | ReplaceIfPresent: true, 265 | Message: ShortMessage{ 266 | DataCoding: UCS2Coding, 267 | Message: []byte{ 268 | 0x00, 0x6E, 0x00, 0x67, 0x00, 0x68, 0x1E, 0xAF, 0x00, 0x20, 0x00, 0x6E, 0x00, 0x67, 0x00, 0x68, 269 | 0x00, 0x69, 0x00, 0xEA, 0x00, 0x6E, 0x00, 0x67, 0x00, 0x20, 0x00, 0x6E, 0x00, 0x67, 0x00, 0x68, 270 | 0x00, 0x69, 0x1E, 0xC5, 0x00, 0x6E, 0x00, 0x67, 0x00, 0x20, 0x00, 0x6E, 0x00, 0x67, 0x1E, 0xA3, 271 | }, 272 | }, 273 | }, 274 | Response: &SubmitSMResp{Header: Header{17, 0x80000004, 0, 13}}, 275 | ResponsePacket: "0000001180000004000000000000000D00", 276 | }, 277 | { 278 | Packet: "0000001000000006000000000000000D", 279 | Expected: &Unbind{Header: Header{16, 0x00000006, 0, 13}}, 280 | Response: &UnbindResp{Header: Header{16, 0x80000006, 0, 13}}, 281 | ResponsePacket: "0000001080000006000000000000000D", 282 | }, 283 | } 284 | 285 | func TestPacket(t *testing.T) { 286 | for _, sample := range mapping { 287 | decoded, err := hex.DecodeString(sample.Packet) 288 | require.NoError(t, err) 289 | 290 | var buf bytes.Buffer 291 | _, err = Marshal(&buf, sample.Expected) 292 | require.NoError(t, err, sample.Expected) 293 | require.Equal(t, decoded, buf.Bytes(), hex.EncodeToString(buf.Bytes())) 294 | 295 | parsed, err := Unmarshal(bytes.NewReader(decoded)) 296 | require.NoError(t, err, sample.Packet) 297 | require.NotNil(t, parsed) 298 | require.Equal(t, sample.Expected, parsed) 299 | 300 | if resp, ok := sample.Expected.(Responsable); ok { 301 | response := resp.Resp() 302 | require.NotNil(t, response) 303 | 304 | decoded, err = hex.DecodeString(sample.ResponsePacket) 305 | require.NoError(t, err) 306 | 307 | buf.Reset() 308 | _, err = Marshal(&buf, response) 309 | require.NoError(t, err) 310 | require.Equal(t, decoded, buf.Bytes(), hex.EncodeToString(buf.Bytes())) 311 | 312 | parsed, err = Unmarshal(&buf) 313 | require.NoError(t, err, resp) 314 | require.NotNil(t, parsed) 315 | require.Equal(t, sample.Response, parsed) 316 | } 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /pdu/pdu.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "reflect" 7 | ) 8 | 9 | func Unmarshal(r io.Reader) (pdu any, err error) { 10 | var buf bytes.Buffer 11 | r = io.TeeReader(r, &buf) 12 | header := new(Header) 13 | if err = readHeaderFrom(r, header); err != nil { 14 | return 15 | } 16 | if _, err = r.Read(make([]byte, header.CommandLength-16)); err != nil { 17 | err = ErrInvalidCommandLength 18 | } 19 | if t, ok := types[header.CommandID]; !ok { 20 | err = ErrInvalidCommandID 21 | } else { 22 | pdu = reflect.New(t).Interface() 23 | _, err = unmarshal(&buf, pdu) 24 | } 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /pdu/pdu_test.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | //goland:noinspection SpellCheckingInspection 12 | func TestReadPDU(t *testing.T) { 13 | failedList := []string{ 14 | "00000000000000000000000000000000", 15 | "000000100000FFFF0000FFFF0000FFFF", 16 | } 17 | for _, packet := range failedList { 18 | decoded, err := hex.DecodeString(packet) 19 | require.NoError(t, err) 20 | _, err = Unmarshal(bytes.NewReader(decoded)) 21 | require.Error(t, err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pdu/registered_delivery.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import "fmt" 4 | 5 | // RegisteredDelivery see SMPP v5, section 4.7.21 (130p) 6 | type RegisteredDelivery struct { 7 | MCDeliveryReceipt byte // ___ _ __ ** 8 | SMEOriginatedAcknowledgment byte // ___ _ ** __ 9 | IntermediateNotification bool // ___ * __ __ 10 | Reserved byte // *** _ __ __ 11 | } 12 | 13 | func (r RegisteredDelivery) ReadByte() (c byte, err error) { 14 | c |= r.MCDeliveryReceipt & 0b11 15 | c |= r.SMEOriginatedAcknowledgment & 0b11 << 2 16 | c |= getBool(r.IntermediateNotification) << 4 17 | c |= r.Reserved & 0b111 << 5 18 | return 19 | } 20 | 21 | func (r *RegisteredDelivery) WriteByte(c byte) error { 22 | r.MCDeliveryReceipt = c & 0b11 23 | r.SMEOriginatedAcknowledgment = c >> 2 & 0b11 24 | r.IntermediateNotification = c>>4&0b1 == 1 25 | r.Reserved = c >> 5 & 0b111 26 | return nil 27 | } 28 | 29 | func (r RegisteredDelivery) String() string { 30 | c, _ := r.ReadByte() 31 | return fmt.Sprintf("%08b", c) 32 | } 33 | -------------------------------------------------------------------------------- /pdu/registered_delivery_test.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestRegisteredDelivery(t *testing.T) { 10 | expected := byte(0b11110101) 11 | var delivery RegisteredDelivery 12 | _ = delivery.WriteByte(expected) 13 | require.Equal(t, delivery, RegisteredDelivery{ 14 | MCDeliveryReceipt: 1, 15 | SMEOriginatedAcknowledgment: 1, 16 | IntermediateNotification: true, 17 | Reserved: 7, 18 | }) 19 | c, _ := delivery.ReadByte() 20 | require.Equal(t, expected, c) 21 | require.Equal(t, "11110101", delivery.String()) 22 | } 23 | -------------------------------------------------------------------------------- /pdu/tag.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "io" 7 | "sort" 8 | ) 9 | 10 | type Tags map[uint16][]byte 11 | 12 | func (t *Tags) ReadFrom(r io.Reader) (n int64, err error) { 13 | var values [2]uint16 14 | var data []byte 15 | tags := make(Tags) 16 | for { 17 | err = binary.Read(r, binary.BigEndian, values[:]) 18 | if err == nil { 19 | data = make([]byte, values[1]) 20 | _, err = r.Read(data) 21 | } 22 | if err == nil { 23 | tags[values[0]] = data 24 | } 25 | if err == io.EOF { 26 | err = nil 27 | break 28 | } 29 | if err != nil { 30 | break 31 | } 32 | } 33 | if len(tags) > 0 { 34 | *t = tags 35 | } 36 | return 37 | } 38 | 39 | func (t Tags) WriteTo(w io.Writer) (n int64, err error) { 40 | var buf bytes.Buffer 41 | var keys []uint16 42 | for tag := range t { 43 | keys = append(keys, tag) 44 | } 45 | sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] }) 46 | err = ErrInvalidTagLength 47 | for _, tag := range keys { 48 | data := t[tag] 49 | length := len(data) 50 | if length == 0 { 51 | continue 52 | } else if length < 0xFFFF { 53 | _ = binary.Write(&buf, binary.BigEndian, tag) 54 | _ = binary.Write(&buf, binary.BigEndian, uint16(len(data))) 55 | buf.Write(data) 56 | } else { 57 | return 58 | } 59 | } 60 | return buf.WriteTo(w) 61 | } 62 | -------------------------------------------------------------------------------- /pdu/tag_test.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | //goland:noinspection SpellCheckingInspection 11 | func TestTag(t *testing.T) { 12 | tlv := make(Tags) 13 | require.Equal(t, tlv[0x0007], []byte(nil)) 14 | { 15 | _, err := tlv.ReadFrom(bytes.NewReader([]byte{0x00, 0x07, 0x00})) 16 | require.Error(t, err) 17 | } 18 | { 19 | expected := []byte{0x00, 0x07, 0x00, 0x01, 0x5F} 20 | _, err := tlv.ReadFrom(bytes.NewReader([]byte{0x00, 0x07, 0x00, 0x01, 0x5F})) 21 | require.NoError(t, err) 22 | require.Equal(t, tlv, Tags{0x0007: []byte{0x5F}}) 23 | var buf bytes.Buffer 24 | _, err = tlv.WriteTo(&buf) 25 | require.NoError(t, err) 26 | require.Equal(t, expected, buf.Bytes()) 27 | } 28 | { 29 | tlv[0x0007] = []byte{} 30 | var buf bytes.Buffer 31 | _, err := tlv.WriteTo(&buf) 32 | require.NoError(t, err) 33 | require.Equal(t, 0, buf.Len()) 34 | tlv[0x0001] = make([]byte, 1) 35 | tlv[0x0002] = make([]byte, 1) 36 | tlv[0x0003] = make([]byte, 1) 37 | _, err = tlv.WriteTo(&buf) 38 | require.NoError(t, err) 39 | tlv[0x0007] = make([]byte, 0x10000) 40 | _, err = tlv.WriteTo(&buf) 41 | require.Error(t, err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pdu/time.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | // Time see SMPP v5, section 4.7.23.4 (132p) 10 | type Time struct{ time.Time } 11 | 12 | func (t *Time) From(input string) (err error) { 13 | t.Time = time.Time{} 14 | if len(input) == 0 { 15 | return 16 | } 17 | parts, symbol := fromTimeString(input) 18 | if !(symbol == '+' || symbol == '-') { 19 | err = ErrUnparseableTime 20 | return 21 | } 22 | t.Time = time.Date( 23 | int(2000+parts[0]), // year 24 | time.Month(parts[1]), // month 25 | int(parts[2]), // day 26 | int(parts[3]), // hour 27 | int(parts[4]), // minute 28 | int(parts[5]), // second 29 | int(parts[6])*1e8, // tenths of second 30 | time.FixedZone("", int(parts[7]*900)), // timezone offset 31 | ) 32 | return 33 | } 34 | 35 | func (t Time) String() string { 36 | if t.Time.IsZero() { 37 | return "" 38 | } 39 | _, offset := t.Zone() 40 | symbol := '+' 41 | if offset < 0 { 42 | offset = -offset 43 | symbol = '-' 44 | } 45 | return fmt.Sprintf( 46 | "%02d%02d%02d%02d%02d%02d%d%02d%c", 47 | t.Year()-2000, // year 48 | int(t.Month()), // month 49 | t.Day(), // day 50 | t.Hour(), // hour 51 | t.Minute(), // minute 52 | t.Second(), // second 53 | t.Nanosecond()/1e8, // tenths of second 54 | offset/900, // offset 55 | symbol, // time-zone symbol 56 | ) 57 | } 58 | 59 | // Duration see SMPP v5, section 4.7.23.5 (132p) 60 | type Duration struct{ time.Duration } 61 | 62 | func (p *Duration) From(input string) (err error) { 63 | p.Duration = 0 64 | if len(input) == 0 { 65 | return 66 | } 67 | parts, symbol := fromTimeString(input) 68 | if symbol != 'R' { 69 | err = ErrUnparseableTime 70 | return 71 | } 72 | bases := []time.Duration{ 73 | time.Hour * 8760, time.Hour * 720, time.Hour * 24, 74 | time.Hour, time.Minute, time.Second, 1e8, 0, 75 | } 76 | for i, part := range parts { 77 | p.Duration += bases[i] * time.Duration(part) 78 | } 79 | return 80 | } 81 | 82 | func (p Duration) String() string { 83 | if p.Duration < time.Second { 84 | return "" 85 | } 86 | ts := p.Duration 87 | parts := []time.Duration{ 88 | time.Hour * 8760, time.Hour * 720, time.Hour * 24, 89 | time.Hour, time.Minute, time.Second, 90 | } 91 | for i, part := range parts { 92 | parts[i] = ts / part 93 | ts %= part 94 | } 95 | return fmt.Sprintf( 96 | "%02d%02d%02d%02d%02d%02d%d00R", 97 | parts[0], parts[1], parts[2], 98 | parts[3], parts[4], parts[5], 99 | int(ts.Nanoseconds()/1e8), 100 | ) 101 | } 102 | 103 | func fromTimeString(input string) (parts [8]int64, symbol byte) { 104 | if len(input) != 16 { 105 | return 106 | } 107 | for i := 0; i < 12; i += 2 { 108 | parts[i/2], _ = strconv.ParseInt(input[i:i+2], 10, 16) 109 | } 110 | parts[6], _ = strconv.ParseInt(input[12:13], 10, 16) 111 | parts[7], _ = strconv.ParseInt(input[13:15], 10, 16) 112 | symbol = input[15] 113 | if symbol == '-' { 114 | parts[7] = -parts[7] 115 | } 116 | return 117 | } 118 | -------------------------------------------------------------------------------- /pdu/time_test.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestTime(t *testing.T) { 10 | expectedList := map[string]string{ 11 | "": "0001-01-01 00:00:00 +0000 UTC", 12 | "000101000000000+": "2000-01-01 00:00:00 +0000 +0000", 13 | "111019080000704-": "2011-10-19 08:00:00.7 -0100 -0100", 14 | "201020182347832+": "2020-10-20 18:23:47.8 +0800 +0800", 15 | "991231235959948+": "2099-12-31 23:59:59.9 +1200 +1200", 16 | } 17 | var timestamp Time 18 | for expected, formatted := range expectedList { 19 | require.NoError(t, timestamp.From(expected)) 20 | require.Equal(t, formatted, timestamp.Time.String()) 21 | require.Equal(t, expected, timestamp.String()) 22 | } 23 | errorList := []string{ 24 | "000101000000000", 25 | } 26 | for _, input := range errorList { 27 | require.Error(t, timestamp.From(input)) 28 | } 29 | } 30 | 31 | func TestDuration(t *testing.T) { 32 | expectedList := map[string]string{ 33 | "": "0s", 34 | "000007000000000R": "168h0m0s", 35 | "010203040506700R": "10276h5m6.7s", 36 | "991025033429000R": "875043h34m29s", 37 | } 38 | var duration Duration 39 | for expected, unix := range expectedList { 40 | require.NoError(t, duration.From(expected)) 41 | require.Equal(t, unix, duration.Duration.String()) 42 | require.Equal(t, expected, duration.String()) 43 | } 44 | errorList := []string{ 45 | "000101000000000", 46 | } 47 | for _, input := range errorList { 48 | require.Error(t, duration.From(input)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pdu/udh.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "io" 8 | "sort" 9 | ) 10 | 11 | type UserDataHeader map[byte][]byte 12 | 13 | func (h UserDataHeader) Len() (length int) { 14 | if h == nil { 15 | return 16 | } 17 | length = 1 18 | for _, data := range h { 19 | length += 2 20 | length += len(data) 21 | } 22 | return 23 | } 24 | 25 | func (h *UserDataHeader) ReadFrom(r io.Reader) (n int64, err error) { 26 | buf := bufio.NewReader(r) 27 | header := make(UserDataHeader) 28 | length, err := buf.ReadByte() 29 | if err != nil { 30 | return 31 | } 32 | var id byte 33 | var data []byte 34 | for i := 0; i < int(length); { 35 | if id, err = buf.ReadByte(); err == nil { 36 | length, err = buf.ReadByte() 37 | } 38 | if length > 0 { 39 | data = make([]byte, length) 40 | _, err = buf.Read(data) 41 | } 42 | if err == nil { 43 | header[id] = data 44 | } 45 | i = buf.Size() 46 | } 47 | if len(header) > 0 { 48 | *h = header 49 | } 50 | return 51 | } 52 | 53 | func (h UserDataHeader) WriteTo(w io.Writer) (n int64, err error) { 54 | if h == nil { 55 | return 56 | } 57 | var keys []byte 58 | for id := range h { 59 | keys = append(keys, id) 60 | } 61 | sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] }) 62 | var buf bytes.Buffer 63 | buf.WriteByte(0) 64 | err = ErrDataTooLarge 65 | for _, id := range keys { 66 | data := h[id] 67 | if len(data) > 0xFF { 68 | return 69 | } 70 | buf.WriteByte(id) 71 | buf.WriteByte(byte(len(data))) 72 | buf.Write(data) 73 | } 74 | data := buf.Bytes() 75 | data[0] = byte(len(data)) - 1 76 | return buf.WriteTo(w) 77 | } 78 | 79 | func (h UserDataHeader) ConcatenatedHeader() *ConcatenatedHeader { 80 | if data, ok := h[0x00]; ok { 81 | return &ConcatenatedHeader{ 82 | Reference: uint16(data[0]), 83 | TotalParts: data[1], 84 | Sequence: data[2], 85 | } 86 | } else if data, ok = h[0x08]; ok { 87 | return &ConcatenatedHeader{ 88 | Reference: binary.BigEndian.Uint16(data[0:2]), 89 | TotalParts: data[2], 90 | Sequence: data[3], 91 | } 92 | } 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /pdu/udh_element.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | ) 7 | 8 | type ConcatenatedHeader struct { 9 | Reference uint16 10 | TotalParts byte 11 | Sequence byte 12 | } 13 | 14 | func (h ConcatenatedHeader) Len() int { 15 | if h.Reference < 0xFF { 16 | return 5 17 | } 18 | return 6 19 | } 20 | 21 | func (h ConcatenatedHeader) Set(udh UserDataHeader) { 22 | var buf bytes.Buffer 23 | _ = binary.Write(&buf, binary.BigEndian, h) 24 | if data := buf.Bytes(); data[0] == 0 { 25 | udh[0x00] = data[1:4] 26 | } else { 27 | udh[0x08] = data 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pdu/udh_test.go: -------------------------------------------------------------------------------- 1 | package pdu 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestUserDataHeader(t *testing.T) { 12 | mapping := map[string]UserDataHeader{ 13 | "0500030C0201": {0x00: []byte{12, 2, 1}}, 14 | "060804F42E0201": {0x08: []byte{0xF4, 0x2E, 2, 1}}, 15 | } 16 | concatenatedMapping := map[string]*ConcatenatedHeader{ 17 | "0500030C0201": {Reference: 12, TotalParts: 2, Sequence: 1}, 18 | "060804F42E0201": {Reference: 62510, TotalParts: 2, Sequence: 1}, 19 | } 20 | for packet, expected := range mapping { 21 | h := make(UserDataHeader) 22 | decoded, err := hex.DecodeString(packet) 23 | require.NoError(t, err) 24 | 25 | _, err = h.ReadFrom(bytes.NewReader(decoded)) 26 | require.NoError(t, err) 27 | require.Equal(t, expected, h) 28 | require.Equal(t, len(decoded), h.Len()) 29 | concatenated := h.ConcatenatedHeader() 30 | concatenated.Set(h) 31 | require.Equal(t, concatenatedMapping[packet], concatenated) 32 | require.Equal(t, len(decoded)-1, concatenated.Len()) 33 | 34 | var buf bytes.Buffer 35 | _, err = h.WriteTo(&buf) 36 | require.NoError(t, err) 37 | require.Equal(t, decoded, buf.Bytes()) 38 | } 39 | { 40 | h := make(UserDataHeader) 41 | _, err := h.ReadFrom(bytes.NewReader(nil)) 42 | require.Error(t, err) 43 | } 44 | { 45 | h := UserDataHeader{0x08: []byte{0, 12, 2, 1}, 0x00: []byte{12, 2, 1}} 46 | var buf bytes.Buffer 47 | _, err := h.WriteTo(&buf) 48 | require.NoError(t, err) 49 | } 50 | } 51 | 52 | func TestUserDataHeader_ConcatenatedHeader(t *testing.T) { 53 | header := UserDataHeader{0x05: nil} 54 | require.Nil(t, header.ConcatenatedHeader()) 55 | } 56 | -------------------------------------------------------------------------------- /serve.go: -------------------------------------------------------------------------------- 1 | package smpp 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "net" 7 | ) 8 | 9 | type Handler interface{ Serve(*Session) } 10 | 11 | type HandlerFunc func(*Session) 12 | 13 | func (h HandlerFunc) Serve(session *Session) { h.Serve(session) } 14 | 15 | func ServeTCP(address string, handler Handler, config *tls.Config) (err error) { 16 | var listener net.Listener 17 | if config == nil { 18 | listener, err = net.Listen("tcp", address) 19 | } else { 20 | listener, err = tls.Listen("tcp", address, config) 21 | } 22 | if err != nil { 23 | return 24 | } 25 | var parent net.Conn 26 | for { 27 | if parent, err = listener.Accept(); err != nil { 28 | return 29 | } 30 | go handler.Serve(NewSession(context.Background(), parent)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package smpp 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "math/rand" 7 | "net" 8 | "sync" 9 | "time" 10 | 11 | "github.com/M2MGateway/go-smpp/pdu" 12 | ) 13 | 14 | type Session struct { 15 | parent net.Conn 16 | receiveQueue chan any 17 | pending *sync.Map 18 | NextSequence func() int32 19 | ReadTimeout time.Duration 20 | WriteTimeout time.Duration 21 | } 22 | 23 | func NewSession(ctx context.Context, parent net.Conn) (session *Session) { 24 | random := rand.New(rand.NewSource(time.Now().Unix())) 25 | session = &Session{ 26 | parent: parent, 27 | receiveQueue: make(chan any), 28 | pending: new(sync.Map), 29 | NextSequence: random.Int31, 30 | ReadTimeout: time.Minute * 15, 31 | WriteTimeout: time.Minute * 15, 32 | } 33 | go session.watch(ctx) 34 | return 35 | } 36 | 37 | //goland:noinspection SpellCheckingInspection 38 | func (c *Session) watch(ctx context.Context) { 39 | var err error 40 | var packet any 41 | for { 42 | select { 43 | case <-ctx.Done(): 44 | return 45 | default: 46 | } 47 | if c.ReadTimeout > 0 { 48 | _ = c.parent.SetReadDeadline(time.Now().Add(c.ReadTimeout)) 49 | } 50 | if packet, err = pdu.Unmarshal(c.parent); err == io.EOF { 51 | return 52 | } 53 | if packet == nil { 54 | continue 55 | } 56 | if status, ok := err.(pdu.CommandStatus); ok { 57 | _ = c.Send(&pdu.GenericNACK{ 58 | Header: pdu.Header{CommandStatus: status, Sequence: pdu.ReadSequence(packet)}, 59 | Tags: pdu.Tags{0xFFFF: []byte(err.Error())}, 60 | }) 61 | continue 62 | } 63 | if callback, ok := c.pending.Load(pdu.ReadSequence(packet)); ok { 64 | callback.(func(any))(packet) 65 | } else { 66 | c.receiveQueue <- packet 67 | } 68 | } 69 | } 70 | 71 | func (c *Session) Submit(ctx context.Context, packet pdu.Responsable) (resp any, err error) { 72 | sequence := c.NextSequence() 73 | pdu.WriteSequence(packet, sequence) 74 | if err = c.Send(packet); err != nil { 75 | return 76 | } 77 | returns := make(chan any, 1) 78 | c.pending.Store(sequence, func(resp any) { returns <- resp }) 79 | select { 80 | case <-ctx.Done(): 81 | err = ErrConnectionClosed 82 | case resp = <-returns: 83 | } 84 | c.pending.Delete(sequence) 85 | return 86 | } 87 | 88 | func (c *Session) Send(packet any) (err error) { 89 | sequence := pdu.ReadSequence(packet) 90 | if sequence == 0 || sequence < 0 { 91 | err = pdu.ErrInvalidSequence 92 | return 93 | } 94 | if c.WriteTimeout > 0 { 95 | err = c.parent.SetWriteDeadline(time.Now().Add(c.WriteTimeout)) 96 | } 97 | if err == nil { 98 | _, err = pdu.Marshal(c.parent, packet) 99 | } 100 | if err == io.EOF { 101 | err = ErrConnectionClosed 102 | } 103 | return 104 | } 105 | 106 | func (c *Session) EnquireLink(ctx context.Context, tick time.Duration, timeout time.Duration) (err error) { 107 | ticker := time.NewTicker(tick) 108 | defer ticker.Stop() 109 | for { 110 | ctx, cancel := context.WithTimeout(ctx, timeout) 111 | if _, err = c.Submit(ctx, new(pdu.EnquireLink)); err != nil { 112 | ticker.Stop() 113 | err = c.Close(ctx) 114 | } 115 | cancel() 116 | <-ticker.C 117 | } 118 | } 119 | 120 | func (c *Session) Close(ctx context.Context) (err error) { 121 | ctx, cancel := context.WithTimeout(ctx, time.Second) 122 | defer cancel() 123 | _, err = c.Submit(ctx, new(pdu.Unbind)) 124 | if err != nil { 125 | return 126 | } 127 | close(c.receiveQueue) 128 | return c.parent.Close() 129 | } 130 | 131 | func (c *Session) PDU() <-chan any { 132 | return c.receiveQueue 133 | } 134 | -------------------------------------------------------------------------------- /sms/README.md: -------------------------------------------------------------------------------- 1 | # SMS TPDU Parser and Builder 2 | 3 | > Need more test case. 4 | 5 | ## References 6 | 7 | - [GSM 03.38](https://www.etsi.org/deliver/etsi_gts/03/0338/05.00.00_60/gsmts_0338v050000p.pdf) 8 | - [GSM 03.40](https://www.etsi.org/deliver/etsi_gts/03/0340/05.03.00_60/gsmts_0340v050300p.pdf) 9 | - 10 | - 11 | - 12 | - 13 | - 14 | -------------------------------------------------------------------------------- /sms/address.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | 8 | "github.com/M2MGateway/go-smpp/coding/gsm7bit" 9 | "github.com/M2MGateway/go-smpp/coding/semioctet" 10 | ) 11 | 12 | type Address struct { 13 | NPI, TON byte 14 | No string 15 | } 16 | 17 | func (p Address) MarshalBinary() (data []byte, err error) { 18 | var kind byte 19 | kind |= p.NPI & 0b1111 20 | kind |= p.TON & 0b111 << 4 21 | kind |= 1 << 7 22 | var buf bytes.Buffer 23 | buf.WriteByte(0x00) 24 | buf.WriteByte(kind) 25 | if p.TON != 0b101 { 26 | _, err = semioctet.EncodeSemiAddress(&buf, p.No) 27 | } else { 28 | _, err = gsm7bit.Packed.NewEncoder().Writer(&buf).Write([]byte(p.No)) 29 | } 30 | data = buf.Bytes() 31 | data[0] = byte(len(data) - 2) 32 | return 33 | } 34 | 35 | func (p *Address) ReadFrom(r io.Reader) (n int64, err error) { 36 | buf := bufio.NewReader(r) 37 | var length, kind byte 38 | if length, err = buf.ReadByte(); err != nil || length == 0 { 39 | return 40 | } 41 | if kind, err = buf.ReadByte(); err != nil { 42 | return 43 | } 44 | p.NPI = kind & 0b1111 45 | p.TON = kind >> 4 & 0b111 46 | length = (length + 1) / 2 47 | data := make([]byte, length) 48 | if _, err = buf.Read(data); err != nil { 49 | return 50 | } 51 | if p.TON != 0b101 { 52 | p.No = semioctet.DecodeSemiAddress(data) 53 | } else { 54 | data, err = gsm7bit.Packed.NewDecoder().Bytes(data) 55 | if err == nil { 56 | p.No = string(data) 57 | } 58 | } 59 | return 60 | } 61 | 62 | func (p *Address) WriteTo(w io.Writer) (n int64, err error) { 63 | if len(p.No) == 0 { 64 | _, err = w.Write([]byte{0}) 65 | return 66 | } 67 | data, _ := p.MarshalBinary() 68 | data[0] *= 2 69 | if p.TON != 0b101 { 70 | data[0] -= 1 71 | } 72 | _, err = w.Write(data) 73 | return 74 | } 75 | 76 | type SCAddress Address 77 | 78 | func (p *SCAddress) ReadFrom(r io.Reader) (n int64, err error) { 79 | buf := bufio.NewReader(r) 80 | var length, kind byte 81 | if length, err = buf.ReadByte(); err != nil || length == 0 { 82 | return 83 | } 84 | if kind, err = buf.ReadByte(); err != nil { 85 | return 86 | } 87 | p.NPI = kind & 0b1111 88 | p.TON = kind >> 4 & 0b111 89 | data := make([]byte, length-1) 90 | if _, err = buf.Read(data); err != nil { 91 | return 92 | } 93 | if p.TON != 0b101 { 94 | p.No = semioctet.DecodeSemiAddress(data) 95 | } else { 96 | data, err = gsm7bit.Packed.NewDecoder().Bytes(data) 97 | if err == nil { 98 | p.No = string(data) 99 | } 100 | } 101 | return 102 | } 103 | 104 | func (p SCAddress) WriteTo(w io.Writer) (n int64, err error) { 105 | if len(p.No) == 0 { 106 | _, err = w.Write([]byte{0}) 107 | return 108 | } 109 | data, _ := Address(p).MarshalBinary() 110 | data[0]++ 111 | _, err = w.Write(data) 112 | return 113 | } 114 | -------------------------------------------------------------------------------- /sms/address_test.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | //goland:noinspection SpellCheckingInspection 12 | func TestAddress(t *testing.T) { 13 | tests := map[string]Address{ 14 | "00": {}, 15 | "0B911604895626F9": {NPI: 1, TON: 1, No: "61409865629"}, 16 | "0ED1EDF27C1E3E97E7": {NPI: 1, TON: 5, No: "messages"}, 17 | //"0DD1EDF27C1E3E9701": {NPI: 1, TON: 5, No: "message"}, 18 | "0ED0D637396C7EBBCB": {NPI: 0, TON: 5, No: "Vodafone"}, 19 | } 20 | for input, expected := range tests { 21 | var address Address 22 | decoded, err := hex.DecodeString(input) 23 | require.NoError(t, err) 24 | _, err = address.ReadFrom(bytes.NewReader(decoded)) 25 | require.NoError(t, err) 26 | require.Equal(t, expected, address) 27 | var buf bytes.Buffer 28 | _, err = address.WriteTo(&buf) 29 | require.NoError(t, err) 30 | require.Equal(t, decoded, buf.Bytes()) 31 | } 32 | } 33 | 34 | func TestSCAddress(t *testing.T) { 35 | tests := map[string]SCAddress{ 36 | "00": {}, 37 | "07911604895626F9": {NPI: 1, TON: 1, No: "61409865629"}, 38 | "08D1EDF27C1E3E97E7": {NPI: 1, TON: 5, No: "messages"}, 39 | "08D0D637396C7EBBCB": {NPI: 0, TON: 5, No: "Vodafone"}, 40 | } 41 | for input, expected := range tests { 42 | var address SCAddress 43 | decoded, err := hex.DecodeString(input) 44 | require.NoError(t, err) 45 | _, err = address.ReadFrom(bytes.NewReader(decoded)) 46 | require.NoError(t, err) 47 | require.Equal(t, expected, address) 48 | var buf bytes.Buffer 49 | _, err = address.WriteTo(&buf) 50 | require.NoError(t, err) 51 | require.Equal(t, decoded, buf.Bytes()) 52 | } 53 | } 54 | 55 | //goland:noinspection SpellCheckingInspection 56 | func TestAddress_ErrorHandler(t *testing.T) { 57 | var address Address 58 | _, err := address.ReadFrom(bytes.NewReader([]byte{0x01})) 59 | require.Error(t, err) 60 | _, err = address.ReadFrom(bytes.NewReader([]byte{0x02, 0x02})) 61 | require.Error(t, err) 62 | var smsc SCAddress 63 | _, err = smsc.ReadFrom(bytes.NewReader([]byte{0x01})) 64 | require.Error(t, err) 65 | _, err = smsc.ReadFrom(bytes.NewReader([]byte{0x02, 0x02})) 66 | require.Error(t, err) 67 | } 68 | -------------------------------------------------------------------------------- /sms/bridge/deliver.go: -------------------------------------------------------------------------------- 1 | package bridge 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/M2MGateway/go-smpp/pdu" 7 | "github.com/M2MGateway/go-smpp/sms" 8 | ) 9 | 10 | func ToDeliverSM(deliver *sms.Deliver) (sm *pdu.DeliverSM, err error) { 11 | var message pdu.ShortMessage 12 | if deliver.Flags.UDHIndicator { 13 | message.UDHeader = pdu.UserDataHeader{} 14 | } 15 | _, err = message.ReadFrom(bytes.NewReader(deliver.UserData)) 16 | sm = &pdu.DeliverSM{ 17 | SourceAddr: pdu.Address{ 18 | TON: deliver.OriginatingAddress.TON, 19 | NPI: deliver.OriginatingAddress.NPI, 20 | No: deliver.OriginatingAddress.No, 21 | }, 22 | ESMClass: pdu.ESMClass{ 23 | MessageType: deliver.Flags.MessageType.Type(), 24 | UDHIndicator: deliver.Flags.UDHIndicator, 25 | ReplyPath: deliver.Flags.ReplyPath, 26 | }, 27 | ProtocolID: deliver.ProtocolIdentifier, 28 | Message: message, 29 | } 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /sms/bridge/submit.go: -------------------------------------------------------------------------------- 1 | package bridge 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/M2MGateway/go-smpp/pdu" 7 | "github.com/M2MGateway/go-smpp/sms" 8 | ) 9 | 10 | func ToSubmit(sm *pdu.SubmitSM) (submit *sms.Submit, err error) { 11 | var userData bytes.Buffer 12 | _, _ = sm.Message.UDHeader.WriteTo(&userData) 13 | userData.Write(sm.Message.Message) 14 | submit = &sms.Submit{ 15 | Flags: sms.SubmitFlags{ 16 | ReplyPath: sm.ESMClass.ReplyPath, 17 | UserDataHeaderIndicator: sm.Message.UDHeader != nil, 18 | }, 19 | DestinationAddress: sms.Address{ 20 | NPI: sm.DestAddr.NPI, 21 | TON: sm.DestAddr.TON, 22 | No: sm.DestAddr.No, 23 | }, 24 | ProtocolIdentifier: sm.ProtocolID, 25 | DataCoding: byte(sm.Message.DataCoding), 26 | UserData: userData.Bytes(), 27 | } 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /sms/indicator.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import "fmt" 4 | 5 | type ParameterIndicator struct { 6 | ProtocolIdentifier bool 7 | DataCoding bool 8 | UserData bool 9 | } 10 | 11 | func (p *ParameterIndicator) Has(abbr string) bool { 12 | switch abbr { 13 | case "PID": 14 | return p.ProtocolIdentifier 15 | case "DCS": 16 | return p.DataCoding 17 | case "UD": 18 | return p.UserData 19 | } 20 | return false 21 | } 22 | 23 | func (p *ParameterIndicator) Set(abbr string) { 24 | switch abbr { 25 | case "PID": 26 | p.ProtocolIdentifier = true 27 | case "DCS": 28 | p.DataCoding = true 29 | case "UD": 30 | p.UserData = true 31 | } 32 | } 33 | 34 | func (p *ParameterIndicator) WriteByte(c byte) error { 35 | return unmarshalFlags(c, p) 36 | } 37 | 38 | func (p *ParameterIndicator) ReadByte() (byte, error) { 39 | return marshalFlags(p) 40 | } 41 | 42 | type Flags struct { 43 | MessageType MessageType 44 | } 45 | 46 | func (p *Flags) setDirection(direction Direction) { 47 | p.MessageType.Set(p.MessageType.Type(), direction) 48 | } 49 | 50 | func (p *Flags) WriteByte(c byte) error { 51 | return unmarshalFlags(c, p) 52 | } 53 | 54 | func (p *Flags) ReadByte() (byte, error) { 55 | return marshalFlags(p) 56 | } 57 | 58 | type DeliverFlags struct { 59 | MessageType MessageType 60 | MoreMessagesToSend bool 61 | ReplyPath bool 62 | UDHIndicator bool 63 | StatusReportIndication bool 64 | } 65 | 66 | func (p *DeliverFlags) setDirection(direction Direction) { 67 | p.MessageType.Set(p.MessageType.Type(), direction) 68 | } 69 | 70 | func (p *DeliverFlags) WriteByte(c byte) error { 71 | return unmarshalFlags(c, p) 72 | } 73 | 74 | func (p *DeliverFlags) ReadByte() (byte, error) { 75 | return marshalFlags(p) 76 | } 77 | 78 | type SubmitFlags struct { 79 | MessageType MessageType 80 | RejectDuplicates bool 81 | ValidityPeriodFormat byte 82 | ReplyPath bool 83 | UserDataHeaderIndicator bool 84 | StatusReportRequest bool 85 | } 86 | 87 | func (p *SubmitFlags) setDirection(direction Direction) { 88 | p.MessageType.Set(p.MessageType.Type(), direction) 89 | } 90 | 91 | func (p *SubmitFlags) WriteByte(c byte) error { 92 | return unmarshalFlags(c, p) 93 | } 94 | 95 | func (p *SubmitFlags) ReadByte() (byte, error) { 96 | return marshalFlags(p) 97 | } 98 | 99 | // FailureCause see GSM 03.40, section 9.2.3.22 (54p) 100 | type FailureCause byte 101 | 102 | //goland:noinspection SpellCheckingInspection 103 | var failureCauseErrors = map[FailureCause]string{ 104 | 0x00: "Telematic interworking not supported", 105 | 0x01: "Short message Type 0 not supported", 106 | 0x02: "Cannot replace short message", 107 | 0x0F: "Unspecified TP-PID error", 108 | 0x10: "Data coding schema (alphabet not supported)", 109 | 0x11: "Message class not supported", 110 | 0x1F: "Unspecified TP-DCS error", 111 | 0x20: "Command cannot be actioned", 112 | 0x21: "Command unsupported", 113 | 0x2F: "Unspecified TP-Command error", 114 | 0x30: "TPDU not supported", 115 | 0x40: "SC busy", 116 | 0x41: "No SC subscription", 117 | 0x42: "SC system failure", 118 | 0x43: "Invalid SME address", 119 | 0x44: "Destination SME barred", 120 | 0x45: "SME Rejected-Duplicate", 121 | 0x50: "SIM SMS storage full", 122 | 0x51: "No SMS storage capability in SIM", 123 | 0x52: "Error in MS", 124 | 0x53: "Memory Capacity Exceeded", 125 | 0x7F: "Unspecified error cause", 126 | } 127 | 128 | //goland:noinspection SpellCheckingInspection 129 | func (f FailureCause) Error() string { 130 | if message, ok := failureCauseErrors[f-0x80]; ok { 131 | return message 132 | } 133 | return fmt.Sprintf("%X", byte(f)) 134 | } 135 | -------------------------------------------------------------------------------- /sms/indicator_test.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestParameterIndicator(t *testing.T) { 11 | var indicator ParameterIndicator 12 | for _, abbr := range []string{"PID", "DCS", "UD", ""} { 13 | if !indicator.Has(abbr) { 14 | indicator.Set(abbr) 15 | } 16 | } 17 | value, err := indicator.ReadByte() 18 | require.NoError(t, err) 19 | require.Equal(t, byte(0b111), value) 20 | err = indicator.WriteByte(0b000) 21 | require.NoError(t, err) 22 | require.Equal(t, indicator, ParameterIndicator{ 23 | ProtocolIdentifier: false, 24 | DataCoding: false, 25 | UserData: false, 26 | }) 27 | } 28 | 29 | func TestFlags(t *testing.T) { 30 | tests := map[byte]any{ 31 | 0b00000011: &Flags{ 32 | MessageType: 0b110, 33 | }, 34 | 0b00111111: &DeliverFlags{ 35 | MessageType: 0b110, 36 | MoreMessagesToSend: true, 37 | ReplyPath: true, 38 | UDHIndicator: true, 39 | StatusReportIndication: true, 40 | }, 41 | 0b11111111: &SubmitFlags{ 42 | MessageType: 0b110, 43 | RejectDuplicates: true, 44 | ValidityPeriodFormat: 0b11, 45 | ReplyPath: true, 46 | UserDataHeaderIndicator: true, 47 | StatusReportRequest: true, 48 | }, 49 | } 50 | for expected, flags := range tests { 51 | value, err := flags.(io.ByteReader).ReadByte() 52 | require.NoError(t, err) 53 | require.Equal(t, expected, value) 54 | err = flags.(io.ByteWriter).WriteByte(0) 55 | require.NoError(t, err) 56 | flags.(directionSetter).setDirection(MO) 57 | } 58 | } 59 | 60 | func TestFailureCause_Error(t *testing.T) { 61 | require.NotEmpty(t, FailureCause(0).Error()) 62 | require.NotEmpty(t, FailureCause(0x80).Error()) 63 | } 64 | -------------------------------------------------------------------------------- /sms/marshal.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "io" 8 | "reflect" 9 | ) 10 | 11 | //goland:noinspection SpellCheckingInspection 12 | func Unmarshal(r io.Reader) (packet any, err error) { 13 | buf := bufio.NewReader(r) 14 | kind, failure, err := getType(buf) 15 | if err != nil { 16 | return 17 | } 18 | switch { 19 | case kind == MessageTypeDeliver: 20 | packet = new(Deliver) 21 | case kind == MessageTypeDeliverReport && failure: 22 | packet = new(DeliverReportError) 23 | case kind == MessageTypeDeliverReport: 24 | packet = new(DeliverReport) 25 | case kind == MessageTypeSubmit: 26 | packet = new(Submit) 27 | case kind == MessageTypeSubmitReport && failure: 28 | packet = new(SubmitReportError) 29 | case kind == MessageTypeSubmitReport: 30 | packet = new(SubmitReport) 31 | case kind == MessageTypeStatusReport: 32 | packet = new(StatusReport) 33 | case kind == MessageTypeCommand: 34 | packet = new(Command) 35 | default: 36 | err = errors.New(kind.String()) 37 | return 38 | } 39 | _, err = unmarshal(buf, packet) 40 | return 41 | } 42 | 43 | func unmarshal(buf *bufio.Reader, packet any) (n int64, err error) { 44 | p := reflect.ValueOf(packet) 45 | if p.Kind() == reflect.Ptr { 46 | p = p.Elem() 47 | } 48 | t := p.Type() 49 | var validityPeriodFormat byte 50 | var parameterIndicator *ParameterIndicator 51 | for i := 0; i < p.NumField(); i++ { 52 | abbr := t.Field(i).Tag.Get("TP") 53 | if parameterIndicator != nil && !parameterIndicator.Has(abbr) { 54 | continue 55 | } 56 | field := p.Field(i).Addr().Interface() 57 | switch field := field.(type) { 58 | case *byte: 59 | *field, err = buf.ReadByte() 60 | case io.ByteWriter: 61 | var value byte 62 | if value, err = buf.ReadByte(); err == nil { 63 | err = field.WriteByte(value) 64 | } 65 | if setter, ok := field.(directionSetter); ok { 66 | switch t.Field(i).Tag.Get("DIR") { 67 | case "MT": 68 | setter.setDirection(MT) 69 | case "MO": 70 | setter.setDirection(MO) 71 | } 72 | } 73 | case *[]byte: 74 | var length byte 75 | if length, err = buf.ReadByte(); err == nil { 76 | *field = make([]byte, length) 77 | _, err = buf.Read(*field) 78 | } 79 | case io.ReaderFrom: 80 | _, err = field.ReadFrom(buf) 81 | case *any: 82 | switch { 83 | case abbr == "VP" && validityPeriodFormat == 0b01: 84 | var duration EnhancedDuration 85 | _, err = duration.ReadFrom(buf) 86 | *field = duration 87 | case abbr == "VP" && validityPeriodFormat == 0b10: 88 | var duration Duration 89 | _, err = duration.ReadFrom(buf) 90 | *field = duration 91 | case abbr == "VP" && validityPeriodFormat == 0b11: 92 | var time Time 93 | _, err = time.ReadFrom(buf) 94 | *field = time 95 | } 96 | } 97 | switch field := field.(type) { 98 | case *SubmitFlags: 99 | validityPeriodFormat = field.ValidityPeriodFormat 100 | case *ParameterIndicator: 101 | parameterIndicator = field 102 | } 103 | if err != nil { 104 | return 105 | } 106 | } 107 | return 108 | } 109 | 110 | func Marshal(w io.Writer, packet any) (n int64, err error) { 111 | p := reflect.ValueOf(packet) 112 | if p.Kind() == reflect.Ptr { 113 | p = p.Elem() 114 | } 115 | t := p.Type() 116 | var validityPeriodFormat byte 117 | var parameterIndicator ParameterIndicator 118 | for i := 0; i < p.NumField(); i++ { 119 | parameterIndicator.Set(t.Field(i).Tag.Get("TP")) 120 | switch field := p.Field(i).Addr().Interface().(type) { 121 | case *any: 122 | switch (*field).(type) { 123 | case EnhancedDuration: 124 | validityPeriodFormat = 0b01 125 | case Duration: 126 | validityPeriodFormat = 0b10 127 | case Time: 128 | validityPeriodFormat = 0b11 129 | } 130 | } 131 | } 132 | var buf bytes.Buffer 133 | for i := 0; i < p.NumField(); i++ { 134 | switch field := p.Field(i).Addr().Interface().(type) { 135 | case *byte: 136 | buf.WriteByte(*field) 137 | case *[]byte: 138 | length := len(*field) 139 | buf.WriteByte(byte(length)) 140 | buf.Write(bytes.TrimRight(*field, "\x00")) 141 | case io.ByteReader: 142 | if flags, ok := field.(*SubmitFlags); ok { 143 | flags.ValidityPeriodFormat = validityPeriodFormat 144 | } 145 | var value byte 146 | if value, err = field.ReadByte(); err == nil { 147 | buf.WriteByte(value) 148 | } 149 | case io.WriterTo: 150 | _, err = field.WriteTo(&buf) 151 | case *any: 152 | switch field := (*field).(type) { 153 | case EnhancedDuration: 154 | _, err = field.WriteTo(&buf) 155 | case Duration: 156 | _, err = field.WriteTo(&buf) 157 | case Time: 158 | _, err = field.WriteTo(&buf) 159 | } 160 | } 161 | if err != nil { 162 | return 163 | } 164 | } 165 | return buf.WriteTo(w) 166 | } 167 | 168 | func getType(buf *bufio.Reader) (kind MessageType, failure bool, err error) { 169 | var peek []byte 170 | if peek, err = buf.Peek(1); err != nil { 171 | return 172 | } 173 | length := int(peek[0]) 174 | if peek, err = buf.Peek(length + 3); err != nil { 175 | return 176 | } 177 | var dir Direction 178 | if length == 0 { 179 | dir = MO 180 | } 181 | kind.Set(peek[length+1]&0b11, dir) 182 | failure = peek[length+2] > 0b001111111 183 | return 184 | } 185 | 186 | func unmarshalFlags(c byte, flags any) (err error) { 187 | v := reflect.ValueOf(flags) 188 | if v.Kind() == reflect.Ptr { 189 | v = v.Elem() 190 | } 191 | var b byte 192 | for i, bits := 0, byte(0); i < v.NumField(); i++ { 193 | b = c >> bits 194 | switch field := v.Field(i).Addr().Interface().(type) { 195 | case *MessageType: 196 | field.Set(b&0b11, field.Direction()) 197 | bits += 2 198 | case *byte: 199 | *field = b & 0b11 200 | bits += 2 201 | case *bool: 202 | *field = b&0b1 == 1 203 | bits++ 204 | } 205 | } 206 | return 207 | } 208 | 209 | func marshalFlags(flags any) (c byte, err error) { 210 | v := reflect.ValueOf(flags) 211 | if v.Kind() == reflect.Ptr { 212 | v = v.Elem() 213 | } 214 | for i, bits := 0, byte(0); i < v.NumField(); i++ { 215 | switch field := (v.Field(i).Interface()).(type) { 216 | case MessageType: 217 | c |= field.Type() << bits 218 | bits += 2 219 | case byte: 220 | c |= (field & 0b11) << bits 221 | bits += 2 222 | case bool: 223 | if field { 224 | c |= 1 << bits 225 | } 226 | bits++ 227 | } 228 | } 229 | return 230 | } 231 | -------------------------------------------------------------------------------- /sms/marshal_test.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_getType(t *testing.T) { 12 | _, _, err := getType(bufio.NewReader(bytes.NewReader(nil))) 13 | require.Error(t, err) 14 | _, _, err = getType(bufio.NewReader(bytes.NewReader([]byte{0x01}))) 15 | require.Error(t, err) 16 | } 17 | -------------------------------------------------------------------------------- /sms/message.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | // Deliver see GSM 03.40, section 9.2.2.1 (35p) 4 | type Deliver struct { 5 | SCAddress SCAddress `TP:"SC"` 6 | Flags DeliverFlags `DIR:"MT"` 7 | OriginatingAddress Address `TP:"OA"` 8 | ProtocolIdentifier byte `TP:"PID"` 9 | DataCoding byte `TP:"DCS"` 10 | ServiceCentreTimestamp Time `TP:"SCTS"` 11 | UserData []byte `TP:"UD"` 12 | } 13 | 14 | // DeliverReport see GSM 03.40, section 9.2.2.1a (37p) 15 | type DeliverReport struct { 16 | SCAddress SCAddress `TP:"SC"` 17 | Flags Flags `DIR:"MO"` 18 | ParameterIndicator ParameterIndicator `TP:"PI"` 19 | ProtocolIdentifier byte `TP:"PID"` 20 | DataCoding byte `TP:"DCS"` 21 | UserData []byte `TP:"UD"` 22 | } 23 | 24 | type DeliverReportError struct { 25 | SCAddress SCAddress `TP:"SC"` 26 | Flags Flags `DIR:"MO"` 27 | FailureCause FailureCause `TP:"FCS"` 28 | } 29 | 30 | // Submit see GSM 03.40, section 9.2.2.2 (39p) 31 | type Submit struct { 32 | SCAddress SCAddress `TP:"SC"` 33 | Flags SubmitFlags `DIR:"MO"` 34 | MessageReference byte `TP:"MR"` 35 | DestinationAddress Address `TP:"DA"` 36 | ProtocolIdentifier byte `TP:"PID"` 37 | DataCoding byte `TP:"DCS"` 38 | ValidityPeriod any `TP:"VP"` 39 | UserData []byte `TP:"UD"` 40 | } 41 | 42 | // SubmitReport see GSM 03.40, section 9.2.2.2a (41p) 43 | type SubmitReport struct { 44 | SCAddress SCAddress `TP:"SC"` 45 | Flags SubmitFlags `DIR:"MT"` 46 | ParameterIndicator ParameterIndicator `TP:"PI"` 47 | ServiceCentreTimestamp Time `TP:"SCTS"` 48 | ProtocolIdentifier byte `TP:"PID"` 49 | DataCoding byte `TP:"DCS"` 50 | UserData []byte `TP:"UD"` 51 | } 52 | 53 | type SubmitReportError DeliverReportError 54 | 55 | // StatusReport see GSM 03.40, section 9.2.2.3 (43p) 56 | type StatusReport struct { 57 | SCAddress SCAddress `TP:"SC"` 58 | Flags Flags `DIR:"MT"` 59 | MessageReference byte `TP:"MR"` 60 | MoreMessagesToSend bool `TP:"MMS"` 61 | RecipientAddress Address `TP:"RA"` 62 | ServiceCentreTimestamp Time `TP:"SCTS"` 63 | DischargeTime Time `TP:"DT"` 64 | Status byte `TP:"ST"` 65 | } 66 | 67 | // Command see GSM 03.40, section 9.2.2.4 (45p) 68 | type Command struct { 69 | SCAddress SCAddress `TP:"SC"` 70 | Flags Flags `DIR:"MO"` 71 | MessageReference byte `TP:"MR"` 72 | StatusReportRequest bool `TP:"SRR"` 73 | ProtocolIdentifier byte `TP:"PID"` 74 | CommandType byte `TP:"CT"` 75 | MessageNumber byte `TP:"MN"` 76 | DestinationAddress Address `TP:"DA"` 77 | CommandData []byte `TP:"CD"` 78 | } 79 | -------------------------------------------------------------------------------- /sms/message_test.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestMarshal(t *testing.T) { 12 | samples := []string{ 13 | "0001000B915121551532F400000CC8F79D9C07E54F61363B04", 14 | "0001010B915121551532F40010104190991D9EA341EDF27C1E3E9743", 15 | "0001AB0B915121551532F400C80F3190BB7C07D9DFE971B91D4EB301", 16 | "0011000B916407281553F80000AA0AE8329BFD4697D9EC37", 17 | "0041000B915121551532F40000631A0A031906200A032104100A032705040A032E05080A043807002B8ACD29A85D9ECFC3E7F21C340EBB41E3B79B1E4EBB41697A989D1EB340E2379BCC02B1C3F27399059AB7C36C3628EC2683C66FF65B5E2683E8653C1D", 18 | "0041000B915121551532F40000A0050003000301986F79B90D4AC3E7F53688FC66BFE5A0799A0E0AB7CB741668FC76CFCB637A995E9783C2E4343C3D4F8FD3EE33A8CC4ED359A079990C22BF41E5747DDE7E9341F4721BFE9683D2EE719A9C26D7DD74509D0E6287C56F791954A683C86FF65B5E06B5C36777181466A7E3F5B0AB4A0795DDE936284C06B5D3EE741B642FBBD3E1360B14AFA7E7", 19 | "0041000B915121551532F400042E0B05040B84C0020003F001010A060403B081EA02066A008509036D6F62696C65746964696E67732E636F6D2F0001", 20 | "0041000B915121551532F400045E0B05040B84C0020003F001010B060403AE81EA02056A0045C60C036D6F62696C65746964696E67732E636F6D2F000AC30620090226162510C3062009030416250103436865636B206F7574204D6F62696C6520546964696E677321000101", 21 | "07911326040000F0040B911346610089F60000208062917314080CC8F71D14969741F977FD07", 22 | "07915892000000F0040B915892214365F700007040213252242331493A283D0795C3F33C88FE06C9CB6132885EC6D341EDF27C1E3E97E7207B3A0C0A5241E377BB1D7693E72E", 23 | "07917283010010F5040BC87238880900F10000993092516195800AE8329BFD4697D9EC37", 24 | "07919761989901F0040B919762995696F000084160621263036178042D0442043E0442002004300431043E043D0435043D0442002004370432043E043D0438043B002004320430043C0020003200200440043004370430002E0020041F043E0441043B04350434043D043804390020002D002000200032003600200438044E043D044F00200432002000320031003A00330035", 25 | "07919762020033F1040B919762995696F0000041606291401561046379180E", 26 | "089158921000100930040ED0D376584E7DBBCB000871601002744523664F6066AB6642672A75338ACB4F8696FB8F4999C1670D52D9002E002059826B3275338ACB6B64670D52D9002C8ACB6309002A003100310031002A00320031002A00310023625351FA002E0020670D52D98CBB003A0020002400310035002E00300030002F6708", 27 | } 28 | for _, pdu := range samples { 29 | decoded, err := hex.DecodeString(pdu) 30 | require.NoError(t, err) 31 | parsed, err := Unmarshal(bytes.NewReader(decoded)) 32 | require.NoError(t, err) 33 | var buf bytes.Buffer 34 | _, err = Marshal(&buf, parsed) 35 | require.NoError(t, err) 36 | require.Equal(t, decoded, buf.Bytes()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sms/message_type.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | type MessageType byte 4 | 5 | const ( 6 | MessageTypeDeliver MessageType = iota // 00 0 MT 7 | MessageTypeDeliverReport // 00 1 MO 8 | MessageTypeSubmitReport // 01 0 MT 9 | MessageTypeSubmit // 01 1 MO 10 | MessageTypeStatusReport // 10 0 MT 11 | MessageTypeCommand // 10 1 MO 12 | ) 13 | 14 | type Direction int 15 | 16 | const ( 17 | MT Direction = iota 18 | MO 19 | ) 20 | 21 | func (t *MessageType) Set(kind byte, dir Direction) { 22 | *t = MessageType(kind<<1|byte(dir)) & 0b111 23 | } 24 | 25 | func (t MessageType) Type() byte { 26 | return byte(t>>1) & 0b11 27 | } 28 | 29 | func (t MessageType) Direction() Direction { 30 | return Direction(t) & 0b1 31 | } 32 | 33 | func (t MessageType) String() string { 34 | switch t { 35 | case MessageTypeDeliverReport: 36 | return "SMS-DELIVER-REPORT" 37 | case MessageTypeDeliver: 38 | return "SMS-DELIVER" 39 | case MessageTypeSubmit: 40 | return "SMS-SUBMIT" 41 | case MessageTypeSubmitReport: 42 | return "SMS-SUBMIT-REPORT" 43 | case MessageTypeCommand: 44 | return "SMS-COMMAND" 45 | case MessageTypeStatusReport: 46 | return "SMS-STATUS-REPORT" 47 | } 48 | return "UNKNOWN" 49 | } 50 | -------------------------------------------------------------------------------- /sms/message_type_test.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestMessageType_String(t *testing.T) { 10 | tests := []MessageType{ 11 | MessageTypeDeliverReport, 12 | MessageTypeDeliver, 13 | MessageTypeSubmit, 14 | MessageTypeSubmitReport, 15 | MessageTypeCommand, 16 | MessageTypeStatusReport, 17 | MessageType(0xFF), 18 | } 19 | for _, kind := range tests { 20 | require.NotEmpty(t, kind.String()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sms/time.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "time" 8 | 9 | "github.com/M2MGateway/go-smpp/coding/semioctet" 10 | ) 11 | 12 | type Time struct{ time.Time } 13 | 14 | func (t *Time) ReadFrom(r io.Reader) (n int64, err error) { 15 | data := make([]byte, 7) 16 | if _, err = r.Read(data); err != nil { 17 | return 18 | } 19 | blocks := semioctet.DecodeSemi(data) 20 | t.Time = time.Date( 21 | 2000+blocks[0], 22 | time.Month(blocks[1]), 23 | blocks[2], 24 | blocks[3], 25 | blocks[4], 26 | blocks[5], 27 | 0, 28 | time.FixedZone("", blocks[6]*900), 29 | ) 30 | return 31 | } 32 | 33 | func (t *Time) WriteTo(w io.Writer) (n int64, err error) { 34 | _, offset := t.Time.Zone() 35 | return semioctet.EncodeSemi( 36 | w, 37 | t.Time.Year()-2000, 38 | int(t.Time.Month()), 39 | t.Time.Day(), 40 | t.Time.Hour(), 41 | t.Time.Minute(), 42 | t.Time.Second(), 43 | offset/900, 44 | ) 45 | } 46 | 47 | type Duration struct{ time.Duration } 48 | 49 | func (d *Duration) ReadFrom(r io.Reader) (n int64, err error) { 50 | data := make([]byte, 1) 51 | if _, err = r.Read(data); err != nil { 52 | return 53 | } 54 | switch n := time.Duration(data[0]); { 55 | case n <= 143: 56 | n++ 57 | d.Duration = 5 * time.Minute * n 58 | case n <= 167: 59 | const halfDays = 12 * time.Hour 60 | const halfHours = 30 * time.Minute 61 | d.Duration = (n-143)*halfHours + halfDays 62 | case n <= 196: 63 | d.Duration = (n - 166) * 24 * time.Hour 64 | default: 65 | d.Duration = (n - 192) * 7 * 24 * time.Hour 66 | } 67 | return 68 | } 69 | 70 | func (d *Duration) WriteTo(w io.Writer) (n int64, err error) { 71 | var period time.Duration 72 | if minutes := d.Duration / time.Minute; minutes <= 5 { 73 | period = 0 74 | } else if hours := d.Duration / time.Hour; hours <= 12 { 75 | period = minutes/5 - 1 76 | } else if hours <= 24 { 77 | const halfDays = 12 * time.Hour 78 | const halfHours = 30 * time.Minute 79 | period = (d.Duration-halfDays)/halfHours + 143 80 | } else if days := hours / 24; days <= 31 { 81 | period = hours/24 + 166 82 | } else if weeks := days / 7; weeks <= 62 { 83 | period = weeks + 192 84 | } else { 85 | period = 255 86 | } 87 | var buf bytes.Buffer 88 | buf.WriteByte(byte(period)) 89 | return buf.WriteTo(w) 90 | } 91 | 92 | type EnhancedDuration struct { 93 | time.Duration 94 | Indicator byte 95 | } 96 | 97 | func (d *EnhancedDuration) ReadFrom(r io.Reader) (n int64, err error) { 98 | buf := bufio.NewReader(r) 99 | if d.Indicator, err = buf.ReadByte(); err != nil { 100 | return 101 | } 102 | length := 6 103 | switch d.Indicator & 0b111 { 104 | case 0b001: // relative 105 | var duration Duration 106 | _, err = duration.ReadFrom(buf) 107 | d.Duration = duration.Duration 108 | length-- 109 | case 0b010: // relative seconds 110 | var second byte 111 | second, err = buf.ReadByte() 112 | d.Duration = time.Second * time.Duration(second) 113 | length-- 114 | case 0b011: // relative hh:mm:ss 115 | data := make([]byte, 3) 116 | _, err = buf.Read(data) 117 | semi := semioctet.DecodeSemi(data) 118 | d.Duration = time.Duration(semi[0])*time.Hour + 119 | time.Duration(semi[1])*time.Minute + 120 | time.Duration(semi[2])*time.Second 121 | length -= len(data) 122 | } 123 | if err == nil { 124 | _, err = buf.Discard(length) 125 | } 126 | return 127 | } 128 | 129 | func (d *EnhancedDuration) WriteTo(w io.Writer) (n int64, err error) { 130 | var buf bytes.Buffer 131 | buf.WriteByte(d.Indicator) 132 | switch d.Indicator & 0b111 { 133 | case 0b001: // relative 134 | _, _ = (&Duration{d.Duration}).WriteTo(&buf) 135 | case 0b010: // relative seconds 136 | buf.WriteByte(byte(d.Duration / time.Second)) 137 | case 0b011: // relative hh:mm:ss 138 | hh, mm, ss := int(d.Hours()), int(d.Minutes()), int(d.Seconds()) 139 | _, _ = semioctet.EncodeSemi(&buf, hh, mm-(hh*60), ss-(mm*60)) 140 | } 141 | buf.Write(make([]byte, 7-buf.Len())) 142 | return buf.WriteTo(w) 143 | } 144 | -------------------------------------------------------------------------------- /sms/time_test.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestTime(t *testing.T) { 13 | expected := "2017-08-31T11:21:54+08:00" 14 | var timestamp Time 15 | decoded, err := hex.DecodeString("71801311124523") 16 | require.NoError(t, err) 17 | _, err = timestamp.ReadFrom(bytes.NewReader(decoded)) 18 | require.NoError(t, err) 19 | require.Equal(t, expected, timestamp.Format(time.RFC3339)) 20 | var buf bytes.Buffer 21 | _, err = timestamp.WriteTo(&buf) 22 | require.NoError(t, err) 23 | require.Equal(t, decoded, buf.Bytes()) 24 | } 25 | 26 | func TestDuration(t *testing.T) { 27 | tests := map[string]Duration{ 28 | "00": {Duration: 5 * time.Minute}, 29 | "83": {Duration: 11 * time.Hour}, 30 | "A5": {Duration: 23 * time.Hour}, 31 | "C3": {Duration: 29 * 24 * time.Hour}, 32 | "FE": {Duration: 62 * 7 * 24 * time.Hour}, 33 | "FF": {Duration: 63 * 7 * 24 * time.Hour}, 34 | } 35 | for input, expected := range tests { 36 | var duration Duration 37 | decoded, err := hex.DecodeString(input) 38 | require.NoError(t, err) 39 | _, err = duration.ReadFrom(bytes.NewReader(decoded)) 40 | require.NoError(t, err) 41 | require.Equal(t, expected, duration) 42 | var buf bytes.Buffer 43 | _, err = duration.WriteTo(&buf) 44 | require.NoError(t, err) 45 | require.Equal(t, decoded, buf.Bytes()) 46 | } 47 | } 48 | 49 | func TestEnhancedDuration(t *testing.T) { 50 | tests := map[string]EnhancedDuration{ 51 | "00000000000000": {}, 52 | "01000000000000": {Indicator: 0b001, Duration: 5 * time.Minute}, 53 | "01010000000000": {Indicator: 0b001, Duration: 10 * time.Minute}, 54 | "01FE0000000000": {Indicator: 0b001, Duration: 62 * 7 * 24 * time.Hour}, 55 | "01FF0000000000": {Indicator: 0b001, Duration: 63 * 7 * 24 * time.Hour}, 56 | "02FF0000000000": {Indicator: 0b010, Duration: 255 * time.Second}, 57 | "03302154000000": {Indicator: 0b011, Duration: 3*time.Hour + 12*time.Minute + 45*time.Second}, 58 | } 59 | for input, expected := range tests { 60 | var duration EnhancedDuration 61 | decoded, err := hex.DecodeString(input) 62 | require.NoError(t, err) 63 | _, err = duration.ReadFrom(bytes.NewReader(decoded)) 64 | require.NoError(t, err) 65 | require.Equal(t, expected, duration) 66 | var buf bytes.Buffer 67 | _, err = duration.WriteTo(&buf) 68 | require.NoError(t, err) 69 | require.Equal(t, decoded, buf.Bytes()) 70 | } 71 | } 72 | 73 | func TestTime_ErrorHandler(t *testing.T) { 74 | var timestamp Time 75 | _, err := timestamp.ReadFrom(bytes.NewReader(nil)) 76 | require.Error(t, err) 77 | var duration Duration 78 | _, err = duration.ReadFrom(bytes.NewReader(nil)) 79 | require.Error(t, err) 80 | var enhancedDuration EnhancedDuration 81 | _, err = enhancedDuration.ReadFrom(bytes.NewReader(nil)) 82 | require.Error(t, err) 83 | } 84 | -------------------------------------------------------------------------------- /sms/types.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | type directionSetter interface { 4 | setDirection(Direction) 5 | } 6 | 7 | type SMPPMarshaller interface { 8 | MarshalSMPP() (any, error) 9 | } 10 | 11 | type SMPPUnmarshaler interface { 12 | UnmarshalSMPP(any) error 13 | } 14 | --------------------------------------------------------------------------------