opensips-mi 0.0.11 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,45 +1,61 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Opensips
2
4
  module MI
5
+ # core class to send command to MI
6
+ # and return responses
3
7
  class Command
8
+ attr_reader :transp
9
+
4
10
  EVENTNOTIFY = {
5
11
  # Aastra
6
- aastra_check_cfg: 'check-sync',
7
- aastra_xml: 'aastra-xml',
12
+ aastra_check_cfg: "check-sync",
13
+ aastra_xml: "aastra-xml",
8
14
  # Digium
9
- digium_check_cfg: 'check-sync',
15
+ digium_check_cfg: "check-sync",
10
16
  # Linksys
11
- linksys_cold_restart: 'reboot_now',
12
- linksys_warm_restart: 'restart_now',
17
+ linksys_cold_restart: "reboot_now",
18
+ linksys_warm_restart: "restart_now",
13
19
  # Polycom
14
- polycom_check_cfg: 'check-sync',
20
+ polycom_check_cfg: "check-sync",
15
21
  # Sipura
16
- sipura_check_cfg: 'resync',
17
- sipura_get_report: 'report',
22
+ sipura_check_cfg: "resync",
23
+ sipura_get_report: "report",
18
24
  # Snom
19
- snom_check_cfg: 'check-sync;reboot=false',
20
- snom_reboot: 'check-sync;reboot=true',
25
+ snom_check_cfg: "check-sync;reboot=false",
26
+ snom_reboot: "check-sync;reboot=true",
21
27
  # Cisco
22
- cisco_check_cfg: 'check-sync',
28
+ cisco_check_cfg: "check-sync",
23
29
  # Avaya
24
- avaya_check_cfg: 'check-sync',
25
- }
26
-
27
- # Interface to mi methods direct call
28
- def method_missing(md, *params, &block)
29
- response = command md.to_s, params
30
- # return special helper output if exists
31
- return response unless response.success
32
- if response.respond_to?(md)
33
- response.send md
34
- else
35
- response
36
- end
30
+ avaya_check_cfg: "check-sync"
31
+ }.freeze
32
+
33
+ def initialize(transp)
34
+ @transp = transp
35
+ end
36
+
37
+ # prepare args, pipe them to send to MI using transport
38
+ # and finally format response
39
+ def command(*params)
40
+ raise ErrorParams, "command missing method name" if params.empty?
41
+
42
+ transp.adapter_request(*params)
43
+ .then { |args| transp.send(*args) }
44
+ .then { |resp| transp.adapter_response(resp) }
45
+ end
46
+
47
+ # meta methods call directly
48
+ def method_missing(cmd, *args)
49
+ command(cmd.to_s, *args)
37
50
  end
38
51
 
52
+ def respond_to_missing?(_name, _include_private = false) = true
53
+
39
54
  # = Interface to t_uac_dlg function of transaction (tm) module
40
55
  # Very cool method from OpenSIPs. Can generate and send SIP request method to destination.
41
56
  # Example of usage:
42
- # - Send NOTIFY with special Event header to force restart SIP phone (equivalent of ASterisk's "sip notify peer")
57
+ # - Send NOTIFY with special Event header to force restart SIP phone
58
+ # (equivalent of ASterisk's "sip notify peer")
43
59
  # - Send PUBLISH to trigger device state change notification
44
60
  # - Send REFER to transfer call
45
61
  # - etc., etc., etc.
@@ -50,10 +66,6 @@ module Opensips
50
66
  # Example:
51
67
  # hf["From"] => "Alice Liddell <sip:alice@wanderland.com>;tag=843887163"
52
68
  #
53
- # Special "nl" header with any value is used to input additional "\r\n". This is
54
- # useful, for example, for message-summary event to separate application body. This is
55
- # because t_uac_dlg expect body parameter as xml only.
56
- #
57
69
  # Thus, using multiple headers with same header-name is not possible with header hash.
58
70
  # However, it is possible to use multiple header-values comma separated (rfc3261, section 7.3.1):
59
71
  # hf["Route"] => "<sip:alice@atlanta.com>, <sip:bob@biloxi.com>"
@@ -67,60 +79,52 @@ module Opensips
67
79
  # == Parameters
68
80
  # method: SIP request method (NOTIFY, PUBLISH etc)
69
81
  # ruri: Request URI, ex.: sip:555@10.0.0.55:5060
70
- # hf: Headers array. Additional headers will be added to request.
82
+ # hf: Headers array. Additional headers will be added to request.
71
83
  # At least "From" and "To" headers must be specify
72
84
  # nhop: Next hop SIP URI (OBP); use "." if no value.
