opensips-mi 0.0.11 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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