Repository: linxGnu/gosmpp Branch: master Commit: 577c73e414d2 Files: 115 Total size: 344.5 KB Directory structure: gitextract_d637zljl/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ └── go.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── connect.go ├── connect_test.go ├── connection.go ├── connection_test.go ├── data/ │ ├── 7bit.go │ ├── 7bit_test.go │ ├── codings.go │ ├── codings_test.go │ ├── header_data.go │ ├── header_data_string.go │ ├── other_codings.go │ ├── pkg.go │ ├── pkg_test.go │ ├── utils.go │ └── utils_test.go ├── errors/ │ ├── pkg.go │ └── pkg_test.go ├── example/ │ ├── smsc_simulator/ │ │ └── smsc.cpp │ ├── transceiver_with_auto_response/ │ │ └── main.go │ ├── transceiver_with_manual_response/ │ │ └── main.go │ ├── transeiver_with_custom_store/ │ │ ├── CustomStore.go │ │ └── main.go │ └── transeiver_with_request_window_and_custom_submitSm/ │ ├── CustomSubmitSM.go │ └── main.go ├── go.mod ├── go.sum ├── pdu/ │ ├── Address.go │ ├── AddressRange.go │ ├── AddressRange_test.go │ ├── Address_test.go │ ├── AlertNotification.go │ ├── AlertNotification_test.go │ ├── BindRequest.go │ ├── BindRequest_test.go │ ├── BindResponse.go │ ├── BindResponse_test.go │ ├── Buffer.go │ ├── Buffer_test.go │ ├── CancelSM.go │ ├── CancelSMResp.go │ ├── CancelSMResp_test.go │ ├── CancelSM_test.go │ ├── DataSM.go │ ├── DataSMResp.go │ ├── DataSMResp_test.go │ ├── DataSM_test.go │ ├── DeliverSM.go │ ├── DeliverSMResp.go │ ├── DeliverSMResp_test.go │ ├── DeliverSM_test.go │ ├── DestinationAddress.go │ ├── DestinationAddress_test.go │ ├── DistributionList.go │ ├── DistributionList_test.go │ ├── EnquireLink.go │ ├── EnquireLinkResp.go │ ├── EnquireLinkResp_test.go │ ├── EnquireLink_test.go │ ├── GenericNack.go │ ├── GenericNack_test.go │ ├── Outbind.go │ ├── Outbind_test.go │ ├── PDU.go │ ├── PDUFactory.go │ ├── PDUFactory_test.go │ ├── PDUHeader.go │ ├── PDUHeader_test.go │ ├── PDU_test.go │ ├── QuerySM.go │ ├── QuerySMResp.go │ ├── QuerySMResp_test.go │ ├── QuerySM_test.go │ ├── ReplaceSM.go │ ├── ReplaceSMResp.go │ ├── ReplaceSMResp_test.go │ ├── ReplaceSM_test.go │ ├── ShortMessage.go │ ├── ShortMessage_test.go │ ├── SubmitMulti.go │ ├── SubmitMultiResp.go │ ├── SubmitMultiResp_test.go │ ├── SubmitMulti_test.go │ ├── SubmitSM.go │ ├── SubmitSMResp.go │ ├── SubmitSMResp_test.go │ ├── SubmitSM_test.go │ ├── TLV.go │ ├── UDH.go │ ├── UDH_test.go │ ├── Unbind.go │ ├── UnbindResp.go │ ├── UnbindResp_test.go │ ├── Unbind_test.go │ ├── UnsuccessSME.go │ ├── UnsuccessSME_test.go │ └── helper_test.go ├── pkg.go ├── receivable.go ├── receivable_test.go ├── request_store.go ├── session.go ├── session_test.go ├── state.go ├── state_test.go ├── transceivable.go ├── transceivable_test.go ├── transmittable.go ├── transmittable_test.go └── types.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "gomod" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" ================================================ FILE: .github/workflows/go.yml ================================================ name: Build on: [push, pull_request] jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Check out uses: actions/checkout@v5 - name: Set up GCC run: | sudo apt install gcc g++ -y shell: bash - name: Set up Go uses: actions/setup-go@v5 with: go-version: 1.24 # - name: Linter # uses: golangci/golangci-lint-action@v8 # with: # version: latest - name: Start SMSC run: | g++ example/smsc_simulator/smsc.cpp -o smsc ./smsc & shell: bash - name: Test Coverage run: go test -v -race -count=1 -coverprofile=coverage.out - name: Convert coverage to lcov uses: jandelgado/gcov2lcov-action@v1 with: version: latest infile: coverage.out outfile: coverage.lcov - name: Coveralls uses: coverallsapp/github-action@master with: github-token: ${{ secrets.github_token }} path-to-lcov: coverage.lcov ================================================ FILE: .gitignore ================================================ .idea/ vendor/ *DS_Store *.out smsc ================================================ FILE: .golangci.yml ================================================ version: "2" linters: # Default set of linters. # The value can be: # - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default # - `all`: enables all linters by default. # - `none`: disables all linters by default. # - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`). # Default: standard default: all # Enable specific linter. enable: - arangolint - asasalint - asciicheck - bidichk - bodyclose - canonicalheader - containedctx - contextcheck - copyloopvar - cyclop - decorder - depguard - dogsled - dupl - dupword - durationcheck - embeddedstructfieldcheck - err113 - errcheck - errchkjson - errname - errorlint - exhaustive - exhaustruct - exptostd - fatcontext - forbidigo - forcetypeassert - funcorder - funlen - ginkgolinter - gocheckcompilerdirectives - gochecknoglobals - gochecknoinits - gochecksumtype - gocognit - goconst - gocritic - gocyclo - godot - godox - goheader - gomoddirectives - gomodguard - goprintffuncname - gosec - gosmopolitan - govet - grouper - iface - importas - inamedparam - ineffassign - interfacebloat - intrange - ireturn - lll - loggercheck - maintidx - makezero - mirror - misspell - mnd - musttag - nakedret - nestif - nilerr - nilnesserr - nilnil - nlreturn - noctx - noinlineerr - nolintlint - nonamedreturns - nosprintfhostport - paralleltest - perfsprint - prealloc - predeclared - promlinter - protogetter - reassign - recvcheck - revive - rowserrcheck - sloglint - spancheck - sqlclosecheck - tagalign - tagliatelle - testableexamples - testifylint - testpackage - thelper - tparallel - unconvert - unparam - unused - usestdlibvars - usetesting - varnamelen - wastedassign - whitespace - wrapcheck - wsl - wsl_v5 - zerologlint # Disable specific linters. disable: - staticcheck - arangolint - asasalint - asciicheck - bidichk - bodyclose - canonicalheader - containedctx - contextcheck - copyloopvar - cyclop - decorder - depguard - dogsled - dupl - dupword - durationcheck - embeddedstructfieldcheck - err113 - errcheck - errchkjson - errname - errorlint - exhaustive - exhaustruct - exptostd - fatcontext - forbidigo - forcetypeassert - funcorder - funlen - ginkgolinter - gocheckcompilerdirectives - gochecknoglobals - gochecknoinits - gochecksumtype - gocognit - goconst - gocritic - gocyclo - godot - godox - goheader - gomoddirectives - gomodguard - goprintffuncname - gosec - gosmopolitan - govet - grouper - iface - importas - inamedparam - ineffassign - interfacebloat - intrange - ireturn - lll - loggercheck - maintidx - makezero - mirror - misspell - mnd - musttag - nakedret - nestif - nilerr - nilnesserr - nilnil - nlreturn - noctx - noinlineerr - nolintlint - nonamedreturns - nosprintfhostport - paralleltest - perfsprint - prealloc - predeclared - promlinter - protogetter - reassign - recvcheck - revive - rowserrcheck - sloglint - spancheck - sqlclosecheck - staticcheck - tagalign - tagliatelle - testableexamples - testifylint - testpackage - thelper - tparallel - unconvert - unparam - unused - usestdlibvars - usetesting - varnamelen - wastedassign - whitespace - wrapcheck - wsl - wsl_v5 - zerologlint ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # gosmpp [![](https://github.com/linxGnu/gosmpp/workflows/Build/badge.svg)]() [![Go Report Card](https://goreportcard.com/badge/github.com/linxGnu/gosmpp)](https://goreportcard.com/report/github.com/linxGnu/gosmpp) [![Coverage Status](https://coveralls.io/repos/github/linxGnu/gosmpp/badge.svg?branch=master)](https://coveralls.io/github/linxGnu/gosmpp?branch=master) [![godoc](https://img.shields.io/badge/docs-GoDoc-green.svg)](https://godoc.org/github.com/linxGnu/gosmpp) SMPP (3.4) Client Library in pure Go. This library is well tested with SMSC simulators: - [Melroselabs SMSC](https://melroselabs.com/services/smsc-simulator/#smsc-simulator-try) ## Installation ``` go get -u github.com/linxGnu/gosmpp ``` ## Usage ### Highlight - From `v0.1.4`, gosmpp is written in event-based style and fully-manage your smpp session, connection, error, rebinding, etc. You only need to implement some hooks: ```go trans, err := gosmpp.NewSession( gosmpp.TRXConnector(gosmpp.NonTLSDialer, auth), gosmpp.Settings{ EnquireLink: 5 * time.Second, ReadTimeout: 10 * time.Second, OnSubmitError: func(_ pdu.PDU, err error) { log.Fatal("SubmitPDU error:", err) }, OnReceivingError: func(err error) { fmt.Println("Receiving PDU/Network error:", err) }, OnRebindingError: func(err error) { fmt.Println("Rebinding but error:", err) }, OnPDU: handlePDU(), OnClosed: func(state gosmpp.State) { fmt.Println(state) }, }, 5*time.Second) if err != nil { log.Println(err) } defer func() { _ = trans.Close() }() ``` ### Version (0.1.4.RC+) - Full example could be found: [here](https://github.com/linxGnu/gosmpp/blob/master/example) - In this example, you should run smsc first: - Build and run SMSC Simulator: ```bash g++ -std=c++11 example/smsc_simulator/smsc.cpp -o smsc ./smsc & ``` - Run smpp client in the example: https://github.com/linxGnu/gosmpp/blob/master/example/main.go ```bash go run example/main.go ``` ### Old version (0.1.3 and previous) Full example could be found: [gist](https://gist.github.com/linxGnu/b488997a0e62b3f6a7060ba2af6391ea) ## Supported PDUs - [x] bind_transmitter - [x] bind_transmitter_resp - [x] bind_receiver - [x] bind_receiver_resp - [x] bind_transceiver - [x] bind_transceiver_resp - [x] outbind - [x] unbind - [x] unbind_resp - [x] submit_sm - [x] submit_sm_resp - [x] submit_sm_multi - [x] submit_sm_multi_resp - [x] data_sm - [x] data_sm_resp - [x] deliver_sm - [x] deliver_sm_resp - [x] query_sm - [x] query_sm_resp - [x] cancel_sm - [x] cancel_sm_resp - [x] replace_sm - [x] replace_sm_resp - [x] enquire_link - [x] enquire_link_resp - [x] alert_notification - [x] generic_nack ================================================ FILE: connect.go ================================================ package gosmpp import ( "fmt" "net" "github.com/linxGnu/gosmpp/data" "github.com/linxGnu/gosmpp/pdu" ) var ( // NonTLSDialer is non-tls connection dialer. NonTLSDialer = func(addr string) (net.Conn, error) { return net.Dial("tcp", addr) } ) // Dialer is connection dialer. type Dialer func(addr string) (net.Conn, error) // Auth represents basic authentication to SMSC. type Auth struct { // SMSC is SMSC address. SMSC string SystemID string Password string SystemType string } type BindError struct { CommandStatus data.CommandStatusType } func (err BindError) Error() string { return fmt.Sprintf("binding error (%s): %s", err.CommandStatus, err.CommandStatus.Desc()) } func newBindRequest(s Auth, bindingType pdu.BindingType, addressRange pdu.AddressRange) (bindReq *pdu.BindRequest) { bindReq = pdu.NewBindRequest(bindingType) bindReq.SystemID = s.SystemID bindReq.Password = s.Password bindReq.SystemType = s.SystemType bindReq.AddressRange = addressRange return } // Connector is connection factory interface. type Connector interface { Connect() (conn *Connection, err error) GetBindType() pdu.BindingType } type connector struct { dialer Dialer auth Auth bindingType pdu.BindingType addressRange pdu.AddressRange } func (c *connector) GetBindType() pdu.BindingType { return c.bindingType } func (c *connector) Connect() (conn *Connection, err error) { conn, err = connect(c.dialer, c.auth.SMSC, newBindRequest(c.auth, c.bindingType, c.addressRange)) return } func connect(dialer Dialer, addr string, bindReq *pdu.BindRequest) (c *Connection, err error) { conn, err := dialer(addr) if err != nil { return } // create wrapped connection c = NewConnection(conn) // send binding request _, err = c.WritePDU(bindReq) if err != nil { _ = conn.Close() return } // catching response var ( p pdu.PDU resp *pdu.BindResp ) for { if p, err = pdu.Parse(c); err != nil { _ = conn.Close() return } if pd, ok := p.(*pdu.BindResp); ok { resp = pd break } } if resp.CommandStatus != data.ESME_ROK { err = BindError{CommandStatus: resp.CommandStatus} _ = conn.Close() } else { c.systemID = resp.SystemID } return } // TXConnector returns a Transmitter (TX) connector. func TXConnector(dialer Dialer, auth Auth) Connector { return &connector{ dialer: dialer, auth: auth, bindingType: pdu.Transmitter, } } // RXConnector returns a Receiver (RX) connector. func RXConnector(dialer Dialer, auth Auth, opts ...connectorOption) Connector { c := &connector{ dialer: dialer, auth: auth, bindingType: pdu.Receiver, } for _, opt := range opts { opt(c) } return c } // TRXConnector returns a Transceiver (TRX) connector. func TRXConnector(dialer Dialer, auth Auth, opts ...connectorOption) Connector { c := &connector{ dialer: dialer, auth: auth, bindingType: pdu.Transceiver, } for _, opt := range opts { opt(c) } return c } type connectorOption func(c *connector) func WithAddressRange(addressRange pdu.AddressRange) connectorOption { return func(c *connector) { c.addressRange = addressRange } } ================================================ FILE: connect_test.go ================================================ package gosmpp import ( "github.com/linxGnu/gosmpp/pdu" "sync/atomic" "testing" "github.com/stretchr/testify/require" ) var currentAuth int32 var auths = [][2]string{ {"689528", "1a97ae"}, } const ( smscAddr = "127.0.0.1:2775" mess = "Thử nghiệm: chuẩn bị nế mễ" ) func nextAuth() Auth { pair := int(atomic.AddInt32(¤tAuth, 1)) % len(auths) return Auth{ SMSC: smscAddr, SystemID: auths[pair][0], Password: auths[pair][1], SystemType: "", } } func TestBindingSMSC(t *testing.T) { checker := func(t *testing.T, c Connector) { conn, err := c.Connect() require.Nil(t, err) require.NotNil(t, conn) _ = conn.Close() } t.Run("TX", func(t *testing.T) { checker(t, TXConnector(NonTLSDialer, nextAuth())) }) t.Run("RX", func(t *testing.T) { checker(t, RXConnector(NonTLSDialer, nextAuth())) }) t.Run("RX", func(t *testing.T) { addrRange := pdu.AddressRange{} addrRange.AddressRange = "31218" checker(t, RXConnector(NonTLSDialer, nextAuth(), WithAddressRange(addrRange))) }) t.Run("TRX", func(t *testing.T) { checker(t, TRXConnector(NonTLSDialer, nextAuth())) }) t.Run("TRX", func(t *testing.T) { addrRange := pdu.AddressRange{} addrRange.AddressRange = "31218" checker(t, TRXConnector(NonTLSDialer, nextAuth(), WithAddressRange(addrRange))) }) } func TestBindingSMSC_Error(t *testing.T) { auth := Auth{SMSC: smscAddr, SystemID: "invalid"} checker := func(t *testing.T, c Connector) { conn, err := c.Connect() require.ErrorContains(t, err, "Invalid System ID") _ = conn.Close() } t.Run("TX", func(t *testing.T) { checker(t, TXConnector(NonTLSDialer, auth)) }) t.Run("RX", func(t *testing.T) { checker(t, RXConnector(NonTLSDialer, auth)) }) t.Run("TRX", func(t *testing.T) { checker(t, TRXConnector(NonTLSDialer, auth)) }) } func TestBindingType(t *testing.T) { auth := Auth{SMSC: smscAddr, SystemID: "invalid"} t.Run("TX", func(t *testing.T) { c := TXConnector(NonTLSDialer, auth) require.Equal(t, c.GetBindType(), pdu.Transmitter) }) t.Run("RX", func(t *testing.T) { c := RXConnector(NonTLSDialer, auth) require.Equal(t, c.GetBindType(), pdu.Receiver) }) t.Run("TRX", func(t *testing.T) { c := TRXConnector(NonTLSDialer, auth) require.Equal(t, c.GetBindType(), pdu.Transceiver) }) } ================================================ FILE: connection.go ================================================ package gosmpp import ( "bufio" "net" "time" "github.com/linxGnu/gosmpp/pdu" ) // Connection wraps over net.Conn with buffered data reader. type Connection struct { systemID string conn net.Conn reader *bufio.Reader } // NewConnection returns a Connection. func NewConnection(conn net.Conn) (c *Connection) { c = &Connection{ conn: conn, reader: bufio.NewReaderSize(conn, 128<<10), } return } // Read reads data from the connection. // Read can be made to time out and return an Error with Timeout() == true // after a fixed time limit; see SetDeadline and SetReadDeadline. func (c *Connection) Read(b []byte) (n int, err error) { n, err = c.reader.Read(b) return } // Write writes data to the connection. // Write can be made to time out and return an Error with Timeout() == true // after a fixed time limit; see SetDeadline and SetWriteDeadline. func (c *Connection) Write(b []byte) (n int, err error) { n, err = c.conn.Write(b) return } // WritePDU data to the connection. func (c *Connection) WritePDU(p pdu.PDU) (n int, err error) { buf := pdu.NewBuffer(make([]byte, 0, 64)) p.Marshal(buf) n, err = c.conn.Write(buf.Bytes()) return } // Close closes the connection. // Any blocked Read or Write operations will be unblocked and return errors. func (c *Connection) Close() error { return c.conn.Close() } // LocalAddr returns the local network address. func (c *Connection) LocalAddr() net.Addr { return c.conn.LocalAddr() } // RemoteAddr returns the remote network address. func (c *Connection) RemoteAddr() net.Addr { return c.conn.RemoteAddr() } // SetDeadline sets the read and write deadlines associated // with the connection. It is equivalent to calling both // SetReadDeadline and SetWriteDeadline. // // A deadline is an absolute time after which I/O operations // fail with a timeout (see type Error) instead of // blocking. The deadline applies to all future and pending // I/O, not just the immediately following call to Read or // Write. After a deadline has been exceeded, the connection // can be refreshed by setting a deadline in the future. // // An idle timeout can be implemented by repeatedly extending // the deadline after successful Read or Write calls. // // A zero value for t means I/O operations will not time out. // // Note that if a TCP connection has keep-alive turned on, // which is the default unless overridden by Dialer.KeepAlive // or ListenConfig.KeepAlive, then a keep-alive failure may // also return a timeout error. On Unix systems a keep-alive // failure on I/O can be detected using // errors.Is(err, syscall.ETIMEDOUT). func (c *Connection) SetDeadline(t time.Time) error { return c.conn.SetDeadline(t) } // SetReadDeadline sets the deadline for future Read calls // and any currently-blocked Read call. // A zero value for t means Read will not time out. func (c *Connection) SetReadDeadline(t time.Time) error { return c.conn.SetReadDeadline(t) } // SetReadTimeout is equivalent to ReadDeadline(now + timeout) func (c *Connection) SetReadTimeout(t time.Duration) error { return c.conn.SetReadDeadline(time.Now().Add(t)) } // SetWriteDeadline sets the deadline for future Write calls // and any currently-blocked Write call. // Even if write times out, it may return n > 0, indicating that // some of the data was successfully written. // A zero value for t means Write will not time out. func (c *Connection) SetWriteDeadline(t time.Time) error { return c.conn.SetWriteDeadline(t) } // SetWriteTimeout is equivalent to WriteDeadline(now + timeout) func (c *Connection) SetWriteTimeout(t time.Duration) error { return c.conn.SetWriteDeadline(time.Now().Add(t)) } ================================================ FILE: connection_test.go ================================================ package gosmpp import ( "net" "testing" "time" "github.com/stretchr/testify/require" ) func TestConnection(t *testing.T) { conn, err := net.Dial("tcp", "smscsim.melroselabs.com:2775") require.Nil(t, err) c := NewConnection(conn) defer func() { _ = c.Close() }() t.Log(c.LocalAddr()) t.Log(c.RemoteAddr()) require.Nil(t, c.SetDeadline(time.Now().Add(5*time.Second))) require.Nil(t, c.SetWriteDeadline(time.Now().Add(5*time.Second))) require.Nil(t, c.SetReadDeadline(time.Now().Add(5*time.Second))) } ================================================ FILE: data/7bit.go ================================================ package data // Source code in this file is copied from: https://github.com/fiorix/go-smpp/master/smpp/encoding/gsm7.go import ( "bytes" "errors" "math" "golang.org/x/text/encoding" "golang.org/x/text/transform" ) // ErrInvalidCharacter means a given character can not be represented in GSM 7-bit encoding. // // This can only happen during encoding. var ErrInvalidCharacter = errors.New("invalid gsm7 character") // ErrInvalidByte means that a given byte is outside of the GSM 7-bit encoding range. // // This can only happen during decoding. var ErrInvalidByte = errors.New("invalid gsm7 byte") /* GSM 7-bit default alphabet and extension table Source: https://en.wikipedia.org/wiki/GSM_03.38#GSM_7-bit_default_alphabet_and_extension_table_of_3GPP_TS_23.038_/_GSM_03.38 */ const escapeSequence = 0x1B var forwardLookup = map[rune]byte{ '@': 0x00, '£': 0x01, '$': 0x02, '¥': 0x03, 'è': 0x04, 'é': 0x05, 'ù': 0x06, 'ì': 0x07, 'ò': 0x08, 'Ç': 0x09, '\n': 0x0a, 'Ø': 0x0b, 'ø': 0x0c, '\r': 0x0d, 'Å': 0x0e, 'å': 0x0f, 'Δ': 0x10, '_': 0x11, 'Φ': 0x12, 'Γ': 0x13, 'Λ': 0x14, 'Ω': 0x15, 'Π': 0x16, 'Ψ': 0x17, 'Σ': 0x18, 'Θ': 0x19, 'Ξ': 0x1a /* 0x1B */, 'Æ': 0x1c, 'æ': 0x1d, 'ß': 0x1e, 'É': 0x1f, ' ': 0x20, '!': 0x21, '"': 0x22, '#': 0x23, '¤': 0x24, '%': 0x25, '&': 0x26, '\'': 0x27, '(': 0x28, ')': 0x29, '*': 0x2a, '+': 0x2b, ',': 0x2c, '-': 0x2d, '.': 0x2e, '/': 0x2f, '0': 0x30, '1': 0x31, '2': 0x32, '3': 0x33, '4': 0x34, '5': 0x35, '6': 0x36, '7': 0x37, '8': 0x38, '9': 0x39, ':': 0x3a, ';': 0x3b, '<': 0x3c, '=': 0x3d, '>': 0x3e, '?': 0x3f, '¡': 0x40, 'A': 0x41, 'B': 0x42, 'C': 0x43, 'D': 0x44, 'E': 0x45, 'F': 0x46, 'G': 0x47, 'H': 0x48, 'I': 0x49, 'J': 0x4a, 'K': 0x4b, 'L': 0x4c, 'M': 0x4d, 'N': 0x4e, 'O': 0x4f, 'P': 0x50, 'Q': 0x51, 'R': 0x52, 'S': 0x53, 'T': 0x54, 'U': 0x55, 'V': 0x56, 'W': 0x57, 'X': 0x58, 'Y': 0x59, 'Z': 0x5a, 'Ä': 0x5b, 'Ö': 0x5c, 'Ñ': 0x5d, 'Ü': 0x5e, '§': 0x5f, '¿': 0x60, 'a': 0x61, 'b': 0x62, 'c': 0x63, 'd': 0x64, 'e': 0x65, 'f': 0x66, 'g': 0x67, 'h': 0x68, 'i': 0x69, 'j': 0x6a, 'k': 0x6b, 'l': 0x6c, 'm': 0x6d, 'n': 0x6e, 'o': 0x6f, 'p': 0x70, 'q': 0x71, 'r': 0x72, 's': 0x73, 't': 0x74, 'u': 0x75, 'v': 0x76, 'w': 0x77, 'x': 0x78, 'y': 0x79, 'z': 0x7a, 'ä': 0x7b, 'ö': 0x7c, 'ñ': 0x7d, 'ü': 0x7e, 'à': 0x7f, } var forwardEscape = map[rune]byte{ '\f': 0x0A, '^': 0x14, '{': 0x28, '}': 0x29, '\\': 0x2F, '[': 0x3C, '~': 0x3D, ']': 0x3E, '|': 0x40, '€': 0x65, } var reverseLookup = map[byte]rune{ 0x00: '@', 0x01: '£', 0x02: '$', 0x03: '¥', 0x04: 'è', 0x05: 'é', 0x06: 'ù', 0x07: 'ì', 0x08: 'ò', 0x09: 'Ç', 0x0a: '\n', 0x0b: 'Ø', 0x0c: 'ø', 0x0d: '\r', 0x0e: 'Å', 0x0f: 'å', 0x10: 'Δ', 0x11: '_', 0x12: 'Φ', 0x13: 'Γ', 0x14: 'Λ', 0x15: 'Ω', 0x16: 'Π', 0x17: 'Ψ', 0x18: 'Σ', 0x19: 'Θ', 0x1a: 'Ξ' /* 0x1B */, 0x1c: 'Æ', 0x1d: 'æ', 0x1e: 'ß', 0x1f: 'É', 0x20: ' ', 0x21: '!', 0x22: '"', 0x23: '#', 0x24: '¤', 0x25: '%', 0x26: '&', 0x27: '\'', 0x28: '(', 0x29: ')', 0x2a: '*', 0x2b: '+', 0x2c: ',', 0x2d: '-', 0x2e: '.', 0x2f: '/', 0x30: '0', 0x31: '1', 0x32: '2', 0x33: '3', 0x34: '4', 0x35: '5', 0x36: '6', 0x37: '7', 0x38: '8', 0x39: '9', 0x3a: ':', 0x3b: ';', 0x3c: '<', 0x3d: '=', 0x3e: '>', 0x3f: '?', 0x40: '¡', 0x41: 'A', 0x42: 'B', 0x43: 'C', 0x44: 'D', 0x45: 'E', 0x46: 'F', 0x47: 'G', 0x48: 'H', 0x49: 'I', 0x4a: 'J', 0x4b: 'K', 0x4c: 'L', 0x4d: 'M', 0x4e: 'N', 0x4f: 'O', 0x50: 'P', 0x51: 'Q', 0x52: 'R', 0x53: 'S', 0x54: 'T', 0x55: 'U', 0x56: 'V', 0x57: 'W', 0x58: 'X', 0x59: 'Y', 0x5a: 'Z', 0x5b: 'Ä', 0x5c: 'Ö', 0x5d: 'Ñ', 0x5e: 'Ü', 0x5f: '§', 0x60: '¿', 0x61: 'a', 0x62: 'b', 0x63: 'c', 0x64: 'd', 0x65: 'e', 0x66: 'f', 0x67: 'g', 0x68: 'h', 0x69: 'i', 0x6a: 'j', 0x6b: 'k', 0x6c: 'l', 0x6d: 'm', 0x6e: 'n', 0x6f: 'o', 0x70: 'p', 0x71: 'q', 0x72: 'r', 0x73: 's', 0x74: 't', 0x75: 'u', 0x76: 'v', 0x77: 'w', 0x78: 'x', 0x79: 'y', 0x7a: 'z', 0x7b: 'ä', 0x7c: 'ö', 0x7d: 'ñ', 0x7e: 'ü', 0x7f: 'à', } var reverseEscape = map[byte]rune{ 0x0A: '\f', 0x14: '^', 0x28: '{', 0x29: '}', 0x2F: '\\', 0x3C: '[', 0x3D: '~', 0x3E: ']', 0x40: '|', 0x65: '€', } // ValidateGSM7String returns the characters, in the given text, that can not be represented in GSM 7-bit encoding. func ValidateGSM7String(text string) []rune { invalidChars := make([]rune, 0, 4) for _, r := range text { if _, ok := forwardLookup[r]; !ok { if _, ok := forwardEscape[r]; !ok { invalidChars = append(invalidChars, r) } } } return invalidChars } // ValidateGSM7Buffer returns the bytes, in the given buffer, that are outside of the GSM 7-bit encoding range. func ValidateGSM7Buffer(buffer []byte) []byte { invalidBytes := make([]byte, 0, 4) count := 0 for count < len(buffer) { b := buffer[count] if b == escapeSequence { count++ if count >= len(buffer) { invalidBytes = append(invalidBytes, b) break } e := buffer[count] if _, ok := reverseEscape[e]; !ok { invalidBytes = append(invalidBytes, b, e) } } else if _, ok := reverseLookup[b]; !ok { invalidBytes = append(invalidBytes, b) } count++ } return invalidBytes } // GetEscapeChars returns the escape characters in the given text, that doesn't exist in GSM 7-bit DEFAULT alphabet table func GetEscapeChars(runeText []rune) []rune { eChars := make([]rune, 0, 4) for _, r := range runeText { if _, ok := forwardEscape[r]; ok { eChars = append(eChars, r) } } return eChars } // IsEscapeChar checks if the given rune is an escape char func IsEscapeChar(c rune) bool { _, exists := forwardEscape[c] return exists } // GSM7 returns a GSM 7-bit Bit Encoding. // // Set the packed flag to true if you wish to convert septets to octets, // this should be false for most SMPP providers. func GSM7(packed bool) encoding.Encoding { return gsm7Encoding{packed: packed} } type gsm7Encoding struct { packed bool } func (g gsm7Encoding) NewDecoder() *encoding.Decoder { return &encoding.Decoder{Transformer: &gsm7Decoder{ packed: g.packed, }} } func (g gsm7Encoding) NewEncoder() *encoding.Encoder { return &encoding.Encoder{Transformer: &gsm7Encoder{ packed: g.packed, }} } func (g gsm7Encoding) String() string { if g.packed { return "GSM 7-bit (Packed)" } return "GSM 7-bit (Unpacked)" } type gsm7Decoder struct { packed bool } func (g *gsm7Decoder) Reset() { /* not needed */ } func unpack(src []byte, packed bool) (septets []byte) { septets = src if packed { septets = make([]byte, 0, len(src)) count := 0 for remain := len(src) - count; remain > 0; { // Unpack by converting octets into septets. switch { case remain >= 7: septets = append(septets, src[count+0]&0x7F<<0) septets = append(septets, (src[count+1]&0x3F<<1)|(src[count+0]&0x80>>7)) septets = append(septets, (src[count+2]&0x1F<<2)|(src[count+1]&0xC0>>6)) septets = append(septets, (src[count+3]&0x0F<<3)|(src[count+2]&0xE0>>5)) septets = append(septets, (src[count+4]&0x07<<4)|(src[count+3]&0xF0>>4)) septets = append(septets, (src[count+5]&0x03<<5)|(src[count+4]&0xF8>>3)) septets = append(septets, (src[count+6]&0x01<<6)|(src[count+5]&0xFC>>2)) if src[count+6] > 0 { septets = append(septets, src[count+6]&0xFE>>1) } count += 7 case remain >= 6: septets = append(septets, src[count+0]&0x7F<<0) septets = append(septets, (src[count+1]&0x3F<<1)|(src[count+0]&0x80>>7)) septets = append(septets, (src[count+2]&0x1F<<2)|(src[count+1]&0xC0>>6)) septets = append(septets, (src[count+3]&0x0F<<3)|(src[count+2]&0xE0>>5)) septets = append(septets, (src[count+4]&0x07<<4)|(src[count+3]&0xF0>>4)) septets = append(septets, (src[count+5]&0x03<<5)|(src[count+4]&0xF8>>3)) count += 6 case remain >= 5: septets = append(septets, src[count+0]&0x7F<<0) septets = append(septets, (src[count+1]&0x3F<<1)|(src[count+0]&0x80>>7)) septets = append(septets, (src[count+2]&0x1F<<2)|(src[count+1]&0xC0>>6)) septets = append(septets, (src[count+3]&0x0F<<3)|(src[count+2]&0xE0>>5)) septets = append(septets, (src[count+4]&0x07<<4)|(src[count+3]&0xF0>>4)) count += 5 case remain >= 4: septets = append(septets, src[count+0]&0x7F<<0) septets = append(septets, (src[count+1]&0x3F<<1)|(src[count+0]&0x80>>7)) septets = append(septets, (src[count+2]&0x1F<<2)|(src[count+1]&0xC0>>6)) septets = append(septets, (src[count+3]&0x0F<<3)|(src[count+2]&0xE0>>5)) count += 4 case remain >= 3: septets = append(septets, src[count+0]&0x7F<<0) septets = append(septets, (src[count+1]&0x3F<<1)|(src[count+0]&0x80>>7)) septets = append(septets, (src[count+2]&0x1F<<2)|(src[count+1]&0xC0>>6)) count += 3 case remain >= 2: septets = append(septets, src[count+0]&0x7F<<0) septets = append(septets, (src[count+1]&0x3F<<1)|(src[count+0]&0x80>>7)) count += 2 case remain >= 1: septets = append(septets, src[count+0]&0x7F<<0) count++ default: return } remain = len(src) - count } } return } func (g *gsm7Decoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { if len(src) == 0 { return 0, 0, nil } septets := unpack(src, g.packed) nSeptet := 0 builder := bytes.NewBufferString("") for nSeptet < len(septets) { b := septets[nSeptet] if b == escapeSequence { nSeptet++ if nSeptet >= len(septets) { return 0, 0, ErrInvalidByte } e := septets[nSeptet] if r, ok := reverseEscape[e]; ok { builder.WriteRune(r) } else { return 0, 0, ErrInvalidByte } } else if r, ok := reverseLookup[b]; ok { builder.WriteRune(r) } else { return 0, 0, ErrInvalidByte } nSeptet++ } text := builder.Bytes() nDst = len(text) if len(dst) < nDst { return 0, 0, transform.ErrShortDst } copy(dst, text) return } type gsm7Encoder struct { packed bool } func (g *gsm7Encoder) Reset() { /* no needed */ } func pack(dst []byte, septets []byte) (nDst int) { nSeptet := 0 for remain := len(septets); remain > 0; { // Pack by converting septets into octets. switch { case remain >= 8: dst[nDst+0] = (septets[nSeptet+0] & 0x7F >> 0) | (septets[nSeptet+1] & 0x01 << 7) dst[nDst+1] = (septets[nSeptet+1] & 0x7E >> 1) | (septets[nSeptet+2] & 0x03 << 6) dst[nDst+2] = (septets[nSeptet+2] & 0x7C >> 2) | (septets[nSeptet+3] & 0x07 << 5) dst[nDst+3] = (septets[nSeptet+3] & 0x78 >> 3) | (septets[nSeptet+4] & 0x0F << 4) dst[nDst+4] = (septets[nSeptet+4] & 0x70 >> 4) | (septets[nSeptet+5] & 0x1F << 3) dst[nDst+5] = (septets[nSeptet+5] & 0x60 >> 5) | (septets[nSeptet+6] & 0x3F << 2) dst[nDst+6] = (septets[nSeptet+6] & 0x40 >> 6) | (septets[nSeptet+7] & 0x7F << 1) nSeptet += 8 nDst += 7 case remain >= 7: dst[nDst+0] = (septets[nSeptet+0] & 0x7F >> 0) | (septets[nSeptet+1] & 0x01 << 7) dst[nDst+1] = (septets[nSeptet+1] & 0x7E >> 1) | (septets[nSeptet+2] & 0x03 << 6) dst[nDst+2] = (septets[nSeptet+2] & 0x7C >> 2) | (septets[nSeptet+3] & 0x07 << 5) dst[nDst+3] = (septets[nSeptet+3] & 0x78 >> 3) | (septets[nSeptet+4] & 0x0F << 4) dst[nDst+4] = (septets[nSeptet+4] & 0x70 >> 4) | (septets[nSeptet+5] & 0x1F << 3) dst[nDst+5] = (septets[nSeptet+5] & 0x60 >> 5) | (septets[nSeptet+6] & 0x3F << 2) dst[nDst+6] = septets[nSeptet+6] & 0x40 >> 6 nSeptet += 7 nDst += 7 case remain >= 6: dst[nDst+0] = (septets[nSeptet+0] & 0x7F >> 0) | (septets[nSeptet+1] & 0x01 << 7) dst[nDst+1] = (septets[nSeptet+1] & 0x7E >> 1) | (septets[nSeptet+2] & 0x03 << 6) dst[nDst+2] = (septets[nSeptet+2] & 0x7C >> 2) | (septets[nSeptet+3] & 0x07 << 5) dst[nDst+3] = (septets[nSeptet+3] & 0x78 >> 3) | (septets[nSeptet+4] & 0x0F << 4) dst[nDst+4] = (septets[nSeptet+4] & 0x70 >> 4) | (septets[nSeptet+5] & 0x1F << 3) dst[nDst+5] = septets[nSeptet+5] & 0x60 >> 5 nSeptet += 6 nDst += 6 case remain >= 5: dst[nDst+0] = (septets[nSeptet+0] & 0x7F >> 0) | (septets[nSeptet+1] & 0x01 << 7) dst[nDst+1] = (septets[nSeptet+1] & 0x7E >> 1) | (septets[nSeptet+2] & 0x03 << 6) dst[nDst+2] = (septets[nSeptet+2] & 0x7C >> 2) | (septets[nSeptet+3] & 0x07 << 5) dst[nDst+3] = (septets[nSeptet+3] & 0x78 >> 3) | (septets[nSeptet+4] & 0x0F << 4) dst[nDst+4] = septets[nSeptet+4] & 0x70 >> 4 nSeptet += 5 nDst += 5 case remain >= 4: dst[nDst+0] = (septets[nSeptet+0] & 0x7F >> 0) | (septets[nSeptet+1] & 0x01 << 7) dst[nDst+1] = (septets[nSeptet+1] & 0x7E >> 1) | (septets[nSeptet+2] & 0x03 << 6) dst[nDst+2] = (septets[nSeptet+2] & 0x7C >> 2) | (septets[nSeptet+3] & 0x07 << 5) dst[nDst+3] = septets[nSeptet+3] & 0x78 >> 3 nSeptet += 4 nDst += 4 case remain >= 3: dst[nDst+0] = (septets[nSeptet+0] & 0x7F >> 0) | (septets[nSeptet+1] & 0x01 << 7) dst[nDst+1] = (septets[nSeptet+1] & 0x7E >> 1) | (septets[nSeptet+2] & 0x03 << 6) dst[nDst+2] = septets[nSeptet+2] & 0x7C >> 2 nSeptet += 3 nDst += 3 case remain >= 2: dst[nDst+0] = (septets[nSeptet+0] & 0x7F >> 0) | (septets[nSeptet+1] & 0x01 << 7) dst[nDst+1] = septets[nSeptet+1] & 0x7E >> 1 nSeptet += 2 nDst += 2 case remain >= 1: dst[nDst+0] = septets[nSeptet+0] & 0x7F >> 0 nSeptet++ nDst++ default: return } remain = len(septets) - nSeptet } return } func (g *gsm7Encoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { if len(src) == 0 { return 0, 0, nil } text := string(src) // work with []rune (a.k.a string) instead of []byte septets := make([]byte, 0, len(text)) for _, r := range text { if v, ok := forwardLookup[r]; ok { septets = append(septets, v) } else if v, ok := forwardEscape[r]; ok { septets = append(septets, escapeSequence, v) } else { return 0, 0, ErrInvalidCharacter } nSrc++ } nDst = len(septets) if g.packed { nDst = int(math.Ceil(float64(len(septets)) * 7 / 8)) } if len(dst) < nDst { return 0, 0, transform.ErrShortDst } if !g.packed { copy(dst, septets) return nDst, nSrc, nil } nDst = pack(dst, septets) return } ================================================ FILE: data/7bit_test.go ================================================ package data // Source code in this file is copied from: https://github.com/fiorix import ( "encoding/hex" "fmt" "reflect" "testing" "golang.org/x/text/transform" ) var validationStringTests = []struct { Text string Expected []rune }{ {Text: "12345678", Expected: []rune{}}, {Text: "12345[6]", Expected: []rune{}}, {Text: "@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\f^{}\\[~]|€ÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà", Expected: []rune{}}, {Text: "你", Expected: []rune{'你'}}, } var validationBufferTests = []struct { Buffer []byte Expected []byte }{ {Buffer: []byte{0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38}, Expected: []byte{}}, {Buffer: []byte{0x31, 0x32, 0x33, 0x34, 0x35, 0x1B, 0x3C, 0x36, 0x1B, 0x3E}, Expected: []byte{}}, {Buffer: []byte{0x31, 0x32, 0x33, 0x34, 0x35, 0x1B}, Expected: []byte{0x1B}}, {Buffer: []byte{0x31, 0x32, 0x33, 0x34, 0x35, 0x1B, 0x00}, Expected: []byte{0x1B, 0x00}}, {Buffer: []byte{0x80, 0x81, 0x82, 0x83}, Expected: []byte{0x80, 0x81, 0x82, 0x83}}, } var packedTests = []struct { Text string Buff []byte }{ {Text: "", Buff: []byte{}}, {Text: "1", Buff: []byte{0x31}}, {Text: "12", Buff: []byte{0x31, 0x19}}, {Text: "123", Buff: []byte{0x31, 0xD9, 0x0C}}, {Text: "1234", Buff: []byte{0x31, 0xD9, 0x8C, 0x06}}, {Text: "12345", Buff: []byte{0x31, 0xD9, 0x8C, 0x56, 0x03}}, {Text: "123456", Buff: []byte{0x31, 0xD9, 0x8C, 0x56, 0xB3, 0x01}}, {Text: "1234567", Buff: []byte{0x31, 0xD9, 0x8C, 0x56, 0xB3, 0xDD, 0x00}}, {Text: "12345678", Buff: []byte{0x31, 0xD9, 0x8C, 0x56, 0xB3, 0xDD, 0x70}}, {Text: "123456789", Buff: []byte{0x31, 0xD9, 0x8C, 0x56, 0xB3, 0xDD, 0x70, 0x39}}, {Text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur nec nunc venenatis, ultricies ipsum id, volutpat ante. Sed pretium ac metus a interdum metus.", Buff: []byte("\xCC\xB7\xBC\xDC\x06\xA5\xE1\xF3\x7A\x1B\x44\x7E\xB3\xDF\x72\xD0\x3C\x4D\x07\x85\xDB\x65\x3A\x0B\x34\x7E\xBB\xE7\xE5\x31\xBD\x4C\xAF\xCB\x41\x61\x72\x1A\x9E\x9E\x8F\xD3\xEE\x33\xA8\xCC\x4E\xD3\x5D\xA0\x61\x5D\x1E\x16\xA7\xE9\x75\x39\xC8\x5D\x1E\x83\xDC\x75\xF7\x18\x64\x2F\xBB\xCB\xEE\x30\x3D\x3D\x67\x81\xEA\x6C\xBA\x3C\x3D\x4E\x97\xE7\xA0\x34\x7C\x5E\x6F\x83\xD2\x64\x16\xC8\xFE\x66\xD7\xE9\xF0\x30\x1D\x14\x76\xD3\xCB\x2E\xD0\xB4\x4C\x06\xC1\xE5\x65\x7A\xBA\xDE\x06\x85\xC7\xA0\x76\x99\x5E\x9F\x83\xC2\xA0\xB4\x9B\x5E\x96\x93\xEB\x6D\x50\xBB\x4C\xAF\xCF\x5D")}, {Text: "\n", Buff: []byte{0x0A}}, {Text: "\r", Buff: []byte{0x0D}}, {Text: "\f", Buff: []byte{0x1B, 0x05}}, {Text: "^{}\\[~]|€", Buff: []byte{0x1B, 0xCA, 0x06, 0xB5, 0x49, 0x6D, 0x5E, 0x1B, 0xDE, 0xA6, 0xB7, 0xF1, 0x6D, 0x80, 0x9B, 0x32}}, {Text: "@£$¥èéùìòÇØøÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà", Buff: []byte("\x80\x80\x60\x40\x28\x18\x0E\x88\xC4\x82\xE1\x78\x40\x22\x92\x09\xA5\x62\xB9\x60\x32\x1A\x4E\xC7\xF3\x01\x85\x44\x23\x52\xC9\x74\x42\xA5\x54\x2B\x56\xCB\xF5\x82\xC5\x64\x33\x5A\xCD\x76\xC3\xE5\x74\x3B\x5E\xCF\xF7\x03\x06\x85\x43\x62\xD1\x78\x44\x26\x95\x4B\x66\xD3\xF9\x84\x46\xA5\x53\x6A\xD5\x7A\xC5\x66\xB5\x5B\x6E\xD7\xFB\x05\x87\xC5\x63\x72\xD9\x7C\x46\xA7\xD5\x6B\x76\xDB\xFD\x86\xC7\xE5\x73\x7A\xDD\x7E\xC7\xE7\xF5\x7B\x7E\xDF\xFF\x07")}, } var unpackedTests = []struct { Text string Buff []byte }{ {Text: "", Buff: []byte{}}, {Text: "1", Buff: []byte{0x31}}, {Text: "12", Buff: []byte{0x31, 0x32}}, {Text: "123", Buff: []byte{0x31, 0x32, 0x33}}, {Text: "1234", Buff: []byte{0x31, 0x32, 0x33, 0x34}}, {Text: "12345", Buff: []byte{0x31, 0x32, 0x33, 0x34, 0x35}}, {Text: "123456", Buff: []byte{0x31, 0x32, 0x33, 0x34, 0x35, 0x36}}, {Text: "1234567", Buff: []byte{0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37}}, {Text: "12345678", Buff: []byte{0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38}}, {Text: "123456789", Buff: []byte{0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39}}, {Text: "12345[6", Buff: []byte{0x31, 0x32, 0x33, 0x34, 0x35, 0x1B, 0x3C, 0x36}}, {Text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur nec nunc venenatis, ultricies ipsum id, volutpat ante. Sed pretium ac metus a interdum metus.", Buff: []byte("\x4C\x6F\x72\x65\x6D\x20\x69\x70\x73\x75\x6D\x20\x64\x6F\x6C\x6F\x72\x20\x73\x69\x74\x20\x61\x6D\x65\x74\x2C\x20\x63\x6F\x6E\x73\x65\x63\x74\x65\x74\x75\x72\x20\x61\x64\x69\x70\x69\x73\x63\x69\x6E\x67\x20\x65\x6C\x69\x74\x2E\x20\x43\x75\x72\x61\x62\x69\x74\x75\x72\x20\x6E\x65\x63\x20\x6E\x75\x6E\x63\x20\x76\x65\x6E\x65\x6E\x61\x74\x69\x73\x2C\x20\x75\x6C\x74\x72\x69\x63\x69\x65\x73\x20\x69\x70\x73\x75\x6D\x20\x69\x64\x2C\x20\x76\x6F\x6C\x75\x74\x70\x61\x74\x20\x61\x6E\x74\x65\x2E\x20\x53\x65\x64\x20\x70\x72\x65\x74\x69\x75\x6D\x20\x61\x63\x20\x6D\x65\x74\x75\x73\x20\x61\x20\x69\x6E\x74\x65\x72\x64\x75\x6D\x20\x6D\x65\x74\x75\x73\x2E")}, {Text: "\n", Buff: []byte{0x0A}}, {Text: "\r", Buff: []byte{0x0D}}, {Text: "\f", Buff: []byte{0x1B, 0x0A}}, {Text: "^{}\\[~]|€", Buff: []byte{0x1B, 0x14, 0x1B, 0x28, 0x1B, 0x29, 0x1B, 0x2F, 0x1B, 0x3C, 0x1B, 0x3D, 0x1B, 0x3E, 0x1B, 0x40, 0x1B, 0x65}}, {Text: "@£$¥èéùìòÇØøÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà", Buff: []byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0B\x0C\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1C\x1D\x1E\x1F\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2A\x2B\x2C\x2D\x2E\x2F\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3A\x3B\x3C\x3D\x3E\x3F\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5A\x5B\x5C\x5D\x5E\x5F\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6A\x6B\x6C\x6D\x6E\x6F\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7A\x7B\x7C\x7D\x7E\x7F")}, } var invalidCharacterTests = []struct { Packed bool Text string }{ {Packed: true, Text: "你"}, {Packed: false, Text: "你"}, } var invalidByteTests = []struct { Packed bool Buff []byte }{ {Packed: false, Buff: []byte{0x80}}, {Packed: false, Buff: []byte{0x1B}}, {Packed: false, Buff: []byte{0x1B, 0x80}}, } func TestGSM7EncodingString(t *testing.T) { tests := []struct { Packed bool Expected string }{ {Packed: true, Expected: "GSM 7-bit (Packed)"}, {Packed: false, Expected: "GSM 7-bit (Unpacked)"}, } for index, row := range tests { actual := fmt.Sprint(GSM7(row.Packed)) if actual != row.Expected { t.Fatalf("%d: expected '%s' but got '%s'", index, row.Expected, actual) } } } func TestValidateGSM7String(t *testing.T) { for index, row := range validationStringTests { actual := ValidateGSM7String(row.Text) if !reflect.DeepEqual(actual, row.Expected) { t.Fatalf("%2d: actual did not equal expected.\nactual: %s\nexpect: %s", index, string(actual), string(row.Expected)) } } } func TestValidateGSM7Buffer(t *testing.T) { for index, row := range validationBufferTests { actual := ValidateGSM7Buffer(row.Buffer) if !reflect.DeepEqual(actual, row.Expected) { t.Fatalf("%2d: actual did not equal expected.\nactual: %s\nexpect: %s", index, hex.EncodeToString(actual), hex.EncodeToString(row.Expected)) } } } func TestPackedEncoder(t *testing.T) { encoder := GSM7(true).NewEncoder() for index, row := range packedTests { es, _, err := transform.Bytes(encoder, []byte(row.Text)) if err != nil { t.Fatalf("%2d: unexpected error: '%s'", index, err.Error()) } if !reflect.DeepEqual(es, row.Buff) { t.Fatalf("%2d: actual did not equal expected.\nactual: %s\nexpect: %s", index, hex.EncodeToString(es), hex.EncodeToString(row.Buff)) } } } func TestUnpackedEncoder(t *testing.T) { encoder := GSM7(false).NewEncoder() for index, row := range unpackedTests { es, _, err := transform.Bytes(encoder, []byte(row.Text)) if err != nil { t.Fatalf("%2d: unexpected error: '%s'", index, err.Error()) } if !reflect.DeepEqual(es, row.Buff) { t.Fatalf("%2d: actual did not equal expected.\nactual: %s\nexpect: %s", index, hex.EncodeToString(es), hex.EncodeToString(row.Buff)) } } } func TestPackedDecoder(t *testing.T) { decoder := GSM7(true).NewDecoder() for index, row := range packedTests { es, _, err := transform.Bytes(decoder, row.Buff) if err != nil { t.Fatalf("%2d: unexpected error: '%s'", index, err.Error()) } if string(es) != row.Text { t.Fatalf("%2d: actual did not equal expected.\nactual: %s\nexpect: %s", index, string(es), row.Text) } } } func TestUnpackedDecoder(t *testing.T) { encoder := GSM7(false).NewDecoder() for index, row := range unpackedTests { es, _, err := transform.Bytes(encoder, row.Buff) if err != nil { t.Fatalf("%2d: unexpected error: '%s'", index, err.Error()) } if string(es) != row.Text { t.Fatalf("%2d: actual did not equal expected.\nactual: %s\nexpect: %s", index, string(es), row.Text) } } } func TestInvalidCharacter(t *testing.T) { for index, row := range invalidCharacterTests { encoder := GSM7(row.Packed).NewEncoder() _, _, err := transform.Bytes(encoder, []byte(row.Text)) if err == nil { t.Fatalf("%2d: expected error but got no error", index) } if err != ErrInvalidCharacter { t.Fatalf("%2d: expected '%s' but got '%s'", index, ErrInvalidCharacter, err.Error()) } } } func TestInvalidByte(t *testing.T) { for index, row := range invalidByteTests { decoder := GSM7(row.Packed).NewDecoder() _, _, err := transform.Bytes(decoder, row.Buff) if err == nil { t.Fatalf("%2d: expected error but got no error", index) } if err != ErrInvalidByte { t.Fatalf("%2d: expected '%s' but got '%s'", index, ErrInvalidByte, err.Error()) } } } ================================================ FILE: data/codings.go ================================================ package data import ( "golang.org/x/text/encoding" "golang.org/x/text/encoding/charmap" "golang.org/x/text/encoding/unicode" ) const ( // GSM7BITCoding is gsm-7bit coding GSM7BITCoding byte = 0x00 // ASCIICoding is ascii coding ASCIICoding byte = 0x01 // BINARY8BIT1Coding is 8-bit binary coding BINARY8BIT1Coding byte = 0x02 // LATIN1Coding is iso-8859-1 coding LATIN1Coding byte = 0x03 // BINARY8BIT2Coding is 8-bit binary coding BINARY8BIT2Coding byte = 0x04 // CYRILLICCoding is iso-8859-5 coding CYRILLICCoding byte = 0x06 // HEBREWCoding is iso-8859-8 coding HEBREWCoding byte = 0x07 // UCS2Coding is UCS2 coding UCS2Coding byte = 0x08 ) // EncDec wraps encoder and decoder interface. type EncDec interface { Encode(str string) ([]byte, error) Decode([]byte) (string, error) } // Encoding interface. type Encoding interface { EncDec DataCoding() byte } func encode(str string, encoder *encoding.Encoder) ([]byte, error) { return encoder.Bytes([]byte(str)) } func decode(data []byte, decoder *encoding.Decoder) (st string, err error) { tmp, err := decoder.Bytes(data) if err == nil { st = string(tmp) } return } // CustomEncoding is wrapper for user-defined data encoding. type CustomEncoding struct { encDec EncDec coding byte } // NewCustomEncoding creates new custom encoding. func NewCustomEncoding(coding byte, encDec EncDec) Encoding { return &CustomEncoding{ coding: coding, encDec: encDec, } } // Encode string. func (c *CustomEncoding) Encode(str string) ([]byte, error) { return c.encDec.Encode(str) } // Decode data to string. func (c *CustomEncoding) Decode(data []byte) (string, error) { return c.encDec.Decode(data) } // DataCoding flag. func (c *CustomEncoding) DataCoding() byte { return c.coding } type gsm7bit struct { packed bool } func (c *gsm7bit) Encode(str string) ([]byte, error) { return encode(str, GSM7(c.packed).NewEncoder()) } func (c *gsm7bit) Decode(data []byte) (string, error) { return decode(data, GSM7(c.packed).NewDecoder()) } func (c *gsm7bit) DataCoding() byte { return GSM7BITCoding } func (c *gsm7bit) ShouldSplit(text string, octetLimit uint) (shouldSplit bool) { if c.packed { return uint((len(text)*7+7)/8) > octetLimit } else { return uint(len(text)) > octetLimit } } func (c *gsm7bit) EncodeSplit(text string, octetLimit uint) (allSeg [][]byte, err error) { if octetLimit < 64 { octetLimit = 134 } allSeg = [][]byte{} runeSlice := []rune(text) fr, to := 0, int(octetLimit) for fr < len(runeSlice) { if to > len(runeSlice) { to = len(runeSlice) } seg, err := c.Encode(string(runeSlice[fr:to])) if err != nil { return nil, err } allSeg = append(allSeg, seg) fr, to = to, to+int(octetLimit) } return } type gsm7bitPacked struct { } func (c *gsm7bitPacked) Encode(str string) ([]byte, error) { return encode(str, GSM7(true).NewEncoder()) } func (c *gsm7bitPacked) Decode(data []byte) (string, error) { return decode(data, GSM7(true).NewDecoder()) } func (c *gsm7bitPacked) DataCoding() byte { return GSM7BITCoding } func (c *gsm7bitPacked) ShouldSplit(text string, octetLimit uint) (shouldSplit bool) { runeSlice := []rune(text) tLen := len(runeSlice) escCharsLen := len(GetEscapeChars(runeSlice)) regCharsLen := tLen - escCharsLen // Esacpe characters occupy 2 octets/septets // https://en.wikipedia.org/wiki/GSM_03.38 // https://www.developershome.com/sms/gsmAlphabet.asp return uint((regCharsLen*7+escCharsLen*2*7+7)/8) > octetLimit } func (c *gsm7bitPacked) GetSeptetCount(runeSlice []rune) int { tLen := len(runeSlice) escCharsLen := len(GetEscapeChars(runeSlice)) regCharsLen := tLen - escCharsLen return escCharsLen*2 + regCharsLen } func (c *gsm7bitPacked) EncodeSplit(text string, octetLimit uint) (allSeg [][]byte, err error) { if octetLimit < 64 { octetLimit = 134 } allSeg = [][]byte{} runeSlice := []rune(text) lim := int(octetLimit * 8 / 7) fr, to := 0, lim for fr < len(runeSlice) { if to > len(runeSlice) { to = len(runeSlice) } to = determineTo(fr, to, lim, runeSlice) seg, err := c.Encode(string(runeSlice[fr:to])) if err != nil { return nil, err } includeLSB := false nSeptet := c.GetSeptetCount(runeSlice[fr:to]) if nSeptet != lim && nSeptet%8 == 0 { // The last octet's LSB should be included during shift includeLSB = true } seg = shiftBitsLeftOne(seg, includeLSB) allSeg = append(allSeg, seg) fr, to = to, to+lim } return } func determineTo(from int, to int, lim int, runeSlice []rune) int { nSeptet := 0 for nSeptet < lim { if IsEscapeChar(runeSlice[from]) { // esc chars counted as 2 septes nSeptet += 2 } else { nSeptet++ } from++ if from == to { break } } to = from if IsEscapeChar(runeSlice[to-1]) { // 9.2.3.24.1 Concatenated Short Messages "A character represented by an escape-sequence shall not be split in the middle." if nSeptet > lim { to-- } } return to } // Shifts the given byte stream one position left, in order to put a padding bit in between UDH and the beginning of the septets of an actual message // Ref1: https://www.etsi.org/deliver/etsi_ts/123000_123099/123040/16.00.00_60/ts_123040v160000p.pdf Page 74 // Ref2: https://help.goacoustic.com/hc/en-us/articles/360043843154--How-character-encoding-affects-SMS-message-length Pls. ref. to the note "..It is added as padding so that the actual 7-bit encoding data begins on a septet boundary—the 50th bit." // Ref3: https://en.wikipedia.org/wiki/Concatenated_SMS "..This means up to 6 bits of zeros need to be inserted at the start of the [message]." func shiftBitsLeftOne(input []byte, includeLSB bool) []byte { shifted := make([]byte, len(input)) for i, b := range input { shifted[i] = b << 1 if i > 0 { shifted[i] |= input[i-1] >> 7 } } if includeLSB { lastOctet := (input[len(input)-1] >> 7 & 0x01) | (0x0D << 1) /* https://en.wikipedia.org/wiki/GSM_03.38 Ref tekst: "..When there are 7 spare bits in the last octet of a message..."*/ shifted = append(shifted, lastOctet) } return shifted } type ascii struct{} func (*ascii) Encode(str string) ([]byte, error) { return []byte(str), nil } func (*ascii) Decode(data []byte) (string, error) { return string(data), nil } func (*ascii) DataCoding() byte { return ASCIICoding } type iso88591 struct{} func (*iso88591) Encode(str string) ([]byte, error) { return encode(str, charmap.ISO8859_1.NewEncoder()) } func (*iso88591) Decode(data []byte) (string, error) { return decode(data, charmap.ISO8859_1.NewDecoder()) } func (*iso88591) DataCoding() byte { return LATIN1Coding } type binary8bit1 struct{} func (*binary8bit1) Encode(_ string) ([]byte, error) { return []byte{}, ErrNotImplEncode } func (*binary8bit1) Decode(_ []byte) (string, error) { return "", ErrNotImplDecode } func (*binary8bit1) DataCoding() byte { return BINARY8BIT1Coding } type binary8bit2 struct{} func (*binary8bit2) Encode(_ string) ([]byte, error) { return []byte{}, ErrNotImplEncode } func (*binary8bit2) Decode(_ []byte) (string, error) { return "", ErrNotImplDecode } func (*binary8bit2) DataCoding() byte { return BINARY8BIT2Coding } type iso88595 struct{} func (*iso88595) Encode(str string) ([]byte, error) { return encode(str, charmap.ISO8859_5.NewEncoder()) } func (*iso88595) Decode(data []byte) (string, error) { return decode(data, charmap.ISO8859_5.NewDecoder()) } func (*iso88595) DataCoding() byte { return CYRILLICCoding } type iso88598 struct{} func (*iso88598) Encode(str string) ([]byte, error) { return encode(str, charmap.ISO8859_8.NewEncoder()) } func (*iso88598) Decode(data []byte) (string, error) { return decode(data, charmap.ISO8859_8.NewDecoder()) } func (*iso88598) DataCoding() byte { return HEBREWCoding } type ucs2 struct{} func (*ucs2) Encode(str string) ([]byte, error) { tmp := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM) return encode(str, tmp.NewEncoder()) } func (*ucs2) Decode(data []byte) (string, error) { tmp := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM) return decode(data, tmp.NewDecoder()) } func (*ucs2) ShouldSplit(text string, octetLimit uint) (shouldSplit bool) { runeSlice := []rune(text) return uint(len(runeSlice)*2) > octetLimit } func (c *ucs2) EncodeSplit(text string, octetLimit uint) (allSeg [][]byte, err error) { if octetLimit < 64 { octetLimit = 134 } allSeg = [][]byte{} runeSlice := []rune(text) hextetLim := int(octetLimit / 2) // round down // hextet = 16 bits, the correct terms should be hexadectet fr, to := 0, hextetLim for fr < len(runeSlice) { if to > len(runeSlice) { to = len(runeSlice) } seg, err := c.Encode(string(runeSlice[fr:to])) if err != nil { return nil, err } allSeg = append(allSeg, seg) fr, to = to, to+hextetLim } return } func (*ucs2) DataCoding() byte { return UCS2Coding } var ( // GSM7BIT is gsm-7bit encoding. GSM7BIT Encoding = &gsm7bit{packed: false} // GSM7BITPACKED is packed gsm-7bit encoding. // Most of SMSC(s) use unpack version. // Should be tested before using. GSM7BITPACKED Encoding = &gsm7bitPacked{} // ASCII is ascii encoding. ASCII Encoding = &ascii{} // BINARY8BIT1 is binary 8-bit encoding. BINARY8BIT1 Encoding = &binary8bit1{} // LATIN1 encoding. LATIN1 Encoding = &iso88591{} // BINARY8BIT2 is binary 8-bit encoding. BINARY8BIT2 Encoding = &binary8bit2{} // CYRILLIC encoding. CYRILLIC Encoding = &iso88595{} // HEBREW encoding. HEBREW Encoding = &iso88598{} // UCS2 encoding. UCS2 Encoding = &ucs2{} ) var codingMap = map[byte]Encoding{ GSM7BITCoding: GSM7BIT, ASCIICoding: ASCII, BINARY8BIT1Coding: BINARY8BIT1, LATIN1Coding: LATIN1, BINARY8BIT2Coding: BINARY8BIT2, CYRILLICCoding: CYRILLIC, HEBREWCoding: HEBREW, UCS2Coding: UCS2, } // FromDataCoding returns encoding from DataCoding value. func FromDataCoding(code byte) (enc Encoding) { enc, ok := codingMap[code] if !ok { // if encoding is reserved (custom) enc = NewCustomEncoding(code, GSM7BIT) // GSM7BIT is a temporary patch to apply to Encode/Decode methods } return } // Splitter extend encoding object by defining a split function // that split a string into multiple segments // Each segment string, when encoded, must be within a certain octet limit type Splitter interface { // ShouldSplit check if the encoded data of given text should be splitted under octetLimit ShouldSplit(text string, octetLimit uint) (should bool) EncodeSplit(text string, octetLimit uint) ([][]byte, error) } ================================================ FILE: data/codings_test.go ================================================ package data import ( "encoding/hex" "log" "testing" "github.com/stretchr/testify/require" ) func fromHex(h string) (v []byte) { var err error v, err = hex.DecodeString(h) if err != nil { log.Fatal(err) } return } func testEncoding(t *testing.T, enc EncDec, original, expected string) { encoded, err := enc.Encode(original) require.Nil(t, err) require.Equal(t, fromHex(expected), encoded) decoded, err := enc.Decode(encoded) require.Nil(t, err) require.Equal(t, original, decoded) } func testEncodingSplit(t *testing.T, enc EncDec, octetLim uint, original string, expected []string, expectDecode []string) { splitter, ok := enc.(Splitter) require.Truef(t, ok, "Encoding must implement Splitter interface") segEncoded, err := splitter.EncodeSplit(original, octetLim) require.Nil(t, err) for i, seg := range segEncoded { require.Equal(t, fromHex(expected[i]), seg) require.LessOrEqualf(t, uint(len(seg)), octetLim, "Segment len must be less than or equal to %d, got %d", octetLim, len(seg)) if enc == GSM7BITPACKED { seg = shiftBitsOneRight(seg) } decoded, err := enc.Decode(seg) require.Nil(t, err) require.Equal(t, expectDecode[i], decoded) } } func shiftBitsOneRight(input []byte) []byte { carry := byte(0) for i := len(input) - 1; i >= 0; i-- { // Save the carry bit from the previous byte nextCarry := input[i] & 0b00000001 // Shift the current byte to the right input[i] >>= 1 // Apply the carry from the previous byte to the current byte input[i] |= carry << 7 // Update the carry for the next byte carry = nextCarry } return input } func TestCoding(t *testing.T) { require.Nil(t, FromDataCoding(12)) require.Equal(t, GSM7BIT, FromDataCoding(0)) require.Equal(t, ASCII, FromDataCoding(1)) require.Equal(t, UCS2, FromDataCoding(8)) require.Equal(t, LATIN1, FromDataCoding(3)) require.Equal(t, CYRILLIC, FromDataCoding(6)) require.Equal(t, HEBREW, FromDataCoding(7)) } func TestGSM7Bit(t *testing.T) { require.EqualValues(t, 0, GSM7BITPACKED.DataCoding()) testEncoding(t, GSM7BITPACKED, "gjwklgjkwP123+?", "67f57dcd3eabd777684c365bfd00") } func TestShouldSplit(t *testing.T) { t.Run("testShouldSplit_GSM7BIT", func(t *testing.T) { octetLim := uint(140) expect := map[string]bool{ "": false, "1": false, "12312312311231231231123123123112312312311231231231123123123112312312311231231231123123123112312312311231231231123123123112312312311234121212": false, "123123123112312312311231231231123123123112312312311231231231123123123112312312311231231231123123123112312312311231231231123123123112342212121": true, } splitter, _ := GSM7BIT.(Splitter) for k, v := range expect { ok := splitter.ShouldSplit(k, octetLim) require.Equalf(t, ok, v, "Test case %s", k) } }) t.Run("testShouldSplit_UCS2", func(t *testing.T) { octetLim := uint(140) expect := map[string]bool{ "": false, "1": false, "ởỀÊộẩừỰÉÊỗọễệớỡồỰỬỪựởặỬ̀ỵổẤỨợỶẰỢộứẶHữẹ̃ẾỆằỄéậÃỡẰộ̀ỀỗứẲữỪữộÊỵòALữộòC": false, /* 70 UCS2 chars */ "ợÁÊGỷẹííỡỮÂIỆàúễẠỮỊệÂỖÍắẵYẠừẲíộờíẵỠựẤằờởể̃ởỵởềệổồUỡỵầễÁÝởÝNè̉ỚổôỊộợKỨệ́": true, /* 71 UCS2 chars */ } splitter, _ := UCS2.(Splitter) for k, v := range expect { ok := splitter.ShouldSplit(k, octetLim) require.Equalf(t, ok, v, "Test case %s", k) } }) t.Run("testShouldSplit_GSM7BITPACKED", func(t *testing.T) { octetLim := uint(140) expect := map[string]bool{ "": false, "1": false, "12312312311231231231123123123112312312311231231231123123123112312312311231231231123123123112312312311231231231123123123112312312311234121212": false, "gjwklgjkwP123+?sasdasdaqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqdqwdqwDQWdqwdqwdqwdqwwqwdqwdqwddqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqdwqdqwqwdqwdqwqwdqw": false, /* 160 regular basic alphabet chars */ "gjwklgjkwP123+?sasdasdaqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqdqwdqwDQWdqwdqwdqwdqwwqwdqwdqwddqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqdwqdqwqwdqwdqwqwdqwd": true, /* 161 regular basic alphabet chars */ "gjwklgjkwP123+?sasdasdaqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqdqwdqwDQWdqwdqwdqwdqwwqwdqwdqwddqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqdwqdqwqwdqwdqwqwdqw{": true, /* 159 regular basic alphabet chars + 1 escape char at the end */ "|}€€|]|€[~€^]€~{~^{|]]|[{|~€^|]^[[{€^]^{€}}^~~]€]~€[€€[]~~[}}]{^}{|}~~]]€^{^|€{^": false, /* 80 escape chars */ "|}€€|]|€[~€^]€~{~^{|]]|[{|~€^|]^[[{€^]^{€}}^~~]€]~€[€€[]~~[}}]{^}{|}~~]]€^{^|€{^{": true, /* 81 escape chars */ } splitter, _ := GSM7BITPACKED.(Splitter) for k, v := range expect { ok := splitter.ShouldSplit(k, octetLim) require.Equalf(t, ok, v, "Test case %s", k) } }) } func TestSplit(t *testing.T) { require.EqualValues(t, 0o0, GSM7BITPACKED.DataCoding()) t.Run("testSplitGSM7Empty", func(t *testing.T) { testEncodingSplit(t, GSM7BIT, 134, "", []string{ "", }, []string{ "", }) }) t.Run("testSplitUCS2", func(t *testing.T) { testEncodingSplit(t, UCS2, 134, "biggest gift của Christmas là có nhiều big/challenging/meaningful problems để sấp mặt làm", []string{ "006200690067006700650073007400200067006900660074002000631ee700610020004300680072006900730074006d006100730020006c00e00020006300f30020006e006800691ec100750020006200690067002f006300680061006c006c0065006e00670069006e0067002f006d00650061006e0069006e006700660075006c00200070", "0072006f0062006c0065006d0073002001111ec3002000731ea500700020006d1eb700740020006c00e0006d", }, []string{ "biggest gift của Christmas là có nhiều big/challenging/meaningful p", "roblems để sấp mặt làm", }) }) t.Run("testSplitUCS2Empty", func(t *testing.T) { testEncodingSplit(t, UCS2, 134, "", []string{ "", }, []string{ "", }) }) // UCS2 character should not be splitted in the middle // here 54 character is encoded to 108 octet, but since there are 107 octet limit, // a whole 2 octet has to be carried over to the next segment t.Run("testSplit_Middle_UCS2", func(t *testing.T) { testEncodingSplit(t, UCS2, 107, "biggest gift của Christmas là có nhiều big/challenging", []string{ "006200690067006700650073007400200067006900660074002000631ee700610020004300680072006900730074006d006100730020006c00e00020006300f30020006e006800691ec100750020006200690067002f006300680061006c006c0065006e00670069006e", "0067", // 0x00 0x67 is "g" }, []string{ "biggest gift của Christmas là có nhiều big/challengin", "g", }) }) } func TestSplit_GSM7BITPACKED(t *testing.T) { require.EqualValues(t, 0o0, GSM7BITPACKED.DataCoding()) t.Run("testSplit_Escape_GSM7BITPACKED", func(t *testing.T) { testEncodingSplit(t, GSM7BITPACKED, 134, "gjwklgjkwP123+?sasdasdaqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqdqwdqwDQWdqwdqwdqwdqwwqwdqwdqwddqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqdwqdqwqwdqwdqwqwdqw{", []string{ "ceeafb9a7d56afefd0986cb6facdc37372784e0ec7efe4f89d1cbf93e37772fc4e8edfc9f13b397e27c7efe4f89d1cbf93e37772fc4e8edfc9f13b397e27c7efe438397e27c7efc4e8951cbf93e37772fc4e8edfeff13b397e27c7ef6472fc4e8edfc9f13b397e27c7efe4f89d1cbf93e37772fc4e8edfc9f13b394ebec7c9f17bfc4e8edfc9", "e2f7f89d1cbf6f50", }, []string{ "gjwklgjkwP123+?sasdasdaqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqdqwdqwDQWdqwdqwdqwdqwwqwdqwdqwddqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqwdqdwqdqwqwdqwd", "qwqwdqw{", }) }) /* Total char count = 160, Esc char count = 1, Regular char count = 159, Seg1 => 153->€ Expected behaviour: Should not split in the middle of ESC chars */ t.Run("testSplit_EscEndOfSeg1_GSM7BITPACKED", func(t *testing.T) { testEncodingSplit(t, GSM7BITPACKED, 134, "pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp€ppppppp", []string{ "e070381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c31b", "3665381c0e87c3e1", }, []string{ "pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp\r", "€ppppppp", }) }) /* Total char count = 160, Esc char count = 2, Regular char count = 158, Seg1 => 152-> ....{ Seg2 => 1-> ....{ Expected behaviour: Should not split in the middle of ESC chars */ t.Run("testSplit_EscEndOfSeg1AndSeg2_1_GSM7BITPACKED", func(t *testing.T) { testEncodingSplit(t, GSM7BITPACKED, 134, "pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp{{pppppppp", []string{ "e070381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0edfa01a", "3628381c0e87c3e170", }, []string{ "pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp{\r", "{pppppppp", }) }) /* Total char count = 160, Esc char count = 2, Regular char count = 158, Seg1 => 152-> ....€ Seg2 => 1-> ....€ Expected behaviour: Should not split in the middle of ESC chars */ t.Run("testSplit_EscEndOfSeg1AndSeg2_2_GSM7BITPACKED", func(t *testing.T) { testEncodingSplit(t, GSM7BITPACKED, 134, "pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp€€pppppppp", []string{ "e070381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0e87c3e170381c0edf941b", "3665381c0e87c3e170", }, []string{ "pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp€\r", "€pppppppp", }) }) /* Total char count = 162, Esc char count = 0, Regular char count = 162, Seg1 => 153 Seg2 => 9 Scenario: All charcters in the GSM7Bit Basic Character Set table (non-escape chars) https://en.wikipedia.org/wiki/GSM_03.38 */ t.Run("testSplit_AllGSM7BitBasicCharset_GSM7BITPACKED", func(t *testing.T) { testEncodingSplit(t, GSM7BITPACKED, 134, "ΩØ;19Ξòå1-¤6aΞΘANanΣ¡>)òΦ3L;aøΛ-o@>I¥1=-ü!N¤&o9Hmda3jΞ@ÅΣlhEE§/:Çù0Θ&:_&Π;KLÅÅ@fÜ-kFH?ΠB5/ÆΓ?55=<Ω¡N2ñ¥*L¤aÖ! ÖΘ+øF£_Ç?øΔΓ-lèòCìnEBmhÉF*<Åi/aΩ¥CDøfGÇ$/=Λ'ÅA3ò#fkù", []string{ "2a8b5d2ca7413c622d922dacc9049d613706e84b212433e62ecca0b4de005f7210ebb5fc2127c9f4ce21dbe4f04cad0138306c74b1f87de9120658c6a48b982cbb25d3e10098bdadb511f9b3086b2fcee457abf57815a053d61fa898a4303704e266560c632092f8312093169b80181edc45611bfd31aa788ef42b5c190c890cf3312178f528", "4e8ee00c3132af0d", }, []string{ "ΩØ;19Ξòå1-¤6aΞΘANanΣ¡>)òΦ3L;aøΛ-o@>I¥1=-ü!N¤&o9Hmda3jΞ@ÅΣlhEE§/:Çù0Θ&:_&Π;KLÅÅ@fÜ-kFH?ΠB5/ÆΓ?55=<Ω¡N2ñ¥*L¤aÖ! ÖΘ+øF£_Ç?øΔΓ-lèòCìnEBmhÉF*<Åi/aΩ¥CDøfGÇ$/=Λ", "'ÅA3ò#fkù", }) }) /* Total char count = 81, Esc char count = 81, Regular char count = 0, Seg1 => 153 Seg2 => 9 Scenario: All charcters in the GSM7Bit Escape Character Set table https://en.wikipedia.org/wiki/GSM_03.38 */ t.Run("testSplit_AllGSM7BitBasicCharset_GSM7BITPACKED", func(t *testing.T) { testEncodingSplit(t, GSM7BITPACKED, 134, "|{[€|^€[{|€{[|^{~[}€|}|^|^[^]€{[]~}€]{{^|^][€]|€~€^[~}^]{]~{^^€^[~|^]|€~|^€{]{~|}", []string{ "36c00d6ac3db9437c00d6553def036a80d7053dea036bc0d7043d9a036bd0d6f93da9437c04d6a03dc5036c00d65c3db5036be4d7983daf036be4d6f93da9437be0d6a83da5036c00d65e3dbf036e58d6f03dc9437bd4d7943d9f036bd4d6a43d9f836a88d6fd3dba036940d6553de5036bc4d6f03dc5036be0d7053def436c00d6553dea01a", "36be0d6ad3db003729", }, []string{ "|{[€|^€[{|€{[|^{~[}€|}|^|^[^]€{[]~}€]{{^|^][€]|€~€^[~}^]{]~{^^€^[~|^]|€~|^€{\r", "]{~|}", }) }) } func TestAscii(t *testing.T) { require.EqualValues(t, 1, ASCII.DataCoding()) testEncoding(t, ASCII, "agjwklgjkwP", "61676a776b6c676a6b7750") } func TestUCS2(t *testing.T) { require.EqualValues(t, 8, UCS2.DataCoding()) testEncoding(t, UCS2, "agjwklgjkwP", "00610067006a0077006b006c0067006a006b00770050") } func TestLatin1(t *testing.T) { require.EqualValues(t, 3, LATIN1.DataCoding()) testEncoding(t, LATIN1, "agjwklgjkwPÓ", "61676a776b6c676a6b7750d3") } func TestCYRILLIC(t *testing.T) { require.EqualValues(t, 6, CYRILLIC.DataCoding()) testEncoding(t, CYRILLIC, "agjwklgjkwPф", "61676A776B6C676A6B7750E4") } func TestHebrew(t *testing.T) { require.EqualValues(t, 7, HEBREW.DataCoding()) testEncoding(t, HEBREW, "agjwklgjkwPץ", "61676A776B6C676A6B7750F5") } func TestOtherCodings(t *testing.T) { testEncoding(t, UTF16BEM, "ngưỡng cứa cuỗc đợi", "feff006e006701b01ee1006e0067002000631ee900610020006300751ed70063002001111ee30069") testEncoding(t, UTF16LEM, "ngưỡng cứa cuỗc đợi", "fffe6e006700b001e11e6e00670020006300e91e6100200063007500d71e630020001101e31e6900") testEncoding(t, UTF16BE, "ngưỡng cứa cuỗc đợi", "006e006701b01ee1006e0067002000631ee900610020006300751ed70063002001111ee30069") testEncoding(t, UTF16LE, "ngưỡng cứa cuỗc đợi", "6e006700b001e11e6e00670020006300e91e6100200063007500d71e630020001101e31e6900") } type noOpEncDec struct{} func (*noOpEncDec) Encode(str string) ([]byte, error) { return []byte(str), nil } func (*noOpEncDec) Decode(data []byte) (string, error) { return string(data), nil } func TestCustomEncoding(t *testing.T) { enc := NewCustomEncoding(GSM7BITCoding, &noOpEncDec{}) require.EqualValues(t, GSM7BITCoding, enc.DataCoding()) encoded, err := enc.Encode("abc") require.NoError(t, err) require.Equal(t, []byte("abc"), encoded) decoded, err := enc.Decode(encoded) require.NoError(t, err) require.Equal(t, "abc", decoded) } ================================================ FILE: data/header_data.go ================================================ //go:generate stringer -type=CommandStatusType,CommandIDType -output header_data_string.go package data // CommandStatusType is type of command status type CommandStatusType int32 // CommandIDType is type of command id. type CommandIDType int32 // nolint const ( // SMPP Command ID Set GENERIC_NACK = CommandIDType(-2147483648) BIND_RECEIVER = CommandIDType(0x00000001) BIND_RECEIVER_RESP = CommandIDType(-2147483647) BIND_TRANSMITTER = CommandIDType(0x00000002) BIND_TRANSMITTER_RESP = CommandIDType(-2147483646) QUERY_SM = CommandIDType(0x00000003) QUERY_SM_RESP = CommandIDType(-2147483645) SUBMIT_SM = CommandIDType(0x00000004) SUBMIT_SM_RESP = CommandIDType(-2147483644) DELIVER_SM = CommandIDType(0x00000005) DELIVER_SM_RESP = CommandIDType(-2147483643) UNBIND = CommandIDType(0x00000006) UNBIND_RESP = CommandIDType(-2147483642) REPLACE_SM = CommandIDType(0x00000007) REPLACE_SM_RESP = CommandIDType(-2147483641) CANCEL_SM = CommandIDType(0x00000008) CANCEL_SM_RESP = CommandIDType(-2147483640) BIND_TRANSCEIVER = CommandIDType(0x00000009) BIND_TRANSCEIVER_RESP = CommandIDType(-2147483639) OUTBIND = CommandIDType(0x0000000B) ENQUIRE_LINK = CommandIDType(0x00000015) ENQUIRE_LINK_RESP = CommandIDType(-2147483627) SUBMIT_MULTI = CommandIDType(0x00000021) SUBMIT_MULTI_RESP = CommandIDType(-2147483615) ALERT_NOTIFICATION = CommandIDType(0x00000102) DATA_SM = CommandIDType(0x00000103) DATA_SM_RESP = CommandIDType(-2147483389) ) // nolint const ( // Command_Status Error Codes ESME_ROK = CommandStatusType(0x00000000) // No Error ESME_RINVMSGLEN = CommandStatusType(0x00000001) // Message Length is invalid ESME_RINVCMDLEN = CommandStatusType(0x00000002) // Command Length is invalid ESME_RINVCMDID = CommandStatusType(0x00000003) // Invalid Command ID ESME_RINVBNDSTS = CommandStatusType(0x00000004) // Incorrect BIND Status for given command ESME_RALYBND = CommandStatusType(0x00000005) // ESME Already in Bound State ESME_RINVPRTFLG = CommandStatusType(0x00000006) // Invalid Priority Flag ESME_RINVREGDLVFLG = CommandStatusType(0x00000007) // Invalid Registered Delivery Flag ESME_RSYSERR = CommandStatusType(0x00000008) // System Error ESME_RINVSRCADR = CommandStatusType(0x0000000A) // Invalid Source Address ESME_RINVDSTADR = CommandStatusType(0x0000000B) // Invalid Dest Addr ESME_RINVMSGID = CommandStatusType(0x0000000C) // Message ID is invalid ESME_RBINDFAIL = CommandStatusType(0x0000000D) // Bind Failed ESME_RINVPASWD = CommandStatusType(0x0000000E) // Invalid Password ESME_RINVSYSID = CommandStatusType(0x0000000F) // Invalid System ID ESME_RCANCELFAIL = CommandStatusType(0x00000011) // Cancel SM Failed ESME_RREPLACEFAIL = CommandStatusType(0x00000013) // Replace SM Failed ESME_RMSGQFUL = CommandStatusType(0x00000014) // Message Queue Full ESME_RINVSERTYP = CommandStatusType(0x00000015) // Invalid Service Type ESME_RADDCUSTFAIL = CommandStatusType(0x00000019) // Failed to Add Customer ESME_RDELCUSTFAIL = CommandStatusType(0x0000001A) // Failed to delete Customer ESME_RMODCUSTFAIL = CommandStatusType(0x0000001B) // Failed to modify customer ESME_RENQCUSTFAIL = CommandStatusType(0x0000001C) // Failed to Enquire Customer ESME_RINVCUSTID = CommandStatusType(0x0000001D) // Invalid Customer ID ESME_RINVCUSTNAME = CommandStatusType(0x0000001F) // Invalid Customer Name ESME_RINVCUSTADR = CommandStatusType(0x00000021) // Invalid Customer Address ESME_RINVADR = CommandStatusType(0x00000022) // Invalid Address ESME_RCUSTEXIST = CommandStatusType(0x00000023) // Customer Exists ESME_RCUSTNOTEXIST = CommandStatusType(0x00000024) // Customer does not exist ESME_RADDDLFAIL = CommandStatusType(0x00000026) // Failed to Add DL ESME_RMODDLFAIL = CommandStatusType(0x00000027) // Failed to modify DL ESME_RDELDLFAIL = CommandStatusType(0x00000028) // Failed to Delete DL ESME_RVIEWDLFAIL = CommandStatusType(0x00000029) // Failed to View DL ESME_RLISTDLSFAIL = CommandStatusType(0x00000030) // Failed to list DLs ESME_RPARAMRETFAIL = CommandStatusType(0x00000031) // Param Retrieve Failed ESME_RINVPARAM = CommandStatusType(0x00000032) // Invalid Param ESME_RINVNUMDESTS = CommandStatusType(0x00000033) // Invalid number of destinations ESME_RINVDLNAME = CommandStatusType(0x00000034) // Invalid Distribution List name ESME_RINVDLMEMBDESC = CommandStatusType(0x00000035) // Invalid DL Member Description ESME_RINVDLMEMBTYP = CommandStatusType(0x00000038) // Invalid DL Member Type ESME_RINVDLMODOPT = CommandStatusType(0x00000039) // Invalid DL Modify Option ESME_RINVDESTFLAG = CommandStatusType(0x00000040) // Destination flag is invalid (submit_multi) ESME_RINVSUBREP = CommandStatusType(0x00000042) // Invalid ‘submit with replace’ request (i.e. submit_sm with replace_if_present_flag set) ESME_RINVESMCLASS = CommandStatusType(0x00000043) // Invalid esm_class field data ESME_RCNTSUBDL = CommandStatusType(0x00000044) // Cannot Submit to Distribution List ESME_RSUBMITFAIL = CommandStatusType(0x00000045) // submit_sm or submit_multi failed ESME_RINVSRCTON = CommandStatusType(0x00000048) // Invalid Source address TON ESME_RINVSRCNPI = CommandStatusType(0x00000049) // Invalid Source address NPI ESME_RINVDSTTON = CommandStatusType(0x00000050) // Invalid Destination address TON ESME_RINVDSTNPI = CommandStatusType(0x00000051) // Invalid Destination address NPI ESME_RINVSYSTYP = CommandStatusType(0x00000053) // Invalid system_type field ESME_RINVREPFLAG = CommandStatusType(0x00000054) // Invalid replace_if_present flag ESME_RINVNUMMSGS = CommandStatusType(0x00000055) // Invalid number of messages ESME_RTHROTTLED = CommandStatusType(0x00000058) // Throttling error (ESME has exceeded allowed message limits) ESME_RPROVNOTALLWD = CommandStatusType(0x00000059) // Provisioning Not Allowed ESME_RINVSCHED = CommandStatusType(0x00000061) // Invalid Scheduled Delivery Time ESME_RINVEXPIRY = CommandStatusType(0x00000062) // Invalid message validity period (Expiry time) ESME_RINVDFTMSGID = CommandStatusType(0x00000063) // Predefined Message Invalid or Not Found ESME_RX_T_APPN = CommandStatusType(0x00000064) // ESME Receiver Temporary App Error Code ESME_RX_P_APPN = CommandStatusType(0x00000065) // ESME Receiver Permanent App Error Code ESME_RX_R_APPN = CommandStatusType(0x00000066) // ESME Receiver Reject Message Error Code ESME_RQUERYFAIL = CommandStatusType(0x00000067) // query_sm request failed ESME_RINVPGCUSTID = CommandStatusType(0x00000080) // Paging Customer ID Invalid No such subscriber ESME_RINVPGCUSTIDLEN = CommandStatusType(0x00000081) // Paging Customer ID length Invalid ESME_RINVCITYLEN = CommandStatusType(0x00000082) // City Length Invalid ESME_RINVSTATELEN = CommandStatusType(0x00000083) // State Length Invalid ESME_RINVZIPPREFIXLEN = CommandStatusType(0x00000084) // Zip Prefix Length Invalid ESME_RINVZIPPOSTFIXLEN = CommandStatusType(0x00000085) // Zip Postfix Length Invalid ESME_RINVMINLEN = CommandStatusType(0x00000086) // MIN Length Invalid ESME_RINVMIN = CommandStatusType(0x00000087) // MIN Invalid (i.e. No such MIN) ESME_RINVPINLEN = CommandStatusType(0x00000088) // PIN Length Invalid ESME_RINVTERMCODELEN = CommandStatusType(0x00000089) // Terminal Code Length Invalid ESME_RINVCHANNELLEN = CommandStatusType(0x0000008A) // Channel Length Invalid ESME_RINVCOVREGIONLEN = CommandStatusType(0x0000008B) // Coverage Region Length Invalid ESME_RINVCAPCODELEN = CommandStatusType(0x0000008C) // Cap Code Length Invalid ESME_RINVMDTLEN = CommandStatusType(0x0000008D) // Message delivery time Length Invalid ESME_RINVPRIORMSGLEN = CommandStatusType(0x0000008E) // Priority Message Length Invalid ESME_RINVPERMSGLEN = CommandStatusType(0x0000008F) // Periodic Messages Length Invalid ESME_RINVPGALERTLEN = CommandStatusType(0x00000090) // Paging Alerts Length Invalid ESME_RINVSMUSERLEN = CommandStatusType(0x00000091) // int16 Message User Group Length Invalid ESME_RINVRTDBLEN = CommandStatusType(0x00000092) // Real Time Data broadcasts Length Invalid ESME_RINVREGDELLEN = CommandStatusType(0x00000093) // Registered Delivery Length Invalid ESME_RINVMSGDISTLEN = CommandStatusType(0x00000094) // Message Distribution Length Invalid ESME_RINVPRIORMSG = CommandStatusType(0x00000095) // Priority Message Length Invalid ESME_RINVMDT = CommandStatusType(0x00000096) // Message delivery time Invalid ESME_RINVPERMSG = CommandStatusType(0x00000097) // Periodic Messages Invalid ESME_RINVMSGDIST = CommandStatusType(0x00000098) // Message Distribution Invalid ESME_RINVPGALERT = CommandStatusType(0x00000099) // Paging Alerts Invalid ESME_RINVSMUSER = CommandStatusType(0x0000009A) // int16 Message User Group Invalid ESME_RINVRTDB = CommandStatusType(0x0000009B) // Real Time Data broadcasts Invalid ESME_RINVREGDEL = CommandStatusType(0x0000009C) // Registered Delivery Invalid ESME_RINVOPTPARLEN = CommandStatusType(0x0000009F) // Invalid Optional Parameter Length ESME_RINVOPTPARSTREAM = CommandStatusType(0x000000C0) // KIF IW Field out of data ESME_ROPTPARNOTALLWD = CommandStatusType(0x000000C1) // Optional Parameter not allowed ESME_RINVPARLEN = CommandStatusType(0x000000C2) // Invalid Parameter Length. ESME_RMISSINGOPTPARAM = CommandStatusType(0x000000C3) // Expected Optional Parameter missing ESME_RINVOPTPARAMVAL = CommandStatusType(0x000000C4) // Invalid Optional Parameter Value ESME_RDELIVERYFAILURE = CommandStatusType(0x000000FE) // Delivery Failure (used for data_sm_resp) ESME_RUNKNOWNERR = CommandStatusType(0x000000FF) // Unknown Error ESME_LAST_ERROR = CommandStatusType(0x0000012C) // THE VALUE OF THE LAST ERROR CODE ) ================================================ FILE: data/header_data_string.go ================================================ // Code generated by "stringer -type=CommandStatusType,CommandIDType -output header_data_string.go"; DO NOT EDIT. package data import "strconv" func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} _ = x[ESME_ROK-0] _ = x[ESME_RINVMSGLEN-1] _ = x[ESME_RINVCMDLEN-2] _ = x[ESME_RINVCMDID-3] _ = x[ESME_RINVBNDSTS-4] _ = x[ESME_RALYBND-5] _ = x[ESME_RINVPRTFLG-6] _ = x[ESME_RINVREGDLVFLG-7] _ = x[ESME_RSYSERR-8] _ = x[ESME_RINVSRCADR-10] _ = x[ESME_RINVDSTADR-11] _ = x[ESME_RINVMSGID-12] _ = x[ESME_RBINDFAIL-13] _ = x[ESME_RINVPASWD-14] _ = x[ESME_RINVSYSID-15] _ = x[ESME_RCANCELFAIL-17] _ = x[ESME_RREPLACEFAIL-19] _ = x[ESME_RMSGQFUL-20] _ = x[ESME_RINVSERTYP-21] _ = x[ESME_RADDCUSTFAIL-25] _ = x[ESME_RDELCUSTFAIL-26] _ = x[ESME_RMODCUSTFAIL-27] _ = x[ESME_RENQCUSTFAIL-28] _ = x[ESME_RINVCUSTID-29] _ = x[ESME_RINVCUSTNAME-31] _ = x[ESME_RINVCUSTADR-33] _ = x[ESME_RINVADR-34] _ = x[ESME_RCUSTEXIST-35] _ = x[ESME_RCUSTNOTEXIST-36] _ = x[ESME_RADDDLFAIL-38] _ = x[ESME_RMODDLFAIL-39] _ = x[ESME_RDELDLFAIL-40] _ = x[ESME_RVIEWDLFAIL-41] _ = x[ESME_RLISTDLSFAIL-48] _ = x[ESME_RPARAMRETFAIL-49] _ = x[ESME_RINVPARAM-50] _ = x[ESME_RINVNUMDESTS-51] _ = x[ESME_RINVDLNAME-52] _ = x[ESME_RINVDLMEMBDESC-53] _ = x[ESME_RINVDLMEMBTYP-56] _ = x[ESME_RINVDLMODOPT-57] _ = x[ESME_RINVDESTFLAG-64] _ = x[ESME_RINVSUBREP-66] _ = x[ESME_RINVESMCLASS-67] _ = x[ESME_RCNTSUBDL-68] _ = x[ESME_RSUBMITFAIL-69] _ = x[ESME_RINVSRCTON-72] _ = x[ESME_RINVSRCNPI-73] _ = x[ESME_RINVDSTTON-80] _ = x[ESME_RINVDSTNPI-81] _ = x[ESME_RINVSYSTYP-83] _ = x[ESME_RINVREPFLAG-84] _ = x[ESME_RINVNUMMSGS-85] _ = x[ESME_RTHROTTLED-88] _ = x[ESME_RPROVNOTALLWD-89] _ = x[ESME_RINVSCHED-97] _ = x[ESME_RINVEXPIRY-98] _ = x[ESME_RINVDFTMSGID-99] _ = x[ESME_RX_T_APPN-100] _ = x[ESME_RX_P_APPN-101] _ = x[ESME_RX_R_APPN-102] _ = x[ESME_RQUERYFAIL-103] _ = x[ESME_RINVPGCUSTID-128] _ = x[ESME_RINVPGCUSTIDLEN-129] _ = x[ESME_RINVCITYLEN-130] _ = x[ESME_RINVSTATELEN-131] _ = x[ESME_RINVZIPPREFIXLEN-132] _ = x[ESME_RINVZIPPOSTFIXLEN-133] _ = x[ESME_RINVMINLEN-134] _ = x[ESME_RINVMIN-135] _ = x[ESME_RINVPINLEN-136] _ = x[ESME_RINVTERMCODELEN-137] _ = x[ESME_RINVCHANNELLEN-138] _ = x[ESME_RINVCOVREGIONLEN-139] _ = x[ESME_RINVCAPCODELEN-140] _ = x[ESME_RINVMDTLEN-141] _ = x[ESME_RINVPRIORMSGLEN-142] _ = x[ESME_RINVPERMSGLEN-143] _ = x[ESME_RINVPGALERTLEN-144] _ = x[ESME_RINVSMUSERLEN-145] _ = x[ESME_RINVRTDBLEN-146] _ = x[ESME_RINVREGDELLEN-147] _ = x[ESME_RINVMSGDISTLEN-148] _ = x[ESME_RINVPRIORMSG-149] _ = x[ESME_RINVMDT-150] _ = x[ESME_RINVPERMSG-151] _ = x[ESME_RINVMSGDIST-152] _ = x[ESME_RINVPGALERT-153] _ = x[ESME_RINVSMUSER-154] _ = x[ESME_RINVRTDB-155] _ = x[ESME_RINVREGDEL-156] _ = x[ESME_RINVOPTPARLEN-159] _ = x[ESME_RINVOPTPARSTREAM-192] _ = x[ESME_ROPTPARNOTALLWD-193] _ = x[ESME_RINVPARLEN-194] _ = x[ESME_RMISSINGOPTPARAM-195] _ = x[ESME_RINVOPTPARAMVAL-196] _ = x[ESME_RDELIVERYFAILURE-254] _ = x[ESME_RUNKNOWNERR-255] _ = x[ESME_LAST_ERROR-300] } const _CommandStatusType_name = "ESME_ROKESME_RINVMSGLENESME_RINVCMDLENESME_RINVCMDIDESME_RINVBNDSTSESME_RALYBNDESME_RINVPRTFLGESME_RINVREGDLVFLGESME_RSYSERRESME_RINVSRCADRESME_RINVDSTADRESME_RINVMSGIDESME_RBINDFAILESME_RINVPASWDESME_RINVSYSIDESME_RCANCELFAILESME_RREPLACEFAILESME_RMSGQFULESME_RINVSERTYPESME_RADDCUSTFAILESME_RDELCUSTFAILESME_RMODCUSTFAILESME_RENQCUSTFAILESME_RINVCUSTIDESME_RINVCUSTNAMEESME_RINVCUSTADRESME_RINVADRESME_RCUSTEXISTESME_RCUSTNOTEXISTESME_RADDDLFAILESME_RMODDLFAILESME_RDELDLFAILESME_RVIEWDLFAILESME_RLISTDLSFAILESME_RPARAMRETFAILESME_RINVPARAMESME_RINVNUMDESTSESME_RINVDLNAMEESME_RINVDLMEMBDESCESME_RINVDLMEMBTYPESME_RINVDLMODOPTESME_RINVDESTFLAGESME_RINVSUBREPESME_RINVESMCLASSESME_RCNTSUBDLESME_RSUBMITFAILESME_RINVSRCTONESME_RINVSRCNPIESME_RINVDSTTONESME_RINVDSTNPIESME_RINVSYSTYPESME_RINVREPFLAGESME_RINVNUMMSGSESME_RTHROTTLEDESME_RPROVNOTALLWDESME_RINVSCHEDESME_RINVEXPIRYESME_RINVDFTMSGIDESME_RX_T_APPNESME_RX_P_APPNESME_RX_R_APPNESME_RQUERYFAILESME_RINVPGCUSTIDESME_RINVPGCUSTIDLENESME_RINVCITYLENESME_RINVSTATELENESME_RINVZIPPREFIXLENESME_RINVZIPPOSTFIXLENESME_RINVMINLENESME_RINVMINESME_RINVPINLENESME_RINVTERMCODELENESME_RINVCHANNELLENESME_RINVCOVREGIONLENESME_RINVCAPCODELENESME_RINVMDTLENESME_RINVPRIORMSGLENESME_RINVPERMSGLENESME_RINVPGALERTLENESME_RINVSMUSERLENESME_RINVRTDBLENESME_RINVREGDELLENESME_RINVMSGDISTLENESME_RINVPRIORMSGESME_RINVMDTESME_RINVPERMSGESME_RINVMSGDISTESME_RINVPGALERTESME_RINVSMUSERESME_RINVRTDBESME_RINVREGDELESME_RINVOPTPARLENESME_RINVOPTPARSTREAMESME_ROPTPARNOTALLWDESME_RINVPARLENESME_RMISSINGOPTPARAMESME_RINVOPTPARAMVALESME_RDELIVERYFAILUREESME_RUNKNOWNERRESME_LAST_ERROR" var _CommandStatusType_map = map[CommandStatusType]string{ 0: _CommandStatusType_name[0:8], 1: _CommandStatusType_name[8:23], 2: _CommandStatusType_name[23:38], 3: _CommandStatusType_name[38:52], 4: _CommandStatusType_name[52:67], 5: _CommandStatusType_name[67:79], 6: _CommandStatusType_name[79:94], 7: _CommandStatusType_name[94:112], 8: _CommandStatusType_name[112:124], 10: _CommandStatusType_name[124:139], 11: _CommandStatusType_name[139:154], 12: _CommandStatusType_name[154:168], 13: _CommandStatusType_name[168:182], 14: _CommandStatusType_name[182:196], 15: _CommandStatusType_name[196:210], 17: _CommandStatusType_name[210:226], 19: _CommandStatusType_name[226:243], 20: _CommandStatusType_name[243:256], 21: _CommandStatusType_name[256:271], 25: _CommandStatusType_name[271:288], 26: _CommandStatusType_name[288:305], 27: _CommandStatusType_name[305:322], 28: _CommandStatusType_name[322:339], 29: _CommandStatusType_name[339:354], 31: _CommandStatusType_name[354:371], 33: _CommandStatusType_name[371:387], 34: _CommandStatusType_name[387:399], 35: _CommandStatusType_name[399:414], 36: _CommandStatusType_name[414:432], 38: _CommandStatusType_name[432:447], 39: _CommandStatusType_name[447:462], 40: _CommandStatusType_name[462:477], 41: _CommandStatusType_name[477:493], 48: _CommandStatusType_name[493:510], 49: _CommandStatusType_name[510:528], 50: _CommandStatusType_name[528:542], 51: _CommandStatusType_name[542:559], 52: _CommandStatusType_name[559:574], 53: _CommandStatusType_name[574:593], 56: _CommandStatusType_name[593:611], 57: _CommandStatusType_name[611:628], 64: _CommandStatusType_name[628:645], 66: _CommandStatusType_name[645:660], 67: _CommandStatusType_name[660:677], 68: _CommandStatusType_name[677:691], 69: _CommandStatusType_name[691:707], 72: _CommandStatusType_name[707:722], 73: _CommandStatusType_name[722:737], 80: _CommandStatusType_name[737:752], 81: _CommandStatusType_name[752:767], 83: _CommandStatusType_name[767:782], 84: _CommandStatusType_name[782:798], 85: _CommandStatusType_name[798:814], 88: _CommandStatusType_name[814:829], 89: _CommandStatusType_name[829:847], 97: _CommandStatusType_name[847:861], 98: _CommandStatusType_name[861:876], 99: _CommandStatusType_name[876:893], 100: _CommandStatusType_name[893:907], 101: _CommandStatusType_name[907:921], 102: _CommandStatusType_name[921:935], 103: _CommandStatusType_name[935:950], 128: _CommandStatusType_name[950:967], 129: _CommandStatusType_name[967:987], 130: _CommandStatusType_name[987:1003], 131: _CommandStatusType_name[1003:1020], 132: _CommandStatusType_name[1020:1041], 133: _CommandStatusType_name[1041:1063], 134: _CommandStatusType_name[1063:1078], 135: _CommandStatusType_name[1078:1090], 136: _CommandStatusType_name[1090:1105], 137: _CommandStatusType_name[1105:1125], 138: _CommandStatusType_name[1125:1144], 139: _CommandStatusType_name[1144:1165], 140: _CommandStatusType_name[1165:1184], 141: _CommandStatusType_name[1184:1199], 142: _CommandStatusType_name[1199:1219], 143: _CommandStatusType_name[1219:1237], 144: _CommandStatusType_name[1237:1256], 145: _CommandStatusType_name[1256:1274], 146: _CommandStatusType_name[1274:1290], 147: _CommandStatusType_name[1290:1308], 148: _CommandStatusType_name[1308:1327], 149: _CommandStatusType_name[1327:1344], 150: _CommandStatusType_name[1344:1356], 151: _CommandStatusType_name[1356:1371], 152: _CommandStatusType_name[1371:1387], 153: _CommandStatusType_name[1387:1403], 154: _CommandStatusType_name[1403:1418], 155: _CommandStatusType_name[1418:1431], 156: _CommandStatusType_name[1431:1446], 159: _CommandStatusType_name[1446:1464], 192: _CommandStatusType_name[1464:1485], 193: _CommandStatusType_name[1485:1505], 194: _CommandStatusType_name[1505:1520], 195: _CommandStatusType_name[1520:1541], 196: _CommandStatusType_name[1541:1561], 254: _CommandStatusType_name[1561:1582], 255: _CommandStatusType_name[1582:1598], 300: _CommandStatusType_name[1598:1613], } func (i CommandStatusType) String() string { if str, ok := _CommandStatusType_map[i]; ok { return str } return "CommandStatusType(" + strconv.FormatInt(int64(i), 10) + ")" } func (i CommandStatusType) Desc() string { switch i { case ESME_ROK: return "No Error" case ESME_RINVMSGLEN: return "Message Length is invalid" case ESME_RINVCMDLEN: return "Command Length is invalid" case ESME_RINVCMDID: return "Invalid Command ID" case ESME_RINVBNDSTS: return "Incorrect BIND Status for given command" case ESME_RALYBND: return "ESME Already in Bound State" case ESME_RINVPRTFLG: return "Invalid Priority Flag" case ESME_RINVREGDLVFLG: return "Invalid Registered Delivery Flag" case ESME_RSYSERR: return "System Error" case ESME_RINVSRCADR: return "Invalid Source Address" case ESME_RINVDSTADR: return "Invalid Dest Addr" case ESME_RINVMSGID: return "Message ID is invalid" case ESME_RBINDFAIL: return "Bind Failed" case ESME_RINVPASWD: return "Invalid Password" case ESME_RINVSYSID: return "Invalid System ID" case ESME_RCANCELFAIL: return "Cancel SM Failed" case ESME_RREPLACEFAIL: return "Replace SM Failed" case ESME_RMSGQFUL: return "Message Queue Full" case ESME_RINVSERTYP: return "Invalid Service Type" case ESME_RADDCUSTFAIL: return "Failed to Add Customer" case ESME_RDELCUSTFAIL: return "Failed to delete Customer" case ESME_RMODCUSTFAIL: return "Failed to modify customer" case ESME_RENQCUSTFAIL: return "Failed to Enquire Customer" case ESME_RINVCUSTID: return "Invalid Customer ID" case ESME_RINVCUSTNAME: return "Invalid Customer Name" case ESME_RINVCUSTADR: return "Invalid Customer Address" case ESME_RINVADR: return "Invalid Address" case ESME_RCUSTEXIST: return "Customer Exists" case ESME_RCUSTNOTEXIST: return "Customer does not exist" case ESME_RADDDLFAIL: return "Failed to Add DL" case ESME_RMODDLFAIL: return "Failed to modify DL" case ESME_RDELDLFAIL: return "Failed to Delete DL" case ESME_RVIEWDLFAIL: return "Failed to View DL" case ESME_RLISTDLSFAIL: return "Failed to list DLs" case ESME_RPARAMRETFAIL: return "Param Retrieve Failed" case ESME_RINVPARAM: return "Invalid Param" case ESME_RINVNUMDESTS: return "Invalid number of destinations" case ESME_RINVDLNAME: return "Invalid Distribution List name" case ESME_RINVDLMEMBDESC: return "Invalid DL Member Description" case ESME_RINVDLMEMBTYP: return "Invalid DL Member Type" case ESME_RINVDLMODOPT: return "Invalid DL Modify Option" case ESME_RINVDESTFLAG: return "Destination flag is invalid (submit_multi)" case ESME_RINVSUBREP: return "Invalid ‘submit with replace’ request (i.e. submit_sm with replace_if_present_flag set)" case ESME_RINVESMCLASS: return "Invalid esm_class field data" case ESME_RCNTSUBDL: return "Cannot Submit to Distribution List" case ESME_RSUBMITFAIL: return "submit_sm or submit_multi failed" case ESME_RINVSRCTON: return "Invalid Source address TON" case ESME_RINVSRCNPI: return "Invalid Source address NPI" case ESME_RINVDSTTON: return "Invalid Destination address TON" case ESME_RINVDSTNPI: return "Invalid Destination address NPI" case ESME_RINVSYSTYP: return "Invalid system_type field" case ESME_RINVREPFLAG: return "Invalid replace_if_present flag" case ESME_RINVNUMMSGS: return "Invalid number of messages" case ESME_RTHROTTLED: return "Throttling error (ESME has exceeded allowed message limits)" case ESME_RPROVNOTALLWD: return "Provisioning Not Allowed" case ESME_RINVSCHED: return "Invalid Scheduled Delivery Time" case ESME_RINVEXPIRY: return "Invalid message validity period (Expiry time)" case ESME_RINVDFTMSGID: return "Predefined Message Invalid or Not Found" case ESME_RX_T_APPN: return "ESME Receiver Temporary App Error Code" case ESME_RX_P_APPN: return "ESME Receiver Permanent App Error Code" case ESME_RX_R_APPN: return "ESME Receiver Reject Message Error Code" case ESME_RQUERYFAIL: return "query_sm request failed" case ESME_RINVPGCUSTID: return "Paging Customer ID Invalid No such subscriber" case ESME_RINVPGCUSTIDLEN: return "Paging Customer ID length Invalid" case ESME_RINVCITYLEN: return "City Length Invalid" case ESME_RINVSTATELEN: return "State Length Invalid" case ESME_RINVZIPPREFIXLEN: return "Zip Prefix Length Invalid" case ESME_RINVZIPPOSTFIXLEN: return "Zip Postfix Length Invalid" case ESME_RINVMINLEN: return "MIN Length Invalid" case ESME_RINVMIN: return "MIN Invalid (i.e. No such MIN)" case ESME_RINVPINLEN: return "PIN Length Invalid" case ESME_RINVTERMCODELEN: return "Terminal Code Length Invalid" case ESME_RINVCHANNELLEN: return "Channel Length Invalid" case ESME_RINVCOVREGIONLEN: return "Coverage Region Length Invalid" case ESME_RINVCAPCODELEN: return "Cap Code Length Invalid" case ESME_RINVMDTLEN: return "Message delivery time Length Invalid" case ESME_RINVPRIORMSGLEN: return "Priority Message Length Invalid" case ESME_RINVPERMSGLEN: return "Periodic Messages Length Invalid" case ESME_RINVPGALERTLEN: return "Paging Alerts Length Invalid" case ESME_RINVSMUSERLEN: return "int16 Message User Group Length Invalid" case ESME_RINVRTDBLEN: return "Real Time Data broadcasts Length Invalid" case ESME_RINVREGDELLEN: return "Registered Delivery Length Invalid" case ESME_RINVMSGDISTLEN: return "Message Distribution Length Invalid" case ESME_RINVPRIORMSG: return "Priority Message Length Invalid" case ESME_RINVMDT: return "Message delivery time Invalid" case ESME_RINVPERMSG: return "Periodic Messages Invalid" case ESME_RINVMSGDIST: return "Message Distribution Invalid" case ESME_RINVPGALERT: return "Paging Alerts Invalid" case ESME_RINVSMUSER: return "int16 Message User Group Invalid" case ESME_RINVRTDB: return "Real Time Data broadcasts Invalid" case ESME_RINVREGDEL: return "Registered Delivery Invalid" case ESME_RINVOPTPARLEN: return "Invalid Optional Parameter Length" case ESME_RINVOPTPARSTREAM: return "Error in the optional part of the PDU Body." case ESME_ROPTPARNOTALLWD: return "Optional Parameter not allowed" case ESME_RINVPARLEN: return "Invalid Parameter Length." case ESME_RMISSINGOPTPARAM: return "Expected Optional Parameter missing" case ESME_RINVOPTPARAMVAL: return "Invalid Optional Parameter Value" case ESME_RDELIVERYFAILURE: return "Delivery Failure (used for data_sm_resp)" case ESME_RUNKNOWNERR: return "Unknown Error" case ESME_LAST_ERROR: return "The value of the last error code" } return i.String() } func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} _ = x[GENERIC_NACK - -2147483648] _ = x[BIND_RECEIVER-1] _ = x[BIND_RECEIVER_RESP - -2147483647] _ = x[BIND_TRANSMITTER-2] _ = x[BIND_TRANSMITTER_RESP - -2147483646] _ = x[QUERY_SM-3] _ = x[QUERY_SM_RESP - -2147483645] _ = x[SUBMIT_SM-4] _ = x[SUBMIT_SM_RESP - -2147483644] _ = x[DELIVER_SM-5] _ = x[DELIVER_SM_RESP - -2147483643] _ = x[UNBIND-6] _ = x[UNBIND_RESP - -2147483642] _ = x[REPLACE_SM-7] _ = x[REPLACE_SM_RESP - -2147483641] _ = x[CANCEL_SM-8] _ = x[CANCEL_SM_RESP - -2147483640] _ = x[BIND_TRANSCEIVER-9] _ = x[BIND_TRANSCEIVER_RESP - -2147483639] _ = x[OUTBIND-11] _ = x[ENQUIRE_LINK-21] _ = x[ENQUIRE_LINK_RESP - -2147483627] _ = x[SUBMIT_MULTI-33] _ = x[SUBMIT_MULTI_RESP - -2147483615] _ = x[ALERT_NOTIFICATION-258] _ = x[DATA_SM-259] _ = x[DATA_SM_RESP - -2147483389] } const ( _CommandIDType_name_0 = "GENERIC_NACKBIND_RECEIVER_RESPBIND_TRANSMITTER_RESPQUERY_SM_RESPSUBMIT_SM_RESPDELIVER_SM_RESPUNBIND_RESPREPLACE_SM_RESPCANCEL_SM_RESPBIND_TRANSCEIVER_RESP" _CommandIDType_name_1 = "ENQUIRE_LINK_RESP" _CommandIDType_name_2 = "SUBMIT_MULTI_RESP" _CommandIDType_name_3 = "DATA_SM_RESP" _CommandIDType_name_4 = "BIND_RECEIVERBIND_TRANSMITTERQUERY_SMSUBMIT_SMDELIVER_SMUNBINDREPLACE_SMCANCEL_SMBIND_TRANSCEIVER" _CommandIDType_name_5 = "OUTBIND" _CommandIDType_name_6 = "ENQUIRE_LINK" _CommandIDType_name_7 = "SUBMIT_MULTI" _CommandIDType_name_8 = "ALERT_NOTIFICATIONDATA_SM" ) var ( _CommandIDType_index_0 = [...]uint8{0, 12, 30, 51, 64, 78, 93, 104, 119, 133, 154} _CommandIDType_index_4 = [...]uint8{0, 13, 29, 37, 46, 56, 62, 72, 81, 97} _CommandIDType_index_8 = [...]uint8{0, 18, 25} ) func (i CommandIDType) String() string { switch { case -2147483648 <= i && i <= -2147483639: i -= -2147483648 return _CommandIDType_name_0[_CommandIDType_index_0[i]:_CommandIDType_index_0[i+1]] case i == -2147483627: return _CommandIDType_name_1 case i == -2147483615: return _CommandIDType_name_2 case i == -2147483389: return _CommandIDType_name_3 case 1 <= i && i <= 9: i -= 1 return _CommandIDType_name_4[_CommandIDType_index_4[i]:_CommandIDType_index_4[i+1]] case i == 11: return _CommandIDType_name_5 case i == 21: return _CommandIDType_name_6 case i == 33: return _CommandIDType_name_7 case 258 <= i && i <= 259: i -= 258 return _CommandIDType_name_8[_CommandIDType_index_8[i]:_CommandIDType_index_8[i+1]] default: return "CommandIDType(" + strconv.FormatInt(int64(i), 10) + ")" } } ================================================ FILE: data/other_codings.go ================================================ package data import ( "golang.org/x/text/encoding/unicode" ) var ( // UTF16BEM is UTF-16 Big Endian with BOM (byte order mark). UTF16BEM = &utf16BEM{} // UTF16LEM is UTF-16 Little Endian with BOM. UTF16LEM = &utf16LEM{} // UTF16BE is UTF-16 Big Endian without BOM. UTF16BE = &utf16BE{} // UTF16LE is UTF-16 Little Endian without BOM. UTF16LE = &utf16LE{} ) type utf16BEM struct{} func (c utf16BEM) Encode(str string) ([]byte, error) { tmp := unicode.UTF16(unicode.BigEndian, unicode.UseBOM) return encode(str, tmp.NewEncoder()) } func (c utf16BEM) Decode(data []byte) (string, error) { tmp := unicode.UTF16(unicode.BigEndian, unicode.UseBOM) return decode(data, tmp.NewDecoder()) } type utf16LEM struct{} func (c utf16LEM) Encode(str string) ([]byte, error) { tmp := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM) return encode(str, tmp.NewEncoder()) } func (c utf16LEM) Decode(data []byte) (string, error) { tmp := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM) return decode(data, tmp.NewDecoder()) } type utf16BE struct{} func (c utf16BE) Encode(str string) ([]byte, error) { tmp := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM) return encode(str, tmp.NewEncoder()) } func (c utf16BE) Decode(data []byte) (string, error) { tmp := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM) return decode(data, tmp.NewDecoder()) } type utf16LE struct{} func (c utf16LE) Encode(str string) ([]byte, error) { tmp := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM) return encode(str, tmp.NewEncoder()) } func (c utf16LE) Decode(data []byte) (string, error) { tmp := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM) return decode(data, tmp.NewDecoder()) } ================================================ FILE: data/pkg.go ================================================ package data import ( "fmt" "sync/atomic" ) //nolint const ( SM_CONNID_LEN = 16 SM_MSG_LEN = 254 SM_SYSID_LEN = 16 SM_MSGID_LEN = 64 SM_PASS_LEN = 9 SM_DATE_LEN = 17 SM_SRVTYPE_LEN = 6 SM_SYSTYPE_LEN = 13 SM_ADDR_LEN = 21 SM_DATA_ADDR_LEN = 65 SM_ADDR_RANGE_LEN = 41 SM_TYPE_LEN = 13 SM_DL_NAME_LEN = 21 SM_PARAM_NAME_LEN = 10 SM_PARAM_VALUE_LEN = 10 SM_MAX_CNT_DEST_ADDR = 254 // GSM specific, short message must be no larger than 140 octets SM_GSM_MSG_LEN = 140 CONNECTION_CLOSED = 0 CONNECTION_OPENED = 1 SM_ACK = 1 SM_NO_ACK = 0 SM_RESPONSE_ACK = 0 SM_RESPONSE_TNACK = 1 SM_RESPONSE_PNACK = 2 // Interface_Version SMPP_V33 int8 = int8(-0x33) SMPP_V34 = byte(0x34) // Address_TON GSM_TON_UNKNOWN = byte(0x00) GSM_TON_INTERNATIONAL = byte(0x01) GSM_TON_NATIONAL = byte(0x02) GSM_TON_NETWORK = byte(0x03) GSM_TON_SUBSCRIBER = byte(0x04) GSM_TON_ALPHANUMERIC = byte(0x05) GSM_TON_ABBREVIATED = byte(0x06) GSM_TON_RESERVED_EXTN = byte(0x07) // Address_NPI GSM_NPI_UNKNOWN = byte(0x00) GSM_NPI_E164 = byte(0x01) GSM_NPI_ISDN = GSM_NPI_E164 GSM_NPI_X121 = byte(0x03) GSM_NPI_TELEX = byte(0x04) GSM_NPI_LAND_MOBILE = byte(0x06) GSM_NPI_NATIONAL = byte(0x08) GSM_NPI_PRIVATE = byte(0x09) GSM_NPI_ERMES = byte(0x0A) GSM_NPI_INTERNET = byte(0x0E) GSM_NPI_WAP_CLIENT_ID = byte(0x12) GSM_NPI_RESERVED_EXTN = byte(0x0F) // Service_Type SERVICE_NULL string = "" SERVICE_CMT string = "CMT" SERVICE_CPT string = "CPT" SERVICE_VMN string = "VMN" SERVICE_VMA string = "VMA" SERVICE_WAP string = "WAP" SERVICE_USSD string = "USSD" SMPP_PROTOCOL = byte(1) SMPPP_PROTOCOL = byte(2) SM_SERVICE_MOBILE_TERMINATED = byte(0) SM_SERVICE_MOBILE_ORIGINATED = byte(1) SM_SERVICE_MOBILE_TRANSCEIVER = byte(2) // State of message at SMSC SM_STATE_EN_ROUTE = 1 // default state for messages in transit SM_STATE_DELIVERED = 2 // message is delivered SM_STATE_EXPIRED = 3 // validity period expired SM_STATE_DELETED = 4 // message has been deleted SM_STATE_UNDELIVERABLE = 5 // undeliverable SM_STATE_ACCEPTED = 6 // message is in accepted state SM_STATE_INVALID = 7 // message is in invalid state SM_STATE_REJECTED = 8 // message is in rejected state //****************** // ESMClass Defines //****************** // Messaging Mode SM_ESM_DEFAULT = 0x00 // Default SMSC Mode or Message Type SM_DATAGRAM_MODE = 0x01 // Use one-shot express mode SM_FORWARD_MODE = 0x02 // Do not use SM_STORE_FORWARD_MODE = 0x03 // Use store & forward // Send/Receive TDMA & CDMA Message Type SM_SMSC_DLV_RCPT_TYPE = 0x04 // Recv Msg contains SMSC delivery receipt SM_ESME_DLV_ACK_TYPE = 0x08 // Send/Recv Msg contains ESME delivery acknowledgement SM_ESME_MAN_USER_ACK_TYPE = 0x10 // Send/Recv Msg contains manual/user acknowledgment SM_CONV_ABORT_TYPE = 0x18 // Recv Msg contains conversation abort (Korean CDMA) SM_INTMD_DLV_NOTIFY_TYPE = 0x20 // Recv Msg contains intermediate notification // GSM Network features SM_NONE_GSM = 0x00 // No specific features selected SM_UDH_GSM = 0x40 // User Data Header indicator set SM_REPLY_PATH_GSM = 0x80 // Reply path set SM_UDH_REPLY_PATH_GSM = 0xC0 // Both UDH & Reply path // Optional Parameter Tags, Min and Max Lengths // Following are the 2 byte tag and min/max lengths for // supported optional parameter (declann) OPT_PAR_MSG_WAIT = 2 // Privacy Indicator OPT_PAR_PRIV_IND = 0x0201 // Source Subaddress OPT_PAR_SRC_SUBADDR = 0x0202 OPT_PAR_SRC_SUBADDR_MIN = 2 OPT_PAR_SRC_SUBADDR_MAX = 23 // Destination Subaddress OPT_PAR_DEST_SUBADDR = 0x0203 OPT_PAR_DEST_SUBADDR_MIN = 2 OPT_PAR_DEST_SUBADDR_MAX = 23 // User Message Reference OPT_PAR_USER_MSG_REF = 0x0204 // User Response Code OPT_PAR_USER_RESP_CODE = 0x0205 // Language Indicator OPT_PAR_LANG_IND = 0x020D // Source Port OPT_PAR_SRC_PORT = 0x020A // Destination Port OPT_PAR_DST_PORT = 0x020B // Concat Msg Ref Num OPT_PAR_SAR_MSG_REF_NUM = 0x020C // Concat Total Segments OPT_PAR_SAR_TOT_SEG = 0x020E // Concat Segment Seqnums OPT_PAR_SAR_SEG_SNUM = 0x020F // SC Interface Version OPT_PAR_SC_IF_VER = 0x0210 // Display Time OPT_PAR_DISPLAY_TIME = 0x1201 // Validity Information OPT_PAR_MS_VALIDITY = 0x1204 // DPF Result OPT_PAR_DPF_RES = 0x0420 // Set DPF OPT_PAR_SET_DPF = 0x0421 // MS Availability Status OPT_PAR_MS_AVAIL_STAT = 0x0422 // Network Error Code OPT_PAR_NW_ERR_CODE = 0x0423 OPT_PAR_NW_ERR_CODE_MIN = 3 OPT_PAR_NW_ERR_CODE_MAX = 3 // Extended int16 Message has no size limit // Delivery Failure Reason OPT_PAR_DEL_FAIL_RSN = 0x0425 // More Messages to Follow OPT_PAR_MORE_MSGS = 0x0426 // Message State OPT_PAR_MSG_STATE = 0x0427 // Callback Number OPT_PAR_CALLBACK_NUM = 0x0381 OPT_PAR_CALLBACK_NUM_MIN = 4 OPT_PAR_CALLBACK_NUM_MAX = 19 // Callback Number Presentation Indicator OPT_PAR_CALLBACK_NUM_PRES_IND = 0x0302 // Callback Number Alphanumeric Tag OPT_PAR_CALLBACK_NUM_ATAG = 0x0303 OPT_PAR_CALLBACK_NUM_ATAG_MIN = 1 OPT_PAR_CALLBACK_NUM_ATAG_MAX = 65 // Number of messages in Mailbox OPT_PAR_NUM_MSGS = 0x0304 // SMS Received Alert OPT_PAR_SMS_SIGNAL = 0x1203 // Message Delivery Alert OPT_PAR_ALERT_ON_MSG_DELIVERY = 0x130C // ITS Reply Type OPT_PAR_ITS_REPLY_TYPE = 0x1380 // ITS Session Info OPT_PAR_ITS_SESSION_INFO = 0x1383 // USSD Service Op OPT_PAR_USSD_SER_OP = 0x0501 // Priority SM_NOPRIORITY = 0 SM_PRIORITY = 1 // Registered delivery // SMSC Delivery Receipt (bits 1 & 0) SM_SMSC_RECEIPT_MASK = byte(0x03) SM_SMSC_RECEIPT_NOT_REQUESTED = byte(0x00) SM_SMSC_RECEIPT_REQUESTED = byte(0x01) SM_SMSC_RECEIPT_ON_FAILURE = byte(0x02) // SME originated acknowledgement (bits 3 & 2) SM_SME_ACK_MASK = byte(0x0c) SM_SME_ACK_NOT_REQUESTED = byte(0x00) SM_SME_ACK_DELIVERY_REQUESTED = byte(0x04) SM_SME_ACK_MANUAL_REQUESTED = byte(0x08) SM_SME_ACK_BOTH_REQUESTED = byte(0x0c) // Intermediate notification (bit 5) SM_NOTIF_MASK = byte(0x010) SM_NOTIF_NOT_REQUESTED = byte(0x000) SM_NOTIF_REQUESTED = byte(0x010) // Replace if Present flag SM_NOREPLACE = 0 SM_REPLACE = 1 // Destination flag SM_DEST_SME_ADDRESS = 1 SM_DEST_DL_NAME = 2 // Higher Layer Message Type SM_LAYER_WDP = 0 SM_LAYER_WCMP = 1 // Operation Class SM_OPCLASS_DATAGRAM = 0 SM_OPCLASS_TRANSACTION = 3 // Originating MSC Address OPT_PAR_ORIG_MSC_ADDR = -32639 // int16(0x8081) OPT_PAR_ORIG_MSC_ADDR_MIN = 1 OPT_PAR_ORIG_MSC_ADDR_MAX = 24 // Destination MSC Address OPT_PAR_DEST_MSC_ADDR = -32638 // int16(0x8082) OPT_PAR_DEST_MSC_ADDR_MIN = 1 OPT_PAR_DEST_MSC_ADDR_MAX = 24 // Unused Tag OPT_PAR_UNUSED = 0xffff // Destination Address Subunit OPT_PAR_DST_ADDR_SUBUNIT = 0x0005 // Destination Network Type OPT_PAR_DST_NW_TYPE = 0x0006 // Destination Bearer Type OPT_PAR_DST_BEAR_TYPE = 0x0007 // Destination Telematics ID OPT_PAR_DST_TELE_ID = 0x0008 // Source Address Subunit OPT_PAR_SRC_ADDR_SUBUNIT = 0x000D // Source Network Type OPT_PAR_SRC_NW_TYPE = 0x000E // Source Bearer Type OPT_PAR_SRC_BEAR_TYPE = 0x000F // Source Telematics ID OPT_PAR_SRC_TELE_ID = 0x0010 // QOS Time to Live OPT_PAR_QOS_TIME_TO_LIVE = 0x0017 OPT_PAR_QOS_TIME_TO_LIVE_MIN = 1 OPT_PAR_QOS_TIME_TO_LIVE_MAX = 4 // Payload Type OPT_PAR_PAYLOAD_TYPE = 0x0019 // Additional Status Info Text OPT_PAR_ADD_STAT_INFO = 0x001D OPT_PAR_ADD_STAT_INFO_MIN = 1 OPT_PAR_ADD_STAT_INFO_MAX = 256 // Receipted Message ID OPT_PAR_RECP_MSG_ID = 0x001E OPT_PAR_RECP_MSG_ID_MIN = 1 OPT_PAR_RECP_MSG_ID_MAX = 65 // Message Payload OPT_PAR_MSG_PAYLOAD = 0x0424 OPT_PAR_MSG_PAYLOAD_MIN = 1 OPT_PAR_MSG_PAYLOAD_MAX = 1500 // User Data Header UDH_CONCAT_MSG_8_BIT_REF = byte(0x00) UDH_CONCAT_MSG_16_BIT_REF = byte(0x08) /** * @deprecated As of version 1.3 of the library there are defined * new encoding constants for base set of encoding supported by Java Runtime. * The CHAR_ENC is replaced by ENC_ASCII * and redefined in this respect. */ DFLT_MSGID string = "" DFLT_MSG string = "" DFLT_SRVTYPE string = "" DFLT_SYSID string = "" DFLT_PASS string = "" DFLT_SYSTYPE string = "" DFLT_ADDR_RANGE string = "" DFLT_DATE string = "" DFLT_ADDR string = "" DFLT_MSG_STATE byte = 0 DFLT_ERR byte = 0 DFLT_SCHEDULE string = "" DFLT_VALIDITY string = "" DFLT_REG_DELIVERY = SM_SMSC_RECEIPT_NOT_REQUESTED | SM_SME_ACK_NOT_REQUESTED | SM_NOTIF_NOT_REQUESTED DFLT_DFLTMSGID = byte(0) DFLT_MSG_LEN = byte(0) DFLT_ESM_CLASS = byte(0) DFLT_DATA_CODING = byte(0) DFLT_PROTOCOLID = byte(0) DFLT_PRIORITY_FLAG = byte(0) DFTL_REPLACE_IFP = byte(0) DFLT_DL_NAME string = "" DFLT_GSM_TON = GSM_TON_UNKNOWN DFLT_GSM_NPI = GSM_NPI_UNKNOWN DFLT_DEST_FLAG = byte(0) // not set MAX_PDU_LEN = 64 << 10 PDU_HEADER_SIZE = 16 // 4 integers TLV_HEADER_SIZE = 4 // 2 int16s: tag & length // all times in milliseconds RECEIVER_TIMEOUT int64 = 60000 CONNECTION_RECEIVE_TIMEOUT int64 = 10000 UNBIND_RECEIVE_TIMEOUT int64 = 5000 CONNECTION_SEND_TIMEOUT int64 = 20000 COMMS_TIMEOUT int64 = 60000 QUEUE_TIMEOUT int64 = 10000 ACCEPT_TIMEOUT int64 = 60000 RECEIVE_BLOCKING int64 = -1 MAX_VALUE_PORT = 65535 MIN_VALUE_PORT = 100 MIN_LENGTH_ADDRESS = 7 ) var ( // ErrNotImplSplitterInterface indicates that encoding does not support Splitter interface ErrNotImplSplitterInterface = fmt.Errorf("Encoding not implementing Splitter interface") // ErrNotImplDecode indicates that encoding does not support Decode method ErrNotImplDecode = fmt.Errorf("Decode is not implemented in this Encoding") // ErrNotImplEncode indicates that encoding does not support Encode method ErrNotImplEncode = fmt.Errorf("Encode is not implemented in this Encoding") ) var defaultTon atomic.Value var defaultNpi atomic.Value func init() { defaultTon.Store(DFLT_GSM_TON) defaultNpi.Store(DFLT_GSM_NPI) } // SetDefaultTon set default ton. func SetDefaultTon(dfltTon byte) { defaultTon.Store(dfltTon) } // GetDefaultTon get default ton. func GetDefaultTon() byte { return defaultTon.Load().(byte) } // SetDefaultNpi set default npi. func SetDefaultNpi(dfltNpi byte) { defaultNpi.Store(dfltNpi) } // GetDefaultNpi get default npi. func GetDefaultNpi() byte { return defaultNpi.Load().(byte) } ================================================ FILE: data/pkg_test.go ================================================ package data import ( "testing" "github.com/stretchr/testify/require" ) func TestDefaultNpi(t *testing.T) { SetDefaultNpi(13) require.EqualValues(t, 13, GetDefaultNpi()) } func TestDefaultTon(t *testing.T) { SetDefaultTon(19) require.EqualValues(t, 19, GetDefaultTon()) } ================================================ FILE: data/utils.go ================================================ package data import "unicode" // FindEncoding returns suitable encoding for a string. // If string is ascii, then GSM7Bit. If not, then UCS2. func FindEncoding(s string) (enc Encoding) { if isASCII(s) { enc = GSM7BIT } else { enc = UCS2 } return } func isASCII(s string) bool { for i := 0; i < len(s); i++ { if s[i] > unicode.MaxASCII { return false } } return true } ================================================ FILE: data/utils_test.go ================================================ package data import ( "testing" "github.com/stretchr/testify/require" ) func TestFindEncoding(t *testing.T) { require.Equal(t, GSM7BIT, FindEncoding("abc30hb3bk2lopzSD=2-^")) require.Equal(t, UCS2, FindEncoding("Trần Lập và ban nhạc Bức tường huyền thoại")) require.Equal(t, UCS2, FindEncoding("Đừng buồn thế dù ngoài kia vẫn mưa nghiễng rợi tý tỵ")) } ================================================ FILE: errors/pkg.go ================================================ package errors import ( "fmt" "github.com/linxGnu/gosmpp/data" ) // SmppErr indicates smpp error(s), compatible with OpenSMPP. type SmppErr struct { err string serialVersionUID int64 } // Error interface. func (s *SmppErr) Error() string { return fmt.Sprintf("Error happened: [%s]. SerialVersionUID: [%d]", s.err, s.serialVersionUID) } var ( // ErrInvalidPDU indicates invalid pdu payload. ErrInvalidPDU error = &SmppErr{err: "PDU payload is invalid", serialVersionUID: -6985061862208729984} // ErrUnknownCommandID indicates unknown command id. ErrUnknownCommandID error = &SmppErr{err: "Unknown command id", serialVersionUID: -5091873576710864441} // ErrWrongDateFormat indicates wrong date format. ErrWrongDateFormat error = &SmppErr{err: "Wrong date format", serialVersionUID: 5831937612139037591} // ErrShortMessageLengthTooLarge indicates short message length is too large. ErrShortMessageLengthTooLarge error = &SmppErr{err: fmt.Sprintf("Encoded short message data exceeds size of %d", data.SM_MSG_LEN), serialVersionUID: 78237205927624} // ErrUDHTooLong UDH-L is larger than total length of short message data ErrUDHTooLong = fmt.Errorf("User Data Header is too long for PDU short message") ) ================================================ FILE: errors/pkg_test.go ================================================ package errors import ( "strings" "testing" "github.com/stretchr/testify/require" ) func TestErr(t *testing.T) { require.True(t, strings.HasPrefix(ErrInvalidPDU.Error(), "Error happened: [")) } ================================================ FILE: example/smsc_simulator/smsc.cpp ================================================ // // smscsimulator.cpp // SMPPLib // // Created by Mark Hay on 12/05/2019. // Copyright © 2019 Melrose Labs. All rights reserved. // // Build: g++ smscsimulator.cpp -o MLSMSCSimulator && ./MLSMSCSimulator #include #include #include #include #include #include #include #include using namespace std; #include #include #include #include #include #include #include #include #define TRUE (1 == 1) #define FALSE (!TRUE) // uint64_t session_id_next = 0; // ID for each session // General uint64_t currentUSecsSinceEpoch(void) { struct timeval tv; gettimeofday(&tv, NULL); return (uint64_t)tv.tv_sec * 1000000 + (uint64_t)tv.tv_usec; } // Client config class ClientConfig { private: ClientConfig() {} ~ClientConfig() {} public: static ClientConfig& instance() { static ClientConfig c; return c; } typedef enum { BINDRESP_WITH_SC_INTERACE_VERSION_TLV } ConfigEntry; bool is(string esme_id, ConfigEntry entry) { return false; } }; // Message deliverer class MessageDeliverer { public: class Message { public: uint8_t source_addr_ton; uint8_t source_addr_npi; char source_addr[32]; uint8_t dest_addr_ton; uint8_t dest_addr_npi; char destination_addr[32]; uint8_t esm_class; uint8_t protocol_id; uint8_t priority_flag; char schedule_delivery_time[32]; char validity_period[32]; uint8_t registered_delivery; uint8_t replace_if_present_flag; uint8_t data_coding; uint8_t sm_default_msg_id; uint8_t sm_length; uint8_t short_message[160]; string smscMessageID; private: string content; public: Message() {} Message(char* in) { content = in; } ~Message() {} string getContent(void) { return content; } void setSource(uint8_t ton, uint8_t npi, char* addr) { source_addr_ton = ton; source_addr_npi = npi; strcpy(source_addr, addr); } void setDestination(uint8_t ton, uint8_t npi, char* addr) { dest_addr_ton = ton; dest_addr_npi = npi; strcpy(destination_addr, addr); } void setRegisteredDelivery(uint8_t val) { registered_delivery = val; } void setShortMessage(uint8_t* sm_in, uint8_t sm_len_in) { memcpy(short_message, sm_in, sm_len_in); sm_length = sm_len_in; } void setSMSCMessageID(char* id) { smscMessageID = id; } }; private: // time-ordered list [using STL map] of lists containing messages typedef map> MessageQueue; MessageQueue mq; MessageDeliverer() {} typedef map MDMap; static MDMap mapInstance; bool empty(void) { return (mq.size() == 0); } bool get(Message& msgout) { if (mq.size() == 0) return false; // no time-lists uint64_t firstListTime = (*mq.begin()).first; uint64_t now = currentUSecsSinceEpoch(); if (firstListTime > now) return false; // nothing due to be deliverered // we have a message (or messages) that are due to be delivered msgout = (*mq.begin()).second.front(); (*mq.begin()).second.pop_front(); // remove from list if ((*mq.begin()).second.size() == 0) // now empty - remove list mq.erase(mq.begin()); return true; } public: ~MessageDeliverer() {} static MessageDeliverer* getInstance(string& systemID) { MDMap::iterator it = mapInstance.find(systemID); if (it == mapInstance.end()) { std::pair res = mapInstance.insert(MDMap::value_type(systemID, new MessageDeliverer)); return (res.first)->second; } else return (*it).second; } static bool getInstance_get(string& systemID, Message& msgout) { MDMap::iterator it = mapInstance.find(systemID); if (it == mapInstance.end()) { return false; } bool found = (*it).second->get(msgout); if ((*it).second->empty()) { delete (*it).second; mapInstance.erase(systemID); } return found; } void add(uint64_t timeDeliver, Message msg) { MessageQueue::iterator it = mq.find(timeDeliver); if (it == mq.end()) { // no time-list exists - create list msgList; // empty message list std::pair ret; ret = mq.insert({timeDeliver, msgList}); if (ret.second == false) return; // failed to insert it = ret.first; // iterator to newly inserted time-list } (*it).second.push_back(msg); // add message to time-list } }; MessageDeliverer::MDMap MessageDeliverer::mapInstance; // Sub-SMPP layer class SMPPSocket { protected: int socket; public: SMPPSocket() {} virtual ~SMPPSocket() {} virtual bool recvA(uint8_t&) = 0; virtual void recv(void) = 0; virtual bool send(uint8_t*, int len) = 0; virtual long bytes_to_read(void) = 0; }; class SMPPSocketUnencrypted : public SMPPSocket { private: public: SMPPSocketUnencrypted() {} SMPPSocketUnencrypted(int socket_in) { socket = socket_in; } ~SMPPSocketUnencrypted() {} long bytes_to_read(void) { long count = 0; ioctl((int)socket, FIONREAD, &count); return count; } bool recvA(uint8_t& oct) { int n = (int)::recv(socket, (void*)&oct, 1, 0); return (n > 0); } void recv(void) {} bool send(uint8_t* buf, int len) { return ::send(socket, (void*)buf, len, 0); } }; // SMPP class SMPP { public: class CmdStatus { public: static const uint64_t ESME_ROK = 0x00000000; static const uint64_t ESME_RINVBNDSTS = 0x00000004; static const uint64_t ESME_INVDSTADR = 0x0000000B; static const uint64_t ESME_RBINDFAIL = 0x0000000D; static const uint64_t ESME_RINVSYSID = 0x0000000F; static const uint64_t ESME_RSUBMITFAIL = 0x00000045; static const uint64_t ESME_RINVSCHED = 0x00000061; }; class CmdID { public: static const uint64_t BindReceiver = 0x00000001; static const uint64_t BindTransmitter = 0x00000002; static const uint64_t QuerySM = 0x00000003; static const uint64_t SubmitSM = 0x00000004; static const uint64_t DeliverSM = 0x00000005; static const uint64_t Unbind = 0x00000006; static const uint64_t ReplaceSM = 0x00000007; static const uint64_t CancelSM = 0x00000008; static const uint64_t BindTransceiver = 0x00000009; static const uint64_t Outbind = 0x0000000B; static const uint64_t EnquireLink = 0x00000015; static const uint64_t SubmitMulti = 0x00000021; static const uint64_t AlertNotification = 0x00000102; static const uint64_t DataSM = 0x00000103; static const uint64_t BroadcastSM = 0x00000111; static const uint64_t QueryBroadcastSM = 0x00000112; static const uint64_t CancelBroadcastSM = 0x00000113; static const uint64_t GenericNack = 0x80000000; static const uint64_t BindReceiverResp = 0x80000001; static const uint64_t BindTransmitterResp = 0x80000002; static const uint64_t QuerySMResp = 0x80000003; static const uint64_t SubmitSMResp = 0x80000004; static const uint64_t DeliverSMResp = 0x80000005; static const uint64_t UnbindResp = 0x80000006; static const uint64_t ReplaceSMResp = 0x80000007; static const uint64_t CancelSMResp = 0x80000008; static const uint64_t BindTransceiverResp = 0x80000009; static const uint64_t EnquireLinkResp = 0x80000015; static const uint64_t SubmitMultiResp = 0x80000021; static const uint64_t DataSMResp = 0x80000103; static const uint64_t BroadcastSMResp = 0x80000111; static const uint64_t QueryBroadcastSMResp = 0x80000112; static const uint64_t CancelBroadcastSMResp = 0x80000113; }; typedef struct { uint64_t cmdid; const char* name; } CmdStrings; #define CMDEXP(A) CmdID::A, #A CmdStrings cmdStrings[34] = {{CMDEXP(BindReceiver)}, {CMDEXP(BindTransmitter)}, {CMDEXP(QuerySM)}, {CMDEXP(SubmitSM)}, {CMDEXP(DeliverSM)}, {CMDEXP(Unbind)}, {CMDEXP(ReplaceSM)}, {CMDEXP(CancelSM)}, {CMDEXP(BindTransceiver)}, {CMDEXP(Outbind)}, {CMDEXP(EnquireLink)}, {CMDEXP(SubmitMulti)}, {CMDEXP(AlertNotification)}, {CMDEXP(DataSM)}, {CMDEXP(BroadcastSM)}, {CMDEXP(QueryBroadcastSM)}, {CMDEXP(CancelBroadcastSM)}, {CMDEXP(GenericNack)}, {CMDEXP(BindReceiverResp)}, {CMDEXP(BindTransmitterResp)}, {CMDEXP(QuerySMResp)}, {CMDEXP(SubmitSMResp)}, {CMDEXP(DeliverSMResp)}, {CMDEXP(UnbindResp)}, {CMDEXP(ReplaceSMResp)}, {CMDEXP(CancelSMResp)}, {CMDEXP(BindTransceiverResp)}, {CMDEXP(EnquireLinkResp)}, {CMDEXP(SubmitMultiResp)}, {CMDEXP(DataSMResp)}, {CMDEXP(BroadcastSMResp)}, {CMDEXP(QueryBroadcastSMResp)}, {CMDEXP(CancelBroadcastSMResp)}}; const char* cmdString(uint64_t id) { for (int i = 0; i < 34; i++) { if (cmdStrings[i].cmdid == id) return cmdStrings[i].name; } return ""; } static void GSMTimeStringShort(time_t& t, char* szTimestamp, int nLen) { if (t < 0) return; szTimestamp[0] = 0x00; tm* ptm = gmtime(&t); if (ptm == NULL) szTimestamp[0] = 0x00; else strftime(szTimestamp, nLen, "%y%m%d%H%M", ptm); } static time_t GSMStringTime(const char* szBuf) { tm atm; int tdif = 0; char chDir = 0; memset(&atm, 0, sizeof(tm)); int nRead = sscanf(szBuf, "%02d%02d%02d%02d%02d%02d%*1d%02d%c", &atm.tm_year, &atm.tm_mon, &atm.tm_mday, &atm.tm_hour, &atm.tm_min, &atm.tm_sec, &tdif, &chDir); time_t t = 0; if (nRead == 5) { if (atm.tm_year >= 70) atm.tm_year += 1900; else atm.tm_year += 2000; t = timegm(&atm); if (t == -1) { // invalid time t = 0; } if (chDir == '+') { t = t - (tdif * 15 * 60); } if (chDir == '-') { t = t + (tdif * 15 * 60); } } return t; } static time_t GSMRelativeTime(char* szBuf) { time_t t = 0; int mday, hour, min, sec; int nRead = sscanf(szBuf, "%*02d%*02d%02d%02d%02d%02d%*1d%*02d%*c", &mday, &hour, &min, &sec); if (nRead == 4) t = ((mday * 24 + hour) * 60 + min) * 60 + sec; return t; } }; class SMPPConnection { public: class SMPPException {}; private: const int max_command_body_length = 1024; bool debug = false; SMPPSocket* socket = NULL; char ip[64]; typedef enum { PV_INVALID, PV_LENGTH, PV_ALL } PDUValidity; typedef struct { PDUValidity valid; // indicate if this header structure has valid contents uint64_t command_length; uint64_t command_id; uint64_t command_status; uint64_t sequence_number; } PDUHeader; typedef struct { bool valid; uint8_t* body; } PDUBody; PDUHeader command_received_header = {PV_INVALID, 0, 0, 0, 0}; PDUBody command_received_body = {false, NULL}; // configuration int enquire_link_period = 0; // seconds // methods uint64_t getInteger(void) { if (socket->bytes_to_read() < 4) throw SMPPException(); uint8_t v0, v1, v2, v3; socket->recvA(v0); socket->recvA(v1); socket->recvA(v2); socket->recvA(v3); return (uint64_t)(((v0 << 8) | v1) << 8 | v2) << 8 | v3; } uint64_t getBytes(uint64_t len, uint8_t* mem) { if (socket->bytes_to_read() < len) throw SMPPException(); for (int i = 0; i < len; i++) { if (socket->recvA(mem[i]) == 0) throw SMPPException(); } return 0; } public: SMPPConnection() { socket = NULL; command_received_header.valid = PV_INVALID; command_received_body.body = new uint8_t[max_command_body_length]; ip[0] = '\0'; } ~SMPPConnection() { if (command_received_body.body) delete[] command_received_body.body; if (socket) delete socket; } void setDebug(bool val) { debug = val; } void allocateSocket(void) { socket = new SMPPSocketUnencrypted; } void allocateSocket(int fdsocket) { socket = new SMPPSocketUnencrypted(fdsocket); } void setIP(char* ip_in) { if (ip_in != NULL) strcpy(ip, ip_in); else strcpy(ip, ""); } char* getIP() { return ip; } uint64_t endian(uint64_t a) { uint64_t b; ((uint8_t*)&b)[0] = ((uint8_t*)&a)[3]; ((uint8_t*)&b)[1] = ((uint8_t*)&a)[2]; ((uint8_t*)&b)[2] = ((uint8_t*)&a)[1]; ((uint8_t*)&b)[3] = ((uint8_t*)&a)[0]; return b; } bool put(uint64_t sequence_number, uint64_t cmdID, uint64_t status, uint8_t* param, int len) { int pdu_len = 16 + len; uint8_t buf[pdu_len]; *((uint64_t*)(buf + 0)) = endian(pdu_len); // command length *((uint64_t*)(buf + 4)) = endian(cmdID); // command ID *((uint64_t*)(buf + 8)) = endian(status); // command status *((uint64_t*)(buf + 12)) = endian(sequence_number); // sequence number if ((param != NULL) && (len != 0)) memcpy((char*)(buf + 16), param, len); // for(int i=0;isend(buf, pdu_len); } uint8_t* getBodyPointer(int& len) { len = (int)(command_received_header.command_length - 16); return command_received_body.body; } bool get() { // require "socket" if (socket == NULL) return false; // get header if (command_received_header.valid != PV_ALL) { // don't have any of the header yet try { command_received_header.command_length = getInteger(); command_received_header.command_id = getInteger(); command_received_header.command_status = getInteger(); command_received_header.sequence_number = getInteger(); command_received_header.valid = PV_ALL; // all header values are now valid } catch (SMPPException e) { // no data if (debug) std::cout << "HEADER No data - ABORT!" << std::endl; return false; } } if (command_received_header.valid != PV_ALL) return false; // don't let past here until we have the header // get body (if present) command_received_body.valid = false; uint64_t body_length = command_received_header.command_length - 16; if (body_length > 0) { try { getBytes(body_length, command_received_body.body); } catch (SMPPException e) { if (debug) std::cout << "BODY No data" << std::endl; return false; } command_received_body.valid = true; command_received_header.valid = PV_INVALID; return true; } else { // empty body command_received_body.valid = true; // empty body (valid) command_received_body.body = NULL; command_received_header.valid = PV_INVALID; return true; } return false; } uint64_t pduCommandID(void) { return command_received_header.command_id; } uint64_t pduSequenceNo(void) { return command_received_header.sequence_number; } }; class Session { protected: int sessionType; public: Session() {} virtual ~Session() {} virtual bool timedCheck(void) = 0; virtual bool run(void) = 0; }; class AdminSession : public Session { public: AdminSession() { sessionType = 0; } AdminSession(int fdsocket) { // conn.allocateSocket(fdsocket); } ~AdminSession() {} bool timedCheck(void) { // return true if to close return true; } bool run(void) { return true; } }; class SMPPSession : public Session { private: SMPPConnection conn; uint64_t session_id; // session state string system_id; string system_type; long sequence_number_in = -1; long sequence_number_out = 1; uint64_t lastPDUFromESMETime = 0; uint64_t enquireLinkRespPending = 0; uint64_t closingTime = 0; public: typedef enum { BS_NONE, BS_TRX, BS_TX, BS_RX } BindState; uint8_t version; BindState bindState; public: SMPPSession() { sessionType = 1; bindState = BS_NONE; version = 0x00; session_id = session_id_next++; } SMPPSession(int fdsocket, char* ip) { bindState = BS_NONE; version = 0x00; conn.allocateSocket(fdsocket); conn.setIP(ip); session_id = session_id_next++; } ~SMPPSession() { if (bindState != BS_NONE) { logCommand("session aborted", "--"); } } void setDebug(bool val) { conn.setDebug(val); } bool getCOctetString(uint8_t* buf_ptr, int buf_len, int& idx, string& strout, int max_param_len) { char str[max_param_len]; int i = 0; while (idx != buf_len) { str[i++] = buf_ptr[idx++]; if (str[i - 1] == '\0') { strout.assign(str); return true; } if (i == max_param_len) return false; } return false; } bool timedCheck(void) { // return true if session to close uint64_t now = currentUSecsSinceEpoch(); // session management // if ((closingTime != 0) && (now >= closingTime)) return true; // session was due to close now if ((lastPDUFromESMETime != 0) && ((now - lastPDUFromESMETime) > (120 * 1000000L))) // wait up to 2 mins of inactivity before enquire link { if (enquireLinkRespPending == 0) { // issue enquire link send(sequence_number_out++, SMPP::CmdID::EnquireLink, SMPP::CmdStatus::ESME_ROK, NULL, 0); enquireLinkRespPending = now; closingTime = 0; } else { if (closingTime == 0) { // waiting on enquire link uint64_t tsinceenquirelinksent = now - enquireLinkRespPending; if (tsinceenquirelinksent > (60 * 1000000L)) // wait up to 1 min on enquire_link response { send(sequence_number_out++, SMPP::CmdID::Unbind, SMPP::CmdStatus::ESME_ROK, NULL, 0); closingTime = now + 5 * 1000000L; // close session in 5 seconds time } } } } // simulate message delivery + delivery receipt generation // if ((system_id.length() > 0) && ((bindState == SMPPSession::BindState::BS_TRX) || (bindState == SMPPSession::BindState::BS_RX))) { MessageDeliverer::Message msg; while (MessageDeliverer::getInstance_get(system_id, msg)) { // indicate message delivered if (msg.registered_delivery != 0) // ESME requested receipt so generate one { uint8_t source_addr_ton = msg.source_addr_ton, source_addr_npi = msg.source_addr_npi; string destination_addr = msg.destination_addr; uint8_t dest_addr_ton = msg.dest_addr_ton, dest_addr_npi = msg.dest_addr_npi; string source_addr = msg.source_addr; string msgid = msg.smscMessageID; generateReceipt(source_addr_ton, source_addr_npi, source_addr, dest_addr_ton, dest_addr_npi, destination_addr, msgid); } if (strstr(msg.destination_addr, system_id.c_str()) != NULL) { // system_id is in destination address - assume MO for // testing purposes generateMO(msg.source_addr_ton, msg.source_addr_npi, msg.source_addr, msg.dest_addr_ton, msg.dest_addr_npi, msg.destination_addr, msg.short_message, msg.sm_length, msg.data_coding); } } } return false; } void generateMO(uint8_t source_addr_ton, uint8_t source_addr_npi, string source_addr, uint8_t dest_addr_ton, uint8_t dest_addr_npi, string destination_addr, uint8_t* short_message, uint8_t sm_length, uint8_t data_coding) { uint8_t sbuf[1024]; int sidx = 0; sbuf[sidx++] = 0x00; // service type sbuf[sidx++] = source_addr_ton; // sbuf[sidx++] = source_addr_npi; // memcpy(sbuf + sidx, source_addr.c_str(), source_addr.length() + 1); // destination_addr sidx += source_addr.length() + 1; sbuf[sidx++] = dest_addr_ton; // sbuf[sidx++] = dest_addr_npi; // memcpy(sbuf + sidx, destination_addr.c_str(), destination_addr.length() + 1); // source_addr sidx += destination_addr.length() + 1; sbuf[sidx++] = 0x00; // esm_class sbuf[sidx++] = 0x00; // protocol_id sbuf[sidx++] = 0x00; // priority_flag sbuf[sidx++] = 0x00; // schedule_delivery_time sbuf[sidx++] = 0x00; // validity_period sbuf[sidx++] = 0x00; // registered_delivery sbuf[sidx++] = 0x00; // replace_if_present_flag sbuf[sidx++] = data_coding; // data_coding sbuf[sidx++] = 0x00; // sm_default_msg_id sbuf[sidx++] = sm_length; // sm_length memcpy((char*)(sbuf + sidx), short_message, sm_length); sidx += sm_length; send(sequence_number_out++, SMPP::CmdID::DeliverSM, SMPP::CmdStatus::ESME_ROK, sbuf, sidx); } void generateReceipt(uint8_t source_addr_ton, uint8_t source_addr_npi, string source_addr, uint8_t dest_addr_ton, uint8_t dest_addr_npi, string destination_addr, string msgid) { // receipt if ((bindState == SMPPSession::BindState::BS_TRX) || (bindState == SMPPSession::BindState::BS_RX)) { uint8_t sbuf[1024]; int sidx = 0; sbuf[sidx++] = 0x00; // service type sbuf[sidx++] = dest_addr_ton; // source_addr_ton sbuf[sidx++] = dest_addr_npi; // source_addr_npi memcpy(sbuf + sidx, destination_addr.c_str(), destination_addr.length() + 1); // source_addr sidx += destination_addr.length() + 1; sbuf[sidx++] = source_addr_ton; // dest_addr_ton sbuf[sidx++] = source_addr_npi; // dest_addr_npi memcpy(sbuf + sidx, source_addr.c_str(), source_addr.length() + 1); // destination_addr sidx += source_addr.length() + 1; sbuf[sidx++] = 0x04; // esm_class [0x04 Short Message contains MC delivery report] sbuf[sidx++] = 0x00; // protocol_id sbuf[sidx++] = 0x00; // priority_flag sbuf[sidx++] = 0x00; // schedule_delivery_time sbuf[sidx++] = 0x00; // validity_period sbuf[sidx++] = 0x00; // registered_delivery sbuf[sidx++] = 0x00; // replace_if_present_flag sbuf[sidx++] = 0x00; // data_coding sbuf[sidx++] = 0x00; // sm_default_msg_id // - short_message containing receipt in text format time_t tSubmitStamp = time(NULL); time_t tDoneStamp = time(NULL); char szSubmitStamp[32]; char szDoneStamp[32]; SMPP::GSMTimeStringShort(tSubmitStamp, szSubmitStamp, sizeof(szSubmitStamp)); SMPP::GSMTimeStringShort(tDoneStamp, szDoneStamp, sizeof(szDoneStamp)); char short_message[160]; sprintf(short_message, "id:%s sub:000 dlvrd:%03d submit date:%s done date:%s stat:%s " "err:%03d text:", msgid.c_str(), 1 /* 1 message delivered */, szSubmitStamp, szDoneStamp, "DELIVRD", 0 /*error*/); sbuf[sidx++] = strlen(short_message) + 1; // sm_length memcpy((char*)(sbuf + sidx), short_message, (int)(strlen(short_message) + 1)); sidx += strlen(short_message) + 1; // TLVs if (bindState == SMPPSession::BindState::BS_TRX) { uint8_t params1[] = { // message_state TLV 0x04, 0x27, // tag 0x00, 0x01, // length 0x02, // value - delivered // network_error TLV 0x04, 0x23, // tag 0x00, 0x03, // length 0x03, 0x00, 0x00, // value - GSM, 0, 0 // receipted_message_id TLV 0x00, 0x1e, // tag 0x00, 0x41, // length // .. message_id (to be appended) }; memcpy(sbuf + sidx, params1, sizeof(params1)); sidx += sizeof(params1); memcpy(sbuf + sidx, msgid.c_str(), msgid.length() + 1); sidx += msgid.length() + 1; } send(sequence_number_out++, SMPP::CmdID::DeliverSM, SMPP::CmdStatus::ESME_ROK, sbuf, sidx); } } bool run(void) { // return true if closed uint64_t now = currentUSecsSinceEpoch(); if ((closingTime != 0) && (now >= closingTime)) return true; // session was due to close now // handle PDUs from ESME // bool allowBind = true; uint64_t cmdid, seqno; uint8_t sbuf[1024]; if (recv(cmdid, seqno)) { // SMPP b; // printf("<< 0x%08llx %s\n",cmdid,b.cmdString(cmdid)); lastPDUFromESMETime = now; if ((cmdid == SMPP::CmdID::BindTransceiver) || (cmdid == SMPP::CmdID::BindReceiver) || (cmdid == SMPP::CmdID::BindTransmitter)) { if (bindState != SMPPSession::BindState::BS_NONE) // PROTOCOL must be in unbound // state for ESME to send bind // command { send(seqno, cmdid + 0x80000000, SMPP::CmdStatus::ESME_RINVBNDSTS, NULL, 0); } else { string esme_system_id = ""; string esme_password = ""; string esme_system_type = ""; uint8_t esme_smpp_ver = 0x00; int ptr_max = 0; uint8_t* ptr = conn.getBodyPointer(ptr_max); int idx = 0; if (!getCOctetString(ptr, ptr_max, idx, esme_system_id, 16)) { allowBind = false; goto parse_complete; } if (!getCOctetString(ptr, ptr_max, idx, esme_password, 9)) { allowBind = false; goto parse_complete; } if (!getCOctetString(ptr, ptr_max, idx, esme_system_type, 13)) { allowBind = false; goto parse_complete; } if (idx < ptr_max) esme_smpp_ver = ptr[idx++]; else { allowBind = false; goto parse_complete; } if ((esme_smpp_ver < 0x34) && (cmdid == SMPP::CmdID::BindTransceiver)) allowBind = false; // PROTOCOL transceiver bind only allowed for v3.4+ parse_complete: // parse complete label if (allowBind) { system_id = esme_system_id; char smsc_system_id[] = "MelroseLabsSMSC"; uint64_t cmdStatus = SMPP::CmdStatus::ESME_ROK; if (system_id == "invalid") { cmdStatus = SMPP::CmdStatus::ESME_RINVSYSID; // If there is an error in the bind_transmitter or bind_receiver request, // the SMSC system_id is not returned (see SMPP reference 4.1.2 and 4.1.4). if (cmdid == SMPP::CmdID::BindTransmitter || cmdid == SMPP::CmdID::BindReceiver) { std::fill_n(sbuf, 1024, 0); } else { memcpy(sbuf, smsc_system_id, strlen(smsc_system_id) + 1); } } else { memcpy(sbuf, smsc_system_id, strlen(smsc_system_id) + 1); } if (ClientConfig::instance().is( esme_system_id, ClientConfig::ConfigEntry:: BINDRESP_WITH_SC_INTERACE_VERSION_TLV)) { uint8_t params[] = { // sc_interface_version TLV 0x02, 0x10, // tag 0x00, 0x01, // length 0x34, // value [v3.4 supported] }; memcpy(sbuf + strlen(smsc_system_id) + 1, params, sizeof(params)); send(seqno, cmdid + 0x80000000, cmdStatus, sbuf, strlen(smsc_system_id) + 1 + sizeof(params)); } else send(seqno, cmdid + 0x80000000, cmdStatus, sbuf, strlen(smsc_system_id) + 1); if (cmdid == SMPP::CmdID::BindTransceiver) bindState = SMPPSession::BindState::BS_TRX; else if (cmdid == SMPP::CmdID::BindTransmitter) bindState = SMPPSession::BindState::BS_TX; else if (cmdid == SMPP::CmdID::BindReceiver) bindState = SMPPSession::BindState::BS_RX; else bindState = SMPPSession::BindState::BS_NONE; version = esme_smpp_ver; } else { send(seqno, SMPP::CmdID::BindTransceiverResp, SMPP::CmdStatus::ESME_RBINDFAIL, NULL, 0); // responding indicating bind unsuccessful bindState = SMPPSession::BindState::BS_NONE; } } } else if (cmdid == SMPP::CmdID::Unbind) { if (bindState != SMPPSession::BindState::BS_NONE) { send(seqno, SMPP::CmdID::UnbindResp, SMPP::CmdStatus::ESME_ROK, NULL, 0); bindState = SMPPSession::BindState::BS_NONE; } else { send(seqno, SMPP::CmdID::UnbindResp, SMPP::CmdStatus::ESME_RINVBNDSTS, NULL, 0); } } else if (cmdid == SMPP::CmdID::EnquireLinkResp) { enquireLinkRespPending = 0; // received pending enquire link } else if (cmdid == SMPP::CmdID::EnquireLink) { send(seqno, SMPP::CmdID::EnquireLinkResp, SMPP::CmdStatus::ESME_ROK, NULL, 0); } else if (cmdid == SMPP::CmdID::GenericNack) { // no response to GenericNack } else if (cmdid == SMPP::CmdID::SubmitSM) { if ((bindState == SMPPSession::BindState::BS_NONE) || (bindState == SMPPSession::BindState::BS_RX)) { send(seqno, SMPP::CmdID::SubmitSMResp, SMPP::CmdStatus::ESME_RINVBNDSTS, NULL, 0); } else { // process requested uint64_t tNow = currentUSecsSinceEpoch(); uint64_t tESMEDeliveryTimeRequired = tNow; // default to immediate delivery bool allowSubmit = true; uint64_t smppSubmitError = SMPP::CmdStatus::ESME_RSUBMITFAIL; string service_type = ""; uint8_t source_addr_ton = 0; uint8_t source_addr_npi = 0; string source_addr = ""; uint8_t dest_addr_ton = 0; uint8_t dest_addr_npi = 0; string destination_addr = ""; string schedule_delivery_time = ""; string validity_period = ""; uint8_t registered_delivery = 0; uint8_t sm_length; uint8_t short_message[160]; uint8_t data_coding; int ptr_max = 0; uint8_t* ptr = conn.getBodyPointer(ptr_max); int idx = 0; if (!getCOctetString(ptr, ptr_max, idx, service_type, 6)) { allowSubmit = false; goto parse_complete_submit; } if (idx < ptr_max) { source_addr_ton = ptr[idx++]; } else { allowSubmit = false; goto parse_complete_submit; } if (idx < ptr_max) { source_addr_npi = ptr[idx++]; } else { allowSubmit = false; goto parse_complete_submit; } if (!getCOctetString(ptr, ptr_max, idx, source_addr, 21)) { allowSubmit = false; goto parse_complete_submit; } if (idx < ptr_max) { dest_addr_ton = ptr[idx++]; } else { allowSubmit = false; goto parse_complete_submit; } if (idx < ptr_max) { dest_addr_npi = ptr[idx++]; } else { allowSubmit = false; goto parse_complete_submit; } if (!getCOctetString(ptr, ptr_max, idx, destination_addr, 21)) { allowSubmit = false; goto parse_complete_submit; } idx++; // esm_class idx++; // protocol_id idx++; // priority_flag if (!getCOctetString(ptr, ptr_max, idx, schedule_delivery_time, 17)) { allowSubmit = false; goto parse_complete_submit; } if (!getCOctetString(ptr, ptr_max, idx, validity_period, 17)) { allowSubmit = false; goto parse_complete_submit; } registered_delivery = ptr[idx++]; // registered_delivery idx++; // replace_if_present_flag data_coding = ptr[idx++]; // data_coding idx++; // sm_default_msg_id sm_length = ptr[idx++]; memcpy(short_message, ptr + idx, sm_length); idx += sm_length; if (schedule_delivery_time.length() > 0) // scheduled delivery { if (schedule_delivery_time[schedule_delivery_time.length() - 1] == 'R') // relative tESMEDeliveryTimeRequired = tNow + SMPP::GSMRelativeTime((char*)schedule_delivery_time.c_str()) * 1000000L; else tESMEDeliveryTimeRequired = (SMPP::GSMStringTime(schedule_delivery_time.c_str()) + 3 + (rand() % 10)) * 1000000L; if (tESMEDeliveryTimeRequired < tNow) // scheduled delivery time is before now { allowSubmit = false; smppSubmitError = SMPP::CmdStatus::ESME_RINVSCHED; goto parse_complete_submit; } } // ... if (destination_addr == "333") { allowSubmit = false; goto parse_complete_submit; } // force ESME_RSUBMITFAIL if (destination_addr.length() < 8) { allowSubmit = false; smppSubmitError = SMPP::CmdStatus::ESME_INVDSTADR; goto parse_complete_submit; } parse_complete_submit: // send response if (allowSubmit) { // accept message and send message ID back to ESME int msgid_len = 64; // PROTOCOL v3.4 message_id field is COctet String of up to // 65 chars (inc NULL terminator) if (version < 0x34) msgid_len = 8; // PROTOCOL v3.3 message_id field is COctet String (hex) // of up to 9 characters (inc NULL terminator) char msgid[msgid_len + 1]; for (int i = 0; i < msgid_len; i++) msgid[i] = (rand() % 16) < 10 ? ('0' + rand() % 10) : ('a' + rand() % 6); msgid[msgid_len] = 0; send(seqno, SMPP::CmdID::SubmitSMResp, SMPP::CmdStatus::ESME_ROK, (uint8_t*)msgid, msgid_len + 1); // sending back MO generateMO(5, 0, "FakeFrom", 1, 1, "FakeTo", short_message, sm_length, data_coding); // add message to deliverer so that receipt will then be sent to // ESME uint64_t timeDeliver = tESMEDeliveryTimeRequired + (3 + (rand() % 10)) * 1000000L; MessageDeliverer::Message msg; msg.setSource(source_addr_ton, source_addr_npi, (char*)source_addr.c_str()); msg.setDestination(dest_addr_ton, dest_addr_npi, (char*)destination_addr.c_str()); msg.setRegisteredDelivery(registered_delivery); msg.setShortMessage(short_message, sm_length); msg.setSMSCMessageID(msgid); MessageDeliverer::getInstance(system_id)->add(timeDeliver, msg); } else send(seqno, SMPP::CmdID::SubmitSMResp, smppSubmitError, NULL, 0); // don't accept message and send NAK back to ESME } } } else { // error or closed return true; } return false; } uint8_t getVersion(void) { return version; } void setVersion(uint8_t version_in) { version = version_in; } void logCommand(uint64_t cmdID, const char* direction) { char buf[640]; SMPP b; sprintf(buf, "0x%08llx %s", cmdID, b.cmdString(cmdID)); logCommand(buf, direction); } void logCommand(char* logline, const char* direction) { time_t rawtime; struct tm* timeinfo; char buffer[80]; time(&rawtime); timeinfo = localtime(&rawtime); strftime(buffer, 80, "%F %X ", timeinfo); printf("%s %s [s%06llx:%-15s] %s\n", buffer, direction, session_id, conn.getIP(), logline); } bool recv(uint64_t& cmdID, uint64_t& seqNo) { bool ret = conn.get(); if (ret) { cmdID = conn.pduCommandID(); seqNo = conn.pduSequenceNo(); logCommand(cmdID, ">>"); } return ret; } bool send(uint64_t seqNo, uint64_t cmdID, uint64_t cmdStatus, uint8_t* param, int len) { // SMPP b; // printf("<< 0x%08llx %s\n",cmdID,b.cmdString(cmdID)); logCommand(cmdID, "<<"); return conn.put(seqNo, cmdID, cmdStatus, param, len); } }; // Sockets reference: // https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_74/rzab6/xnonblock.htm int dolisten(int portno) { struct sockaddr_in serv_addr; /* First call to socket() function */ int listensockfd = socket(AF_INET, SOCK_STREAM, 0); if (listensockfd < 0) { perror("ERROR opening socket"); return -1; } int rc, on = 1; /*************************************************************/ /* Allow socket descriptor to be reuseable */ /*************************************************************/ rc = setsockopt(listensockfd, SOL_SOCKET, SO_REUSEADDR, (char*)&on, sizeof(on)); if (rc < 0) { perror("setsockopt() failed"); close(listensockfd); exit(-1); } /*************************************************************/ /* Set socket to be nonblocking. All of the sockets for */ /* the incoming connections will also be nonblocking since */ /* they will inherit that state from the listening socket. */ /*************************************************************/ rc = ioctl(listensockfd, FIONBIO, (char*)&on); if (rc < 0) { perror("ioctl() failed"); close(listensockfd); exit(-1); } /* Initialize socket structure */ bzero((char*)&serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(portno); /* Now bind the host address using bind() call.*/ if (::bind(listensockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) { perror("ERROR on binding"); return -1; } /* Now start listening for the clients, here * process will go in sleep mode and will wait * for the incoming connection */ listen(listensockfd, 32); return listensockfd; } Session* sessionSockMap[32000]; // array of SMPP session by socket int main(int argc, const char* argv[]) { printf("%s build time: %s %s\n", argv[0], __DATE__, __TIME__); // int portSMPP = 2775; int listensockfdSMPP = dolisten(portSMPP); if (listensockfdSMPP == -1) { perror("Failed to listen on SMPP port"); exit(1); } std::cout << "Listening for SMPP on port " << portSMPP << std::endl; // int portAdmin = 8775; int listensockfdAdmin = dolisten(portAdmin); if (listensockfdAdmin == -1) { perror("Failed to listen on admin port"); exit(1); } std::cout << "Listening for admin on port " << portAdmin << std::endl; // Initialize the master fd_set fd_set master_set, working_set; FD_ZERO(&master_set); int max_sd = listensockfdSMPP; FD_SET(listensockfdSMPP, &master_set); if (listensockfdAdmin > max_sd) max_sd = listensockfdAdmin; FD_SET(listensockfdAdmin, &master_set); // int rc, i, desc_ready; int end_server = FALSE; int new_sd; int close_conn; struct timeval timeout; // Loop waiting for incoming connects or for incoming data do { // Copy the master fd_set over to the working fd_set. memcpy(&working_set, &master_set, sizeof(master_set)); // Initialize the timeval struct to 1 second. If no // activity after 1 second then wake-up and cycle. timeout.tv_sec = 1; timeout.tv_usec = 0; // Call select() and wait for it to complete. rc = select(max_sd + 1, &working_set, NULL, NULL, &timeout); // Check to see if the select call failed. if (rc < 0) { perror(" select() failed"); break; } // Check to see if select call timed out. if (rc == 0) { // perform periodic session tasks for (i = 0; i <= max_sd; ++i) { if (sessionSockMap[i] != NULL) if (sessionSockMap[i]->timedCheck()) { printf(" %ld Force close on connection - %d.\n", time(NULL), i); if (sessionSockMap[i] != NULL) { delete sessionSockMap[i]; sessionSockMap[i] = NULL; } /* If the close_conn flag was turned on, we need to clean up this active connection. This clean up process includes removing the descriptor from the master set and determining the new maximum descriptor value based on the bits that are still turned on in the master set. */ close(i); FD_CLR(i, &master_set); if (i == max_sd) { while (FD_ISSET(max_sd, &master_set) == FALSE) { max_sd -= 1; } } } } continue; } /**********************************************************/ /* One or more descriptors are readable. Need to */ /* determine which ones they are. */ /**********************************************************/ desc_ready = rc; for (i = 0; i <= max_sd && desc_ready > 0; ++i) { /*******************************************************/ /* Check to see if this descriptor is ready */ /*******************************************************/ if (FD_ISSET(i, &working_set)) { /****************************************************/ /* A descriptor was found that was readable - one */ /* less has to be looked for. This is being done */ /* so that we can stop looking at the working set */ /* once we have found all of the descriptors that */ /* were ready. */ /****************************************************/ desc_ready -= 1; /***********************************************************/ /* Check to see if this is the listening socket (SMPP) */ /***********************************************************/ if (i == listensockfdSMPP) { // printf(" Listening socket (SMPP) is readable\n"); /*************************************************/ /* Accept all incoming connections that are */ /* queued up on the listening socket before we */ /* loop back and call select again. */ /*************************************************/ do { /**********************************************/ /* Accept each incoming connection. If */ /* accept fails with EWOULDBLOCK, then we */ /* have accepted all of them. Any other */ /* failure on accept will cause us to end the */ /* server. */ /**********************************************/ struct sockaddr_in client_addr; int clen = sizeof(sockaddr_in); new_sd = accept(listensockfdSMPP, (struct sockaddr*)&client_addr, (socklen_t*)&clen); if (new_sd < 0) { if (errno != EWOULDBLOCK) { perror(" accept() failed (SMPP)"); if (errno != EMFILE) end_server = TRUE; } break; } char* ip = inet_ntoa(client_addr.sin_addr); // SMPPSession* newsession = new SMPPSession(new_sd, ip); sessionSockMap[new_sd] = newsession; /**********************************************/ /* Add the new incoming connection to the */ /* master read set */ /**********************************************/ // printf(" New incoming connection - %d\n", new_sd); FD_SET(new_sd, &master_set); if (new_sd > max_sd) max_sd = new_sd; /**********************************************/ /* Loop back up and accept another incoming */ /* connection */ /**********************************************/ } while (new_sd != -1); } else /***********************************************************/ /* Check to see if this is the listening socket (admin) */ /***********************************************************/ if (i == listensockfdAdmin) { // printf(" Listening socket (admin) is readable\n"); /*************************************************/ /* Accept all incoming connections that are */ /* queued up on the listening socket before we */ /* loop back and call select again. */ /*************************************************/ do { /**********************************************/ /* Accept each incoming connection. If */ /* accept fails with EWOULDBLOCK, then we */ /* have accepted all of them. Any other */ /* failure on accept will cause us to end the */ /* server. */ /**********************************************/ new_sd = accept(listensockfdAdmin, NULL, NULL); if (new_sd < 0) { if (errno != EWOULDBLOCK) { perror(" accept() failed (admin)"); if (errno != EMFILE) end_server = TRUE; } break; } // AdminSession* newsession = new AdminSession(new_sd); sessionSockMap[new_sd] = newsession; /**********************************************/ /* Add the new incoming connection to the */ /* master read set */ /**********************************************/ // printf(" New incoming connection (Admin) - %d\n", new_sd); FD_SET(new_sd, &master_set); if (new_sd > max_sd) max_sd = new_sd; /**********************************************/ /* Loop back up and accept another incoming */ /* connection */ /**********************************************/ } while (new_sd != -1); } /****************************************************/ /* This is not the listening socket, therefore an */ /* existing connection must be readable */ /****************************************************/ else { // printf(" Descriptor %d is readable\n", i); close_conn = FALSE; bool closed = sessionSockMap[i]->run(); if (closed) { // printf(" Closing connection - %d\n", i); if (sessionSockMap[i] != NULL) { delete sessionSockMap[i]; sessionSockMap[i] = NULL; } close_conn = TRUE; } else close_conn = FALSE; /*************************************************/ /* If the close_conn flag was turned on, we need */ /* to clean up this active connection. This */ /* clean up process includes removing the */ /* descriptor from the master set and */ /* determining the new maximum descriptor value */ /* based on the bits that are still turned on in */ /* the master set. */ /*************************************************/ if (close_conn) { close(i); FD_CLR(i, &master_set); if (i == max_sd) { while (FD_ISSET(max_sd, &master_set) == FALSE) max_sd -= 1; } } } /* End of existing connection is readable */ } /* End of if (FD_ISSET(i, &working_set)) */ } /* End of loop through selectable descriptors */ } while (end_server == FALSE); // close(listensockfdSMPP); close(listensockfdAdmin); std::cout << "No longer listening on port " << portSMPP << std::endl; return 0; } ================================================ FILE: example/transceiver_with_auto_response/main.go ================================================ package main import ( "fmt" "log" "strings" "sync" "time" "github.com/linxGnu/gosmpp" "github.com/linxGnu/gosmpp/data" "github.com/linxGnu/gosmpp/pdu" ) func main() { var wg sync.WaitGroup wg.Add(1) go sendingAndReceiveSMS(&wg) wg.Wait() } func sendingAndReceiveSMS(wg *sync.WaitGroup) { defer wg.Done() auth := gosmpp.Auth{ SMSC: "localhost:2775", SystemID: "169994", Password: "EDXPJU", SystemType: "", } trans, err := gosmpp.NewSession( gosmpp.TRXConnector(gosmpp.NonTLSDialer, auth), gosmpp.Settings{ EnquireLink: 5 * time.Second, ReadTimeout: 10 * time.Second, OnSubmitError: func(_ pdu.PDU, err error) { log.Fatal("SubmitPDU error:", err) }, OnReceivingError: func(err error) { fmt.Println("Receiving PDU/Network error:", err) }, OnRebindingError: func(err error) { fmt.Println("Rebinding but error:", err) }, OnPDU: handlePDU(), OnClosed: func(state gosmpp.State) { fmt.Println(state) }, }, 5*time.Second) if err != nil { log.Fatal(err) } defer func() { _ = trans.Close() }() // sending SMS(s) for i := 0; i < 1800; i++ { if err = trans.Transceiver().Submit(newSubmitSM()); err != nil { fmt.Println(err) } time.Sleep(time.Second) } } func handlePDU() func(pdu.PDU, bool) { concatenated := map[uint8][]string{} return func(p pdu.PDU, _ bool) { switch pd := p.(type) { case *pdu.SubmitSMResp: fmt.Printf("SubmitSMResp:%+v\n", pd) case *pdu.GenericNack: fmt.Println("GenericNack Received") case *pdu.EnquireLinkResp: fmt.Println("EnquireLinkResp Received") case *pdu.DataSM: fmt.Printf("DataSM:%+v\n", pd) case *pdu.DeliverSM: fmt.Printf("DeliverSM:%+v\n", pd) log.Println(pd.Message.GetMessage()) // region concatenated sms (sample code) message, err := pd.Message.GetMessage() if err != nil { log.Fatal(err) } totalParts, sequence, reference, found := pd.Message.UDH().GetConcatInfo() if found { if _, ok := concatenated[reference]; !ok { concatenated[reference] = make([]string, totalParts) } concatenated[reference][sequence-1] = message } if !found { log.Println(message) } else if parts, ok := concatenated[reference]; ok && isConcatenatedDone(parts, totalParts) { log.Println(strings.Join(parts, "")) delete(concatenated, reference) } // endregion } } } func newSubmitSM() *pdu.SubmitSM { // build up submitSM srcAddr := pdu.NewAddress() srcAddr.SetTon(5) srcAddr.SetNpi(0) _ = srcAddr.SetAddress("00" + "522241") destAddr := pdu.NewAddress() destAddr.SetTon(1) destAddr.SetNpi(1) _ = destAddr.SetAddress("99" + "522241") submitSM := pdu.NewSubmitSM().(*pdu.SubmitSM) submitSM.SourceAddr = srcAddr submitSM.DestAddr = destAddr _ = submitSM.Message.SetMessageWithEncoding("Đừng buồn thế dù ngoài kia vẫn mưa nghiễng rợi tý tỵ", data.UCS2) submitSM.ProtocolID = 0 submitSM.RegisteredDelivery = 1 submitSM.ReplaceIfPresentFlag = 0 submitSM.EsmClass = 0 return submitSM } func isConcatenatedDone(parts []string, total byte) bool { for _, part := range parts { if part != "" { total-- } } return total == 0 } ================================================ FILE: example/transceiver_with_manual_response/main.go ================================================ package main import ( "fmt" "log" "sync" "time" "github.com/linxGnu/gosmpp" "github.com/linxGnu/gosmpp/data" "github.com/linxGnu/gosmpp/pdu" ) func main() { var wg sync.WaitGroup wg.Add(1) go sendingAndReceiveSMS(&wg) wg.Wait() } func sendingAndReceiveSMS(wg *sync.WaitGroup) { defer wg.Done() auth := gosmpp.Auth{ SMSC: "localhost:2775", SystemID: "169994", Password: "EDXPJU", SystemType: "", } trans, err := gosmpp.NewSession( gosmpp.TRXConnector(gosmpp.NonTLSDialer, auth), gosmpp.Settings{ EnquireLink: 5 * time.Second, ReadTimeout: 10 * time.Second, OnSubmitError: func(_ pdu.PDU, err error) { log.Fatal("SubmitPDU error:", err) }, OnReceivingError: func(err error) { fmt.Println("Receiving PDU/Network error:", err) }, OnRebindingError: func(err error) { fmt.Println("Rebinding but error:", err) }, OnAllPDU: handlePDU(), OnClosed: func(state gosmpp.State) { fmt.Println(state) }, }, 5*time.Second) if err != nil { log.Fatal(err) } defer func() { _ = trans.Close() }() // sending SMS(s) for i := 0; i < 30; i++ { if err = trans.Transceiver().Submit(newSubmitSM()); err != nil { fmt.Println(err) } time.Sleep(time.Second) } } func handlePDU() func(pdu.PDU) (pdu.PDU, bool) { return func(p pdu.PDU) (pdu.PDU, bool) { switch pd := p.(type) { case *pdu.Unbind: fmt.Println("Unbind Received") return pd.GetResponse(), true case *pdu.UnbindResp: fmt.Println("UnbindResp Received") case *pdu.SubmitSMResp: fmt.Println("SubmitSMResp Received") case *pdu.GenericNack: fmt.Println("GenericNack Received") case *pdu.EnquireLinkResp: fmt.Println("EnquireLinkResp Received") case *pdu.EnquireLink: fmt.Println("EnquireLink Received") return pd.GetResponse(), false case *pdu.DataSM: fmt.Println("DataSM receiver") return pd.GetResponse(), false case *pdu.DeliverSM: fmt.Println("DeliverSM receiver") return pd.GetResponse(), false } return nil, false } } func newSubmitSM() *pdu.SubmitSM { // build up submitSM srcAddr := pdu.NewAddress() srcAddr.SetTon(5) srcAddr.SetNpi(0) _ = srcAddr.SetAddress("00" + "522241") destAddr := pdu.NewAddress() destAddr.SetTon(1) destAddr.SetNpi(1) _ = destAddr.SetAddress("99" + "522241") submitSM := pdu.NewSubmitSM().(*pdu.SubmitSM) submitSM.SourceAddr = srcAddr submitSM.DestAddr = destAddr _ = submitSM.Message.SetMessageWithEncoding("Đừng buồn thế dù ngoài kia vẫn mưa nghiễng rợi tý tỵ", data.UCS2) submitSM.ProtocolID = 0 submitSM.RegisteredDelivery = 1 submitSM.ReplaceIfPresentFlag = 0 submitSM.EsmClass = 0 return submitSM } ================================================ FILE: example/transeiver_with_custom_store/CustomStore.go ================================================ package main import ( "bytes" "context" "encoding/gob" "errors" "fmt" "strconv" "time" "github.com/allegro/bigcache/v3" "github.com/linxGnu/gosmpp/pdu" "github.com/linxGnu/gosmpp" ) // This is a just an example how to implement a custom store. // // Your implementation must be concurrency safe // // In this example we use bigcache https://github.com/allegro/bigcache // Warning: // - This is just an example and should be tested before using in production // - We are serializing with gob, some field cannot be serialized for simplicity // - We recommend you implement your own serialization/deserialization if you choose to use bigcache type CustomStore struct { store *bigcache.BigCache } func NewCustomStore() CustomStore { cache, _ := bigcache.New(context.Background(), bigcache.DefaultConfig(30*time.Second)) return CustomStore{ store: cache, } } func (s CustomStore) Set(ctx context.Context, request gosmpp.Request) error { select { case <-ctx.Done(): fmt.Println("Task cancelled") return ctx.Err() default: b, _ := serialize(request) err := s.store.Set(strconv.Itoa(int(request.PDU.GetSequenceNumber())), b) if err != nil { return err } return nil } } func (s CustomStore) Get(ctx context.Context, sequenceNumber int32) (gosmpp.Request, bool) { select { case <-ctx.Done(): fmt.Println("Task cancelled") return gosmpp.Request{}, false default: bRequest, err := s.store.Get(strconv.Itoa(int(sequenceNumber))) if err != nil { return gosmpp.Request{}, false } request, err := deserialize(bRequest) if err != nil { return gosmpp.Request{}, false } return request, true } } func (s CustomStore) List(ctx context.Context) []gosmpp.Request { var requests []gosmpp.Request select { case <-ctx.Done(): return requests default: it := s.store.Iterator() for it.SetNext() { value, err := it.Value() if err != nil { return requests } request, _ := deserialize(value.Value()) requests = append(requests, request) } return requests } } func (s CustomStore) Delete(ctx context.Context, sequenceNumber int32) error { select { case <-ctx.Done(): return ctx.Err() default: err := s.store.Delete(strconv.Itoa(int(sequenceNumber))) if err != nil { return err } return nil } } func (s CustomStore) Clear(ctx context.Context) error { select { case <-ctx.Done(): return ctx.Err() default: err := s.store.Reset() if err != nil { return err } return nil } } func (s CustomStore) Length(ctx context.Context) (int, error) { select { case <-ctx.Done(): return 0, ctx.Err() default: return s.store.Len(), nil } } func serialize(request gosmpp.Request) ([]byte, error) { buf := pdu.NewBuffer(make([]byte, 0, 64)) request.PDU.Marshal(buf) b := bytes.Buffer{} e := gob.NewEncoder(&b) err := e.Encode(requestGob{ Pdu: buf.Bytes(), TimeSent: time.Time{}, }) if err != nil { return b.Bytes()[:], errors.New("serialization failed") } return b.Bytes(), nil } func deserialize(bRequest []byte) (request gosmpp.Request, err error) { r := requestGob{} b := bytes.Buffer{} _, err = b.Write(bRequest) if err != nil { return request, errors.New("deserialization failed") } d := gob.NewDecoder(&b) err = d.Decode(&r) if err != nil { return request, errors.New("deserialization failed") } p, err := pdu.Parse(bytes.NewReader(r.Pdu)) if err != nil { return gosmpp.Request{}, err } return gosmpp.Request{ PDU: p, TimeSent: r.TimeSent, }, nil } type requestGob struct { Pdu []byte TimeSent time.Time } ================================================ FILE: example/transeiver_with_custom_store/main.go ================================================ package main import ( "errors" "fmt" "log" "sync" "time" "github.com/linxGnu/gosmpp" "github.com/linxGnu/gosmpp/data" "github.com/linxGnu/gosmpp/pdu" ) func main() { var wg sync.WaitGroup wg.Add(1) go sendingAndReceiveSMS(&wg) wg.Wait() } func sendingAndReceiveSMS(wg *sync.WaitGroup) { defer wg.Done() auth := gosmpp.Auth{ SMSC: "localhost:2775", SystemID: "169994", Password: "EDXPJU", SystemType: "", } trans, err := gosmpp.NewSession( gosmpp.TRXConnector(gosmpp.NonTLSDialer, auth), gosmpp.Settings{ EnquireLink: 5 * time.Second, ReadTimeout: 10 * time.Second, OnSubmitError: func(p pdu.PDU, err error) { if errors.Is(err, gosmpp.ErrWindowsFull) { log.Println("SubmitPDU error:", err) } else { log.Fatal("SubmitPDU error:", err) } }, OnReceivingError: func(err error) { fmt.Println("Receiving PDU/Network error:", err) }, OnRebindingError: func(err error) { fmt.Println("Rebinding but error:", err) }, OnClosed: func(state gosmpp.State) { fmt.Println(state) }, WindowedRequestTracking: &gosmpp.WindowedRequestTracking{ OnReceivedPduRequest: handleReceivedPduRequest(), OnExpectedPduResponse: handleExpectedPduResponse(), OnExpiredPduRequest: handleExpirePduRequest(), OnClosePduRequest: handleOnClosePduRequest(), PduExpireTimeOut: 30 * time.Second, ExpireCheckTimer: 10 * time.Second, MaxWindowSize: 30, EnableAutoRespond: false, }, }, 5*time.Second, gosmpp.WithRequestStore(NewCustomStore()), ) if err != nil { log.Fatal(err) } defer func() { _ = trans.Close() }() // sending SMS(s) for i := 0; i < 60; i++ { var size int size, err = trans.GetWindowSize() if err != nil { fmt.Println("could not get window size: ", err) } else { fmt.Println("Current window size: ", size) } p := newSubmitSM() if err = trans.Transceiver().Submit(p); err != nil { fmt.Println(err) } fmt.Printf("Sent SubmitSM with id: %+v\n", p) time.Sleep(1 * time.Second) } time.Sleep(1 * time.Second) } func handleExpirePduRequest() func(pdu.PDU) bool { return func(p pdu.PDU) bool { switch p.(type) { case *pdu.SubmitSM: fmt.Printf("Expired SubmitSM: %+v\n", p) case *pdu.EnquireLink: fmt.Printf("Expired EnquireLink: %+v\n", p) return true // if the enquire_link expired, usually means the bind is stale case *pdu.DataSM: fmt.Printf("Expired DataSM: %+v\n", p) default: fmt.Printf("Expired PDU: %+v\n", p) } return false } } func handleOnClosePduRequest() func(pdu.PDU) { return func(p pdu.PDU) { switch p.(type) { case *pdu.Unbind: fmt.Printf("OnClose Unbind: %+v\n", p) case *pdu.SubmitSM: fmt.Printf("OnClose SubmitSM: %+v\n", p) case *pdu.EnquireLink: fmt.Printf("OnClose EnquireLink: %+v\n", p) case *pdu.DataSM: fmt.Printf("OnClose DataSM: %+v\n", p) } } } func handleExpectedPduResponse() func(response gosmpp.Response) { return func(response gosmpp.Response) { switch response.PDU.(type) { case *pdu.UnbindResp: fmt.Println("UnbindResp Received") fmt.Printf("OriginalSM id:%+v\n", response.OriginalRequest.PDU) case *pdu.SubmitSMResp: fmt.Printf("SubmitSMResp Received: %+v\n", response.PDU) fmt.Printf("OriginalSM SubmitSM:%+v\n", response.OriginalRequest.PDU) case *pdu.EnquireLinkResp: fmt.Println("EnquireLinkResp Received") fmt.Printf("Original EnquireLink:%+v\n", response.OriginalRequest.PDU) } } } func handleReceivedPduRequest() func(pdu.PDU) (pdu.PDU, bool) { return func(p pdu.PDU) (pdu.PDU, bool) { switch pd := p.(type) { case *pdu.Unbind: fmt.Println("Unbind Received") return pd.GetResponse(), true case *pdu.GenericNack: fmt.Println("GenericNack Received") case *pdu.EnquireLinkResp: fmt.Println("EnquireLinkResp Received") case *pdu.EnquireLink: fmt.Println("EnquireLink Received") return pd.GetResponse(), false case *pdu.DataSM: fmt.Println("DataSM Received") return pd.GetResponse(), false case *pdu.DeliverSM: fmt.Println("DeliverSM Received") return pd.GetResponse(), false } return nil, false } } func newSubmitSM() *pdu.SubmitSM { // build up submitSM srcAddr := pdu.NewAddress() srcAddr.SetTon(5) srcAddr.SetNpi(0) _ = srcAddr.SetAddress("00" + "522241") destAddr := pdu.NewAddress() destAddr.SetTon(1) destAddr.SetNpi(1) _ = destAddr.SetAddress("99" + "522241") submitSM := pdu.NewSubmitSM().(*pdu.SubmitSM) submitSM.SourceAddr = srcAddr submitSM.DestAddr = destAddr _ = submitSM.Message.SetMessageWithEncoding("Đừng buồn thế dù ngoài kia vẫn mưa nghiễng rợi tý tỵ", data.UCS2) submitSM.ProtocolID = 0 submitSM.RegisteredDelivery = 1 submitSM.ReplaceIfPresentFlag = 0 submitSM.EsmClass = 0 return submitSM } ================================================ FILE: example/transeiver_with_request_window_and_custom_submitSm/CustomSubmitSM.go ================================================ package main import ( "math/rand" "strconv" "github.com/linxGnu/gosmpp/pdu" ) // CustomSubmitSM by embedding the PDU interface // and adding messageId as an extra field to SubmitSM type CustomSubmitSM struct { pdu.PDU messageId string } // newCustomSubmitSM returns CustomSubmitSM PDU. // Using rand.Int to generate new id for each CustomSubmitSM func newCustomSubmitSM() CustomSubmitSM { return CustomSubmitSM{ PDU: newSubmitSM(), messageId: strconv.Itoa(rand.Int()), } } ================================================ FILE: example/transeiver_with_request_window_and_custom_submitSm/main.go ================================================ package main import ( "errors" "fmt" "log" "sync" "time" "github.com/linxGnu/gosmpp" "github.com/linxGnu/gosmpp/data" "github.com/linxGnu/gosmpp/pdu" ) func main() { var wg sync.WaitGroup wg.Add(1) go sendingAndReceiveSMS(&wg) wg.Wait() } func sendingAndReceiveSMS(wg *sync.WaitGroup) { defer wg.Done() auth := gosmpp.Auth{ SMSC: "localhost:2775", SystemID: "169994", Password: "EDXPJU", SystemType: "", } trans, err := gosmpp.NewSession( gosmpp.TRXConnector(gosmpp.NonTLSDialer, auth), gosmpp.Settings{ EnquireLink: 5 * time.Second, ReadTimeout: 10 * time.Second, OnSubmitError: func(p pdu.PDU, err error) { if errors.Is(err, gosmpp.ErrWindowsFull) { log.Println("SubmitPDU error: ", err) } else { log.Fatal("SubmitPDU error: ", err) } }, OnReceivingError: func(err error) { fmt.Println("Receiving PDU/Network error: ", err) }, OnRebindingError: func(err error) { fmt.Println("Rebinding but error: ", err) }, OnClosed: func(state gosmpp.State) { fmt.Println("Bind has been closed: ", state.String()) }, WindowedRequestTracking: &gosmpp.WindowedRequestTracking{ OnReceivedPduRequest: handleReceivedPduRequest(), OnExpectedPduResponse: handleExpectedPduResponse(), OnExpiredPduRequest: handleExpirePduRequest(), OnClosePduRequest: handleOnClosePduRequest(), PduExpireTimeOut: 30 * time.Second, ExpireCheckTimer: 10 * time.Second, MaxWindowSize: 30, StoreAccessTimeOut: 1 * time.Second, EnableAutoRespond: false, }, }, 5*time.Second) if err != nil { log.Fatal(err) } defer func() { _ = trans.Close() }() // sending SMS(s) for i := 0; i < 60; i++ { var size int size, err = trans.GetWindowSize() if err != nil { fmt.Println("could not get window size: ", err) } else { fmt.Println("Current window size: ", size) } p := newCustomSubmitSM() if err = trans.Transceiver().Submit(p); err != nil { fmt.Println(err) } fmt.Printf("Sent CustomSubmitSM with id: %+v\n", p.messageId) time.Sleep(1 * time.Second) } time.Sleep(1 * time.Second) } func handleExpirePduRequest() func(pdu.PDU) bool { return func(p pdu.PDU) bool { switch p.(type) { case *pdu.Unbind: fmt.Printf("Expired Unbind :%+v\n", p) fmt.Println("Unbind Expired") case *pdu.SubmitSM: fmt.Printf("Expired SubmitSM :%+v\n", p) case *pdu.EnquireLink: fmt.Printf("Expired EnquireLink:%+v\n", p) return true // if the enquire_link expired, usually means the bind is stale case *pdu.DataSM: fmt.Printf("Expired DataSM:%+v\n", p) } return false } } func handleOnClosePduRequest() func(pdu.PDU) { return func(p pdu.PDU) { switch p.(type) { case *pdu.Unbind: fmt.Printf("OnClose Unbind:%+v\n", p) case *pdu.SubmitSM: fmt.Printf("OnClose SubmitSM:%+v\n", p) case *pdu.EnquireLink: fmt.Printf("OnClose EnquireLink:%+v\n", p) case *pdu.DataSM: fmt.Printf("OnClose DataSM:%+v\n", p) } } } func handleExpectedPduResponse() func(response gosmpp.Response) { return func(response gosmpp.Response) { switch response.PDU.(type) { case *pdu.UnbindResp: fmt.Println("UnbindResp Received") fmt.Printf("OriginalSM id:%+v\n", response.OriginalRequest.PDU) case *pdu.SubmitSMResp: fmt.Printf("SubmitSMResp Received: %+v\n", response.PDU) fmt.Printf("OriginalSM SubmitSM:%+v\n", response.OriginalRequest.PDU) case *pdu.EnquireLinkResp: fmt.Println("EnquireLinkResp Received") fmt.Printf("Original EnquireLink:%+v\n", response.OriginalRequest.PDU) } } } func handleReceivedPduRequest() func(pdu.PDU) (pdu.PDU, bool) { return func(p pdu.PDU) (pdu.PDU, bool) { switch pd := p.(type) { case *pdu.Unbind: fmt.Println("Unbind Received") return pd.GetResponse(), true case *pdu.GenericNack: fmt.Println("GenericNack Received") case *pdu.EnquireLinkResp: fmt.Println("EnquireLinkResp Received") case *pdu.EnquireLink: fmt.Println("EnquireLink Received") return pd.GetResponse(), false case *pdu.DataSM: fmt.Println("DataSM Received") return pd.GetResponse(), false case *pdu.DeliverSM: fmt.Println("DeliverSM Received") return pd.GetResponse(), false } return nil, false } } func newSubmitSM() *pdu.SubmitSM { // build up submitSM srcAddr := pdu.NewAddress() srcAddr.SetTon(5) srcAddr.SetNpi(0) _ = srcAddr.SetAddress("00" + "522241") destAddr := pdu.NewAddress() destAddr.SetTon(1) destAddr.SetNpi(1) _ = destAddr.SetAddress("99" + "522241") submitSM := pdu.NewSubmitSM().(*pdu.SubmitSM) submitSM.SourceAddr = srcAddr submitSM.DestAddr = destAddr _ = submitSM.Message.SetMessageWithEncoding("Đừng buồn thế dù ngoài kia vẫn mưa nghiễng rợi tý tỵ", data.UCS2) submitSM.ProtocolID = 0 submitSM.RegisteredDelivery = 1 submitSM.ReplaceIfPresentFlag = 0 submitSM.EsmClass = 0 return submitSM } ================================================ FILE: go.mod ================================================ module github.com/linxGnu/gosmpp go 1.24.0 require ( github.com/allegro/bigcache/v3 v3.1.0 github.com/orcaman/concurrent-map/v2 v2.0.1 github.com/stretchr/testify v1.11.1 golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b golang.org/x/text v0.33.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk= github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c= github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: pdu/Address.go ================================================ package pdu import ( "fmt" "github.com/linxGnu/gosmpp/data" ) // Address smpp address of src and dst. type Address struct { ton byte npi byte address string } // NewAddress returns new address with default max length. func NewAddress() Address { return Address{ton: data.GetDefaultTon(), npi: data.GetDefaultNpi()} } // NewAddressWithAddr returns new address. func NewAddressWithAddr(addr string) (a Address, err error) { a = NewAddress() err = a.SetAddress(addr) return } // NewAddressWithTonNpi returns new address with ton, npi. func NewAddressWithTonNpi(ton, npi byte) Address { return Address{ton: ton, npi: npi} } // NewAddressWithTonNpiAddr returns new address with ton, npi, addr string. func NewAddressWithTonNpiAddr(ton, npi byte, addr string) (a Address, err error) { a = NewAddressWithTonNpi(ton, npi) err = a.SetAddress(addr) return } // Unmarshal from buffer. func (c *Address) Unmarshal(b *ByteBuffer) (err error) { if c.ton, err = b.ReadByte(); err == nil { if c.npi, err = b.ReadByte(); err == nil { c.address, err = b.ReadCString() } } return } // Marshal to buffer. func (c *Address) Marshal(b *ByteBuffer) { b.Grow(3 + len(c.address)) _ = b.WriteByte(c.ton) _ = b.WriteByte(c.npi) _ = b.WriteCString(c.address) } // SetTon sets ton. func (c *Address) SetTon(ton byte) { c.ton = ton } // SetNpi sets npi. func (c *Address) SetNpi(npi byte) { c.npi = npi } // SetAddress sets address. func (c *Address) SetAddress(addr string) (err error) { if len(addr) > data.SM_ADDR_LEN { err = fmt.Errorf("Address len exceed limit. (%d > %d)", len(addr), data.SM_ADDR_LEN) } else { c.address = addr } return } // Ton returns assigned ton. func (c Address) Ton() byte { return c.ton } // Npi returns assigned npi. func (c Address) Npi() byte { return c.npi } // Address returns assigned address (in string). func (c Address) Address() string { return c.address } // String implement stringer interface func (c Address) String() string { return c.address } ================================================ FILE: pdu/AddressRange.go ================================================ package pdu import "github.com/linxGnu/gosmpp/data" // AddressRange smpp address range of src and dst. type AddressRange struct { Ton byte Npi byte AddressRange string } // NewAddressRange create new AddressRange with default max length. func NewAddressRange() AddressRange { return AddressRange{Ton: data.GetDefaultTon(), Npi: data.GetDefaultNpi()} } // NewAddressRangeWithAddr create new AddressRange. func NewAddressRangeWithAddr(addr string) AddressRange { ar := NewAddressRange() ar.AddressRange = addr return ar } // NewAddressRangeWithTonNpi create new AddressRange with ton, npi. func NewAddressRangeWithTonNpi(ton, npi byte) AddressRange { return AddressRange{Ton: ton, Npi: npi} } // NewAddressRangeWithTonNpiAddr returns new address with ton, npi, addr string. func NewAddressRangeWithTonNpiAddr(ton, npi byte, addr string) AddressRange { ar := NewAddressRangeWithTonNpi(ton, npi) ar.AddressRange = addr return ar } // Unmarshal from buffer. func (c *AddressRange) Unmarshal(b *ByteBuffer) (err error) { if c.Ton, err = b.ReadByte(); err == nil { if c.Npi, err = b.ReadByte(); err == nil { c.AddressRange, err = b.ReadCString() } } return } // Marshal to buffer. func (c *AddressRange) Marshal(b *ByteBuffer) { b.Grow(3 + len(c.AddressRange)) _ = b.WriteByte(c.Ton) _ = b.WriteByte(c.Npi) _ = b.WriteCString(c.AddressRange) } ================================================ FILE: pdu/AddressRange_test.go ================================================ package pdu import ( "testing" "github.com/stretchr/testify/require" ) func TestAddressRange(t *testing.T) { t.Run("new", func(t *testing.T) { a := NewAddressRangeWithAddr("abc") require.Equal(t, "abc", a.AddressRange) }) t.Run("newTonNpi", func(t *testing.T) { a := NewAddressRangeWithTonNpi(3, 7) a.AddressRange = "123456789" require.EqualValues(t, 3, a.Ton) require.EqualValues(t, 7, a.Npi) require.Equal(t, "123456789", a.AddressRange) }) t.Run("newTonNpiAddr", func(t *testing.T) { a := NewAddressRangeWithTonNpiAddr(3, 7, "123456789") require.EqualValues(t, 3, a.Ton) require.EqualValues(t, 7, a.Npi) require.Equal(t, "123456789", a.AddressRange) }) t.Run("unmarshal", func(t *testing.T) { buf := NewBuffer(fromHex("315b7068616e746f6d537472696b6500")) var a AddressRange require.Nil(t, a.Unmarshal(buf)) require.Zero(t, buf.Len()) require.Equal(t, "phantomStrike", a.AddressRange) require.EqualValues(t, 49, a.Ton) require.EqualValues(t, 91, a.Npi) }) t.Run("marshal", func(t *testing.T) { a := AddressRange{} a.AddressRange = "phantomOpera" a.Ton = 95 a.Npi = 13 buf := NewBuffer(nil) a.Marshal(buf) require.Equal(t, fromHex("5f0d7068616e746f6d4f7065726100"), buf.Bytes()) }) } ================================================ FILE: pdu/Address_test.go ================================================ package pdu import ( "testing" "github.com/stretchr/testify/require" ) func TestAddress(t *testing.T) { t.Run("new", func(t *testing.T) { a, err := NewAddressWithAddr("abc") require.Nil(t, err) require.Equal(t, "abc", a.Address()) }) t.Run("newWithAddr", func(t *testing.T) { _, err := NewAddressWithAddr("1234567890123456789012") require.NotNil(t, err) }) t.Run("newTonNpi", func(t *testing.T) { a := NewAddressWithTonNpi(3, 7) require.Nil(t, a.SetAddress("123456789")) require.EqualValues(t, 3, a.Ton()) require.EqualValues(t, 7, a.Npi()) require.Equal(t, "123456789", a.Address()) a.SetTon(11) a.SetNpi(19) require.EqualValues(t, 11, a.Ton()) require.EqualValues(t, 19, a.Npi()) }) t.Run("newTonNpiAddr", func(t *testing.T) { a, err := NewAddressWithTonNpiAddr(3, 7, "123456789") require.Nil(t, err) require.EqualValues(t, 3, a.Ton()) require.EqualValues(t, 7, a.Npi()) require.Equal(t, "123456789", a.Address()) a.SetTon(11) a.SetNpi(19) require.EqualValues(t, 11, a.Ton()) require.EqualValues(t, 19, a.Npi()) }) t.Run("unmarshal", func(t *testing.T) { buf := NewBuffer(fromHex("315b7068616e746f6d537472696b6500")) var a Address require.Nil(t, a.Unmarshal(buf)) require.Zero(t, buf.Len()) require.Equal(t, "phantomStrike", a.Address()) require.EqualValues(t, 49, a.Ton()) require.EqualValues(t, 91, a.Npi()) }) t.Run("marshal", func(t *testing.T) { a, err := NewAddressWithAddr("phantomOpera") require.Nil(t, err) a.SetTon(95) a.SetNpi(13) buf := NewBuffer(nil) a.Marshal(buf) require.Equal(t, fromHex("5f0d7068616e746f6d4f7065726100"), buf.Bytes()) }) } ================================================ FILE: pdu/AlertNotification.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // AlertNotification PDU is sent by the SMSC to the ESME, when the SMSC has detected that // a particular mobile subscriber has become available and a delivery pending flag had been // set for that subscriber from a previous data_sm operation. type AlertNotification struct { base SourceAddr Address EsmeAddr Address } // NewAlertNotification create new alert notification pdu. func NewAlertNotification() PDU { a := &AlertNotification{ base: newBase(), } a.CommandID = data.ALERT_NOTIFICATION return a } // CanResponse implements PDU interface. func (a *AlertNotification) CanResponse() bool { return false } // GetResponse implements PDU interface. func (a *AlertNotification) GetResponse() PDU { return nil } // Marshal implements PDU interface. func (a *AlertNotification) Marshal(b *ByteBuffer) { a.base.marshal(b, func(b *ByteBuffer) { a.SourceAddr.Marshal(b) a.EsmeAddr.Marshal(b) }) } // Unmarshal implements PDU interface. func (a *AlertNotification) Unmarshal(b *ByteBuffer) error { return a.base.unmarshal(b, func(b *ByteBuffer) (err error) { if err = a.SourceAddr.Unmarshal(b); err == nil { err = a.EsmeAddr.Unmarshal(b) } return }) } ================================================ FILE: pdu/AlertNotification_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestAlertNotification(t *testing.T) { a := NewAlertNotification().(*AlertNotification) require.False(t, a.CanResponse()) require.Nil(t, a.GetResponse()) _ = a.SourceAddr.SetAddress("Alice") a.SourceAddr.SetTon(13) a.SourceAddr.SetNpi(15) _ = a.EsmeAddr.SetAddress("Bob") a.EsmeAddr.SetTon(19) a.EsmeAddr.SetNpi(7) b := NewBuffer(nil) a.Marshal(b) expectAfterParse(t, b, a, data.ALERT_NOTIFICATION) } ================================================ FILE: pdu/BindRequest.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // BindingType indicates type of binding. type BindingType byte const ( // Receiver indicates Receiver binding. Receiver BindingType = iota // Transceiver indicates Transceiver binding. Transceiver // Transmitter indicate Transmitter binding. Transmitter ) // BindRequest represents a bind request. type BindRequest struct { base SystemID string Password string SystemType string InterfaceVersion byte AddressRange AddressRange BindingType BindingType } // NewBindRequest returns new bind request. func NewBindRequest(t BindingType) (b *BindRequest) { b = &BindRequest{ base: newBase(), BindingType: t, SystemID: data.DFLT_SYSID, Password: data.DFLT_PASS, SystemType: data.DFLT_SYSTYPE, AddressRange: AddressRange{}, InterfaceVersion: data.SMPP_V34, } switch t { case Transceiver: b.CommandID = data.BIND_TRANSCEIVER case Receiver: b.CommandID = data.BIND_RECEIVER case Transmitter: b.CommandID = data.BIND_TRANSMITTER } return } // NewBindTransmitter returns new bind transmitter pdu. func NewBindTransmitter() PDU { return NewBindRequest(Transmitter) } // NewBindTransceiver returns new bind transceiver pdu. func NewBindTransceiver() PDU { return NewBindRequest(Transceiver) } // NewBindReceiver returns new bind receiver pdu. func NewBindReceiver() PDU { return NewBindRequest(Receiver) } // CanResponse implements PDU interface. func (b *BindRequest) CanResponse() bool { return true } // GetResponse implements PDU interface. func (b *BindRequest) GetResponse() PDU { return NewBindResp(*b) } // Marshal implements PDU interface. func (b *BindRequest) Marshal(w *ByteBuffer) { b.base.marshal(w, func(w *ByteBuffer) { w.Grow(len(b.SystemID) + len(b.Password) + len(b.SystemType) + 4) _ = w.WriteCString(b.SystemID) _ = w.WriteCString(b.Password) _ = w.WriteCString(b.SystemType) _ = w.WriteByte(b.InterfaceVersion) b.AddressRange.Marshal(w) }) } // Unmarshal implements PDU interface. func (b *BindRequest) Unmarshal(w *ByteBuffer) error { return b.base.unmarshal(w, func(w *ByteBuffer) (err error) { if b.SystemID, err = w.ReadCString(); err == nil { if b.Password, err = w.ReadCString(); err == nil { if b.SystemType, err = w.ReadCString(); err == nil { if b.InterfaceVersion, err = w.ReadByte(); err == nil { err = b.AddressRange.Unmarshal(w) } } } } return }) } ================================================ FILE: pdu/BindRequest_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestBindRequest(t *testing.T) { t.Run("receiver", func(t *testing.T) { req := NewBindReceiver().(*BindRequest) require.True(t, req.CanResponse()) req.SequenceNumber = 13 validate(t, req.GetResponse(), "0000001180000001000000000000000d00", data.BIND_RECEIVER_RESP) req.SystemID = "system_id_fake" req.Password = "password" req.SystemType = "only" req.InterfaceVersion = 44 req.AddressRange.AddressRange = "emptY" req.AddressRange.Ton = 23 req.AddressRange.Npi = 101 validate(t, req, "0000003600000001000000000000000d73797374656d5f69645f66616b650070617373776f7264006f6e6c79002c1765656d70745900", data.BIND_RECEIVER, ) }) t.Run("transmitter", func(t *testing.T) { req := NewBindTransmitter().(*BindRequest) require.True(t, req.CanResponse()) req.SequenceNumber = 13 validate(t, req.GetResponse(), "0000001180000002000000000000000d00", data.BIND_TRANSMITTER_RESP) req.SystemID = "system_id_fake" req.Password = "password" req.SystemType = "only" req.InterfaceVersion = 44 req.AddressRange.AddressRange = "emptY" req.AddressRange.Ton = 23 req.AddressRange.Npi = 101 validate(t, req, "0000003600000002000000000000000d73797374656d5f69645f66616b650070617373776f7264006f6e6c79002c1765656d70745900", data.BIND_TRANSMITTER, ) }) t.Run("transceiver", func(t *testing.T) { req := NewBindTransceiver().(*BindRequest) require.True(t, req.CanResponse()) req.SequenceNumber = 13 validate(t, req.GetResponse(), "0000001180000009000000000000000d00", data.BIND_TRANSCEIVER_RESP) req.SystemID = "system_id_fake" req.Password = "password" req.SystemType = "only" req.InterfaceVersion = 44 req.AddressRange.AddressRange = "emptY" req.AddressRange.Ton = 23 req.AddressRange.Npi = 101 validate(t, req, "0000003600000009000000000000000d73797374656d5f69645f66616b650070617373776f7264006f6e6c79002c1765656d70745900", data.BIND_TRANSCEIVER, ) }) } ================================================ FILE: pdu/BindResponse.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // BindResp PDU. type BindResp struct { base SystemID string } // NewBindResp returns BindResp. func NewBindResp(req BindRequest) (c *BindResp) { c = &BindResp{ base: newBase(), } c.SequenceNumber = req.SequenceNumber switch req.BindingType { case Transceiver: c.CommandID = data.BIND_TRANSCEIVER_RESP case Receiver: c.CommandID = data.BIND_RECEIVER_RESP case Transmitter: c.CommandID = data.BIND_TRANSMITTER_RESP } return } // NewBindTransmitterResp returns new bind transmitter resp. func NewBindTransmitterResp() PDU { c := &BindResp{ base: newBase(), } c.CommandID = data.BIND_TRANSMITTER_RESP return c } // NewBindTransceiverResp returns new bind transceiver resp. func NewBindTransceiverResp() PDU { c := &BindResp{ base: newBase(), } c.CommandID = data.BIND_TRANSCEIVER_RESP return c } // NewBindReceiverResp returns new bind receiver resp. func NewBindReceiverResp() PDU { c := &BindResp{ base: newBase(), } c.CommandID = data.BIND_RECEIVER_RESP return c } // CanResponse implements PDU interface. func (c *BindResp) CanResponse() bool { return false } // GetResponse implements PDU interface. func (c *BindResp) GetResponse() PDU { return nil } // Marshal implements PDU interface. func (c *BindResp) Marshal(b *ByteBuffer) { c.base.marshal(b, func(w *ByteBuffer) { w.Grow(len(c.SystemID) + 1) _ = w.WriteCString(c.SystemID) }) } // Unmarshal implements PDU interface. func (c *BindResp) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, func(w *ByteBuffer) (err error) { if c.CommandID == data.BIND_TRANSCEIVER_RESP || c.CommandStatus == data.ESME_ROK { c.SystemID, err = w.ReadCString() } return }) } ================================================ FILE: pdu/BindResponse_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestBindResponse(t *testing.T) { t.Run("receiver", func(t *testing.T) { v := NewBindReceiverResp().(*BindResp) require.False(t, v.CanResponse()) require.Nil(t, v.GetResponse()) v.SequenceNumber = 13 v.SystemID = "system_id_fake" validate(t, v, "0000001f80000001000000000000000d73797374656d5f69645f66616b6500", data.BIND_RECEIVER_RESP, ) }) t.Run("transmitter", func(t *testing.T) { v := NewBindTransmitterResp().(*BindResp) require.False(t, v.CanResponse()) require.Nil(t, v.GetResponse()) v.SequenceNumber = 13 v.SystemID = "system_id_fake" validate(t, v, "0000001f80000002000000000000000d73797374656d5f69645f66616b6500", data.BIND_TRANSMITTER_RESP, ) }) t.Run("transceiver", func(t *testing.T) { v := NewBindTransceiverResp().(*BindResp) require.False(t, v.CanResponse()) require.Nil(t, v.GetResponse()) v.SequenceNumber = 13 v.SystemID = "system_id_fake" validate(t, v, "0000001f80000009000000000000000d73797374656d5f69645f66616b6500", data.BIND_TRANSCEIVER_RESP, ) }) } ================================================ FILE: pdu/Buffer.go ================================================ package pdu import ( "bytes" "encoding/binary" "fmt" "github.com/linxGnu/gosmpp/data" ) const ( // SizeByte is size of byte. SizeByte = 1 // SizeShort is size of short. SizeShort = 2 // SizeInt is size of int. SizeInt = 4 // SizeLong is size of long. SizeLong = 8 ) var ( // ErrBufferNotEnoughByteToRead indicates not enough byte(s) to read from buffer. ErrBufferNotEnoughByteToRead = fmt.Errorf("Not enough byte to read from buffer") endianese = binary.BigEndian ) // ByteBuffer wraps over bytes.Buffer with additional features. type ByteBuffer struct { *bytes.Buffer } // NewBuffer create new buffer from preallocated buffer array. func NewBuffer(inp []byte) *ByteBuffer { if inp == nil { inp = make([]byte, 0, 512) } return &ByteBuffer{Buffer: bytes.NewBuffer(inp)} } // ReadN read n-bytes from buffer. func (c *ByteBuffer) ReadN(n int) (r []byte, err error) { if n > 0 { if c.Len() >= n { // optimistic branching r = make([]byte, n) _, _ = c.Read(r) } else { err = ErrBufferNotEnoughByteToRead } } return } // ReadShort reads short from buffer. func (c *ByteBuffer) ReadShort() (r int16, err error) { v, err := c.ReadN(SizeShort) if err == nil { r = int16(endianese.Uint16(v)) } return } // WriteShort writes short to buffer. func (c *ByteBuffer) WriteShort(v int16) { var b [SizeShort]byte endianese.PutUint16(b[:], uint16(v)) _, _ = c.Write(b[:]) } // ReadInt reads int from buffer. func (c *ByteBuffer) ReadInt() (r int32, err error) { v, err := c.ReadN(SizeInt) if err == nil { r = int32(endianese.Uint32(v)) } return } // WriteInt writes int to buffer. func (c *ByteBuffer) WriteInt(v int32) { var b [SizeInt]byte endianese.PutUint32(b[:], uint32(v)) _, _ = c.Write(b[:]) } // WriteBuffer appends buffer. func (c *ByteBuffer) WriteBuffer(d *ByteBuffer) { if d != nil { _, _ = c.Write(d.Bytes()) } } func (c *ByteBuffer) writeString(st string, isCString bool, enc data.Encoding) (err error) { if len(st) > 0 { var payload []byte if payload, err = enc.Encode(st); err == nil { _, _ = c.Write(payload) } } if err == nil && isCString { _ = c.WriteByte(0) } return } // WriteCString writes c-string. func (c *ByteBuffer) WriteCString(s string) error { return c.writeString(s, true, data.ASCII) } // WriteCStringWithEnc write c-string with encoding. func (c *ByteBuffer) WriteCStringWithEnc(s string, enc data.Encoding) error { return c.writeString(s, true, enc) } // ReadCString read c-string. func (c *ByteBuffer) ReadCString() (st string, err error) { buf, err := c.ReadBytes(0) if err == nil && len(buf) > 0 { // optimistic branching st = string(buf[:len(buf)-1]) } return } // HexDump returns hex dump. func (c *ByteBuffer) HexDump() string { return fmt.Sprintf("%x", c.Buffer.Bytes()) } ================================================ FILE: pdu/Buffer_test.go ================================================ package pdu import ( "strings" "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestBuffer(t *testing.T) { b := NewBuffer(nil) require.Nil(t, b.WriteCStringWithEnc("agjwklgjkwPץ", data.HEBREW)) require.Equal(t, "61676A776B6C676A6B7750F500", strings.ToUpper(b.HexDump())) } ================================================ FILE: pdu/CancelSM.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // CancelSM PDU is issued by the ESME to cancel one or more previously submitted short messages // that are still pending delivery. The command may specify a particular message to cancel, or // all messages for a particular source, destination and service_type are to be cancelled. type CancelSM struct { base ServiceType string MessageID string SourceAddr Address DestAddr Address } // NewCancelSM returns CancelSM PDU. func NewCancelSM() PDU { c := &CancelSM{ base: newBase(), ServiceType: data.DFLT_SRVTYPE, MessageID: data.DFLT_MSGID, SourceAddr: NewAddress(), DestAddr: NewAddress(), } c.CommandID = data.CANCEL_SM return c } // CanResponse implements PDU interface. func (c *CancelSM) CanResponse() bool { return true } // GetResponse implements PDU interface. func (c *CancelSM) GetResponse() PDU { return NewCancelSMRespFromReq(c) } // Marshal implements PDU interface. func (c *CancelSM) Marshal(b *ByteBuffer) { c.base.marshal(b, func(b *ByteBuffer) { b.Grow(len(c.ServiceType) + len(c.MessageID) + 2) _ = b.WriteCString(c.ServiceType) _ = b.WriteCString(c.MessageID) c.SourceAddr.Marshal(b) c.DestAddr.Marshal(b) }) } // Unmarshal implements PDU interface. func (c *CancelSM) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, func(b *ByteBuffer) (err error) { if c.ServiceType, err = b.ReadCString(); err == nil { if c.MessageID, err = b.ReadCString(); err == nil { if err = c.SourceAddr.Unmarshal(b); err == nil { err = c.DestAddr.Unmarshal(b) } } } return }) } ================================================ FILE: pdu/CancelSMResp.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // CancelSMResp PDU. type CancelSMResp struct { base } // NewCancelSMResp returns CancelSMResp. func NewCancelSMResp() PDU { c := &CancelSMResp{ base: newBase(), } c.CommandID = data.CANCEL_SM_RESP return c } // NewCancelSMRespFromReq returns CancelSMResp. func NewCancelSMRespFromReq(req *CancelSM) PDU { c := NewCancelSMResp().(*CancelSMResp) if req != nil { c.SequenceNumber = req.SequenceNumber } return c } // CanResponse implements PDU interface. func (c *CancelSMResp) CanResponse() bool { return false } // GetResponse implements PDU interface. func (c *CancelSMResp) GetResponse() PDU { return nil } // Marshal implements PDU interface. func (c *CancelSMResp) Marshal(b *ByteBuffer) { c.base.marshal(b, nil) } // Unmarshal implements PDU interface. func (c *CancelSMResp) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, nil) } ================================================ FILE: pdu/CancelSMResp_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestCancelSMResp(t *testing.T) { req := NewCancelSM().(*CancelSM) req.SequenceNumber = 11 v := NewCancelSMRespFromReq(req).(*CancelSMResp) require.False(t, v.CanResponse()) require.Nil(t, v.GetResponse()) validate(t, v, "0000001080000008000000000000000b", data.CANCEL_SM_RESP, ) } ================================================ FILE: pdu/CancelSM_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestCancelSM(t *testing.T) { v := NewCancelSM().(*CancelSM) require.True(t, v.CanResponse()) v.SequenceNumber = 13 validate(t, v.GetResponse(), "0000001080000008000000000000000d", data.CANCEL_SM_RESP, ) v.ServiceType = "abc" v.MessageID = "def" _ = v.SourceAddr.SetAddress("Alicer") v.SourceAddr.SetTon(28) v.SourceAddr.SetNpi(29) _ = v.DestAddr.SetAddress("Bobo") v.DestAddr.SetTon(30) v.DestAddr.SetNpi(31) validate(t, v, "0000002800000008000000000000000d61626300646566001c1d416c69636572001e1f426f626f00", data.CANCEL_SM, ) } ================================================ FILE: pdu/DataSM.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // DataSM PDU is used to transfer data between the SMSC and the ESME. // It may be used by both the ESME and SMSC. type DataSM struct { base ServiceType string SourceAddr Address DestAddr Address EsmClass byte RegisteredDelivery byte DataCoding byte } // NewDataSM returns new data sm pdu. func NewDataSM() PDU { c := &DataSM{ base: newBase(), ServiceType: data.DFLT_SRVTYPE, SourceAddr: NewAddress(), DestAddr: NewAddress(), EsmClass: data.DFLT_ESM_CLASS, RegisteredDelivery: data.DFLT_REG_DELIVERY, DataCoding: data.DFLT_DATA_CODING, } c.CommandID = data.DATA_SM return c } // CanResponse implements PDU interface. func (c *DataSM) CanResponse() bool { return true } // GetResponse implements PDU interface. func (c *DataSM) GetResponse() PDU { return NewDataSMRespFromReq(c) } // Marshal implements PDU interface. func (c *DataSM) Marshal(b *ByteBuffer) { c.base.marshal(b, func(b *ByteBuffer) { b.Grow(len(c.ServiceType) + 4) _ = b.WriteCString(c.ServiceType) c.SourceAddr.Marshal(b) c.DestAddr.Marshal(b) _ = b.WriteByte(c.EsmClass) _ = b.WriteByte(c.RegisteredDelivery) _ = b.WriteByte(c.DataCoding) }) } // Unmarshal implements PDU interface. func (c *DataSM) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, func(b *ByteBuffer) (err error) { if c.ServiceType, err = b.ReadCString(); err == nil { if err = c.SourceAddr.Unmarshal(b); err == nil { if err = c.DestAddr.Unmarshal(b); err == nil { if c.EsmClass, err = b.ReadByte(); err == nil { if c.RegisteredDelivery, err = b.ReadByte(); err == nil { c.DataCoding, err = b.ReadByte() } } } } } return }) } ================================================ FILE: pdu/DataSMResp.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // DataSMResp PDU. type DataSMResp struct { base MessageID string } // NewDataSMResp returns DataSMResp. func NewDataSMResp() PDU { c := &DataSMResp{ base: newBase(), MessageID: data.DFLT_MSGID, } c.CommandID = data.DATA_SM_RESP return c } // NewDataSMRespFromReq returns DataSMResp. func NewDataSMRespFromReq(req *DataSM) PDU { c := NewDataSMResp().(*DataSMResp) if req != nil { c.SequenceNumber = req.SequenceNumber } return c } // CanResponse implements PDU interface. func (c *DataSMResp) CanResponse() bool { return false } // GetResponse implements PDU interface. func (c *DataSMResp) GetResponse() PDU { return nil } // Marshal implements PDU interface. func (c *DataSMResp) Marshal(b *ByteBuffer) { c.base.marshal(b, func(b *ByteBuffer) { b.Grow(len(c.MessageID) + 1) _ = b.WriteCString(c.MessageID) }) } // Unmarshal implements PDU interface. func (c *DataSMResp) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, func(b *ByteBuffer) (err error) { c.MessageID, err = b.ReadCString() return }) } ================================================ FILE: pdu/DataSMResp_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestDataSMResp(t *testing.T) { req := NewDataSM().(*DataSM) req.SequenceNumber = 13 v := NewDataSMRespFromReq(req).(*DataSMResp) require.False(t, v.CanResponse()) require.Nil(t, v.GetResponse()) v.MessageID = "testMID" validate(t, v, "0000001880000103000000000000000d746573744d494400", data.DATA_SM_RESP, ) } ================================================ FILE: pdu/DataSM_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestDataSM(t *testing.T) { v := NewDataSM().(*DataSM) require.True(t, v.CanResponse()) v.SequenceNumber = 13 validate(t, v.GetResponse(), "0000001180000103000000000000000d00", data.DATA_SM_RESP, ) v.ServiceType = "abc" _ = v.SourceAddr.SetAddress("Alicer") v.SourceAddr.SetTon(28) v.SourceAddr.SetNpi(29) _ = v.DestAddr.SetAddress("Bobo") v.DestAddr.SetTon(30) v.DestAddr.SetNpi(31) v.EsmClass = 77 v.RegisteredDelivery = 83 v.DataCoding = 91 v.RegisterOptionalParam(Field{Tag: TagDestBearerType, Data: []byte{95}}) tagged, ok := v.OptionalParameters[TagDestBearerType] require.True(t, ok) require.Equal(t, TagDestBearerType, tagged.Tag) require.Equal(t, []byte{95}, tagged.Data) require.Equal(t, "_", tagged.String()) require.Equal(t, "0007", tagged.Tag.Hex()) tagged.Data = []byte{95, 0} require.Equal(t, "_", tagged.String()) validate(t, v, "0000002c00000103000000000000000d616263001c1d416c69636572001e1f426f626f004d535b000700015f", data.DATA_SM, ) } ================================================ FILE: pdu/DeliverSM.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // DeliverSM PDU is issued by the SMSC to send a message to an ESME. // Using this command, the SMSC may route a short message to the ESME for delivery. type DeliverSM struct { base ServiceType string SourceAddr Address DestAddr Address EsmClass byte ProtocolID byte PriorityFlag byte ScheduleDeliveryTime string // not used ValidityPeriod string // not used RegisteredDelivery byte ReplaceIfPresentFlag byte // not used Message ShortMessage } // NewDeliverSM returns DeliverSM PDU. func NewDeliverSM() PDU { message, _ := NewShortMessage("") c := &DeliverSM{ base: newBase(), ServiceType: data.DFLT_SRVTYPE, SourceAddr: NewAddress(), DestAddr: NewAddress(), EsmClass: data.DFLT_ESM_CLASS, ProtocolID: data.DFLT_PROTOCOLID, PriorityFlag: data.DFLT_PRIORITY_FLAG, ScheduleDeliveryTime: data.DFLT_SCHEDULE, ValidityPeriod: data.DFLT_VALIDITY, RegisteredDelivery: data.DFLT_REG_DELIVERY, ReplaceIfPresentFlag: data.DFTL_REPLACE_IFP, Message: message, } c.CommandID = data.DELIVER_SM return c } // CanResponse implements PDU interface. func (c *DeliverSM) CanResponse() bool { return true } // GetResponse implements PDU interface. func (c *DeliverSM) GetResponse() PDU { return NewDeliverSMRespFromReq(c) } // Marshal implements PDU interface. func (c *DeliverSM) Marshal(b *ByteBuffer) { c.base.marshal(b, func(b *ByteBuffer) { b.Grow(len(c.ServiceType) + len(c.ScheduleDeliveryTime) + len(c.ValidityPeriod) + 10) _ = b.WriteCString(c.ServiceType) c.SourceAddr.Marshal(b) c.DestAddr.Marshal(b) _ = b.WriteByte(c.EsmClass) _ = b.WriteByte(c.ProtocolID) _ = b.WriteByte(c.PriorityFlag) _ = b.WriteCString(c.ScheduleDeliveryTime) _ = b.WriteCString(c.ValidityPeriod) _ = b.WriteByte(c.RegisteredDelivery) _ = b.WriteByte(c.ReplaceIfPresentFlag) c.Message.Marshal(b) }) } // Unmarshal implements PDU interface. func (c *DeliverSM) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, func(b *ByteBuffer) (err error) { if c.ServiceType, err = b.ReadCString(); err == nil { if err = c.SourceAddr.Unmarshal(b); err == nil { if err = c.DestAddr.Unmarshal(b); err == nil { if c.EsmClass, err = b.ReadByte(); err == nil { if c.ProtocolID, err = b.ReadByte(); err == nil { if c.PriorityFlag, err = b.ReadByte(); err == nil { if c.ScheduleDeliveryTime, err = b.ReadCString(); err == nil { if c.ValidityPeriod, err = b.ReadCString(); err == nil { if c.RegisteredDelivery, err = b.ReadByte(); err == nil { if c.ReplaceIfPresentFlag, err = b.ReadByte(); err == nil { err = c.Message.Unmarshal(b, (c.EsmClass&data.SM_UDH_GSM) > 0) } } } } } } } } } } return }) } ================================================ FILE: pdu/DeliverSMResp.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // DeliverSMResp PDU. type DeliverSMResp struct { base MessageID string } // NewDeliverSMResp returns new DeliverSMResp. func NewDeliverSMResp() PDU { c := &DeliverSMResp{ base: newBase(), MessageID: data.DFLT_MSGID, } c.CommandID = data.DELIVER_SM_RESP return c } // NewDeliverSMRespFromReq returns new DeliverSMResp. func NewDeliverSMRespFromReq(req *DeliverSM) PDU { c := NewDeliverSMResp().(*DeliverSMResp) if req != nil { c.SequenceNumber = req.SequenceNumber } return c } // CanResponse implements PDU interface. func (c *DeliverSMResp) CanResponse() bool { return false } // GetResponse implements PDU interface. func (c *DeliverSMResp) GetResponse() PDU { return nil } // Marshal implements PDU interface. func (c *DeliverSMResp) Marshal(b *ByteBuffer) { c.base.marshal(b, func(b *ByteBuffer) { b.Grow(len(c.MessageID) + 1) _ = b.WriteCString(c.MessageID) }) } // Unmarshal implements PDU interface. func (c *DeliverSMResp) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, func(b *ByteBuffer) (err error) { c.MessageID, err = b.ReadCString() return }) } ================================================ FILE: pdu/DeliverSMResp_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestDeliverSMResp(t *testing.T) { req := NewDeliverSM().(*DeliverSM) req.SequenceNumber = 13 v := NewDeliverSMRespFromReq(req).(*DeliverSMResp) require.False(t, v.CanResponse()) require.Nil(t, v.GetResponse()) v.MessageID = "testMID" validate(t, v, "0000001880000005000000000000000d746573744d494400", data.DELIVER_SM_RESP, ) } ================================================ FILE: pdu/DeliverSM_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestDeliverSM(t *testing.T) { v := NewDeliverSM().(*DeliverSM) require.True(t, v.CanResponse()) v.SequenceNumber = 13 validate(t, v.GetResponse(), "0000001180000005000000000000000d00", data.DELIVER_SM_RESP, ) v.ServiceType = "abc" _ = v.SourceAddr.SetAddress("Alicer") v.SourceAddr.SetTon(28) v.SourceAddr.SetNpi(29) _ = v.DestAddr.SetAddress("Bobo") v.DestAddr.SetTon(30) v.DestAddr.SetNpi(31) v.EsmClass = 13 v.ProtocolID = 99 v.PriorityFlag = 61 v.RegisteredDelivery = 83 _ = v.Message.SetMessageWithEncoding("nghắ nghiêng nghiễng ngả", data.UCS2) v.Message.message = "" validate(t, v, "0000005e00000005000000000000000d616263001c1d416c69636572001e1f426f626f000d633d00005300080030006e006700681eaf0020006e00670068006900ea006e00670020006e0067006800691ec5006e00670020006e00671ea3", data.DELIVER_SM, ) } func TestDeliverSMwithUDH(t *testing.T) { v := NewDeliverSM().(*DeliverSM) require.True(t, v.CanResponse()) v.SequenceNumber = 13 validate(t, v.GetResponse(), "0000001180000005000000000000000d00", data.DELIVER_SM_RESP, ) v.ServiceType = "abc" _ = v.SourceAddr.SetAddress("Alicer") v.SourceAddr.SetTon(28) v.SourceAddr.SetNpi(29) _ = v.DestAddr.SetAddress("Bobo") v.DestAddr.SetTon(30) v.DestAddr.SetNpi(31) v.EsmClass = 77 v.ProtocolID = 99 v.PriorityFlag = 61 v.RegisteredDelivery = 83 _ = v.Message.SetMessageWithEncoding("nghắ nghiêng nghiễng ngả", data.UCS2) v.Message.message = "" v.Message.SetUDH(UDH{NewIEConcatMessage(2, 1, 254)}) validate(t, v, "0000006400000005000000000000000d616263001c1d416c69636572001e1f426f626f004d633d00005300080036050003fe0201006e006700681eaf0020006e00670068006900ea006e00670020006e0067006800691ec5006e00670020006e00671ea3", data.DELIVER_SM, ) } ================================================ FILE: pdu/DestinationAddress.go ================================================ package pdu import ( "fmt" "github.com/linxGnu/gosmpp/data" ) // DestinationAddress represents Address or Distribution List based on destination flag. type DestinationAddress struct { destFlag byte address Address dl DistributionList } // NewDestinationAddress returns new DestinationAddress. func NewDestinationAddress() (c DestinationAddress) { c.destFlag = data.DFLT_DEST_FLAG return } // Unmarshal from buffer. func (c *DestinationAddress) Unmarshal(b *ByteBuffer) (err error) { if c.destFlag, err = b.ReadByte(); err == nil { switch c.destFlag { case data.SM_DEST_SME_ADDRESS: err = c.address.Unmarshal(b) case data.SM_DEST_DL_NAME: err = c.dl.Unmarshal(b) default: err = fmt.Errorf("Unrecognize dest_flag %d", c.destFlag) } } return } // Marshal to buffer. func (c *DestinationAddress) Marshal(b *ByteBuffer) { switch c.destFlag { case data.SM_DEST_DL_NAME: _ = b.WriteByte(data.SM_DEST_DL_NAME) c.dl.Marshal(b) default: _ = b.WriteByte(data.SM_DEST_SME_ADDRESS) c.address.Marshal(b) } } // Address returns underlying Address. func (c *DestinationAddress) Address() Address { return c.address } // DistributionList returns underlying DistributionList. func (c *DestinationAddress) DistributionList() DistributionList { return c.dl } // SetAddress marks DistributionAddress as a SME Address and assign. func (c *DestinationAddress) SetAddress(addr Address) { c.destFlag = data.SM_DEST_SME_ADDRESS c.address = addr } // SetDistributionList marks DistributionAddress as a DistributionList and assign. func (c *DestinationAddress) SetDistributionList(list DistributionList) { c.destFlag = data.SM_DEST_DL_NAME c.dl = list } // HasValue returns true if underlying DistributionList/Address is assigned. func (c *DestinationAddress) HasValue() bool { return c.destFlag != data.DFLT_DEST_FLAG } // IsAddress returns true if DestinationAddress is a SME Address. func (c *DestinationAddress) IsAddress() bool { return c.destFlag == data.SM_DEST_SME_ADDRESS } // IsDistributionList returns true if DestinationAddress is a DistributionList. func (c *DestinationAddress) IsDistributionList() bool { return c.destFlag == byte(data.SM_DEST_DL_NAME) } // DestinationAddresses represents list of DestinationAddress. type DestinationAddresses struct { l []DestinationAddress } // NewDestinationAddresses returns list of DestinationAddress. func NewDestinationAddresses() (u DestinationAddresses) { u.l = make([]DestinationAddress, 0, 8) return } // Add to list. func (c *DestinationAddresses) Add(addresses ...DestinationAddress) { c.l = append(c.l, addresses...) } // Get list. func (c *DestinationAddresses) Get() []DestinationAddress { return c.l } // Unmarshal from buffer. func (c *DestinationAddresses) Unmarshal(b *ByteBuffer) (err error) { var n byte if n, err = b.ReadByte(); err == nil { c.l = make([]DestinationAddress, n) var i byte for ; i < n; i++ { if err = c.l[i].Unmarshal(b); err != nil { return } } } return } // Marshal to buffer. func (c *DestinationAddresses) Marshal(b *ByteBuffer) { n := byte(len(c.l)) _ = b.WriteByte(n) var i byte for ; i < n; i++ { c.l[i].Marshal(b) } } ================================================ FILE: pdu/DestinationAddress_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestDestinationAddress(t *testing.T) { t.Run("validDESTAddr", func(t *testing.T) { addr := NewAddress() require.Nil(t, addr.SetAddress("Bob1")) d1 := NewDestinationAddress() d1.SetAddress(addr) require.EqualValues(t, data.SM_DEST_SME_ADDRESS, d1.destFlag) require.True(t, d1.IsAddress()) require.False(t, d1.IsDistributionList()) require.True(t, d1.HasValue()) require.Equal(t, "Bob1", d1.Address().Address()) require.Equal(t, "", d1.DistributionList().Name()) dl, err := NewDistributionList("List1") require.Nil(t, err) d2 := NewDestinationAddress() d2.SetDistributionList(dl) require.EqualValues(t, data.SM_DEST_DL_NAME, d2.destFlag) require.False(t, d2.IsAddress()) require.True(t, d2.IsDistributionList()) require.True(t, d2.HasValue()) require.Equal(t, "", d2.Address().Address()) require.Equal(t, "List1", d2.DistributionList().Name()) }) t.Run("invalidDEST", func(t *testing.T) { buf := NewBuffer(nil) _ = buf.WriteByte(51) var d DestinationAddress require.NotNil(t, d.Unmarshal(buf)) }) t.Run("invalidDESTs", func(t *testing.T) { buf := NewBuffer(nil) _ = buf.WriteByte(1) _ = buf.WriteByte(51) var d DestinationAddresses require.NotNil(t, d.Unmarshal(buf)) }) } ================================================ FILE: pdu/DistributionList.go ================================================ package pdu import ( "fmt" "github.com/linxGnu/gosmpp/data" ) // DistributionList represents group of contacts. type DistributionList struct { name string } // NewDistributionList returns a new DistributionList. func NewDistributionList(name string) (c DistributionList, err error) { err = c.SetName(name) return } // Unmarshal from buffer. func (c *DistributionList) Unmarshal(b *ByteBuffer) (err error) { c.name, err = b.ReadCString() return } // Marshal to buffer. func (c *DistributionList) Marshal(b *ByteBuffer) { b.Grow(1 + len(c.name)) _ = b.WriteCString(c.name) } // SetName sets DistributionList name. func (c *DistributionList) SetName(name string) (err error) { if len(name) > data.SM_DL_NAME_LEN { err = fmt.Errorf("Distribution List name exceed limit. (%d > %d)", len(name), data.SM_DL_NAME_LEN) } else { c.name = name } return } // Name returns name of DistributionList func (c DistributionList) Name() string { return c.name } ================================================ FILE: pdu/DistributionList_test.go ================================================ package pdu import ( "testing" "github.com/stretchr/testify/require" ) func TestDistributionList(t *testing.T) { _, err := NewDistributionList("1234567890123456789012") require.NotNil(t, err) } ================================================ FILE: pdu/EnquireLink.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // EnquireLink PDU. This message can be sent by either the ESME or SMSC // and is used to provide a confidence- check of the communication path between // an ESME and an SMSC. On receipt of this request the receiving party should // respond with an enquire_link_resp, thus verifying that the application // level connection between the SMSC and the ESME is functioning. // The ESME may also respond by sending any valid SMPP primitive. type EnquireLink struct { base } // NewEnquireLink returns new EnquireLink PDU. func NewEnquireLink() PDU { c := &EnquireLink{ base: newBase(), } c.CommandID = data.ENQUIRE_LINK return c } // CanResponse implements PDU interface. func (c *EnquireLink) CanResponse() bool { return true } // GetResponse implements PDU interface. func (c *EnquireLink) GetResponse() PDU { return NewEnquireLinkRespFromReq(c) } // Marshal implements PDU interface. func (c *EnquireLink) Marshal(b *ByteBuffer) { c.base.marshal(b, nil) } // Unmarshal implements PDU interface. func (c *EnquireLink) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, nil) } ================================================ FILE: pdu/EnquireLinkResp.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // EnquireLinkResp PDU. type EnquireLinkResp struct { base } // NewEnquireLinkResp returns EnquireLinkResp. func NewEnquireLinkResp() PDU { c := &EnquireLinkResp{ base: newBase(), } c.CommandID = data.ENQUIRE_LINK_RESP return c } // NewEnquireLinkRespFromReq returns EnquireLinkResp. func NewEnquireLinkRespFromReq(req *EnquireLink) PDU { c := NewEnquireLinkResp().(*EnquireLinkResp) if req != nil { c.SequenceNumber = req.SequenceNumber } return c } // CanResponse implements PDU interface. func (c *EnquireLinkResp) CanResponse() bool { return false } // GetResponse implements PDU interface. func (c *EnquireLinkResp) GetResponse() PDU { return nil } // Marshal implements PDU interface. func (c *EnquireLinkResp) Marshal(b *ByteBuffer) { c.base.marshal(b, nil) } // Unmarshal implements PDU interface. func (c *EnquireLinkResp) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, nil) } ================================================ FILE: pdu/EnquireLinkResp_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestEnquireLinkResp(t *testing.T) { req := NewEnquireLink().(*EnquireLink) req.SequenceNumber = 13 v := NewEnquireLinkRespFromReq(req).(*EnquireLinkResp) require.False(t, v.CanResponse()) require.Nil(t, v.GetResponse()) validate(t, v, "0000001080000015000000000000000d", data.ENQUIRE_LINK_RESP, ) } ================================================ FILE: pdu/EnquireLink_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestEnquireLink(t *testing.T) { v := NewEnquireLink().(*EnquireLink) require.True(t, v.CanResponse()) v.SequenceNumber = 13 validate(t, v.GetResponse(), "0000001080000015000000000000000d", data.ENQUIRE_LINK_RESP, ) validate(t, v, "0000001000000015000000000000000d", data.ENQUIRE_LINK, ) } ================================================ FILE: pdu/GenericNack.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // GenericNack PDU is a generic negative acknowledgement to an SMPP PDU submitted // with an invalid message header. A generic_nack response is returned in the following cases: // // - Invalid command_length // If the receiving SMPP entity, on decoding an SMPP PDU, detects an invalid command_length // (either too short or too long), it should assume that the data is corrupt. In such cases // a generic_nack PDU must be returned to the message originator. // // - Unknown command_id // If an unknown or invalid command_id is received, a generic_nack PDU must also be returned to the originator. type GenericNack struct { base } // NewGenericNack returns new GenericNack PDU. func NewGenericNack() PDU { c := &GenericNack{ base: newBase(), } c.CommandID = data.GENERIC_NACK return c } // CanResponse implements PDU interface. func (c *GenericNack) CanResponse() bool { return false } // GetResponse implements PDU interface. func (c *GenericNack) GetResponse() PDU { return nil } // Marshal implements PDU interface. func (c *GenericNack) Marshal(b *ByteBuffer) { c.base.marshal(b, nil) } // Unmarshal implements PDU interface. func (c *GenericNack) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, nil) } ================================================ FILE: pdu/GenericNack_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestGNack(t *testing.T) { v := NewGenericNack().(*GenericNack) require.False(t, v.CanResponse()) require.Nil(t, v.GetResponse()) require.True(t, v.IsGNack()) v.SequenceNumber = 13 validate(t, v, "0000001080000000000000000000000d", data.GENERIC_NACK, ) } ================================================ FILE: pdu/Outbind.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // Outbind PDU is used by the SMSC to signal an ESME to originate a bind_receiver request to the SMSC. type Outbind struct { base SystemID string Password string } // NewOutbind returns Outbind PDU. func NewOutbind() PDU { c := &Outbind{ base: newBase(), } c.CommandID = data.OUTBIND return c } // CanResponse implements PDU interface. func (c *Outbind) CanResponse() bool { return false } // GetResponse implements PDU interface. func (c *Outbind) GetResponse() PDU { return nil } // Marshal implements PDU interface. func (c *Outbind) Marshal(b *ByteBuffer) { c.base.marshal(b, func(b *ByteBuffer) { b.Grow(len(c.SystemID) + len(c.Password) + 2) _ = b.WriteCString(c.SystemID) _ = b.WriteCString(c.Password) }) } // Unmarshal implements PDU interface. func (c *Outbind) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, func(b *ByteBuffer) (err error) { if c.SystemID, err = b.ReadCString(); err == nil { c.Password, err = b.ReadCString() } return }) } ================================================ FILE: pdu/Outbind_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestOutbind(t *testing.T) { v := NewOutbind().(*Outbind) require.False(t, v.CanResponse()) require.Nil(t, v.GetResponse()) require.True(t, v.IsOk()) v.SequenceNumber = 13 v.SystemID = "inventory" v.Password = "ipassword" validate(t, v, "000000240000000b000000000000000d696e76656e746f7279006970617373776f726400", data.OUTBIND, ) } ================================================ FILE: pdu/PDU.go ================================================ package pdu import ( "io" "github.com/linxGnu/gosmpp/data" "github.com/linxGnu/gosmpp/errors" ) // PDU represents PDU interface. type PDU interface { // Marshal PDU to buffer. Marshal(*ByteBuffer) // Unmarshal PDU from buffer. Unmarshal(*ByteBuffer) error // CanResponse indicates that PDU could response to SMSC. CanResponse() bool // GetResponse PDU. GetResponse() PDU // RegisterOptionalParam assigns an optional param. RegisterOptionalParam(Field) // GetHeader returns PDU header. GetHeader() Header // IsOk returns true if command status is OK. IsOk() bool // IsGNack returns true if PDU is GNack. IsGNack() bool // AssignSequenceNumber assigns sequence number auto-incrementally. AssignSequenceNumber() // ResetSequenceNumber resets sequence number. ResetSequenceNumber() // GetSequenceNumber returns assigned sequence number. GetSequenceNumber() int32 // SetSequenceNumber manually sets sequence number. SetSequenceNumber(int32) } type base struct { Header OptionalParameters map[Tag]Field } func newBase() (v base) { v.OptionalParameters = make(map[Tag]Field) v.AssignSequenceNumber() return } // GetHeader returns pdu header. func (c *base) GetHeader() Header { return c.Header } func (c *base) unmarshal(b *ByteBuffer, bodyReader func(*ByteBuffer) error) (err error) { fullLen := b.Len() if err = c.Header.Unmarshal(b); err == nil { // try to unmarshal body if bodyReader != nil { err = bodyReader(b) } if err == nil { // command length cmdLength := int(c.CommandLength) // got - total read byte(s) got := fullLen - b.Len() if got > cmdLength { err = errors.ErrInvalidPDU return } // body < command_length, still have optional parameters ? if got < cmdLength { var optParam []byte if optParam, err = b.ReadN(cmdLength - got); err == nil { err = c.unmarshalOptionalParam(optParam) } if err != nil { return } } // validate again if b.Len() != fullLen-cmdLength { err = errors.ErrInvalidPDU } } } return } func (c *base) unmarshalOptionalParam(optParam []byte) (err error) { buf := NewBuffer(optParam) for buf.Len() > 0 { var field Field if err = field.Unmarshal(buf); err == nil { c.OptionalParameters[field.Tag] = field } else { return } } return } // Marshal to buffer. func (c *base) marshal(b *ByteBuffer, bodyWriter func(*ByteBuffer)) { bodyBuf := NewBuffer(nil) // body if bodyWriter != nil { bodyWriter(bodyBuf) } // optional body for _, v := range c.OptionalParameters { v.Marshal(bodyBuf) } // write header c.CommandLength = int32(data.PDU_HEADER_SIZE + bodyBuf.Len()) c.Header.Marshal(b) // write body and its optional params b.WriteBuffer(bodyBuf) } // RegisterOptionalParam register optional param. func (c *base) RegisterOptionalParam(tlv Field) { c.OptionalParameters[tlv.Tag] = tlv } // IsOk is status ok. func (c *base) IsOk() bool { return c.CommandStatus == data.ESME_ROK } // IsGNack is generic n-ack. func (c *base) IsGNack() bool { return c.CommandID == data.GENERIC_NACK } // Parse PDU from reader. func Parse(r io.Reader) (pdu PDU, err error) { var headerBytes [16]byte if _, err = io.ReadFull(r, headerBytes[:]); err != nil { return } header := ParseHeader(headerBytes) if header.CommandLength < 16 || header.CommandLength > data.MAX_PDU_LEN { err = errors.ErrInvalidPDU return } // read pdu body bodyBytes := make([]byte, header.CommandLength-16) if len(bodyBytes) > 0 { if _, err = io.ReadFull(r, bodyBytes); err != nil { return } } // try to create pdu if pdu, err = CreatePDUFromCmdID(header.CommandID); err == nil { buf := NewBuffer(make([]byte, 0, header.CommandLength)) _, _ = buf.Write(headerBytes[:]) if len(bodyBytes) > 0 { _, _ = buf.Write(bodyBytes) } err = pdu.Unmarshal(buf) } return } ================================================ FILE: pdu/PDUFactory.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" "github.com/linxGnu/gosmpp/errors" ) type pduGenerator func() PDU var pduMap = map[data.CommandIDType]pduGenerator{ data.BIND_TRANSMITTER: NewBindTransmitter, data.BIND_TRANSMITTER_RESP: NewBindTransmitterResp, data.BIND_TRANSCEIVER: NewBindTransceiver, data.BIND_TRANSCEIVER_RESP: NewBindTransceiverResp, data.BIND_RECEIVER: NewBindReceiver, data.BIND_RECEIVER_RESP: NewBindReceiverResp, data.UNBIND: NewUnbind, data.UNBIND_RESP: NewUnbindResp, data.OUTBIND: NewOutbind, data.SUBMIT_SM: NewSubmitSM, data.SUBMIT_SM_RESP: NewSubmitSMResp, data.SUBMIT_MULTI: NewSubmitMulti, data.SUBMIT_MULTI_RESP: NewSubmitMultiResp, data.DELIVER_SM: NewDeliverSM, data.DELIVER_SM_RESP: NewDeliverSMResp, data.DATA_SM: NewDataSM, data.DATA_SM_RESP: NewDataSMResp, data.QUERY_SM: NewQuerySM, data.QUERY_SM_RESP: NewQuerySMResp, data.CANCEL_SM: NewCancelSM, data.CANCEL_SM_RESP: NewCancelSMResp, data.REPLACE_SM: NewReplaceSM, data.REPLACE_SM_RESP: NewReplaceSMResp, data.ENQUIRE_LINK: NewEnquireLink, data.ENQUIRE_LINK_RESP: NewEnquireLinkResp, data.ALERT_NOTIFICATION: NewAlertNotification, data.GENERIC_NACK: NewGenericNack, } // CreatePDUFromCmdID creates PDU from cmd id. func CreatePDUFromCmdID(cmdID data.CommandIDType) (PDU, error) { if g, ok := pduMap[cmdID]; ok { return g(), nil } return nil, errors.ErrUnknownCommandID } ================================================ FILE: pdu/PDUFactory_test.go ================================================ package pdu import ( "testing" "github.com/stretchr/testify/require" ) func TestInvalidCmdID(t *testing.T) { v, err := CreatePDUFromCmdID(-12) require.Nil(t, v) require.NotNil(t, err) } ================================================ FILE: pdu/PDUHeader.go ================================================ package pdu import ( "encoding/binary" "sync/atomic" "github.com/linxGnu/gosmpp/data" ) func nextSequenceNumber(s *int32) (v int32) { // & 0x7FFFFFFF: cater for integer overflow // Allowed range is 0x01 to 0x7FFFFFFF. This // will still result in a single invalid value // of 0x00 every ~2 billion PDUs (not too bad): if v = atomic.AddInt32(s, 1) & 0x7FFFFFFF; v <= 0 { v = 1 } return } // Header represents PDU header. type Header struct { CommandLength int32 CommandID data.CommandIDType CommandStatus data.CommandStatusType SequenceNumber int32 } // ParseHeader parses PDU header. func ParseHeader(v [16]byte) (h Header) { h.CommandLength = int32(binary.BigEndian.Uint32(v[:])) h.CommandID = data.CommandIDType(binary.BigEndian.Uint32(v[4:])) h.CommandStatus = data.CommandStatusType(binary.BigEndian.Uint32(v[8:])) h.SequenceNumber = int32(binary.BigEndian.Uint32(v[12:])) return } // Unmarshal from buffer. func (c *Header) Unmarshal(b *ByteBuffer) (err error) { var id, status int32 c.CommandLength, err = b.ReadInt() if err == nil { id, err = b.ReadInt() if err == nil { c.CommandID = data.CommandIDType(id) if status, err = b.ReadInt(); err == nil { c.CommandStatus = data.CommandStatusType(status) c.SequenceNumber, err = b.ReadInt() } } } return } var sequenceNumber int32 // AssignSequenceNumber assigns sequence number auto-incrementally. func (c *Header) AssignSequenceNumber() { c.SetSequenceNumber(nextSequenceNumber(&sequenceNumber)) } // ResetSequenceNumber resets sequence number. func (c *Header) ResetSequenceNumber() { c.SequenceNumber = 1 } // GetSequenceNumber returns assigned sequence number. func (c *Header) GetSequenceNumber() int32 { return c.SequenceNumber } // SetSequenceNumber manually sets sequence number. func (c *Header) SetSequenceNumber(v int32) { c.SequenceNumber = v } // Marshal to buffer. func (c *Header) Marshal(b *ByteBuffer) { b.Grow(16) b.WriteInt(c.CommandLength) b.WriteInt(int32(c.CommandID)) b.WriteInt(int32(c.CommandStatus)) b.WriteInt(c.SequenceNumber) } ================================================ FILE: pdu/PDUHeader_test.go ================================================ package pdu import ( "math" "testing" "github.com/stretchr/testify/require" ) func TestNextSeq(t *testing.T) { var v int32 = math.MaxInt32 require.EqualValues(t, 1, nextSequenceNumber(&v)) } ================================================ FILE: pdu/PDU_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/errors" "github.com/stretchr/testify/require" ) func TestParsePDU(t *testing.T) { t.Run("valid", func(t *testing.T) { buf := NewBuffer(fromHex("00000010800000060000000000000001")) _, err := Parse(buf) require.Nil(t, err) }) t.Run("submit_sm_resp with command_status != 0 ", func(t *testing.T) { buf := NewBuffer(fromHex("00000010800000040000005800000001")) _, err := Parse(buf) require.Nil(t, err) }) t.Run("eof", func(t *testing.T) { buf := NewBuffer(nil) _, err := Parse(buf) require.NotNil(t, err) }) t.Run("invalidCmdLength", func(t *testing.T) { buf := NewBuffer(fromHex("0000000f800000060000000000000001")) _, err := Parse(buf) require.Equal(t, errors.ErrInvalidPDU, err) buf = NewBuffer(fromHex("3800000f800000060000000000000001")) _, err = Parse(buf) require.Equal(t, errors.ErrInvalidPDU, err) }) t.Run("invalidBody", func(t *testing.T) { buf := NewBuffer(fromHex("0000001e00000003000000000000000161776179001c1d416c69636572")) _, err := Parse(buf) require.NotNil(t, err) }) t.Run("invalidPayload", func(t *testing.T) { buf := NewBuffer(fromHex("000000118000000400000000000000010012")) var b base require.NotNil(t, b.unmarshal(buf, func(buf *ByteBuffer) error { return nil })) buf = NewBuffer(fromHex("000000118000000400000000000000010012333333333333333333")) require.NotNil(t, b.unmarshal(buf, func(buf *ByteBuffer) error { _, _ = buf.ReadN(8) return nil })) }) } ================================================ FILE: pdu/QuerySM.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // QuerySM PDU is issued by the ESME to query the status of a previously submitted short message. // The matching mechanism is based on the SMSC assigned message_id and source address. Where the // original submit_sm, data_sm or submit_multi ‘source address’ was defaulted to NULL, then the // source address in the query_sm command should also be set to NULL. type QuerySM struct { base MessageID string SourceAddr Address } // NewQuerySM returns new QuerySM PDU. func NewQuerySM() PDU { c := &QuerySM{ SourceAddr: NewAddress(), } c.CommandID = data.QUERY_SM return c } // CanResponse implements PDU interface. func (c *QuerySM) CanResponse() bool { return true } // GetResponse implements PDU interface. func (c *QuerySM) GetResponse() PDU { return NewQuerySMRespFromReq(c) } // Marshal implements PDU interface. func (c *QuerySM) Marshal(b *ByteBuffer) { c.base.marshal(b, func(b *ByteBuffer) { b.Grow(len(c.MessageID) + 1) _ = b.WriteCString(c.MessageID) c.SourceAddr.Marshal(b) }) } // Unmarshal implements PDU interface. func (c *QuerySM) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, func(b *ByteBuffer) (err error) { if c.MessageID, err = b.ReadCString(); err == nil { err = c.SourceAddr.Unmarshal(b) } return }) } ================================================ FILE: pdu/QuerySMResp.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // QuerySMResp PDU. type QuerySMResp struct { base MessageID string FinalDate string MessageState byte ErrorCode byte } // NewQuerySMResp returns new QuerySM PDU. func NewQuerySMResp() PDU { c := &QuerySMResp{ base: newBase(), FinalDate: data.DFLT_DATE, MessageState: data.DFLT_MSG_STATE, ErrorCode: data.DFLT_ERR, } c.CommandID = data.QUERY_SM_RESP return c } // NewQuerySMRespFromReq returns new QuerySM PDU. func NewQuerySMRespFromReq(req *QuerySM) PDU { c := NewQuerySMResp().(*QuerySMResp) if req != nil { c.SequenceNumber = req.SequenceNumber } return c } // CanResponse implements PDU interface. func (c *QuerySMResp) CanResponse() bool { return false } // GetResponse implements PDU interface. func (c *QuerySMResp) GetResponse() PDU { return nil } // Marshal implements PDU interface. func (c *QuerySMResp) Marshal(b *ByteBuffer) { c.base.marshal(b, func(b *ByteBuffer) { b.Grow(len(c.MessageID) + len(c.FinalDate) + 4) _ = b.WriteCString(c.MessageID) _ = b.WriteCString(c.FinalDate) _ = b.WriteByte(c.MessageState) _ = b.WriteByte(c.ErrorCode) }) } // Unmarshal implements PDU interface. func (c *QuerySMResp) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, func(b *ByteBuffer) (err error) { if c.MessageID, err = b.ReadCString(); err == nil { if c.FinalDate, err = b.ReadCString(); err == nil { if c.MessageState, err = b.ReadByte(); err == nil { c.ErrorCode, err = b.ReadByte() } } } return }) } ================================================ FILE: pdu/QuerySMResp_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestQuerySMResp(t *testing.T) { req := NewQuerySM().(*QuerySM) req.SequenceNumber = 13 v := NewQuerySMRespFromReq(req).(*QuerySMResp) require.False(t, v.CanResponse()) require.Nil(t, v.GetResponse()) validate(t, v, "0000001480000003000000000000000d00000000", data.QUERY_SM_RESP, ) } ================================================ FILE: pdu/QuerySM_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestQuerySM(t *testing.T) { v := NewQuerySM().(*QuerySM) require.True(t, v.CanResponse()) v.SequenceNumber = 13 validate(t, v.GetResponse(), "0000001480000003000000000000000d00000000", data.QUERY_SM_RESP, ) v.MessageID = "away" _ = v.SourceAddr.SetAddress("Alicer") v.SourceAddr.SetTon(28) v.SourceAddr.SetNpi(29) validate(t, v, "0000001e00000003000000000000000d61776179001c1d416c6963657200", data.QUERY_SM, ) } ================================================ FILE: pdu/ReplaceSM.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // ReplaceSM PDU is issued by the ESME to replace a previously submitted short message // that is still pending delivery. The matching mechanism is based on the message_id and // source address of the original message. Where the original submit_sm ‘source address’ // was defaulted to NULL, then the source address in the replace_sm command should also be NULL. type ReplaceSM struct { base MessageID string SourceAddr Address ScheduleDeliveryTime string ValidityPeriod string RegisteredDelivery byte Message ShortMessage } // NewReplaceSM returns ReplaceSM PDU. func NewReplaceSM() PDU { message, _ := NewShortMessage("") message.withoutDataCoding = true c := &ReplaceSM{ base: newBase(), SourceAddr: NewAddress(), ScheduleDeliveryTime: data.DFLT_SCHEDULE, ValidityPeriod: data.DFLT_VALIDITY, RegisteredDelivery: data.DFLT_REG_DELIVERY, Message: message, } c.CommandID = data.REPLACE_SM return c } // CanResponse implements PDU interface. func (c *ReplaceSM) CanResponse() bool { return true } // GetResponse implements PDU interface. func (c *ReplaceSM) GetResponse() PDU { return NewReplaceSMRespFromReq(c) } // Marshal implements PDU interface. func (c *ReplaceSM) Marshal(b *ByteBuffer) { c.base.marshal(b, func(b *ByteBuffer) { b.Grow(len(c.MessageID) + len(c.ScheduleDeliveryTime) + len(c.ValidityPeriod) + 4) _ = b.WriteCString(c.MessageID) c.SourceAddr.Marshal(b) _ = b.WriteCString(c.ScheduleDeliveryTime) _ = b.WriteCString(c.ValidityPeriod) _ = b.WriteByte(c.RegisteredDelivery) c.Message.Marshal(b) }) } // Unmarshal implements PDU interface. func (c *ReplaceSM) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, func(b *ByteBuffer) (err error) { if c.MessageID, err = b.ReadCString(); err == nil { if err = c.SourceAddr.Unmarshal(b); err == nil { if c.ScheduleDeliveryTime, err = b.ReadCString(); err == nil { if c.ValidityPeriod, err = b.ReadCString(); err == nil { if c.RegisteredDelivery, err = b.ReadByte(); err == nil { err = c.Message.Unmarshal(b, false) } } } } } return }) } ================================================ FILE: pdu/ReplaceSMResp.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // ReplaceSMResp PDU. type ReplaceSMResp struct { base } // NewReplaceSMResp returns ReplaceSMResp. func NewReplaceSMResp() PDU { c := &ReplaceSMResp{ base: newBase(), } c.CommandID = data.REPLACE_SM_RESP return c } // NewReplaceSMRespFromReq returns ReplaceSMResp. func NewReplaceSMRespFromReq(req *ReplaceSM) PDU { c := NewReplaceSMResp().(*ReplaceSMResp) if req != nil { c.SequenceNumber = req.SequenceNumber } return c } // CanResponse implements PDU interface. func (c *ReplaceSMResp) CanResponse() bool { return false } // GetResponse implements PDU interface. func (c *ReplaceSMResp) GetResponse() PDU { return nil } // Marshal implements PDU interface. func (c *ReplaceSMResp) Marshal(b *ByteBuffer) { c.base.marshal(b, nil) } // Unmarshal implements PDU interface. func (c *ReplaceSMResp) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, nil) } ================================================ FILE: pdu/ReplaceSMResp_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestReplaceSMResp(t *testing.T) { req := NewReplaceSM().(*ReplaceSM) req.SequenceNumber = 13 v := NewReplaceSMRespFromReq(req).(*ReplaceSMResp) require.False(t, v.CanResponse()) require.Nil(t, v.GetResponse()) validate(t, v, "0000001080000007000000000000000d", data.REPLACE_SM_RESP, ) } ================================================ FILE: pdu/ReplaceSM_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestReplaceSM(t *testing.T) { v := NewReplaceSM().(*ReplaceSM) require.True(t, v.CanResponse()) require.True(t, v.Message.withoutDataCoding) v.SequenceNumber = 13 validate(t, v.GetResponse(), "0000001080000007000000000000000d", data.REPLACE_SM_RESP, ) v.MessageID = "ID_Her" _ = v.SourceAddr.SetAddress("Alicer") v.SourceAddr.SetTon(28) v.SourceAddr.SetNpi(29) v.RegisteredDelivery = 83 _ = v.Message.SetMessageWithEncoding("nightwish", data.GSM7BIT) v.Message.message = "" require.Equal(t, data.GSM7BIT, v.Message.Encoding()) message, err := v.Message.GetMessage() require.Nil(t, err) require.Equal(t, "nightwish", message) validate(t, v, "0000002e00000007000000000000000d49445f486572001c1d416c696365720000005300096e6967687477697368", data.REPLACE_SM, ) } ================================================ FILE: pdu/ShortMessage.go ================================================ package pdu import ( "sync/atomic" "github.com/linxGnu/gosmpp/data" "github.com/linxGnu/gosmpp/errors" ) var ref = uint32(0) // ShortMessage message. type ShortMessage struct { SmDefaultMsgID byte message string enc data.Encoding udHeader UDH messageData []byte withoutDataCoding bool // purpose of ReplaceSM usage } // NewShortMessage returns new ShortMessage. func NewShortMessage(message string) (s ShortMessage, err error) { err = s.SetMessageWithEncoding(message, data.GSM7BIT) return } // NewShortMessageWithEncoding returns new ShortMessage with predefined encoding. func NewShortMessageWithEncoding(message string, enc data.Encoding) (s ShortMessage, err error) { err = s.SetMessageWithEncoding(message, enc) return } // NewBinaryShortMessage returns new ShortMessage. func NewBinaryShortMessage(messageData []byte) (s ShortMessage, err error) { err = s.SetMessageDataWithEncoding(messageData, data.BINARY8BIT2) return } // NewBinaryShortMessageWithEncoding returns new ShortMessage with predefined encoding. func NewBinaryShortMessageWithEncoding(messageData []byte, enc data.Encoding) (s ShortMessage, err error) { err = s.SetMessageDataWithEncoding(messageData, enc) return } // NewLongMessage returns long message splitted into multiple short message func NewLongMessage(message string) (s []*ShortMessage, err error) { return NewLongMessageWithEncoding(message, data.GSM7BIT) } // NewLongMessageWithEncoding returns long message splitted into multiple short message with encoding of choice func NewLongMessageWithEncoding(message string, enc data.Encoding) (s []*ShortMessage, err error) { sm := &ShortMessage{ message: message, enc: enc, } return sm.split() } // SetMessageWithEncoding sets message with encoding. func (c *ShortMessage) SetMessageWithEncoding(message string, enc data.Encoding) (err error) { if c.messageData, err = enc.Encode(message); err == nil { if len(c.messageData) > data.SM_MSG_LEN { err = errors.ErrShortMessageLengthTooLarge } else { c.message = message c.enc = enc } if c.enc == data.GSM7BITPACKED { // to prevent unwanted "@" runeSlice := []rune(c.message) tLen := len(runeSlice) escCharsLen := len(data.GetEscapeChars(runeSlice)) regCharsLen := tLen - escCharsLen nSeptet := escCharsLen*2 + regCharsLen if (nSeptet+1)%8 == 0 { c.messageData[len(c.messageData)-1] = (c.messageData[len(c.messageData)-1] & 0x01) | (0x0D << 1) /* https://en.wikipedia.org/wiki/GSM_03.38 Ref tekst: "..When there are 7 spare bits in the last octet of a message..."*/ } } } return } // SetLongMessageWithEnc sets ShortMessage with message longer than 256 bytes // callers are expected to call Split() after this func (c *ShortMessage) SetLongMessageWithEnc(message string, enc data.Encoding) (err error) { c.message = message c.enc = enc return } // UDH gets user data header for short message func (c *ShortMessage) UDH() UDH { return c.udHeader } // SetUDH sets user data header for short message // also appends udh to the beginning of messageData func (c *ShortMessage) SetUDH(udh UDH) { c.udHeader = udh } // SetMessageDataWithEncoding sets underlying raw data which is used for pdu marshalling. func (c *ShortMessage) SetMessageDataWithEncoding(d []byte, enc data.Encoding) (err error) { if len(d) > data.SM_MSG_LEN { err = errors.ErrShortMessageLengthTooLarge } else { c.messageData = d c.enc = enc } return } // GetMessageData returns underlying binary message. func (c *ShortMessage) GetMessageData() (d []byte, err error) { return c.messageData, nil } // GetMessage returns underlying message. func (c *ShortMessage) GetMessage() (st string, err error) { enc := c.enc if enc == nil { enc = data.GSM7BIT } st, err = c.GetMessageWithEncoding(enc) return } // GetMessageWithEncoding returns (decoded) underlying message. func (c *ShortMessage) GetMessageWithEncoding(enc data.Encoding) (st string, err error) { if len(c.messageData) > 0 { st, err = enc.Decode(c.messageData) } return } // split one short message and split into multiple short message, with UDH // according to 33GP TS 23.040 section 9.2.3.24.1 // // NOTE: split() will return array of length 1 if data length is still within the limit // The encoding interface can implement the data.Splitter interface for ad-hoc splitting rule func (c *ShortMessage) split() (multiSM []*ShortMessage, err error) { var encoding data.Encoding if c.enc == nil { encoding = data.GSM7BIT } else { encoding = c.enc } // check if encoding implements data.Splitter splitter, ok := encoding.(data.Splitter) // check if encoding implements data.Splitter or split is necessary if !ok || !splitter.ShouldSplit(c.message, data.SM_GSM_MSG_LEN) { err = c.SetMessageWithEncoding(c.message, c.enc) multiSM = []*ShortMessage{c} return } // Reserve 6 bytes for concat message UDH // // Good references: // - https://help.goacoustic.com/hc/en-us/articles/360043843154--How-character-encoding-affects-SMS-message-length // - https://www.twilio.com/docs/glossary/what-is-gsm-7-character-encoding // // Limitation is 160 GSM-7 characters and we also need 6 bytes for UDH // -> 134 octets per segment // -> this leaves 153 GSM-7 characters per segment. segments, err := splitter.EncodeSplit(c.message, data.SM_GSM_MSG_LEN-6) if err != nil { return nil, err } // prealloc result multiSM = make([]*ShortMessage, 0, len(segments)) // all segments will have the same ref id ref := getRefNum() // construct SM(s) for i, seg := range segments { // create new SM, encode data multiSM = append(multiSM, &ShortMessage{ enc: c.enc, // message: we don't really care messageData: seg, withoutDataCoding: c.withoutDataCoding, udHeader: UDH{NewIEConcatMessage(uint8(len(segments)), uint8(i+1), uint8(ref))}, }) } return } // Marshal implements PDU interface. func (c *ShortMessage) Marshal(b *ByteBuffer) { var ( udhBin []byte n = byte(len(c.messageData)) ) // Prepend UDH to message data if there are any if c.udHeader != nil && c.udHeader.UDHL() > 0 { udhBin, _ = c.udHeader.MarshalBinary() } b.Grow(int(n) + 3) var coding byte if c.enc == nil { coding = data.GSM7BITCoding } else { coding = c.enc.DataCoding() } // data_coding if !c.withoutDataCoding { _ = b.WriteByte(coding) } // sm_default_msg_id _ = b.WriteByte(c.SmDefaultMsgID) // sm_length if udhBin != nil { _ = b.WriteByte(byte(int(n) + len(udhBin))) b.Write(udhBin) } else { _ = b.WriteByte(n) } // short_message _, _ = b.Write(c.messageData[:n]) } // Unmarshal implements PDU interface. func (c *ShortMessage) Unmarshal(b *ByteBuffer, udhi bool) (err error) { var dataCoding, n byte if !c.withoutDataCoding { if dataCoding, err = b.ReadByte(); err != nil { return } } if c.SmDefaultMsgID, err = b.ReadByte(); err != nil { return } if n, err = b.ReadByte(); err != nil { return } if c.messageData, err = b.ReadN(int(n)); err != nil { return } c.enc = data.FromDataCoding(dataCoding) // If short message length is non zero, short message contains User-Data Header // Else UDH should be in TLV field MessagePayload if udhi && n > 0 { udh := UDH{} _, err = udh.UnmarshalBinary(c.messageData) if err != nil { return } c.udHeader = udh f := c.udHeader.UDHL() if f > len(c.messageData) { err = errors.ErrUDHTooLong return } c.messageData = c.messageData[f:] } return } // Encoding returns message encoding. func (c *ShortMessage) Encoding() data.Encoding { return c.enc } // returns an atomically incrementing number each time it's called func getRefNum() uint32 { return atomic.AddUint32(&ref, 1) } // NOTE: // When coding splitting function, I have 4 choices of abstraction // 1. Split the message before encode // 2. Split the message after encoded // 3. Split the message DURING encoding (before bit packing) // 4. Encode, unpack, split // // Disadvantages: // 1. The only way to really know if each segment will fit into 134 octet limit is // to do some kind of simulated encoding, where you calculate the total octet // by iterating through each character one by one. // Too cumbersome // // 2. When breaking string at octet position 134, I have to detemeine which // character is it ( by doing some kind of decoding) // a. If the character code point does not fit in the octet // boundary, it has to be carried-over to the next segment. // The remaining bits after extracting the carry-over // has to be filled with zero. // b. If it is an escape character, then I have to backtrack // even further since escape chars are not allowed to be splitted // in the middle. // Since the second bytes of escape chars can be confused with // normal chars, I must always lookback 2 character ( repeat step a for at least 2 septet ) // c. After extracting the carry-on // -> Option 2 is very hard when bit packing is already applied // // 3. Options 3 require extending Encoding interface, // The not good point is not being able to utilize the encoder's Transform() method // The good point is you don't have to do bit packing twice // 4. Terrible option // All this headaches really only apply to variable length encoding. // When using fixed length encoding, you can really split the source message BEFORE encodes. ================================================ FILE: pdu/ShortMessage_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/linxGnu/gosmpp/errors" "github.com/stretchr/testify/require" ) type customEncoder struct{} func (*customEncoder) Encode(str string) ([]byte, error) { return []byte(str), nil } func (*customEncoder) Decode(data []byte) (string, error) { return string(data), nil } func TestShortMessage(t *testing.T) { t.Run("invalidCoding", func(t *testing.T) { var s ShortMessage require.NotNil(t, s.SetMessageWithEncoding("agjwklgjkwPфngưỡng", data.LATIN1)) }) t.Run("customCoding", func(t *testing.T) { var s ShortMessage customCoding := data.NewCustomEncoding(246, &customEncoder{}) err := s.SetMessageDataWithEncoding([]byte{0x61, 0x62, 0x63}, customCoding) // "abc" require.NoError(t, err) require.EqualValues(t, 246, s.Encoding().DataCoding()) m, err := s.GetMessage() require.Nil(t, err) require.Equal(t, "abc", m) // try to get message string with other encoding m, err = s.GetMessageWithEncoding(data.FromDataCoding(data.UCS2Coding)) require.Nil(t, err) require.NotEqual(t, "abc", m) // get message string with custom encoding m, err = s.GetMessageWithEncoding(customCoding) require.Nil(t, err) require.Equal(t, "abc", m) }) t.Run("customCodingFromPeer", func(t *testing.T) { var senderSM ShortMessage // set custom data coding for test customCoding := data.NewCustomEncoding(0x19, &customEncoder{}) err := senderSM.SetMessageDataWithEncoding([]byte{0x61, 0x62, 0x63}, customCoding) // "abc" require.NoError(t, err) b := NewBuffer(nil) senderSM.Marshal(b) // From here the message is not know anymore to the receiver in terms of encoding methods, but the receiver wants to know the encoding code once receiving the packet var receivedSM ShortMessage err = receivedSM.Unmarshal(b, false) require.NoError(t, err) require.NotNil(t, receivedSM.Encoding()) }) t.Run("invalidSize", func(t *testing.T) { var s ShortMessage require.Equal(t, errors.ErrShortMessageLengthTooLarge, s.SetMessageWithEncoding("agjwklgjkwPфngưỡngasdfasdfasdfasdagjwklgjkwPфngưỡngasdfasdfasdfasdagjwklgjkwPфngưỡngasdfasdfasdfasdagjwklgjkwPфngưỡngasdfasdfasdfasd", data.UCS2)) }) t.Run("getMessageWithoutCoding", func(t *testing.T) { var s ShortMessage s.messageData = []byte{0x61, 0x62, 0x63} m, err := s.GetMessage() require.Nil(t, err) require.Equal(t, "abc", m) }) t.Run("getMessageData", func(t *testing.T) { s, err := NewBinaryShortMessage([]byte{0x00, 0x01, 0x02, 0x03}) require.NoError(t, err) messageData, err := s.GetMessageData() require.NoError(t, err) require.Equal(t, "00010203", toHex(messageData)) }) t.Run("marshalBinaryMessage", func(t *testing.T) { s, err := NewBinaryShortMessage([]byte{0x00, 0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) buf := NewBuffer(nil) s.Marshal(buf) require.Equal(t, "0400050001020304", toHex(buf.Bytes())) }) t.Run("marshalWithoutCoding", func(t *testing.T) { var s ShortMessage err := s.SetMessageDataWithEncoding([]byte("abc"), nil) require.NoError(t, err) s.messageData = append(s.messageData, 0) s.enc = nil buf := NewBuffer(nil) s.Marshal(buf) require.Equal(t, "00000461626300", toHex(buf.Bytes())) }) t.Run("marshalWithCoding", func(t *testing.T) { s, err := NewShortMessageWithEncoding("abc", data.GSM7BIT) require.NoError(t, err) buf := NewBuffer(nil) s.Marshal(buf) require.Equal(t, "000003616263", toHex(buf.Bytes())) }) t.Run("marshalWithCoding160chars", func(t *testing.T) { s, err := NewShortMessageWithEncoding("abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab", data.GSM7BIT) require.NoError(t, err) buf := NewBuffer(nil) s.Marshal(buf) require.Equal(t, 116, len(buf.Bytes())) }) t.Run("marshalGSM7WithUDHConcat", func(t *testing.T) { s, err := NewShortMessageWithEncoding("abc", data.GSM7BIT) require.NoError(t, err) s.SetUDH(UDH{NewIEConcatMessage(2, 1, 12)}) buf := NewBuffer(nil) s.Marshal(buf) require.Equal(t, "0000090500030c0201616263", toHex(buf.Bytes())) }) t.Run("unmarshalBinaryWithUDHConcat", func(t *testing.T) { s := &ShortMessage{} buf := NewBuffer([]byte{0x04, 0x00, 0x09, 0x05, 0x00, 0x03, 0x0c, 0x02, 0x01, 0x01, 0x02, 0x03}) // check encoding require.NoError(t, s.Unmarshal(buf, true)) require.Equal(t, data.BINARY8BIT2, s.Encoding()) // check message messageData, err := s.GetMessageData() require.NoError(t, err) require.Equal(t, []byte{0x01, 0x02, 0x03}, messageData) }) t.Run("unmarshalGSM7WithUDHConcat", func(t *testing.T) { s := &ShortMessage{} buf := NewBuffer([]byte{0x00, 0x00, 0x09, 0x05, 0x00, 0x03, 0x0c, 0x02, 0x01, 0x61, 0x62, 0x63}) // check encoding require.NoError(t, s.Unmarshal(buf, true)) require.Equal(t, data.GSM7BIT, s.Encoding()) // check message message, err := s.GetMessageWithEncoding(s.Encoding()) require.NoError(t, err) require.Equal(t, "abc", message) }) t.Run("shortMessageSplitGSM7_169chars", func(t *testing.T) { // over gsm7 chars limit ( 169/160 ), split sm, err := NewLongMessageWithEncoding("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz1234123456789", data.GSM7BIT) require.NoError(t, err) require.Equal(t, 2, len(sm)) }) t.Run("shortMessageSplitGSM7_160chars", func(t *testing.T) { // over gsm7 chars limit ( 160/160 ), split sm, err := NewLongMessageWithEncoding("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz1234", data.GSM7BIT) require.NoError(t, err) require.Equal(t, 2, len(sm)) }) t.Run("shortMessageSplitUCS2_89chars", func(t *testing.T) { // over UCS2 chars limit (89/67), split sm, err := NewLongMessageWithEncoding("biggest gift của Christmas là có nhiều big/challenging/meaningful problems để sấp mặt làm", data.UCS2) require.NoError(t, err) require.Equal(t, 2, len(sm)) }) t.Run("shortMessageSplitUCS2_67chars", func(t *testing.T) { // still within UCS2 chars limit (67/67), not split sm, err := NewLongMessageWithEncoding("biggest gift của Christmas là có nhiều big/challenging/meaningful p", data.UCS2) require.NoError(t, err) require.Equal(t, 1, len(sm)) }) t.Run("shortMessageSplitGSM7_empty", func(t *testing.T) { // over UCS2 chars limit (89/67), split sm, err := NewLongMessageWithEncoding("", data.GSM7BIT) require.NoError(t, err) require.Equal(t, 1, len(sm)) }) t.Run("indempotentMarshal", func(t *testing.T) { // over gsm7 chars limit ( 160/160 ), split multiSM, err := NewLongMessageWithEncoding("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz1234", data.GSM7BIT) require.NoError(t, err) for i := range multiSM { b1, b2 := NewBuffer(nil), NewBuffer(nil) multiSM[i].Marshal(b1) multiSM[i].Marshal(b2) require.Equal(t, b1.Bytes(), b2.Bytes()) } }) } ================================================ FILE: pdu/SubmitMulti.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // SubmitMulti PDU is used to submit an SMPP message for delivery to multiple recipients // or to one or more Distribution Lists. The submit_multi PDU does not support // the transaction message mode. type SubmitMulti struct { base ServiceType string SourceAddr Address DestAddrs DestinationAddresses EsmClass byte ProtocolID byte PriorityFlag byte ScheduleDeliveryTime string ValidityPeriod string // not used RegisteredDelivery byte ReplaceIfPresentFlag byte // not used Message ShortMessage } // NewSubmitMulti returns NewSubmitMulti PDU. func NewSubmitMulti() PDU { message, _ := NewShortMessage("") c := &SubmitMulti{ base: newBase(), ServiceType: data.DFLT_SRVTYPE, SourceAddr: NewAddress(), DestAddrs: NewDestinationAddresses(), EsmClass: data.DFLT_ESM_CLASS, ProtocolID: data.DFLT_PROTOCOLID, PriorityFlag: data.DFLT_PRIORITY_FLAG, ScheduleDeliveryTime: data.DFLT_SCHEDULE, ValidityPeriod: data.DFLT_VALIDITY, RegisteredDelivery: data.DFLT_REG_DELIVERY, ReplaceIfPresentFlag: data.DFTL_REPLACE_IFP, Message: message, } c.CommandID = data.SUBMIT_MULTI return c } // CanResponse implements PDU interface. func (c *SubmitMulti) CanResponse() bool { return true } // GetResponse implements PDU interface. func (c *SubmitMulti) GetResponse() PDU { return NewSubmitMultiRespFromReq(c) } // Marshal implements PDU interface. func (c *SubmitMulti) Marshal(b *ByteBuffer) { c.base.marshal(b, func(b *ByteBuffer) { b.Grow(len(c.ServiceType) + len(c.ScheduleDeliveryTime) + len(c.ValidityPeriod) + 10) _ = b.WriteCString(c.ServiceType) c.SourceAddr.Marshal(b) c.DestAddrs.Marshal(b) _ = b.WriteByte(c.EsmClass) _ = b.WriteByte(c.ProtocolID) _ = b.WriteByte(c.PriorityFlag) _ = b.WriteCString(c.ScheduleDeliveryTime) _ = b.WriteCString(c.ValidityPeriod) _ = b.WriteByte(c.RegisteredDelivery) _ = b.WriteByte(c.ReplaceIfPresentFlag) c.Message.Marshal(b) }) } // Unmarshal implements PDU interface. func (c *SubmitMulti) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, func(b *ByteBuffer) (err error) { if c.ServiceType, err = b.ReadCString(); err == nil { if err = c.SourceAddr.Unmarshal(b); err == nil { if err = c.DestAddrs.Unmarshal(b); err == nil { if c.EsmClass, err = b.ReadByte(); err == nil { if c.ProtocolID, err = b.ReadByte(); err == nil { if c.PriorityFlag, err = b.ReadByte(); err == nil { if c.ScheduleDeliveryTime, err = b.ReadCString(); err == nil { if c.ValidityPeriod, err = b.ReadCString(); err == nil { if c.RegisteredDelivery, err = b.ReadByte(); err == nil { if c.ReplaceIfPresentFlag, err = b.ReadByte(); err == nil { err = c.Message.Unmarshal(b, (c.EsmClass&data.SM_UDH_GSM) > 0) } } } } } } } } } } return }) } ================================================ FILE: pdu/SubmitMultiResp.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // SubmitMultiResp PDU. type SubmitMultiResp struct { base MessageID string UnsuccessSMEs UnsuccessSMEs } // NewSubmitMultiResp returns new SubmitMultiResp. func NewSubmitMultiResp() PDU { c := &SubmitMultiResp{ base: newBase(), MessageID: data.DFLT_MSGID, UnsuccessSMEs: NewUnsuccessSMEs(), } c.CommandID = data.SUBMIT_MULTI_RESP return c } // NewSubmitMultiRespFromReq returns new SubmitMultiResp. func NewSubmitMultiRespFromReq(req *SubmitMulti) PDU { c := NewSubmitMultiResp().(*SubmitMultiResp) if req != nil { c.SequenceNumber = req.SequenceNumber } return c } // CanResponse implements PDU interface. func (c *SubmitMultiResp) CanResponse() bool { return false } // GetResponse implements PDU interface. func (c *SubmitMultiResp) GetResponse() PDU { return nil } // Marshal implements PDU interface. func (c *SubmitMultiResp) Marshal(b *ByteBuffer) { c.base.marshal(b, func(b *ByteBuffer) { b.Grow(len(c.MessageID) + 1) _ = b.WriteCString(c.MessageID) c.UnsuccessSMEs.Marshal(b) }) } // Unmarshal implements PDU interface. func (c *SubmitMultiResp) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, func(b *ByteBuffer) (err error) { if c.MessageID, err = b.ReadCString(); err == nil { err = c.UnsuccessSMEs.Unmarshal(b) } return }) } ================================================ FILE: pdu/SubmitMultiResp_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestSubmitMultiResp(t *testing.T) { req := NewSubmitMulti().(*SubmitMulti) req.SequenceNumber = 13 v := NewSubmitMultiRespFromReq(req).(*SubmitMultiResp) require.False(t, v.CanResponse()) require.Nil(t, v.GetResponse()) v.MessageID = "football" addr1 := NewUnsuccessSMEWithTonNpi(38, 33, 19) require.Nil(t, addr1.SetAddress("Bob1")) require.EqualValues(t, 19, addr1.ErrorStatusCode()) addr2, err := NewUnsuccessSMEWithAddr("Bob2", 20) require.Nil(t, err) require.EqualValues(t, 20, addr2.ErrorStatusCode()) v.UnsuccessSMEs.Add(addr1, addr2) require.Equal(t, []UnsuccessSME{addr1, addr2}, v.UnsuccessSMEs.Get()) validate(t, v, "0000003080000021000000000000000d666f6f7462616c6c00022621426f623100000000130000426f62320000000014", data.SUBMIT_MULTI_RESP, ) } ================================================ FILE: pdu/SubmitMulti_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestSubmitMulti(t *testing.T) { v := NewSubmitMulti().(*SubmitMulti) require.True(t, v.CanResponse()) v.SequenceNumber = 13 validate(t, v.GetResponse(), "0000001280000021000000000000000d0000", data.SUBMIT_MULTI_RESP, ) v.ServiceType = "abc" _ = v.SourceAddr.SetAddress("Alicer") v.SourceAddr.SetTon(28) v.SourceAddr.SetNpi(29) addr := NewAddress() require.Nil(t, addr.SetAddress("Bob1")) d1 := NewDestinationAddress() d1.SetAddress(addr) dl, err := NewDistributionList("List1") require.Nil(t, err) d2 := NewDestinationAddress() d2.SetDistributionList(dl) dl, err = NewDistributionList("List2") require.Nil(t, err) d3 := NewDestinationAddress() d3.SetDistributionList(dl) v.DestAddrs.Add(d1, d2, d3) require.Equal(t, []DestinationAddress{d1, d2, d3}, v.DestAddrs.Get()) v.EsmClass = 13 v.ProtocolID = 99 v.PriorityFlag = 61 v.RegisteredDelivery = 83 v.Message, err = NewShortMessageWithEncoding("nghắ nghiêng nghiễng ngả", data.UCS2) require.Nil(t, err) v.Message.message = "" validate(t, v, "0000006e00000021000000000000000d616263001c1d416c696365720003010000426f623100024c6973743100024c69737432000d633d00005300080030006e006700681eaf0020006e00670068006900ea006e00670020006e0067006800691ec5006e00670020006e00671ea3", data.SUBMIT_MULTI, ) } func TestSubmitMultiwithUDH(t *testing.T) { v := NewSubmitMulti().(*SubmitMulti) require.True(t, v.CanResponse()) v.SequenceNumber = 13 validate(t, v.GetResponse(), "0000001280000021000000000000000d0000", data.SUBMIT_MULTI_RESP, ) v.ServiceType = "abc" _ = v.SourceAddr.SetAddress("Alicer") v.SourceAddr.SetTon(28) v.SourceAddr.SetNpi(29) addr := NewAddress() require.Nil(t, addr.SetAddress("Bob1")) d1 := NewDestinationAddress() d1.SetAddress(addr) dl, err := NewDistributionList("List1") require.Nil(t, err) d2 := NewDestinationAddress() d2.SetDistributionList(dl) dl, err = NewDistributionList("List2") require.Nil(t, err) d3 := NewDestinationAddress() d3.SetDistributionList(dl) v.DestAddrs.Add(d1, d2, d3) require.Equal(t, []DestinationAddress{d1, d2, d3}, v.DestAddrs.Get()) v.EsmClass = 77 v.ProtocolID = 99 v.PriorityFlag = 61 v.RegisteredDelivery = 83 v.Message, err = NewShortMessageWithEncoding("nghắ nghiêng nghiễng ngả", data.UCS2) require.Nil(t, err) v.Message.message = "" v.Message.SetUDH(UDH{NewIEConcatMessage(2, 1, 254)}) validate(t, v, "0000007400000021000000000000000d616263001c1d416c696365720003010000426f623100024c6973743100024c69737432004d633d00005300080036050003fe0201006e006700681eaf0020006e00670068006900ea006e00670020006e0067006800691ec5006e00670020006e00671ea3", data.SUBMIT_MULTI, ) } ================================================ FILE: pdu/SubmitSM.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // SubmitSM PDU is used by an ESME to submit a short message to the SMSC for onward // transmission to a specified short message entity (SME). The submit_sm PDU does // not support the transaction message mode. type SubmitSM struct { base ServiceType string SourceAddr Address DestAddr Address EsmClass byte ProtocolID byte PriorityFlag byte ScheduleDeliveryTime string // not used ValidityPeriod string // not used RegisteredDelivery byte ReplaceIfPresentFlag byte // not used Message ShortMessage } // NewSubmitSM returns SubmitSM PDU. func NewSubmitSM() PDU { message, _ := NewShortMessage("") c := &SubmitSM{ base: newBase(), ServiceType: data.DFLT_SRVTYPE, SourceAddr: NewAddress(), DestAddr: NewAddress(), EsmClass: data.DFLT_ESM_CLASS, ProtocolID: data.DFLT_PROTOCOLID, PriorityFlag: data.DFLT_PRIORITY_FLAG, ScheduleDeliveryTime: data.DFLT_SCHEDULE, ValidityPeriod: data.DFLT_VALIDITY, RegisteredDelivery: data.DFLT_REG_DELIVERY, ReplaceIfPresentFlag: data.DFTL_REPLACE_IFP, Message: message, } c.CommandID = data.SUBMIT_SM return c } // ShouldSplit check if this the user data of submitSM PDU func (c *SubmitSM) ShouldSplit() bool { // GSM standard mandates that User Data must be no longer than 140 octet return len(c.Message.messageData) > data.SM_GSM_MSG_LEN } // CanResponse implements PDU interface. func (c *SubmitSM) CanResponse() bool { return true } // GetResponse implements PDU interface. func (c *SubmitSM) GetResponse() PDU { return NewSubmitSMRespFromReq(c) } // Split split a single long text message into multiple SubmitSM PDU, // Each have the TPUD within the GSM's User Data limit of 140 octet // If the message is short enough and doesn't need splitting, // Split() returns an array of length 1 func (c *SubmitSM) Split() (multiSubSM []*SubmitSM, err error) { multiSubSM = []*SubmitSM{} multiMsg, err := c.Message.split() if err != nil { return } esmClass := c.EsmClass // no need to "or" with SM_UDH_GSM when a message has a single part if len(multiMsg) > 1 { esmClass = c.EsmClass | data.SM_UDH_GSM // must set to indicate UDH } for _, msg := range multiMsg { multiSubSM = append(multiSubSM, &SubmitSM{ base: c.base, ServiceType: c.ServiceType, SourceAddr: c.SourceAddr, DestAddr: c.DestAddr, EsmClass: esmClass, ProtocolID: c.ProtocolID, PriorityFlag: c.PriorityFlag, ScheduleDeliveryTime: c.ScheduleDeliveryTime, ValidityPeriod: c.ValidityPeriod, RegisteredDelivery: c.RegisteredDelivery, ReplaceIfPresentFlag: c.ReplaceIfPresentFlag, Message: *msg, }) } return } // Marshal implements PDU interface. func (c *SubmitSM) Marshal(b *ByteBuffer) { c.base.marshal(b, func(b *ByteBuffer) { b.Grow(len(c.ServiceType) + len(c.ScheduleDeliveryTime) + len(c.ValidityPeriod) + 10) _ = b.WriteCString(c.ServiceType) c.SourceAddr.Marshal(b) c.DestAddr.Marshal(b) _ = b.WriteByte(c.EsmClass) _ = b.WriteByte(c.ProtocolID) _ = b.WriteByte(c.PriorityFlag) _ = b.WriteCString(c.ScheduleDeliveryTime) _ = b.WriteCString(c.ValidityPeriod) _ = b.WriteByte(c.RegisteredDelivery) _ = b.WriteByte(c.ReplaceIfPresentFlag) c.Message.Marshal(b) }) } // Unmarshal implements PDU interface. func (c *SubmitSM) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, func(b *ByteBuffer) (err error) { if c.ServiceType, err = b.ReadCString(); err == nil { if err = c.SourceAddr.Unmarshal(b); err == nil { if err = c.DestAddr.Unmarshal(b); err == nil { if c.EsmClass, err = b.ReadByte(); err == nil { if c.ProtocolID, err = b.ReadByte(); err == nil { if c.PriorityFlag, err = b.ReadByte(); err == nil { if c.ScheduleDeliveryTime, err = b.ReadCString(); err == nil { if c.ValidityPeriod, err = b.ReadCString(); err == nil { if c.RegisteredDelivery, err = b.ReadByte(); err == nil { if c.ReplaceIfPresentFlag, err = b.ReadByte(); err == nil { err = c.Message.Unmarshal(b, (c.EsmClass&data.SM_UDH_GSM) > 0) } } } } } } } } } } return }) } ================================================ FILE: pdu/SubmitSMResp.go ================================================ package pdu import ( "errors" "github.com/linxGnu/gosmpp/data" "io" ) // SubmitSMResp PDU. type SubmitSMResp struct { base MessageID string } // NewSubmitSMResp returns new SubmitSMResp. func NewSubmitSMResp() PDU { c := &SubmitSMResp{ base: newBase(), MessageID: data.DFLT_MSGID, } c.CommandID = data.SUBMIT_SM_RESP return c } // NewSubmitSMRespFromReq returns new SubmitSMResp. func NewSubmitSMRespFromReq(req *SubmitSM) PDU { c := NewSubmitSMResp().(*SubmitSMResp) if req != nil { c.SequenceNumber = req.SequenceNumber } return c } // CanResponse implements PDU interface. func (c *SubmitSMResp) CanResponse() bool { return false } // GetResponse implements PDU interface. func (c *SubmitSMResp) GetResponse() PDU { return nil } // Marshal implements PDU interface. func (c *SubmitSMResp) Marshal(b *ByteBuffer) { c.base.marshal(b, func(b *ByteBuffer) { b.Grow(len(c.MessageID) + 1) _ = b.WriteCString(c.MessageID) }) } // Unmarshal implements PDU interface. func (c *SubmitSMResp) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, func(b *ByteBuffer) (err error) { c.MessageID, err = b.ReadCString() if errors.Is(err, io.EOF) { return nil } return }) } ================================================ FILE: pdu/SubmitSMResp_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestSubmitSMResp(t *testing.T) { req := NewSubmitSM().(*SubmitSM) req.SequenceNumber = 13 v := NewSubmitSMRespFromReq(req).(*SubmitSMResp) require.False(t, v.CanResponse()) require.Nil(t, v.GetResponse()) v.MessageID = "football" validate(t, v, "0000001980000004000000000000000d666f6f7462616c6c00", data.SUBMIT_SM_RESP, ) } ================================================ FILE: pdu/SubmitSM_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestSubmitSM(t *testing.T) { v := NewSubmitSM().(*SubmitSM) require.True(t, v.CanResponse()) v.SequenceNumber = 13 validate(t, v.GetResponse(), "0000001180000004000000000000000d00", data.SUBMIT_SM_RESP, ) v.ServiceType = "abc" _ = v.SourceAddr.SetAddress("Alicer") v.SourceAddr.SetTon(28) v.SourceAddr.SetNpi(29) _ = v.DestAddr.SetAddress("Bob") v.DestAddr.SetTon(79) v.DestAddr.SetNpi(80) v.EsmClass = 77 ^ data.SM_UDH_GSM v.ProtocolID = 99 v.PriorityFlag = 61 v.RegisteredDelivery = 83 _ = v.Message.SetMessageWithEncoding("nghắ nghiêng nghiễng ngả", data.UCS2) v.Message.message = "" validate(t, v, "0000005d00000004000000000000000d616263001c1d416c69636572004f50426f62000d633d00005300080030006e006700681eaf0020006e00670068006900ea006e00670020006e0067006800691ec5006e00670020006e00671ea3", data.SUBMIT_SM, ) } ================================================ FILE: pdu/TLV.go ================================================ package pdu // Source code in this file is copied from: https://github.com/fiorix import ( "encoding/binary" "encoding/hex" ) // Tag is the tag of a Tag-Length-Value (TLV) field. type Tag uint16 // Hex returns hexadecimal representation of tag func (t Tag) Hex() string { var bin [2]byte binary.BigEndian.PutUint16(bin[:], uint16(t)) return hex.EncodeToString(bin[:]) } // Common Tag-Length-Value (TLV) tags. const ( TagDestAddrSubunit Tag = 0x0005 TagDestNetworkType Tag = 0x0006 TagDestBearerType Tag = 0x0007 TagDestTelematicsID Tag = 0x0008 TagSourceAddrSubunit Tag = 0x000D TagSourceNetworkType Tag = 0x000E TagSourceBearerType Tag = 0x000F TagSourceTelematicsID Tag = 0x0010 TagQosTimeToLive Tag = 0x0017 TagPayloadType Tag = 0x0019 TagAdditionalStatusInfoText Tag = 0x001D TagReceiptedMessageID Tag = 0x001E TagMsMsgWaitFacilities Tag = 0x0030 TagPrivacyIndicator Tag = 0x0201 TagSourceSubaddress Tag = 0x0202 TagDestSubaddress Tag = 0x0203 TagUserMessageReference Tag = 0x0204 TagUserResponseCode Tag = 0x0205 TagSourcePort Tag = 0x020A TagDestinationPort Tag = 0x020B TagSarMsgRefNum Tag = 0x020C TagLanguageIndicator Tag = 0x020D TagSarTotalSegments Tag = 0x020E TagSarSegmentSeqnum Tag = 0x020F TagCallbackNumPresInd Tag = 0x0302 TagCallbackNumAtag Tag = 0x0303 TagNumberOfMessages Tag = 0x0304 TagCallbackNum Tag = 0x0381 TagDpfResult Tag = 0x0420 TagSetDpf Tag = 0x0421 TagMsAvailabilityStatus Tag = 0x0422 TagNetworkErrorCode Tag = 0x0423 TagMessagePayload Tag = 0x0424 TagDeliveryFailureReason Tag = 0x0425 TagMoreMessagesToSend Tag = 0x0426 TagMessageStateOption Tag = 0x0427 TagUssdServiceOp Tag = 0x0501 TagDisplayTime Tag = 0x1201 TagSmsSignal Tag = 0x1203 TagMsValidity Tag = 0x1204 TagAlertOnMessageDelivery Tag = 0x130C TagItsReplyType Tag = 0x1380 TagItsSessionInfo Tag = 0x1383 ) // Field is a PDU Tag-Length-Value (TLV) field type Field struct { Tag Tag Data []byte } // String implements the Data interface. func (t *Field) String() string { if l := len(t.Data); l > 0 && t.Data[l-1] == 0x00 { return string(t.Data[:l-1]) } return string(t.Data) } // Marshal to writer. func (t *Field) Marshal(w *ByteBuffer) { if len(t.Data) > 0 { w.Grow(4 + len(t.Data)) w.WriteShort(int16(t.Tag)) w.WriteShort(int16(len(t.Data))) _, _ = w.Write(t.Data) } } // Unmarshal from reader. func (t *Field) Unmarshal(b *ByteBuffer) (err error) { var tag, ln int16 if tag, err = b.ReadShort(); err == nil { t.Tag = Tag(tag) if ln, err = b.ReadShort(); err == nil { t.Data, err = b.ReadN(int(ln)) } } return } ================================================ FILE: pdu/UDH.go ================================================ package pdu import ( "bytes" "fmt" "github.com/linxGnu/gosmpp/data" ) // For now, this package only support message uses of UDH for message concatenation // No plan for supporting other Enhanced Messaging Service // Credit to https://github.com/warthog618/sms // UDH represent User Data Header // as defined in 3GPP TS 23.040 Section 9.2.3.24. type UDH []InfoElement // UDHL returns length (octets) of encoded UDH, including the UDHL byte. // // If there is no InfoElement (IE), returns 0. If total length exceed 255, return -1. func (u UDH) UDHL() (l int) { if len(u) == 0 { return } for i := range u { if len(u[i].Data) > 255 { return -1 } // to account for id and type bytes if l += 2 + len(u[i].Data); l > 255 { return -1 } } // include the udhlength byte itself l++ return } // MarshalBinary marshal UDH into bytes array // The first byte is UDHL // MarshalBinary preserve InformationElement order as they appears in the UDH // // If the total UDHL is larger than what length(byte) can specified, // this will truncate IE until total length fit within 256, if you want to // check if any IE has been truncated, see if UDHL() < -1, notes on UDHL() func (u UDH) MarshalBinary() (b []byte, err error) { reservedLength := u.UDHL() if reservedLength == -1 { err = fmt.Errorf("header limit (255 in marshal size) exceeds") return } if reservedLength == 0 { return } // reserve the first byte for UDHL buf := bytes.NewBuffer(make([]byte, 1, reservedLength)) // marshalling elements length := 0 for i := 0; i < len(u); i++ { // Begin marshaling UDH data, each IE is composed of 3 parts: // [ ID_1, LENGTH_1, DATA_N ] // When adding a new IE, if total length ID + LEN + DATA // exceed 256, we skip that IE altogether addition := 2 + len(u[i].Data) // limit exceeded, break loop? if length += addition; length > 255 { length -= addition break } buf.WriteByte(u[i].ID) buf.WriteByte(byte(len(u[i].Data))) buf.Write(u[i].Data) } // only set buffer when UDHL length is not zero if length > 0 { // final assignment and encode length b = buf.Bytes() b[0] = byte(length) } return } // UnmarshalBinary reads the InformationElements from raw binary UDH. // Unmarshal preserve InfoElement order as they appears in the raw data // The src contains the complete UDH, including the UDHL and all IEs. // Returns the number of bytes read from src, and the first error // detected while unmarshalling. // // Since UDHL can only represented in 1 byte, UnmarshalBinary // will only read up to a maximum of 256 byte regardless of src length func (u *UDH) UnmarshalBinary(src []byte) (int, error) { if len(src) < 1 { return 0, fmt.Errorf("decode error UDHL %d underflow", 0) } udhl := int(src[0]) if udhl == 0 { return 0, fmt.Errorf("error: UDHL length is 0, probably sender mistake forgot to include UDH but still set UDH flag in ESME_CLASS") } // check length, excluding first UDHL byte if len(src)-1 < udhl { return 0, fmt.Errorf("decode error UDH underflow, expect len %d got %d", udhl, len(src)) } // count number of bytes which are read var ( read = 1 // UDHL byte ) ies := []InfoElement{} for read < udhl { // loop until we still have data to read ie := InfoElement{} r, err := ie.UnmarshalBinary(src[read:]) if err != nil { return 0, err } // moving forward read += r // add info elements ies = append(ies, ie) } *u = UDH(ies) return read, nil } // FindInfoElement find the first occurrence of the Information Element with id func (u UDH) FindInfoElement(id byte) (ie *InfoElement, found bool) { for i := range u { if u[i].ID == id { return &u[i], true } } return nil, false } // GetConcatInfo return the FIRST concatenated message IE, func (u UDH) GetConcatInfo() (totalParts, partNum, mref byte, found bool) { if len(u) == 0 { found = false return } if ie, ok := u.FindInfoElement(data.UDH_CONCAT_MSG_8_BIT_REF); ok && len(ie.Data) == 3 { mref = ie.Data[0] totalParts = ie.Data[1] partNum = ie.Data[2] found = ok } return } // InfoElement represent a 3 parts Information-Element // as defined in 3GPP TS 23.040 Section 9.2.3.24 // Each InfoElement is comprised of it's identifier and data type InfoElement struct { ID byte Data []byte } // NewIEConcatMessage turn a new IE element for concat message info // IE.Data is populated at time of object creation func NewIEConcatMessage(totalParts, partNum, mref byte) InfoElement { return InfoElement{ ID: data.UDH_CONCAT_MSG_8_BIT_REF, Data: []byte{byte(mref), byte(totalParts), byte(partNum)}, } } // UnmarshalBinary unmarshal IE from binary in src, only read a single IE, // expect src at least of length 2 with correct IE format: // // [ ID_1, LENGTH_1, DATA_N ] func (ie *InfoElement) UnmarshalBinary(src []byte) (int, error) { if len(src) < 2 { return 0, fmt.Errorf("decode error InfoElement underflow, len = %d", len(src)) } // second byte is len ieLen := int(src[1]) // check length, excluding first 2 bytes if len(src)-2 < ieLen { return 0, fmt.Errorf("decode error InfoElement underflow, expect length %d, got %d", ieLen, len(src)) } ie.ID = src[0] // first byte is ID ie.Data = src[2:(ieLen + 2)] // 3rd byte onward is data return 2 + ieLen, nil } ================================================ FILE: pdu/UDH_test.go ================================================ package pdu import ( "testing" "github.com/stretchr/testify/require" ) func TestUserDataHeader(t *testing.T) { t.Run("marshalBinaryUDHConcatMessage", func(t *testing.T) { u := UDH{NewIEConcatMessage(2, 1, 12)} b, err := u.MarshalBinary() require.NoError(t, err) require.Equal(t, "0500030c0201", toHex(b)) }) t.Run("marshalBinaryUDHConcatMessage (8 bit)", func(t *testing.T) { u := UDH{NewIEConcatMessage(2, 1, 12)} b, err := u.MarshalBinary() require.NoError(t, err) require.Equal(t, "0500030c0201", toHex(b)) totalParts, sequence, reference, found := u.GetConcatInfo() require.True(t, found) require.Equal(t, totalParts, byte(2)) require.Equal(t, sequence, byte(1)) require.Equal(t, reference, uint8(12)) }) t.Run("unmarshalBinaryUDHConcatMessage", func(t *testing.T) { u, rd := new(UDH), []byte{0x05, 0x00, 0x03, 0x0c, 0x02, 0x01} read, err := u.UnmarshalBinary(rd) require.False(t, read <= 0) require.NoError(t, err) b, err := u.MarshalBinary() require.NoError(t, err) require.Equal(t, "0500030c0201", toHex(b)) }) t.Run("unmarshalBinaryUDHConcatMessage failed", func(t *testing.T) { failedList := [][]byte{ {0x04, 0x00, 0x02, 0x02, 0x01}, {0x04, 0x08, 0x02, 0x02, 0x01}, } u := new(UDH) for _, data := range failedList { _, _ = u.UnmarshalBinary(data) _, _, _, found := u.GetConcatInfo() require.False(t, found, data) } }) t.Run("marshalBinaryTruncateLongIE", func(t *testing.T) { u := UDH{NewIEConcatMessage(2, 1, 12)} for i := 0; i < 255; i++ { u = append(u, NewIEConcatMessage(2, 1, 12)) } require.LessOrEqual(t, u.UDHL(), 256) // UDHL must not exceed 256 ( including UDHL byte ) _, err := u.MarshalBinary() require.Error(t, err) }) } ================================================ FILE: pdu/Unbind.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // Unbind PDU is to deregister an instance of an ESME from the SMSC and inform the SMSC // that the ESME no longer wishes to use this network connection for the submission or // delivery of messages. type Unbind struct { base } // NewUnbind returns Unbind PDU. func NewUnbind() PDU { c := &Unbind{ base: newBase(), } c.CommandID = data.UNBIND return c } // CanResponse implements PDU interface. func (c *Unbind) CanResponse() bool { return true } // GetResponse implements PDU interface. func (c *Unbind) GetResponse() PDU { return NewUnbindRespFromReq(c) } // Marshal implements PDU interface. func (c *Unbind) Marshal(b *ByteBuffer) { c.base.marshal(b, nil) } // Unmarshal implements PDU interface. func (c *Unbind) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, nil) } ================================================ FILE: pdu/UnbindResp.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // UnbindResp PDU. type UnbindResp struct { base } // NewUnbindResp returns UnbindResp. func NewUnbindResp() PDU { c := &UnbindResp{ base: newBase(), } c.CommandID = data.UNBIND_RESP return c } // NewUnbindRespFromReq returns UnbindResp. func NewUnbindRespFromReq(req *Unbind) PDU { c := NewUnbindResp().(*UnbindResp) if req != nil { c.SequenceNumber = req.SequenceNumber } return c } // CanResponse implements PDU interface. func (c *UnbindResp) CanResponse() bool { return false } // GetResponse implements PDU interface. func (c *UnbindResp) GetResponse() PDU { return nil } // Marshal implements PDU interface. func (c *UnbindResp) Marshal(b *ByteBuffer) { c.base.marshal(b, nil) } // Unmarshal implements PDU interface. func (c *UnbindResp) Unmarshal(b *ByteBuffer) error { return c.base.unmarshal(b, nil) } ================================================ FILE: pdu/UnbindResp_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestUnbindResp(t *testing.T) { req := NewUnbind().(*Unbind) req.SequenceNumber = 13 v := NewUnbindRespFromReq(req).(*UnbindResp) require.False(t, v.CanResponse()) require.Nil(t, v.GetResponse()) validate(t, v, "0000001080000006000000000000000d", data.UNBIND_RESP, ) } ================================================ FILE: pdu/Unbind_test.go ================================================ package pdu import ( "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func TestUnbind(t *testing.T) { v := NewUnbind().(*Unbind) require.True(t, v.CanResponse()) v.SequenceNumber = 13 validate(t, v.GetResponse(), "0000001080000006000000000000000d", data.UNBIND_RESP, ) validate(t, v, "0000001000000006000000000000000d", data.UNBIND, ) } ================================================ FILE: pdu/UnsuccessSME.go ================================================ package pdu import ( "github.com/linxGnu/gosmpp/data" ) // UnsuccessSME indicates submission was unsuccessful and the respective errors. type UnsuccessSME struct { Address errorStatusCode data.CommandStatusType } // NewUnsuccessSME returns new UnsuccessSME func NewUnsuccessSME() (c UnsuccessSME) { c = UnsuccessSME{ Address: NewAddress(), errorStatusCode: data.ESME_ROK, } return } // NewUnsuccessSMEWithAddr returns new UnsuccessSME with address. func NewUnsuccessSMEWithAddr(addr string, status data.CommandStatusType) (c UnsuccessSME, err error) { c = NewUnsuccessSME() if err = c.SetAddress(addr); err == nil { c.SetErrorStatusCode(status) } return } // NewUnsuccessSMEWithTonNpi create new address with ton, npi and error code. func NewUnsuccessSMEWithTonNpi(ton, npi byte, status data.CommandStatusType) UnsuccessSME { return UnsuccessSME{ Address: NewAddressWithTonNpi(ton, npi), errorStatusCode: status, } } // Unmarshal from buffer. func (c *UnsuccessSME) Unmarshal(b *ByteBuffer) (err error) { var st int32 if err = c.Address.Unmarshal(b); err == nil { st, err = b.ReadInt() if err == nil { c.errorStatusCode = data.CommandStatusType(st) } } return } // Marshal to buffer. func (c *UnsuccessSME) Marshal(b *ByteBuffer) { c.Address.Marshal(b) b.WriteInt(int32(c.errorStatusCode)) } // SetErrorStatusCode sets error status code. func (c *UnsuccessSME) SetErrorStatusCode(v data.CommandStatusType) { c.errorStatusCode = v } // ErrorStatusCode returns assigned status code. func (c *UnsuccessSME) ErrorStatusCode() data.CommandStatusType { return c.errorStatusCode } // UnsuccessSMEs represents list of UnsuccessSME. type UnsuccessSMEs struct { l []UnsuccessSME } // NewUnsuccessSMEs returns list of UnsuccessSME. func NewUnsuccessSMEs() (u UnsuccessSMEs) { u.l = make([]UnsuccessSME, 0, 8) return } // Add to list. func (c *UnsuccessSMEs) Add(us ...UnsuccessSME) { c.l = append(c.l, us...) } // Get list. func (c *UnsuccessSMEs) Get() []UnsuccessSME { return c.l } // Unmarshal from buffer. func (c *UnsuccessSMEs) Unmarshal(b *ByteBuffer) (err error) { var n byte if n, err = b.ReadByte(); err == nil { c.l = make([]UnsuccessSME, n) var i byte for ; i < n; i++ { if err = c.l[i].Unmarshal(b); err != nil { return } } } return } // Marshal to buffer. func (c *UnsuccessSMEs) Marshal(b *ByteBuffer) { n := byte(len(c.l)) _ = b.WriteByte(n) var i byte for ; i < n; i++ { c.l[i].Marshal(b) } } ================================================ FILE: pdu/UnsuccessSME_test.go ================================================ package pdu import ( "testing" "github.com/stretchr/testify/require" ) func TestMalformUSME(t *testing.T) { t.Run("malformSME", func(t *testing.T) { b := NewBuffer(nil) var u UnsuccessSME require.NotNil(t, u.Unmarshal(b)) }) t.Run("malformSMEs", func(t *testing.T) { b := NewBuffer(nil) _ = b.WriteByte(1) var u UnsuccessSMEs require.NotNil(t, u.Unmarshal(b)) }) } ================================================ FILE: pdu/helper_test.go ================================================ package pdu import ( "encoding/hex" "log" "testing" "github.com/linxGnu/gosmpp/data" "github.com/stretchr/testify/require" ) func fromHex(h string) (v []byte) { var err error v, err = hex.DecodeString(h) if err != nil { log.Fatal(err) } return } func toHex(v []byte) (h string) { h = hex.EncodeToString(v) return } func validate(t *testing.T, p PDU, hexValue string, expectCommandID data.CommandIDType) { buf := NewBuffer(nil) p.Marshal(buf) require.Equal(t, fromHex(hexValue), buf.Bytes()) expectAfterParse(t, buf, p, expectCommandID) } func expectAfterParse(t *testing.T, b *ByteBuffer, expect PDU, expectCommandID data.CommandIDType) { c, err := Parse(b) require.Nil(t, err) require.Equal(t, expect, c) require.EqualValues(t, expectCommandID, c.GetHeader().CommandID) require.Zero(t, b.Len()) } ================================================ FILE: pkg.go ================================================ package gosmpp import ( "io" "time" "github.com/linxGnu/gosmpp/pdu" ) // Transceiver interface. type Transceiver interface { io.Closer Submit(pdu.PDU) error SystemID() string } // Transmitter interface. type Transmitter interface { io.Closer Submit(pdu.PDU) error SystemID() string } // Receiver interface. type Receiver interface { io.Closer SystemID() string } // Settings for TX (transmitter), RX (receiver), TRX (transceiver). type Settings struct { // ReadTimeout is timeout for reading PDU from SMSC. // Underlying net.Conn will be stricted with ReadDeadline(now + timeout). // This setting is very important to detect connection failure. // // Must: ReadTimeout > max(0, EnquireLink) ReadTimeout time.Duration // WriteTimeout is timeout for submitting PDU. WriteTimeout time.Duration // EnquireLink periodically sends EnquireLink to SMSC. // The duration must not be smaller than 1 minute. // // Zero duration disables auto enquire link. EnquireLink time.Duration // OnPDU handles received PDU from SMSC. // // `Responded` flag indicates this pdu is responded automatically, // no manual respond needed. // // Will be ignored if OnAllPDU or WindowedRequestTracking is set OnPDU PDUCallback // OnAllPDU handles all received PDU from SMSC. // // This pdu is NOT responded to automatically, // manual response/handling is needed // // User can also decide to close bind by retuning true, default is false // // Will be ignored if WindowedRequestTracking is set OnAllPDU AllPDUCallback // OnReceivingError notifies happened error while reading PDU // from SMSC. OnReceivingError ErrorCallback // OnSubmitError notifies fail-to-submit PDU with along error. OnSubmitError PDUErrorCallback // OnRebindingError notifies error while rebinding. OnRebindingError ErrorCallback // OnClosed notifies `closed` event due to State. OnClosed ClosedCallback // OnRebind notifies `rebind` event due to State. OnRebind RebindCallback // SMPP Bind Window tracking feature config *WindowedRequestTracking response func(pdu.PDU) } // WindowedRequestTracking settings for TX (transmitter) and TRX (transceiver) request store. type WindowedRequestTracking struct { // OnReceivedPduRequest handles received PDU request from SMSC. // // User can also decide to close bind by retuning true, default is false OnReceivedPduRequest AllPDUCallback // OnExpectedPduResponse handles expected PDU response from SMSC. // Only triggered when the original request is found in the window cache // // Handle is optional // If not set, response will be dropped OnExpectedPduResponse func(Response) // OnUnexpectedPduResponse handles unexpected PDU response from SMSC. // Only triggered if the original request is not found in the window cache // // Handle is optional // If not set, response will be dropped OnUnexpectedPduResponse func(pdu.PDU) // OnExpiredPduRequest handles expired PDU request with no response received // // Mandatory: the PduExpireTimeOut must be set // Handle is optional // If not set, expired PDU will be removed from cache // the bind can be closed by retuning true on closeBind. OnExpiredPduRequest func(pdu.PDU) (closeBind bool) // OnClosePduRequest will return all PDU request found in the store when the bind closes OnClosePduRequest func(pdu.PDU) // Set the time duration to expire a request sent to the SMSC // // Zero duration disables pdu expire check, and the cache may fill up over time with expired PDU request // Recommended: equal or less to the value set in ReadTimeout + EnquireLink PduExpireTimeOut time.Duration // The time period between each check of the expired PDU in the cache // // Zero duration disables pdu expire check and the cache may fill up over time with expired PDU request // Recommended: Less or half the time set in for PduExpireTimeOut // Don't be too aggressive, there is a performance hit if the check is done often ExpireCheckTimer time.Duration // The maximum number of pending requests sent to the SMSC // // Maximum value is 255 MaxWindowSize uint8 // if enabled, EnquireLink and Unbind request will be responded to automatically EnableAutoRespond bool // Set the time duration to expire a request for storing or retrieving data from request store // // Value must be greater than 0 // 200 to 1000 milliseconds is a good starting point StoreAccessTimeOut time.Duration } ================================================ FILE: receivable.go ================================================ package gosmpp import ( "context" "sync" "sync/atomic" "time" "github.com/linxGnu/gosmpp/pdu" ) type receivable struct { ctx context.Context cancel context.CancelFunc wg sync.WaitGroup settings Settings conn *Connection aliveState int32 requestStore RequestStore } func newReceivable(conn *Connection, settings Settings, requestStore RequestStore) *receivable { r := &receivable{ settings: settings, conn: conn, requestStore: requestStore, } r.ctx, r.cancel = context.WithCancel(context.Background()) return r } func (t *receivable) close(state State) (err error) { if atomic.CompareAndSwapInt32(&t.aliveState, Alive, Closed) { // cancel to notify stop t.cancel() // set read deadline for current blocking read _ = t.conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond)) // wait daemons t.wg.Wait() // close connection to notify daemons to stop if state != StoppingProcessOnly { err = t.conn.Close() } // notify receiver closed if t.settings.OnClosed != nil { t.settings.OnClosed(state) } } return } func (t *receivable) closing(state State) { go func() { _ = t.close(state) }() } func (t *receivable) start() { t.wg.Add(1) go func() { defer t.wg.Done() t.loop() }() } func (t *receivable) loop() { var err error for { select { case <-t.ctx.Done(): return default: } // read pdu from conn var p pdu.PDU if err = t.conn.SetReadTimeout(t.settings.ReadTimeout); err == nil { p, err = pdu.Parse(t.conn) } if err != nil { if atomic.LoadInt32(&t.aliveState) == Alive { if t.settings.OnReceivingError != nil { t.settings.OnReceivingError(err) } t.closing(InvalidStreaming) } return } var closeOnUnbind bool if p != nil { if t.settings.WindowedRequestTracking != nil && t.settings.OnExpectedPduResponse != nil { closeOnUnbind = t.handleWindowPdu(p) } else if t.settings.OnAllPDU != nil { closeOnUnbind = t.handleAllPdu(p) } else { closeOnUnbind = t.handleOrClose(p) } if closeOnUnbind { t.closing(UnbindClosing) } } } } func (t *receivable) handleWindowPdu(p pdu.PDU) (closing bool) { if t.settings.WindowedRequestTracking != nil && t.settings.OnExpectedPduResponse != nil && p != nil { // This case must match the same request item list in transmittable write func switch pp := p.(type) { case *pdu.CancelSMResp, *pdu.DataSMResp, *pdu.DeliverSMResp, *pdu.EnquireLinkResp, *pdu.QuerySMResp, *pdu.ReplaceSMResp, *pdu.SubmitMultiResp, *pdu.SubmitSMResp: if t.settings.OnExpectedPduResponse != nil { ctx, cancelFunc := context.WithTimeout(context.Background(), t.settings.StoreAccessTimeOut) defer cancelFunc() request, ok := t.requestStore.Get(ctx, p.GetSequenceNumber()) if ok { _ = t.requestStore.Delete(ctx, p.GetSequenceNumber()) response := Response{ PDU: p, OriginalRequest: request, } t.settings.OnExpectedPduResponse(response) } else if t.settings.OnUnexpectedPduResponse != nil { t.settings.OnUnexpectedPduResponse(p) } } case *pdu.EnquireLink: if t.settings.EnableAutoRespond { t.settings.response(pp.GetResponse()) } else if t.settings.OnReceivedPduRequest != nil { r, _ := t.settings.OnReceivedPduRequest(p) t.settings.response(r) } case *pdu.Unbind: if t.settings.EnableAutoRespond { t.settings.response(pp.GetResponse()) // wait to send response before closing time.Sleep(50 * time.Millisecond) closing = true } else if t.settings.OnReceivedPduRequest != nil { r, closeBind := t.settings.OnReceivedPduRequest(p) t.settings.response(r) if closeBind { time.Sleep(50 * time.Millisecond) closing = true } } default: if t.settings.OnReceivedPduRequest != nil { r, closeBind := t.settings.OnReceivedPduRequest(p) t.settings.response(r) if closeBind { time.Sleep(50 * time.Millisecond) closing = true } } } } return } func (t *receivable) handleAllPdu(p pdu.PDU) (closing bool) { if t.settings.OnAllPDU != nil && p != nil { r, closeBind := t.settings.OnAllPDU(p) t.settings.response(r) if closeBind { time.Sleep(50 * time.Millisecond) closing = true } } return } func (t *receivable) handleOrClose(p pdu.PDU) (closing bool) { if p != nil { switch pp := p.(type) { case *pdu.EnquireLink: t.settings.response(pp.GetResponse()) case *pdu.Unbind: t.settings.response(pp.GetResponse()) // wait to send response before closing time.Sleep(50 * time.Millisecond) closing = true default: var responded bool if p.CanResponse() { t.settings.response(p.GetResponse()) responded = true } if t.settings.OnPDU != nil { t.settings.OnPDU(p, responded) } } } return } ================================================ FILE: receivable_test.go ================================================ package gosmpp import ( "fmt" "github.com/stretchr/testify/assert" "testing" "time" "github.com/linxGnu/gosmpp/pdu" "github.com/stretchr/testify/require" ) func TestReceive(t *testing.T) { auth := nextAuth() receiver, err := NewSession( RXConnector(NonTLSDialer, auth), Settings{ ReadTimeout: 2 * time.Second, OnReceivingError: func(err error) { t.Log(err) }, OnRebindingError: func(err error) { t.Log(err) }, OnPDU: func(p pdu.PDU, _ bool) { t.Log(p) }, OnClosed: func(state State) { t.Log(state) }, }, 5*time.Second) require.Nil(t, err) require.NotNil(t, receiver) defer func() { _ = receiver.Close() }() require.Equal(t, "MelroseLabsSMSC", receiver.Receiver().SystemID()) time.Sleep(time.Second) receiver.rebind() } func Test_receivable_handleAllPdu(t1 *testing.T) { type fields struct { settings Settings } type args struct { p pdu.PDU } tests := []struct { name string fields fields args args wantClosing bool }{ { name: "nil setting", fields: fields{}, args: args{}, wantClosing: false, }, { name: "nil pdu", fields: fields{}, args: args{}, wantClosing: false, }, { name: "DeliverSM pdu", fields: fields{ settings: newTransceivable(nil, Settings{ OnAllPDU: receivableHandleAllPDU(t1), }, nil).in.settings, }, args: args{ p: pdu.NewDeliverSM(), }, wantClosing: false, }, { name: "EnquireLink pdu", fields: fields{ settings: newTransceivable(nil, Settings{ OnAllPDU: receivableHandleAllPDU(t1), }, nil).in.settings, }, args: args{ p: pdu.NewEnquireLink(), }, wantClosing: false, }, /*{ name: "Undind pdu", // run this as the last test case fields: fields{ settings: newTransceivable(nil, Settings{ OnAllPDU: receivableHandleAllPDU(t1), }).in.settings, }, args: args{ p: pdu.NewUnbind(), }, wantClosing: true, },*/ } for _, tt := range tests { t1.Run(tt.name, func(t1 *testing.T) { t := &receivable{ settings: tt.fields.settings, } assert.Equalf(t1, tt.wantClosing, t.handleAllPdu(tt.args.p), "handleAllPdu(%v)", tt.args.p) }) } } func receivableHandleAllPDU(t1 *testing.T) func(pdu.PDU) (pdu.PDU, bool) { return func(p pdu.PDU) (pdu.PDU, bool) { switch pd := p.(type) { case *pdu.Unbind: fmt.Println("Unbind Received") return pd.GetResponse(), true case *pdu.UnbindResp: t1.Log("UnbindResp Received") case *pdu.SubmitSMResp: t1.Log("SubmitSMResp Received") case *pdu.GenericNack: t1.Log("GenericNack Received") case *pdu.EnquireLinkResp: t1.Log("EnquireLinkResp Received") case *pdu.EnquireLink: t1.Log("EnquireLink Received") return pd.GetResponse(), false case *pdu.DataSM: t1.Log("DataSM received") return pd.GetResponse(), false case *pdu.DeliverSM: t1.Log("DeliverSM received") return pd.GetResponse(), false } return nil, false } } func Test_receivable_handleOrClose(t1 *testing.T) { type fields struct { settings Settings } type args struct { p pdu.PDU } tests := []struct { name string fields fields args args wantClosing bool }{ { name: "nil setting", fields: fields{}, args: args{}, wantClosing: false, }, { name: "nil pdu", fields: fields{}, args: args{}, wantClosing: false, }, { name: "EnquireLink pdu", fields: fields{ settings: newTransceivable(nil, Settings{}, nil).in.settings, }, args: args{ p: pdu.NewEnquireLink(), }, wantClosing: false, }, /*{ name: "Undind pdu", // run this as the last test case fields: fields{ settings: newTransceivable(nil, Settings{}).in.settings, }, args: args{ p: pdu.NewUnbind(), }, wantClosing: true, },*/ } for _, tt := range tests { t1.Run(tt.name, func(t1 *testing.T) { t := &receivable{ settings: tt.fields.settings, } assert.Equalf(t1, tt.wantClosing, t.handleOrClose(tt.args.p), "handleOrClose(%v)", tt.args.p) }) } } func Test_receivable_handleWindowPdu(t1 *testing.T) { type fields struct { settings Settings } type args struct { p pdu.PDU } tests := []struct { name string fields fields args args wantClosing bool }{ { name: "nil setting", fields: fields{}, args: args{}, wantClosing: false, }, { name: "nil pdu", fields: fields{}, args: args{}, wantClosing: false, }, { name: "EnquireLink pdu", fields: fields{}, args: args{ p: pdu.NewEnquireLink(), }, wantClosing: false, }, { name: "EnquireLinkResp pdu", fields: fields{}, args: args{ p: pdu.NewEnquireLink().GetResponse(), }, wantClosing: false, }, { name: "DeliverSM pdu", fields: fields{}, args: args{ p: pdu.NewDeliverSM(), }, wantClosing: false, }, { name: "SubmitSMResp pdu", fields: fields{}, args: args{ p: pdu.NewSubmitSM().GetResponse(), }, wantClosing: false, }, } for _, tt := range tests { t1.Run(tt.name, func(t1 *testing.T) { t := &receivable{ settings: tt.fields.settings, } assert.Equalf(t1, tt.wantClosing, t.handleWindowPdu(tt.args.p), "handleWindowPdu(%v)", tt.args.p) }) } } ================================================ FILE: request_store.go ================================================ package gosmpp import ( "context" "fmt" "github.com/linxGnu/gosmpp/pdu" cmap "github.com/orcaman/concurrent-map/v2" "golang.org/x/exp/maps" "strconv" "time" ) // Request represent a request tracked by the RequestStore type Request struct { pdu.PDU TimeSent time.Time } // Response represents a response from a Request in the RequestStore type Response struct { pdu.PDU OriginalRequest Request } // RequestStore interface used for WindowedRequestTracking type RequestStore interface { Set(ctx context.Context, request Request) error Get(ctx context.Context, sequenceNumber int32) (Request, bool) List(ctx context.Context) []Request Delete(ctx context.Context, sequenceNumber int32) error Clear(ctx context.Context) error Length(ctx context.Context) (int, error) } type DefaultStore struct { store cmap.ConcurrentMap[string, Request] } func NewDefaultStore() DefaultStore { return DefaultStore{ store: cmap.New[Request](), } } func (s DefaultStore) Set(ctx context.Context, request Request) error { select { case <-ctx.Done(): fmt.Println("Task cancelled") return ctx.Err() default: s.store.Set(strconv.Itoa(int(request.PDU.GetSequenceNumber())), request) return nil } } func (s DefaultStore) Get(ctx context.Context, sequenceNumber int32) (Request, bool) { select { case <-ctx.Done(): fmt.Println("Task cancelled") return Request{}, false default: return s.store.Get(strconv.Itoa(int(sequenceNumber))) } } func (s DefaultStore) List(ctx context.Context) []Request { select { case <-ctx.Done(): return []Request{} default: return maps.Values(s.store.Items()) } } func (s DefaultStore) Delete(ctx context.Context, sequenceNumber int32) error { select { case <-ctx.Done(): return ctx.Err() default: s.store.Remove(strconv.Itoa(int(sequenceNumber))) return nil } } func (s DefaultStore) Clear(ctx context.Context) error { select { case <-ctx.Done(): return ctx.Err() default: s.store.Clear() return nil } } func (s DefaultStore) Length(ctx context.Context) (int, error) { select { case <-ctx.Done(): return 0, ctx.Err() default: return s.store.Count(), nil } } ================================================ FILE: session.go ================================================ package gosmpp import ( "errors" "fmt" "github.com/linxGnu/gosmpp/pdu" "sync/atomic" "time" ) var ( ErrWindowSizeEqualZero = errors.New("request window size cannot be 0") ErrExpireCheckTimerNotSet = errors.New("ExpireCheckTimer cannot be 0 if PduExpireTimeOut is set") ErrStoreAccessTimeOutEqualZero = errors.New("StoreAccessTimeOut window size cannot be 0") ErrWindowSizeNotAvailableOnReceiverBinds = errors.New("window size not available on receiver binds") ) // Session represents session for TX, RX, TRX. type Session struct { c Connector originalOnClosed func(State) settings Settings rebindingInterval time.Duration trx atomic.Value // transceivable state int32 rebinding int32 requestStore RequestStore } type SessionOption func(session *Session) // NewSession creates new session for TX, RX, TRX. // // Session will `non-stop`, automatically rebind (create new and authenticate connection with SMSC) when // unexpected error happened. // // `rebindingInterval` indicates duration that Session has to wait before rebinding again. // // Setting `rebindingInterval <= 0` will disable `auto-rebind` functionality. func NewSession(c Connector, settings Settings, rebindingInterval time.Duration, opts ...SessionOption) (session *Session, err error) { // Loop through each option if settings.ReadTimeout <= 0 || settings.ReadTimeout <= settings.EnquireLink { return nil, fmt.Errorf("invalid settings: ReadTimeout must greater than max(0, EnquireLink)") } var requestStore RequestStore = nil if settings.WindowedRequestTracking != nil { requestStore = NewDefaultStore() if settings.MaxWindowSize == 0 { return nil, ErrWindowSizeEqualZero } if settings.StoreAccessTimeOut == 0 { return nil, ErrStoreAccessTimeOutEqualZero } if settings.PduExpireTimeOut > 0 && settings.ExpireCheckTimer == 0 { return nil, ErrExpireCheckTimerNotSet } } conn, err := c.Connect() if err == nil { session = &Session{ c: c, rebindingInterval: rebindingInterval, originalOnClosed: settings.OnClosed, requestStore: requestStore, } for _, opt := range opts { opt(session) } if rebindingInterval > 0 { newSettings := settings newSettings.OnClosed = func(state State) { switch state { case ExplicitClosing: return default: if session.originalOnClosed != nil { session.originalOnClosed(state) } session.rebind() } } session.settings = newSettings } else { session.settings = settings } // bind to session trans := newTransceivable(conn, session.settings, session.requestStore) trans.start() session.trx.Store(trans) } return } func WithRequestStore(store RequestStore) SessionOption { return func(s *Session) { s.requestStore = store } } func (s *Session) bound() *transceivable { r, _ := s.trx.Load().(*transceivable) return r } // Transmitter returns bound Transmitter. func (s *Session) Transmitter() Transmitter { return s.bound() } // Receiver returns bound Receiver. func (s *Session) Receiver() Receiver { return s.bound() } // Transceiver returns bound Transceiver. func (s *Session) Transceiver() Transceiver { return s.bound() } func (s *Session) GetWindowSize() (int, error) { if s.c.GetBindType() == pdu.Transmitter || s.c.GetBindType() == pdu.Transceiver { size, err := s.bound().GetWindowSize() if err != nil { return 0, err } return size, nil } return 0, ErrWindowSizeNotAvailableOnReceiverBinds } // Close session. func (s *Session) Close() (err error) { if atomic.CompareAndSwapInt32(&s.state, Alive, Closed) { err = s.close() } return } func (s *Session) close() (err error) { if b := s.bound(); b != nil { err = b.Close() } return } func (s *Session) rebind() { if atomic.CompareAndSwapInt32(&s.rebinding, 0, 1) { _ = s.close() for atomic.LoadInt32(&s.state) == Alive { conn, err := s.c.Connect() if err != nil { if s.settings.OnRebindingError != nil { s.settings.OnRebindingError(err) } time.Sleep(s.rebindingInterval) } else { // bind to session trans := newTransceivable(conn, s.settings, s.requestStore) trans.start() s.trx.Store(trans) // reset rebinding state atomic.StoreInt32(&s.rebinding, 0) if s.settings.OnRebind != nil { s.settings.OnRebind() } return } } } } ================================================ FILE: session_test.go ================================================ package gosmpp import ( "testing" "time" "github.com/stretchr/testify/require" ) func TestInvalidSessionSettings(t *testing.T) { auth := nextAuth() _, err := NewSession( TXConnector(NonTLSDialer, auth), Settings{}, 2*time.Second) require.Error(t, err) _, err = NewSession( RXConnector(NonTLSDialer, auth), Settings{ ReadTimeout: 200 * time.Millisecond, EnquireLink: 333 * time.Millisecond, }, 2*time.Second) require.Error(t, err) } func TestGetWindowSize(t *testing.T) { auth := nextAuth() s, err := NewSession( TXConnector(NonTLSDialer, auth), Settings{ EnquireLink: 5 * time.Second, ReadTimeout: 10 * time.Second, WindowedRequestTracking: &WindowedRequestTracking{ OnReceivedPduRequest: handleReceivedPduRequest(t), MaxWindowSize: 10, StoreAccessTimeOut: 100 * time.Millisecond, }, }, 2*time.Second) require.Nil(t, err) size, err := s.GetWindowSize() if err != nil { return } require.Nil(t, err) require.Equal(t, 0, size) err = s.Close() require.Nil(t, err) s, err = NewSession( RXConnector(NonTLSDialer, auth), Settings{ EnquireLink: 5 * time.Second, ReadTimeout: 10 * time.Second, WindowedRequestTracking: &WindowedRequestTracking{ OnReceivedPduRequest: handleReceivedPduRequest(t), MaxWindowSize: 10, StoreAccessTimeOut: 100 * time.Millisecond, }, }, 2*time.Second) require.Nil(t, err) size, err = s.GetWindowSize() if err != nil { return } require.Nil(t, err) require.Equal(t, -1, size) err = s.Close() require.Nil(t, err) s, err = NewSession( TRXConnector(NonTLSDialer, auth), Settings{ EnquireLink: 5 * time.Second, ReadTimeout: 10 * time.Second, WindowedRequestTracking: &WindowedRequestTracking{ MaxWindowSize: 10, StoreAccessTimeOut: 100 * time.Millisecond, }, }, 2*time.Second) require.NoError(t, err) size, err = s.GetWindowSize() if err != nil { return } require.Nil(t, err) require.Equal(t, 0, size) err = s.Close() require.Nil(t, err) s, err = NewSession( TRXConnector(NonTLSDialer, auth), Settings{ EnquireLink: 5 * time.Second, ReadTimeout: 10 * time.Second, WindowedRequestTracking: &WindowedRequestTracking{ ExpireCheckTimer: 5, PduExpireTimeOut: 10, MaxWindowSize: 10, StoreAccessTimeOut: 100 * time.Millisecond, }, }, 2*time.Second) require.NoError(t, err) size, err = s.GetWindowSize() if err != nil { return } require.Nil(t, err) require.Equal(t, -1, size) err = s.Close() require.Nil(t, err) } ================================================ FILE: state.go ================================================ package gosmpp const ( Alive int32 = iota Closed ) // State represents Transmitter/Receiver/Transceiver state. type State byte const ( // ExplicitClosing indicates that Transmitter/Receiver/Transceiver is closed // explicitly (from outside). ExplicitClosing State = iota // StoppingProcessOnly stops daemons but does not close underlying net conn. StoppingProcessOnly // InvalidStreaming indicates Transceiver/Receiver data reading state is // invalid due to network connection or SMSC responsed with an invalid PDU // which potentially damages other following PDU(s). // // In both cases, Transceiver/Receiver is closed implicitly. InvalidStreaming // ConnectionIssue indicates that Transmitter/Receiver/Transceiver is closed // due to network connection issue or SMSC is not available anymore. ConnectionIssue // UnbindClosing indicates Receiver got unbind request from SMSC and closed due to this request. UnbindClosing ) // String interface. func (s *State) String() string { switch *s { case ExplicitClosing: return "ExplicitClosing" case StoppingProcessOnly: return "StoppingProcessOnly" case InvalidStreaming: return "InvalidStreaming" case ConnectionIssue: return "ConnectionIssue" case UnbindClosing: return "UnbindClosing" default: return "" } } ================================================ FILE: state_test.go ================================================ package gosmpp import ( "github.com/stretchr/testify/assert" "testing" ) func TestState_String(t *testing.T) { tests := []struct { name string s State want string }{ { name: "ExplicitClosing", s: ExplicitClosing, want: "ExplicitClosing", }, { name: "StoppingProcessOnly", s: StoppingProcessOnly, want: "StoppingProcessOnly", }, { name: "InvalidStreaming", s: InvalidStreaming, want: "InvalidStreaming", }, { name: "ConnectionIssue", s: ConnectionIssue, want: "ConnectionIssue", }, { name: "UnbindClosing", s: UnbindClosing, want: "UnbindClosing", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equalf(t, tt.want, tt.s.String(), "String()") }) } } ================================================ FILE: transceivable.go ================================================ package gosmpp import ( "context" "errors" "github.com/linxGnu/gosmpp/pdu" "sync" "sync/atomic" "time" ) var ( ErrWindowNotConfigured = errors.New("window settings not configured") ) type transceivable struct { settings Settings ctx context.Context cancel context.CancelFunc wg sync.WaitGroup conn *Connection in *receivable out *transmittable aliveState int32 requestStore RequestStore } type TransceivableOption func(session *Session) func newTransceivable(conn *Connection, settings Settings, requestStore RequestStore) *transceivable { t := &transceivable{ settings: settings, conn: conn, requestStore: requestStore, } t.ctx, t.cancel = context.WithCancel(context.Background()) t.out = newTransmittable(conn, Settings{ WriteTimeout: settings.WriteTimeout, EnquireLink: settings.EnquireLink, OnSubmitError: settings.OnSubmitError, OnClosed: func(state State) { switch state { case ConnectionIssue: // also close input _ = t.in.close(ExplicitClosing) if t.settings.OnClosed != nil { t.settings.OnClosed(ConnectionIssue) } default: return } }, WindowedRequestTracking: settings.WindowedRequestTracking, }, requestStore) t.in = newReceivable(conn, Settings{ ReadTimeout: settings.ReadTimeout, OnPDU: settings.OnPDU, OnAllPDU: settings.OnAllPDU, OnReceivingError: settings.OnReceivingError, OnClosed: func(state State) { switch state { case InvalidStreaming, UnbindClosing: // also close output _ = t.out.close(ExplicitClosing) if t.settings.OnClosed != nil { t.settings.OnClosed(state) } default: return } }, WindowedRequestTracking: settings.WindowedRequestTracking, response: func(p pdu.PDU) { _ = t.Submit(p) }, }, requestStore, ) return t } func (t *transceivable) start() { if t.settings.WindowedRequestTracking != nil && t.settings.ExpireCheckTimer > 0 { t.wg.Add(1) go func() { defer t.wg.Done() t.windowCleanup() }() } t.out.start() t.in.start() } // SystemID returns tagged SystemID which is attached with bind_resp from SMSC. func (t *transceivable) SystemID() string { return t.conn.systemID } // Close transceiver and stop underlying daemons. func (t *transceivable) Close() (err error) { return t.closing(ExplicitClosing) } // Submit a PDU. func (t *transceivable) Submit(p pdu.PDU) error { return t.out.Submit(p) } func (t *transceivable) GetWindowSize() (int, error) { if t.settings.WindowedRequestTracking != nil { ctx, cancelFunc := context.WithTimeout(context.Background(), t.settings.StoreAccessTimeOut) defer cancelFunc() return t.requestStore.Length(ctx) } return 0, ErrWindowNotConfigured } func (t *transceivable) windowCleanup() { ticker := time.NewTicker(t.settings.ExpireCheckTimer) defer ticker.Stop() for { select { case <-t.ctx.Done(): return case <-ticker.C: ctx, cancelFunc := context.WithTimeout(context.Background(), t.settings.StoreAccessTimeOut) for _, request := range t.requestStore.List(ctx) { if time.Since(request.TimeSent) > t.settings.PduExpireTimeOut { _ = t.requestStore.Delete(ctx, request.GetSequenceNumber()) if t.settings.OnExpiredPduRequest != nil { if t.settings.OnExpiredPduRequest(request.PDU) { _ = t.closing(ConnectionIssue) } } } } cancelFunc() //defer should not be used because we are inside loop } } } func (t *transceivable) closing(state State) (err error) { if atomic.CompareAndSwapInt32(&t.aliveState, Alive, Closed) { t.cancel() // closing input and output _ = t.out.close(StoppingProcessOnly) _ = t.in.close(StoppingProcessOnly) // close underlying conn err = t.conn.Close() // notify transceiver closed if t.settings.OnClosed != nil { t.settings.OnClosed(state) } t.wg.Wait() } return } ================================================ FILE: transceivable_test.go ================================================ package gosmpp import ( "sync/atomic" "testing" "time" "github.com/linxGnu/gosmpp/data" "github.com/linxGnu/gosmpp/pdu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( countSubmitSMResp, countDeliverSM int32 ) func handlePDU(t *testing.T) func(pdu.PDU, bool) { return func(p pdu.PDU, responded bool) { switch pd := p.(type) { case *pdu.SubmitSMResp: require.False(t, responded) require.EqualValues(t, data.ESME_ROK, pd.CommandStatus) require.NotZero(t, len(pd.MessageID)) atomic.AddInt32(&countSubmitSMResp, 1) case *pdu.GenericNack: require.False(t, responded) t.Fatal(pd) case *pdu.DataSM: require.True(t, responded) t.Logf("%+v\n", pd) case *pdu.DeliverSM: require.True(t, responded) require.EqualValues(t, data.ESME_ROK, pd.CommandStatus) _mess, err := pd.Message.GetMessageWithEncoding(data.UCS2) assert.Nil(t, err) if mess == _mess { atomic.AddInt32(&countDeliverSM, 1) } } } } func transceivableHandleAllPDU(t *testing.T) func(pdu.PDU) (pdu.PDU, bool) { return func(p pdu.PDU) (pdu.PDU, bool) { switch pd := p.(type) { case *pdu.SubmitSMResp: require.EqualValues(t, data.ESME_ROK, pd.CommandStatus) require.NotZero(t, len(pd.MessageID)) atomic.AddInt32(&countSubmitSMResp, 1) return nil, false case *pdu.GenericNack: t.Fatal(pd) return nil, false case *pdu.DataSM: t.Logf("%+v\n", pd) return p.GetResponse(), false case *pdu.Unbind: t.Logf("%+v\n", pd) return p.GetResponse(), true case *pdu.DeliverSM: require.EqualValues(t, data.ESME_ROK, pd.CommandStatus) _mess, err := pd.Message.GetMessageWithEncoding(data.UCS2) assert.Nil(t, err) if mess == _mess { atomic.AddInt32(&countDeliverSM, 1) } return p.GetResponse(), false } return nil, false } } func TestTRXSubmitSM(t *testing.T) { auth := nextAuth() trans, err := NewSession( TRXConnector(NonTLSDialer, auth), Settings{ ReadTimeout: 2 * time.Second, WriteTimeout: 3 * time.Second, EnquireLink: 200 * time.Millisecond, OnSubmitError: func(_ pdu.PDU, err error) { t.Fatal(err) }, OnReceivingError: func(err error) { t.Log(err) }, OnRebindingError: func(err error) { t.Log(err) }, OnPDU: handlePDU(t), OnClosed: func(state State) { t.Log(state) }, }, 5*time.Second) require.Nil(t, err) require.NotNil(t, trans) defer func() { _ = trans.Close() }() require.Equal(t, "MelroseLabsSMSC", trans.Transceiver().SystemID()) // sending 20 SMS for i := 0; i < 20; i++ { err = trans.Transceiver().Submit(newSubmitSM(auth.SystemID)) require.Nil(t, err) time.Sleep(50 * time.Millisecond) } time.Sleep(5 * time.Second) // wait response received require.True(t, atomic.LoadInt32(&countSubmitSMResp) >= 15) // rebind and submit again trans.rebind() err = trans.Transceiver().Submit(newSubmitSM(auth.SystemID)) require.Nil(t, err) time.Sleep(time.Second) require.True(t, atomic.LoadInt32(&countSubmitSMResp) >= 16) } func TestTRXSubmitSM_with_OnAllPDU(t *testing.T) { auth := nextAuth() trans, err := NewSession( TRXConnector(NonTLSDialer, auth), Settings{ ReadTimeout: 2 * time.Second, WriteTimeout: 3 * time.Second, EnquireLink: 200 * time.Millisecond, OnSubmitError: func(_ pdu.PDU, err error) { t.Fatal(err) }, OnReceivingError: func(err error) { t.Log(err) }, OnRebindingError: func(err error) { t.Log(err) }, OnAllPDU: transceivableHandleAllPDU(t), OnClosed: func(state State) { t.Log(state) }, }, 5*time.Second) require.Nil(t, err) require.NotNil(t, trans) defer func() { _ = trans.Close() }() require.Equal(t, "MelroseLabsSMSC", trans.Transceiver().SystemID()) // sending 20 SMS for i := 0; i < 20; i++ { err = trans.Transceiver().Submit(newSubmitSM(auth.SystemID)) require.Nil(t, err) time.Sleep(50 * time.Millisecond) } time.Sleep(5 * time.Second) // wait response received require.True(t, atomic.LoadInt32(&countSubmitSMResp) >= 15) // rebind and submit again trans.rebind() err = trans.Transceiver().Submit(newSubmitSM(auth.SystemID)) require.Nil(t, err) time.Sleep(time.Second) require.True(t, atomic.LoadInt32(&countSubmitSMResp) >= 16) } func newSubmitSM(systemID string) *pdu.SubmitSM { // build up submitSM srcAddr := pdu.NewAddress() srcAddr.SetTon(5) srcAddr.SetNpi(0) _ = srcAddr.SetAddress(systemID) destAddr := pdu.NewAddress() destAddr.SetTon(1) destAddr.SetNpi(1) _ = destAddr.SetAddress("12" + systemID) submitSM := pdu.NewSubmitSM().(*pdu.SubmitSM) submitSM.SourceAddr = srcAddr submitSM.DestAddr = destAddr _ = submitSM.Message.SetMessageWithEncoding(mess, data.UCS2) submitSM.ProtocolID = 0 submitSM.RegisteredDelivery = 1 submitSM.ReplaceIfPresentFlag = 0 submitSM.EsmClass = 0 return submitSM } func TestTRXSubmitSM_with_WindowConfig(t *testing.T) { auth := nextAuth() trans, err := NewSession( TRXConnector(NonTLSDialer, auth), Settings{ ReadTimeout: 2 * time.Second, WriteTimeout: 3 * time.Second, EnquireLink: 200 * time.Millisecond, OnSubmitError: func(_ pdu.PDU, err error) { t.Fatal(err) }, OnReceivingError: func(err error) { t.Log(err) }, OnRebindingError: func(err error) { t.Log(err) }, WindowedRequestTracking: &WindowedRequestTracking{ OnReceivedPduRequest: handleReceivedPduRequest(t), OnExpectedPduResponse: handleExpectedPduResponse(t), OnExpiredPduRequest: nil, PduExpireTimeOut: 30 * time.Second, ExpireCheckTimer: 10 * time.Second, MaxWindowSize: 30, EnableAutoRespond: false, StoreAccessTimeOut: 100 * time.Millisecond, }, OnClosed: func(state State) { t.Log(state) }, }, 5*time.Second) require.Nil(t, err) require.NotNil(t, trans) defer func() { _ = trans.Close() }() require.Equal(t, "MelroseLabsSMSC", trans.Transceiver().SystemID()) // sending 20 SMS for i := 0; i < 50; i++ { err = trans.Transceiver().Submit(newSubmitSM(auth.SystemID)) require.Nil(t, err) time.Sleep(50 * time.Millisecond) } time.Sleep(5 * time.Second) // wait response received require.True(t, atomic.LoadInt32(&countSubmitSMResp) >= 15) // rebind and submit again trans.rebind() err = trans.Transceiver().Submit(newSubmitSM(auth.SystemID)) require.Nil(t, err) time.Sleep(time.Second) require.True(t, atomic.LoadInt32(&countSubmitSMResp) >= 16) } func TestTRXSubmitSM_with_WindowConfig_and_AutoRespond(t *testing.T) { auth := nextAuth() trans, err := NewSession( TRXConnector(NonTLSDialer, auth), Settings{ ReadTimeout: 2 * time.Second, WriteTimeout: 3 * time.Second, EnquireLink: 200 * time.Millisecond, OnSubmitError: func(_ pdu.PDU, err error) { t.Fatal(err) }, OnReceivingError: func(err error) { t.Log(err) }, OnRebindingError: func(err error) { t.Log(err) }, WindowedRequestTracking: &WindowedRequestTracking{ OnReceivedPduRequest: handleReceivedPduRequest(t), OnExpectedPduResponse: handleExpectedPduResponse(t), OnExpiredPduRequest: nil, PduExpireTimeOut: 30 * time.Second, ExpireCheckTimer: 10 * time.Second, MaxWindowSize: 30, EnableAutoRespond: true, StoreAccessTimeOut: 100 * time.Millisecond, }, OnClosed: func(state State) { t.Log("rebinded") }, }, 5*time.Second) require.Nil(t, err) require.NotNil(t, trans) defer func() { _ = trans.Close() }() require.Equal(t, "MelroseLabsSMSC", trans.Transceiver().SystemID()) // sending 20 SMS for i := 0; i < 20; i++ { err = trans.Transceiver().Submit(newSubmitSM(auth.SystemID)) require.Nil(t, err) time.Sleep(50 * time.Millisecond) } time.Sleep(5 * time.Second) // wait response received require.True(t, atomic.LoadInt32(&countSubmitSMResp) >= 15) // rebind and submit again trans.rebind() err = trans.Transceiver().Submit(newSubmitSM(auth.SystemID)) require.Nil(t, err) time.Sleep(time.Second) require.True(t, atomic.LoadInt32(&countSubmitSMResp) >= 16) } func handleReceivedPduRequest(t *testing.T) func(pdu.PDU) (pdu.PDU, bool) { return func(p pdu.PDU) (pdu.PDU, bool) { switch pd := p.(type) { case *pdu.Unbind: return pd.GetResponse(), true case *pdu.GenericNack: t.Fatal(pd) case *pdu.EnquireLink: return pd.GetResponse(), false case *pdu.DataSM: t.Logf("%+v\n", pd) return pd.GetResponse(), false case *pdu.DeliverSM: require.EqualValues(t, data.ESME_ROK, pd.CommandStatus) _mess, err := pd.Message.GetMessageWithEncoding(data.UCS2) assert.Nil(t, err) if mess == _mess { atomic.AddInt32(&countDeliverSM, 1) } return pd.GetResponse(), false } return nil, false } } func handleExpectedPduResponse(t *testing.T) func(response Response) { return func(response Response) { switch pp := response.PDU.(type) { case *pdu.UnbindResp: //t.Logf("%+v\n", pp) case *pdu.SubmitSMResp: require.NotZero(t, len(pp.MessageID)) atomic.AddInt32(&countSubmitSMResp, 1) t.Logf("%+v with original %+v\n", pp, response.OriginalRequest.PDU) case *pdu.EnquireLinkResp: t.Logf("%+v\n", pp) } } } func Test_newTransceivable(t *testing.T) { t.Run("always receive a non nil response", func(t *testing.T) { trans := newTransceivable(nil, Settings{}, nil) assert.NotNil(t, trans.in.settings.response) }) } ================================================ FILE: transmittable.go ================================================ package gosmpp import ( "context" "errors" "net" "runtime" "sync" "sync/atomic" "time" "github.com/linxGnu/gosmpp/pdu" ) var ( // ErrConnectionClosing indicates transmitter is closing. Can not send any PDU. ErrConnectionClosing = errors.New("connection is closing, can not send PDU to SMSC") ErrWindowsFull = errors.New("window full") ) type transmittable struct { settings Settings wg sync.WaitGroup input chan pdu.PDU conn *Connection aliveState int32 pendingWrite int32 requestStore RequestStore } func newTransmittable(conn *Connection, settings Settings, requestStore RequestStore) *transmittable { t := &transmittable{ settings: settings, conn: conn, input: make(chan pdu.PDU, 1), aliveState: Alive, pendingWrite: 0, requestStore: requestStore, } return t } func (t *transmittable) close(state State) (err error) { if atomic.CompareAndSwapInt32(&t.aliveState, Alive, Closed) { for atomic.LoadInt32(&t.pendingWrite) != 0 { runtime.Gosched() } // notify daemon close(t.input) // wait daemon t.wg.Wait() // try to send unbind _, _ = t.write(pdu.NewUnbind()) // close connection if state != StoppingProcessOnly { err = t.conn.Close() } // notify transmitter closed if t.settings.OnClosed != nil { t.settings.OnClosed(state) } // concurrent-map has no func to verify initialization // we need to do the same check in if t.settings.WindowedRequestTracking != nil { ctx, cancelFunc := context.WithTimeout(context.Background(), t.settings.StoreAccessTimeOut) defer cancelFunc() var size int size, err = t.requestStore.Length(ctx) if err != nil { return err } if size > 0 { for _, request := range t.requestStore.List(ctx) { if t.settings.OnClosePduRequest != nil { t.settings.OnClosePduRequest(request.PDU) } err = t.requestStore.Delete(ctx, request.GetSequenceNumber()) if err != nil { return err } } } } } return } func (t *transmittable) closing(state State) { go func() { _ = t.close(state) }() } // Submit a PDU. func (t *transmittable) Submit(p pdu.PDU) (err error) { atomic.AddInt32(&t.pendingWrite, 1) if atomic.LoadInt32(&t.aliveState) == Alive { t.input <- p } else { err = ErrConnectionClosing } atomic.AddInt32(&t.pendingWrite, -1) return } func (t *transmittable) start() { t.wg.Add(1) if t.settings.EnquireLink > 0 { go func() { defer t.wg.Done() t.loopWithEnquireLink() }() } else { go func() { defer t.wg.Done() t.loop() }() } } func (t *transmittable) drain() { for range t.input { } } func (t *transmittable) loop() { defer t.drain() for p := range t.input { if p != nil { n, err := t.write(p) if t.check(p, n, err) { return } } } } func (t *transmittable) loopWithEnquireLink() { ticker := time.NewTicker(t.settings.EnquireLink) defer func() { ticker.Stop() t.drain() }() for { select { case <-ticker.C: eqp := pdu.NewEnquireLink() n, err := t.write(eqp) if t.check(eqp, n, err) { return } case p, ok := <-t.input: if !ok { return } if p != nil { n, err := t.write(p) if t.check(p, n, err) { return } } } } } // check error and do closing if need func (t *transmittable) check(p pdu.PDU, n int, err error) (closing bool) { if err == nil { return } if t.settings.OnSubmitError != nil { t.settings.OnSubmitError(p, err) } if n == 0 { if errors.Is(err, ErrWindowsFull) { closing = false } else if nErr, ok := err.(net.Error); ok { closing = nErr.Timeout() } else { closing = true } } else { closing = true // force closing } if closing { t.closing(ConnectionIssue) // start closing } return } // low level writing func (t *transmittable) write(p pdu.PDU) (n int, err error) { if t.settings.WriteTimeout > 0 { err = t.conn.SetWriteTimeout(t.settings.WriteTimeout) } if err != nil { return } if t.settings.WindowedRequestTracking != nil && t.settings.MaxWindowSize > 0 && isAllowPDU(p) { ctx, cancelFunc := context.WithTimeout(context.Background(), t.settings.StoreAccessTimeOut) defer cancelFunc() var length int length, err = t.requestStore.Length(ctx) if err != nil { return 0, err } if length < int(t.settings.MaxWindowSize) { n, err = t.conn.WritePDU(p) if err != nil { return 0, err } request := Request{ PDU: p, TimeSent: time.Now(), } err = t.requestStore.Set(ctx, request) if err != nil { return 0, err } } else { return 0, ErrWindowsFull } } else { n, err = t.conn.WritePDU(p) } return } func isAllowPDU(p pdu.PDU) bool { if p.CanResponse() { switch p.(type) { case *pdu.BindRequest, *pdu.Unbind: return false } return true } return false } ================================================ FILE: transmittable_test.go ================================================ package gosmpp import ( "fmt" "net" "sync" "sync/atomic" "testing" "time" "github.com/linxGnu/gosmpp/pdu" "github.com/stretchr/testify/require" ) func TestTransmit(t *testing.T) { t.Run("Binding", func(t *testing.T) { auth := nextAuth() transmitter, err := NewSession( TXConnector(NonTLSDialer, auth), Settings{ ReadTimeout: 2 * time.Second, OnPDU: func(p pdu.PDU, _ bool) { t.Logf("%+v\n", p) }, OnSubmitError: func(_ pdu.PDU, err error) { t.Fatal(err) }, OnRebindingError: func(err error) { t.Fatal(err) }, OnClosed: func(state State) { t.Log(state) }, }, -1) require.Nil(t, err) require.NotNil(t, transmitter) defer func() { _ = transmitter.Close() }() require.Equal(t, "MelroseLabsSMSC", transmitter.Transmitter().SystemID()) err = transmitter.Transmitter().Submit(newSubmitSM(auth.SystemID)) require.Nil(t, err) time.Sleep(400 * time.Millisecond) transmitter.rebind() err = transmitter.Transmitter().Submit(newSubmitSM(auth.SystemID)) require.Nil(t, err) }) errorHandling := func(t *testing.T, trigger func(*transmittable)) { conn, err := net.Dial("tcp", smscAddr) require.NoError(t, err) var tr transmittable tr.input = make(chan pdu.PDU, 1) c := NewConnection(conn) defer func() { _ = c.Close() // write on closed conn? n, err := tr.write(pdu.NewEnquireLink()) require.NotNil(t, err) require.Zero(t, n) }() // fake settings tr.conn = c var count int32 tr.settings.OnClosed = func(State) { atomic.AddInt32(&count, 1) } tr.settings.OnSubmitError = func(p pdu.PDU, err error) { require.NotNil(t, err) _, ok := p.(*pdu.CancelSM) require.True(t, ok) } // do trigger trigger(&tr) time.Sleep(300 * time.Millisecond) require.NotZero(t, atomic.LoadInt32(&count)) } t.Run("ErrorHandling", func(t *testing.T) { errorHandling(t, func(tr *transmittable) { var p pdu.CancelSM tr.check(&p, 100, fmt.Errorf("fake error")) }) errorHandling(t, func(tr *transmittable) { var p pdu.CancelSM tr.check(&p, 0, fmt.Errorf("fake error")) }) }) t.Run("SubmitErr", func(t *testing.T) { var tr transmittable tr.input = make(chan pdu.PDU, 1) tr.aliveState = 1 err := tr.Submit(nil) require.Error(t, err) tr.aliveState = 0 err = tr.Submit(nil) require.NoError(t, err) }) } func TestConcurrentSubmitClose(t *testing.T) { auth := nextAuth() transmitter, err := NewSession( TXConnector(NonTLSDialer, auth), Settings{ ReadTimeout: 2 * time.Second, OnPDU: func(p pdu.PDU, _ bool) { t.Logf("%+v\n", p) }, OnSubmitError: func(_ pdu.PDU, err error) { t.Fatal(err) }, OnRebindingError: func(err error) { t.Fatal(err) }, OnClosed: func(state State) { t.Log(state) }, }, -1) require.Nil(t, err) require.NotNil(t, transmitter) defer func() { _ = transmitter.Close() }() require.Equal(t, "MelroseLabsSMSC", transmitter.Transmitter().SystemID()) var wg sync.WaitGroup wg.Add(1) toStart := make(chan struct{}, 1) go func() { defer wg.Done() time.Sleep(time.Millisecond) for i := 0; i < 100; i++ { err := transmitter.Transmitter().Submit(newSubmitSM(auth.SystemID)) require.True(t, err == nil || err == ErrConnectionClosing) if i == 10 { toStart <- struct{}{} } } }() wg.Add(1) go func() { defer wg.Done() <-toStart _ = transmitter.close() }() wg.Wait() } ================================================ FILE: types.go ================================================ package gosmpp import ( "github.com/linxGnu/gosmpp/pdu" ) // PDUCallback handles received PDU. type PDUCallback func(pdu pdu.PDU, responded bool) // AllPDUCallback handles all received PDU. // // This pdu is NOT responded to automatically, manual response/handling is needed // and the bind can be closed by retuning true on closeBind. type AllPDUCallback func(pdu pdu.PDU) (responsePdu pdu.PDU, closeBind bool) // PDUErrorCallback notifies fail-to-submit PDU with along error. type PDUErrorCallback func(pdu pdu.PDU, err error) // ErrorCallback notifies happened error while reading PDU. type ErrorCallback func(error) // ClosedCallback notifies closed event due to State. type ClosedCallback func(State) // RebindCallback notifies rebind event due to State. type RebindCallback func()