73
- # socket: Local socket to be used for sending the request; use "." if no value. Ex.: udp:10.130.8.21:5060
74
- # body: (optional, may not be present) request body (if present, requires the "Content-Type" and "Content-length" headers)
75
- #
76
- def uac_dlg method, ruri, hf, next_hop = ?., socket = ?., body = nil
77
- mandatory_hf = Array['To', 'From']
78
- mandatory_hf += ['Content-Type'] unless body.nil?
79
- mandatory_hf.map{|h|h.downcase}.each do |n|
80
- raise ArgumentError,
81
- "Missing mandatory header #{n.capitalize}" unless hf.keys.map{|h| h.downcase}.include?(n)
82
- end
83
- # compile headers to string
84
- headers = hf.map{|name,val| name.eql?("nl") ? "" : "#{name}: #{val}"}.join "\r\n"
85
+ # socket: Local socket to be used for sending the request; use "." if no value.
86
+ # Ex.: udp:10.130.8.21:5060
87
+ # body: (optional, may not be present) request body (if present, requires the "Content-Type"
88
+ # and "Content-length" headers)
89
+ def uac_dlg(method, ruri, hdrs, next_hop = ".", socket = ".", body = nil)
90
+ validate_hf(hdrs)
91
+
92
+ headers = hdrs.map { |name, val| "#{name}: #{val}" }.join("\r\n")
85
93
  headers << "\r\n\r\n"
86
94
 
87
- # set_header is a hack for xmlrpc which fails if headers are quoted
88
- params = [method, ruri, next_hop, socket, set_header(headers)]
95
+ params = [method, ruri, next_hop, socket, headers]
89
96
  params << body unless body.nil?
90
- # send it and return Response
91
- command 't_uac_dlg', params
97
+ command "t_uac_dlg", params
92
98
  end
93
99
 
94
100
  # = NOTIFY check-sync like event
95
- # NOTIFY Events to restart phone, force configuration reload or
96
- # report for some SIP IP phone models.
101
+ # NOTIFY Events to restart phone, force configuration reload or
102
+ # report for some SIP IP phone models.
97
103
  # The events list was taken from Asterisk configuration file (sip_notify.conf)
98
104
  # Note that SIP IP phones usually should be configured to accept special notify
99
105
  # event to reboot. For example, Polycom configuration option to enable special
100
106
  # event would be:
101
107
  # voIpProt.SIP.specialEvent.checkSync.alwaysReboot="1"
102
108
  #
103
- # This function will generate To/From/Event headers. Will use random tag for
104
- # From header.
109
+ # This function will generate To/From/Event headers. Will use random tag for
110
+ # From header.
105
111
  # *NOTE*: This function will not generate To header tag. This is not complying with
106
- # SIP protocol specification (rfc3265). NOTIFY must be part of a subscription
112
+ # SIP protocol specification (rfc3265). NOTIFY must be part of a subscription
107
113
  # dialog. However, it works for the most of the SIP IP phone models.
108
114
  # == Parameters
109
- # - uri: Valid client contact URI (sip:alice@10.0.0.100:5060).
115
+ # - uri: Valid client contact URI (sip:alice@10.0.0.100:5060).
110
116
  # To get client URI use *ul_show_contact => contact* function
111
117
  # - event: One of the events from EVENTNOTIFY constant hash
112
- # - hf: Header fields. Add To/From header fields here if you do not want them
118
+ # - hf: Header fields. Add To/From header fields here if you do not want them
113
119
  # to be auto-generated. Header field example:
114
120
  # hf['To'] => '<sip:alice@wanderland.com>'
115
- #
116
- def event_notify uri, event, hf = {}
117
- raise ArgumentError,
118
- "Invalid notify event: #{event.to_s}" unless EVENTNOTIFY.keys.include?(event)
119
- hf['To'] = "<#{uri}>" unless hf.keys.map{|k|k.downcase}.include?('to')
120
- hf['From'] = "<#{uri}>;tag=#{SecureRandom.hex}" unless hf.keys.map{|k|k.downcase}.include?('from')
121
- hf['Event'] = EVENTNOTIFY[event]
122
-
123
- uac_dlg "NOTIFY", uri, hf
121
+ def event_notify(uri, event, hdrs = {})
122
+ hnames = hdrs.keys.map { |k| k.to_s.downcase } || []
123
+ hdrs["To"] = "<#{uri}>" unless hnames.include?("to")
124
+ hdrs["From"] = "<#{uri}>;tag=#{rand(1 << 32)}" unless hnames.include?("from")
125
+ hdrs["Event"] = EVENTNOTIFY[event] || event.to_s
126
+
127
+ uac_dlg "NOTIFY", uri, hdrs
124
128
  end
