1
0
mirror of https://github.com/mainflux/mainflux.git synced 2025-04-29 13:49:28 +08:00
Ivan Milošević 66d3da0531
NOISSUE - Add SMPP notifier (#1464)
* Add SMPP notifier

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* fix readme
remove env file

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* resolve conversations

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* Remove debug log

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* Rename transmiter and transformer fields in struct

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* fix typo

Signed-off-by: Ivan Milosevic <iva@blokovi.com>
2021-10-06 14:34:23 +02:00

301 lines
6.5 KiB
Go

// Copyright 2015 go-smpp authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package smpp
import (
"context"
"crypto/tls"
"io"
"math"
"sync"
"time"
"github.com/fiorix/go-smpp/smpp/pdu"
"github.com/fiorix/go-smpp/smpp/pdu/pdufield"
)
// ConnStatus is an abstract interface for a connection status change.
type ConnStatus interface {
Status() ConnStatusID
Error() error
}
type connStatus struct {
s ConnStatusID
err error
}
func (c *connStatus) Status() ConnStatusID { return c.s }
func (c *connStatus) Error() error { return c.err }
// ConnStatusID represents a connection status change.
type ConnStatusID uint8
// Supported connection statuses.
const (
Connected ConnStatusID = iota + 1
Disconnected
ConnectionFailed
BindFailed
)
var connStatusText = map[ConnStatusID]string{
Connected: "Connected",
Disconnected: "Disconnected",
ConnectionFailed: "Connection failed",
BindFailed: "Bind failed",
}
// String implements the Stringer interface.
func (cs ConnStatusID) String() string {
return connStatusText[cs]
}
// ClientConn provides a persistent client connection that handles
// reconnection with a back-off algorithm.
type ClientConn interface {
// Bind starts the client connection and returns a
// channel that is triggered every time the connection
// status changes.
Bind() <-chan ConnStatus
// Closer embeds the Closer interface. When Close is
// called, client sends the Unbind command first and
// terminates the connection upon response, or 1s timeout.
Closer
}
// RateLimiter defines an interface for pacing the sending
// of short messages to a client connection.
//
// The Transmitter or Transceiver using the RateLimiter holds a
// single context.Context per client connection, passed to Wait
// prior to sending short messages.
//
// Suitable for use with package golang.org/x/time/rate.
type RateLimiter interface {
// Wait blocks until the limiter permits an event to happen.
Wait(ctx context.Context) error
}
// client provides a persistent client connection.
type client struct {
Addr string
TLS *tls.Config
Status chan ConnStatus
BindFunc func(c Conn) error
EnquireLink time.Duration
EnquireLinkTimeout time.Duration
RespTimeout time.Duration
BindInterval time.Duration
WindowSize uint
RateLimiter RateLimiter
// internal stuff.
inbox chan pdu.Body
conn *connSwitch
stop chan struct{}
once sync.Once
lmctx context.Context
// time of the last received EnquireLinkResp
eliTime time.Time
eliMtx sync.RWMutex
}
func (c *client) init() {
c.conn = &connSwitch{}
c.stop = make(chan struct{})
if c.RateLimiter != nil {
c.lmctx = context.Background()
}
if c.EnquireLink < 10*time.Second {
c.EnquireLink = 10 * time.Second
}
if c.EnquireLinkTimeout == 0 {
c.EnquireLinkTimeout = 3 * c.EnquireLink
}
}
// Bind starts the connection manager and blocks until Close is called.
// It must be called in a goroutine.
func (c *client) Bind() {
delay := 1.0
const maxdelay = 120.0
for !c.closed() {
eli := make(chan struct{})
c.inbox = make(chan pdu.Body)
conn, err := Dial(c.Addr, c.TLS)
if err != nil {
c.notify(&connStatus{
s: ConnectionFailed,
err: err,
})
goto retry
}
c.conn.Set(conn)
if err = c.BindFunc(c.conn); err != nil {
c.notify(&connStatus{s: BindFailed, err: err})
goto retry
}
go c.enquireLink(eli)
c.notify(&connStatus{s: Connected})
delay = 1
for {
p, err := c.conn.Read()
if err != nil {
c.notify(&connStatus{
s: Disconnected,
err: err,
})
break
}
switch p.Header().ID {
case pdu.EnquireLinkID:
pResp := pdu.NewEnquireLinkRespSeq(p.Header().Seq)
err := c.conn.Write(pResp)
if err != nil {
break
}
case pdu.EnquireLinkRespID:
c.updateEliTime()
default:
c.inbox <- p
}
}
retry:
close(eli)
c.conn.Close()
close(c.inbox)
delayDuration := c.BindInterval
if delayDuration == 0 {
delay = math.Min(delay*math.E, maxdelay)
delayDuration = time.Duration(delay) * time.Second
}
c.trysleep(delayDuration)
}
close(c.Status)
}
func (c *client) enquireLink(stop chan struct{}) {
// for the first check set time as Now()
c.updateEliTime()
for {
select {
case <-time.After(c.EnquireLink):
// check the time of the last received EnquireLinkResp
c.eliMtx.RLock()
if time.Since(c.eliTime) >= c.EnquireLinkTimeout {
c.conn.Write(pdu.NewUnbind())
c.conn.Close()
c.eliMtx.RUnlock()
return
}
c.eliMtx.RUnlock()
// send the EnquireLink
err := c.conn.Write(pdu.NewEnquireLink())
if err != nil {
return
}
case <-stop:
return
case <-c.stop:
return
}
}
}
func (c *client) updateEliTime() {
c.eliMtx.Lock()
c.eliTime = time.Now()
c.eliMtx.Unlock()
}
func (c *client) notify(ev ConnStatus) {
select {
case c.Status <- ev:
default:
}
}
// Read reads PDU binary data off the wire and returns it.
func (c *client) Read() (pdu.Body, error) {
select {
case pdu := <-c.inbox:
return pdu, nil
case <-c.stop:
return nil, io.EOF
}
}
// Write serializes the given PDU and writes to the connection.
func (c *client) Write(w pdu.Body) error {
if c.RateLimiter != nil {
c.RateLimiter.Wait(c.lmctx)
}
return c.conn.Write(w)
}
// Close terminates the current connection and stop any further attempts.
func (c *client) Close() error {
c.once.Do(func() {
close(c.stop)
if err := c.conn.Write(pdu.NewUnbind()); err == nil {
select {
case <-c.inbox: // TODO: validate UnbindResp
case <-time.After(time.Second):
}
}
c.conn.Close()
})
return nil
}
// trysleep for the given duration, or return if Close is called.
func (c *client) trysleep(d time.Duration) {
select {
case <-time.After(d):
case <-c.stop:
}
}
// closed returns true after Close is called once.
func (c *client) closed() bool {
select {
case <-c.stop:
return true
default:
return false
}
}
// respTimeout returns a channel that fires based on the configured
// response timeout, or the default 1s.
func (c *client) respTimeout() <-chan time.Time {
if c.RespTimeout == 0 {
return time.After(time.Second)
}
return time.After(c.RespTimeout)
}
// bind attempts to bind the connection.
func bind(c Conn, p pdu.Body) (pdu.Body, error) {
f := p.Fields()
f.Set(pdufield.InterfaceVersion, 0x34)
err := c.Write(p)
if err != nil {
return nil, err
}
resp, err := c.Read()
if err != nil {
return nil, err
}
h := resp.Header()
if h.Status != 0 {
return nil, h.Status
}
return resp, nil
}