mirror of
https://github.com/mainflux/mainflux.git
synced 2025-04-29 13:49:28 +08:00

* 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>
301 lines
6.5 KiB
Go
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
|
|
}
|