125
129
 
126
130
  # = Presence MWI
@@ -135,39 +139,28 @@ module Opensips
135
139
  # - old: (optional) Old messages
136
140
  # - urg_new: (optional) New urgent messages
137
141
  # - urg_old: (optional) Old urgent messages
138
- #
139
- def mwi_update uri, vmaccount, new, old = 0, urg_new = 0, urg_old = 0
140
- mbody = Hash[
141
- 'Messages-Waiting' => (new > 0 ? "yes" : "no"),
142
- 'Message-Account' => vmaccount,
143
- 'Voice-Message' => "#{new}/#{old} (#{urg_new}/#{urg_old})",
144
- ]
145
- hf = Hash[
146
- 'To' => "<#{uri}>",
147
- 'From' => "<#{uri}>;tag=#{SecureRandom.hex}",
148
- 'Event' => "message-summary",
149
- 'Subscription-State'=> "active",
150
- 'Content-Type' => "application/simple-message-summary",
151
- 'nl' => "",
152
- ]
153
-
154
- uac_dlg "NOTIFY", uri, hf.merge(mbody)
142
+ def mwi_update(uri, vmaccount, newmsgs, old = 0, urg_new = 0, urg_old = 0)
143
+ hbody = { "Messages-Waiting" => (newmsgs.positive? ? "yes" : "no"),
144
+ "Message-Account" => vmaccount,
145
+ "Voice-Message" => "#{newmsgs}/#{old} (#{urg_new}/#{urg_old})" }
146
+ hdrs = { "To" => "<#{uri}>",
147
+ "From" => "<#{uri}>;tag=#{rand(1 << 32)}",
148
+ "Event" => "message-summary",
149
+ "Subscription-State" => "active",
150
+ "Content-Type" => "application/simple-message-summary" }
151
+
152
+ body = hbody.map { |k, v| "#{k}: #{v}" }.join("\r\n") << "\r\n\r\n"
153
+ uac_dlg "NOTIFY", uri, hdrs, ".", ".", body
155
154
  end
156
155
 
157
156
  private
158
- def set_header(header);"\"#{header}\"";end
159
-
160
- def host_valid? params
161
- raise ArgumentError,
162
- 'Missing socket host' if params[:host].nil?
163
- raise ArgumentError,
164
- 'Missing socket port' if params[:port].nil?
165
- Socket.getaddrinfo(params[:host], nil) rescue
166
- raise SocketError, "Invalid host #{params[:host]}"
167
- raise SocketError,
168
- "Invalid port #{params[:port]}" unless (1..(2**16-1)).include?(params[:port])
169
- true
170
- end
157
+
158
+ def validate_hf(headers)
159
+ names = headers&.keys&.map { |k| k.to_s.downcase } || []
160
+ return if names.include?("to") && names.include?("from")
161
+
162
+ raise ArgumentError, "invalid headers value. must be a hash and have 'To' and 'From' headers"
163
+ end
171
164
  end
172
165
  end
173
166
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Opensips
6
+ module MI
7
+ module Transport
8
+ # abstruct class for transport protocols
9
+ class Abstract
10
+ # send a command to connection and return response
11
+ def send(_command)
12
+ raise NotImplementedError
13
+ end
14
+
15
+ # request adapter method
16
+ # by default if does jsonrpc v2 as string
17
+ # xmlrpc overload this message
18
+ def adapter_request(cmd, *args)
19
+ rpc = {
20
+ jsonrpc: "2.0",
21
+ id: rand(1 << 16),
22
+ method: cmd
23
+ }
24
+
25
+ unless args.empty?
26
+ params = args.flatten
27
+ rpc[:params] = params[0].is_a?(Hash) ? params[0] : params
28
+ end
29
+
30
+ JSON.generate(rpc)
31
+ end
32
+
33
+ # response adapter by default parses jsonrpc response
34
+ # to an object. xmlrp overloads this method
35
+ def adapter_response(body)
36
+ resp = JSON.parse(body)
37
+ if resp["result"]
38
+ { result: resp["result"] }
39
+ elsif resp["error"]
40
+ { error: resp["error"] }
41
+ else
42
+ { error: { "message" => "invalid response: #{body}" } }
43
+ end
44
+ rescue JSON::ParserError => e
45
+ { error: { "message" => %(JSON::ParserError: #{e}) } }
46
+ end
47
+
48
+ protected
49
+
50
+ def raise_invalid_params
51
+ raise Opensips::MI::ErrorParams,
52
+ "invalid params. Expecting a hash with :url and optional :timeout"
53
+ end
54
+
55
+ def seturi(url)
56
+ @uri = URI(url)
57
+ rescue URI::InvalidURIError
58
+ raise Opensips::MI::ErrorParams,
59
+ "invalid http host url: #{url}"
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,42 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "abstract"
4
+ require "socket"
5
+ require "timeout"
6
+
1
7
  module Opensips
2
8
  module MI
3
9
  module Transport
4
- class Datagram < Opensips::MI::Command
5
- RECVMAXLEN = 2**16 - 1
6
- TIMEOUT = 3
10
+ # datagram UDP transport to communicate with MI
11
+ class Datagram < Abstract
12
+ def initialize(args)
13
+ super()
14
+ raise_invalid_params unless args.is_a?(Hash)
15
+ @host, @port, @timeout = args.values_at(:host, :port, :timeout)
16
+ raise_invalid_params if @host.nil? || @port.nil?
17
+ raise_invalid_port unless @port.to_i.between?(1, 1 << 16)
18
+ @timeout ||= 5
19
+ connect
20
+ end
7
21
 
8
- class << self
9
- def init(params)
10
- Datagram.new params
22
+ def send(command)
23
+ Timeout.timeout(
24
+ @timeout,
25
+ Opensips::MI::ErrorSendTimeout,
26
+ "timeout send command to #{@host}:#{@port} within #{@timeout} sec"
27
+ ) do
28
+ @sock.send command, 0
29
+ msg, = @sock.recvfrom(1500)
30
+ msg
11
31
  end
12
32
  end
13
33
 
14
- def initialize(params)
15
- host_valid? params
16
- @sock = Socketry::UDP::Socket.connect(params[:host], params[:port])
17
- @timeout = params[:timeout].to_i
18
- end
34
+ protected
19
35
 
20
- def command(cmd, params = [])
21
- request = ":#{cmd}:\n"
22
- params.each do |c|
23
- request << "#{c}\n"
24
- end
25
- response = send(request)
26
- Opensips::MI::Response.new response.split(?\n)
36
+ def raise_invalid_params
37
+ raise Opensips::MI::ErrorParams,
38
+ "invalid params. Expecting a hash with :host, :port and optional :timeout"
27
39
  end
28
40
 
29
- def tout
30
- @timeout > 0 ? @timeout : TIMEOUT
41
+ def raise_invalid_port
42
+ raise Opensips::MI::ErrorParams, "invalid port '#{@port}'"
31
43
  end
32
44
 
33
45
  private
34
- def send(request)
35
- @sock.send request
36
- response = @sock.recvfrom RECVMAXLEN, timeout: tout
37
- response.message
38
- rescue => e
39
- "500 #{e}"
46
+
47
+ def connect
48
+ @sock = UDPSocket.new
49
+ Timeout.timeout(
50
+ @timeout,
51
+ Opensips::MI::ErrorResolveTimeout,
52
+ "failed to resolve address #{@host}:#{@port} within #{@timeout} sec"
53
+ ) { @sock.connect(@host, @port) }
40
54
  end
41
55
  end
42
56
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require_relative "abstract"
5
+
6
+ module Opensips
7
+ module MI
8
+ module Transport
9
+ # HTTP transport to communicate with MI
10
+ class HTTP < Abstract
11
+ def initialize(args)
12
+ super()
13
+ raise_invalid_params unless args.is_a?(Hash)
14
+ url, @timeout = args.values_at(:url, :timeout)
15
+ raise_invalid_params if url.nil?
16
+ seturi(url)
17
+ @timeout ||= 5
18
+ connect
19
+ end
20
+
21
+ def send(cmd)
22
+ resp = @client.post(@uri.path, cmd, { "Content-Type" => "application/json" })
23
+ unless resp.code.eql? "200"
24
+ raise Opensips::MI::ErrorHTTPReq,
25
+ "invalid MI HTTP response: #{resp.message}"
26
+ end
27
+ resp.body
28
+ end
29
+
30
+ private
31
+
32
+ def connect
33
+ @client = Net::HTTP.new(@uri.host, @uri.port)
34
+ @client.read_timeout = @timeout
35
+ @client.write_timeout = @timeout
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,35 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "abstract"
4
+ require "xmlrpc/client"
5
+
1
6
  module Opensips
2
7
  module MI
3
8
  module Transport
4
- class Xmlrpc < Opensips::MI::Command
5
- RPCSEG = 'RPC2'
6
- class << self
7
- def init(params)
8
- Xmlrpc.new params
9
- end
9
+ # XML-RPC protocol for MI
10
+ class Xmlrpc < Abstract
11
+ def initialize(args)
12
+ super()
13
+ raise_invalid_params unless args.is_a?(Hash)
14
+ url, @timeout = args.values_at(:url, :timeout)
15
+ raise_invalid_params if url.nil?
16
+ seturi(url)
17
+ @client = XMLRPC::Client.new2(@uri.to_s, nil, @timeout)
10
18
  end
11
19
 
12
- def initialize(params)
13
- host_valid? params
14
- uri = "http://#{params[:host]}:#{params[:port]}/#{RPCSEG}"
15
- @client = XMLRPC::Client.new_from_uri(uri, nil, 3)
16
- rescue => e
17
- raise e.class,
18
- "Can not connect OpenSIPs server.\n#{e.message}"
20
+ def send(*cmd)
21
+ @client.call(*cmd)
22
+ rescue XMLRPC::FaultException => e
23
+ { error: { "message" => e.message } }
24
+ rescue StandardError => e
25
+ raise Opensips::MI::ErrorHTTPReq, e
19
26
  end
20
27
 
21
- def command(cmd, params = [])
22
- response = ["200 OK"]
23
- response += @client.call(cmd, *params).split(?\n)
24
- response << ""
25
- rescue => e
26
- response = ["600 " << e.message]
27
- ensure
28
- return Opensips::MI::Response.new response
28
+ # overload resonse adapter for xmlrpc
29
+ def adapter_response(resp)
30
+ if resp[:error]
31
+ resp
32
+ else
33
+ { result: resp }
34
+ end
29
35
  end
30
36
 
31
- def set_header(header);header;end
37
+ # overload request adapter for xmlrpc
38
+ def adapter_request(*args)
39
+ args.flatten => [cmd, *rest]
40
+ return [cmd] if rest.empty?
41
+
42
+ rest = rest.flatten
43
+ return [cmd, rest.first.map { |_, v| v }].flatten if rest.first.is_a?(Hash)
32
44
 
45
+ [cmd, rest].flatten
46
+ end
33
47
  end
34
48
  end
35
49
  end
@@ -1,9 +1,12 @@
1
- require_relative "transport/fifo"
1
+ # frozen_string_literal: true
2
+
2
3
  require_relative "transport/datagram"
4
+ require_relative "transport/http"
3
5
  require_relative "transport/xmlrpc"
4
6
 
5
7
  module Opensips
6
8
  module MI
9
+ # module transport protocols implementation
7
10
  module Transport
8
11
  end
9
12
  end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Opensips
2
4
  module MI
3
- VERSION = "0.0.11"
5
+ VERSION = "1.0.0"
4
6
  end
5
7
  end
data/lib/opensips/mi.rb CHANGED
@@ -1,22 +1,27 @@
1
- require 'ostruct'
2
- require 'fcntl'
3
- require 'securerandom'
4
- require 'socketry'
5
- require 'xmlrpc/client'
1
+ # frozen_string_literal: true
6
2
 
7
3
  require "opensips/mi/version"
8
- require "opensips/mi/response"
9
4
  require "opensips/mi/command"
10
5
  require "opensips/mi/transport"
11
6
 
12
7
  module Opensips
8
+ # OpenSIPS Managemen Interface core module
13
9
  module MI
14
- def self.connect(transport, params)
15
- # send to transport class
16
- Transport.const_get(transport.to_s.capitalize).init params
17
- rescue NameError
18
- raise NameError, "Unknown transport method: " << transport.to_s
10
+ class Error < StandardError; end
11
+ class ErrorParams < Error; end
12
+ class ErrorResolveTimeout < Error; end
13
+ class ErrorSendTimeout < Error; end
14
+ class ErrorHTTPReq < Error; end
15
+
16
+ def self.connect(transport_proto, params = {})
17
+ transp = case transport_proto
18
+ when :datagram then Transport::Datagram.new(params)
19
+ when :http then Transport::HTTP.new(params)
20
+ when :xmlrpc then Transport::Xmlrpc.new(params)
21
+ else
22
+ raise Error, "Unknown transport method: #{transport_proto}"
23
+ end
24
+ Command.new(transp)
19
25
  end
20
26
  end
21
27
  end
22
-
data/lib/opensips.rb CHANGED
@@ -1,4 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "opensips/mi"
2
4
 
5
+ # Ruby module to connect OpenSIPS Management Interface
6
+ # to send commands and read responses
3
7
  module Opensips
4
8
  end
@@ -0,0 +1,6 @@
1
+ module Opensips
2
+ module Mi
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end