asterisk-ajam 0.0.1
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.
- checksums.yaml +15 -0
- data/.gitignore +17 -0
- data/.travis.yml +3 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +128 -0
- data/Rakefile +26 -0
- data/asterisk-ajam.gemspec +26 -0
- data/example/ajam.rb +31 -0
- data/lib/asterisk/ajam/response.rb +113 -0
- data/lib/asterisk/ajam/session.rb +158 -0
- data/lib/asterisk/ajam/version.rb +5 -0
- data/lib/asterisk/ajam.rb +12 -0
- data/spec/asterisk/ajam/response_spec.rb +103 -0
- data/spec/asterisk/ajam/session_spec.rb +109 -0
- data/spec/spec_helper.rb +83 -0
- metadata +133 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ZTg2MWYzN2QwNzg3ZjViMmMxYzFlNGNjZmM1Y2JmZTY4ZGI2MGEyNw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZTVmY2E0OTdlNzVhNDBmNDFjZjkwMmQ4NTRkN2U5NDBjZGMxOGU0ZQ==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MDViOGI3OTU3NTVkMTE5ZjY0NDcxMzc3N2FiYTA0NjZkMWI0ZTZmZGQwYjBk
|
10
|
+
ZWJhYTQzMzc5MmRiMzkwOWYxMWYzOGFiZjE4MDc1MmUwMjYxYzcwYWI3ZDg4
|
11
|
+
YjcwNjU5NzQzZWVlYTIyNmZiZDdiMGFjZmI5NTM2NDcyZGZkZjE=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZDJiMmZiNTkxMWVlZDdhZjUxODM1NzRmOGI0OThmOGJjNzFkZGUzY2QzZjE0
|
14
|
+
NjlmNDY2NzQ3Y2UzMGM3MjJjZDMyNDI1NDYwYTcxZTg0MTc1MmU1NzIwNTc3
|
15
|
+
MjlhZjcxNDM5OTY5MjA0OTk2NzE1NGRlNDc1ODA2NDU2M2ZkNGM=
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Stas Kobzar
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
# Asterisk::AJAM
|
2
|
+
[](https://travis-ci.org/staskobzar/asterisk-ajam)
|
3
|
+
[](https://coveralls.io/r/staskobzar/asterisk-ajam?branch=master)
|
4
|
+
|
5
|
+
* * *
|
6
|
+
This is a very simple library that allows to comunicate with [Asterisk PBX](http://www.asterisk.org/) using [Asterisk Management Interface (AMI) actions](https://wiki.asterisk.org/wiki/display/AST/Asterisk+11+AMI+Actions).
|
7
|
+
Library communicates with AMI through HTTP(s) protocol. HTTP interface is provided by Asterisk PBX and called [AJAM](https://wiki.asterisk.org/wiki/pages/viewpage.action?pageId=4817256)
|
8
|
+
|
9
|
+
This library does not provide events interface to AMI. It only sends actions/commands and read responses back. If you need real AMI interface please, check [Adhearsion framework](http://adhearsion.com/) which is very powerful and provides a lot of functionality.
|
10
|
+
|
11
|
+
This library can use HTTTP Authentication username/password for AJAM servers behind proxy protected with basic access authentication scheme and HTTPS (SSL).
|
12
|
+
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
gem 'asterisk-ajam'
|
19
|
+
|
20
|
+
And then execute:
|
21
|
+
|
22
|
+
$ bundle install
|
23
|
+
|
24
|
+
Or install it yourself as:
|
25
|
+
|
26
|
+
$ gem install asterisk-ajam
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
You can check file [example/ajam.rb](https://github.com/staskobzar/asterisk-ajam/blob/master/example/ajam.rb) for some examples (comes with source).
|
31
|
+
|
32
|
+
Simple example:
|
33
|
+
```ruby
|
34
|
+
require 'asterisk/ajam'
|
35
|
+
ajam = Asterisk::AJAM.connect :uri => 'http://127.0.0.1:8088/mxml',
|
36
|
+
:ami_user => 'admin',
|
37
|
+
:ami_password => 'passw0rd'
|
38
|
+
if ajam.connected?
|
39
|
+
ajam.command 'dialplan reload'
|
40
|
+
res = ajam.action_sippeers
|
41
|
+
res.list.each {|peer| puts peer['objectname']}
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
List of argument options for ```Asterisk::AJAM.connect```:
|
46
|
+
|
47
|
+
* ```:uri``` URI of AJAM server. See [Asterisk documentation](https://wiki.asterisk.org/wiki/pages/viewpage.action?pageId=4817256) which explains how to configure AJAM server.
|
48
|
+
* ```:ami_user``` AMI user username
|
49
|
+
* ```:ami_password``` AMI user password
|
50
|
+
* ```:proxy_user``` proxy username for HTTP Authentication
|
51
|
+
* ```:proxy_pass``` proxy password for HTTP Authentication
|
52
|
+
|
53
|
+
If URI scheme is "https" then library enables SSL use. Please, note that current version does not enable verify mode of Net::HTTPS ruby library so it will accept any SSL certiificates without verifications.
|
54
|
+
|
55
|
+
```Asterisk::AJAM.connect``` returns ```Asterisk::AJAM::Session``` class which provides several useful methods:
|
56
|
+
|
57
|
+
```connected?``` returns true if successfully logged in AMI
|
58
|
+
|
59
|
+
```command``` send command to AMI and read response. Example:
|
60
|
+
```ruby
|
61
|
+
ajam.command "dialplan reload"
|
62
|
+
ajam.command "core restart when convenient"
|
63
|
+
```
|
64
|
+
|
65
|
+
```action_NAME``` sends action to AMI. NAME must be replaced with AMI action name (see: [Asterisk documentation](https://wiki.asterisk.org/wiki/display/AST/Asterisk+11+AMI+Actions)). Accepts hash as an argument. Example:
|
66
|
+
```ruby
|
67
|
+
ajam.action_agents # no arguments
|
68
|
+
ajam.action_mailboxcount mailbox: "5555@default"
|
69
|
+
ajam.action_originate :channel => 'Local/local-chan@from-pstn',
|
70
|
+
:application => 'MusicOnHold',
|
71
|
+
:callerid => 'John Doe <5555555>'
|
72
|
+
```
|
73
|
+
|
74
|
+
Actions and Commands return ```Asterisk::AJAM::Response``` class with several methods where response data can be found:
|
75
|
+
|
76
|
+
* list
|
77
|
+
* attribute
|
78
|
+
* data
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
res = ajam.action_meetmelist
|
82
|
+
pp res.list
|
83
|
+
pp res.attribute
|
84
|
+
pp res.data
|
85
|
+
```
|
86
|
+
|
87
|
+
## Using Apache proxy tutorial
|
88
|
+
As an additional security measure Apache proxy with HTTP Authentication can be used. Here is an example of Apache Virtual Host configuration as Proxy to AJAM server
|
89
|
+
|
90
|
+
```
|
91
|
+
Listen 8888
|
92
|
+
NameVirtualHost *:8888
|
93
|
+
<VirtualHost *:8888>
|
94
|
+
<Proxy *>
|
95
|
+
Order deny,allow
|
96
|
+
Allow from all
|
97
|
+
AuthName "AJAM"
|
98
|
+
AuthType Basic
|
99
|
+
AuthUserFile /etc/httpd/.htpasswd
|
100
|
+
require valid-user
|
101
|
+
</Proxy>
|
102
|
+
ProxyPass / http://ajamhost.com:8088/
|
103
|
+
ProxyPassReverse / http://ajamhost.com:8088/
|
104
|
+
</VirtualHost>
|
105
|
+
```
|
106
|
+
|
107
|
+
Generate password file:
|
108
|
+
```
|
109
|
+
htpasswd -b -c /etc/httpd/.htpasswd admin passw0rd
|
110
|
+
```
|
111
|
+
|
112
|
+
Connect to proxy:
|
113
|
+
```ruby
|
114
|
+
ajam = Asterisk::AJAM.connect :uri => 'http://ajamproxy.com:8888/mxml',
|
115
|
+
:ami_user => 'admin',
|
116
|
+
:ami_password => 'amp111',
|
117
|
+
:proxy_user => 'admin',
|
118
|
+
:proxy_pass => 'passw0rd'
|
119
|
+
```
|
120
|
+
|
121
|
+
## Contributing
|
122
|
+
|
123
|
+
1. Fork it
|
124
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
125
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
126
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
127
|
+
5. Create new Pull Request
|
128
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'rdoc/task'
|
4
|
+
|
5
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
6
|
+
t.rspec_opts = "--color"
|
7
|
+
end
|
8
|
+
|
9
|
+
RSpec::Core::RakeTask.new(:spec_doc) do |t|
|
10
|
+
t.rspec_opts = "--color --format doc"
|
11
|
+
end
|
12
|
+
|
13
|
+
RSpec::Core::RakeTask.new(:spec_html) do |t|
|
14
|
+
t.rspec_opts = "--format html -o spec/reports/index.html"
|
15
|
+
end
|
16
|
+
|
17
|
+
Rake::RDocTask.new do |rd|
|
18
|
+
rd.main = "README.md"
|
19
|
+
rd.rdoc_files.include("README.md","lib/**/*.rb")
|
20
|
+
rd.title = 'Asterisk::AJAM: Ruby module for interacting with Asterisk management interface (AMI) via HTTP'
|
21
|
+
rd.rdoc_dir = "doc"
|
22
|
+
rd.options << '--line-numbers'
|
23
|
+
rd.options << '--all'
|
24
|
+
end
|
25
|
+
|
26
|
+
task :default => :spec
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'asterisk/ajam/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "asterisk-ajam"
|
8
|
+
spec.version = Asterisk::AJAM::VERSION
|
9
|
+
spec.authors = ["Stas Kobzar"]
|
10
|
+
spec.email = ["stas@modulis.ca"]
|
11
|
+
spec.description = %q{Ruby module for interacting with Asterisk management interface (AMI) via HTTP}
|
12
|
+
spec.summary = %q{AMI via HTTP}
|
13
|
+
spec.homepage = "https://github.com/staskobzar/asterisk-ajam"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
21
|
+
spec.add_development_dependency "rake"
|
22
|
+
spec.add_development_dependency "rdoc"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
#spec.add_development_dependency "fakeweb"
|
25
|
+
spec.add_runtime_dependency "libxml-ruby"
|
26
|
+
end
|
data/example/ajam.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'asterisk/ajam'
|
3
|
+
require 'pp'
|
4
|
+
ajam = Asterisk::AJAM.connect :uri => 'http://127.0.0.1:8088/mxml',
|
5
|
+
:ami_user => 'admin',
|
6
|
+
:ami_password => 'amp111',
|
7
|
+
:proxy_user => 'admin',
|
8
|
+
:proxy_pass => 'admin'
|
9
|
+
|
10
|
+
pp ajam.connected?
|
11
|
+
# actions
|
12
|
+
res = ajam.action_sippeers
|
13
|
+
pp res.list
|
14
|
+
|
15
|
+
ajam = Asterisk::AJAM.connect :uri => 'https://127.0.0.1:9443/mxml',
|
16
|
+
:ami_user => 'admin',
|
17
|
+
:ami_password => 'amp111',
|
18
|
+
:use_ssl => true
|
19
|
+
res = ajam.action_corestatus
|
20
|
+
pp res.attribute
|
21
|
+
pp res.list
|
22
|
+
|
23
|
+
res = ajam.action_sipshowpeer peer: '5555'
|
24
|
+
pp res.attribute
|
25
|
+
|
26
|
+
# commands
|
27
|
+
res = ajam.command 'sip show peer 5555'
|
28
|
+
pp res.data
|
29
|
+
|
30
|
+
res = ajam.command 'dialplan reload'
|
31
|
+
pp res.data
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'libxml'
|
2
|
+
|
3
|
+
#
|
4
|
+
# = asterisk/ajam/response.rb
|
5
|
+
#
|
6
|
+
module Asterisk
|
7
|
+
module AJAM
|
8
|
+
# Exception raised when HTTP response body is invalid
|
9
|
+
class InvalidHTTPBody < StandardError;end #:nodoc:
|
10
|
+
|
11
|
+
#
|
12
|
+
# Generic class to process and store responses from
|
13
|
+
# Asterisk AJAM server. Stores data from HTTP response
|
14
|
+
# and xml document received from Asterisk server
|
15
|
+
#
|
16
|
+
class Response
|
17
|
+
|
18
|
+
# HTTP response code
|
19
|
+
attr_reader :code
|
20
|
+
|
21
|
+
# AJAM session id
|
22
|
+
attr_reader :session_id
|
23
|
+
|
24
|
+
# Response eventlist from action (for example: sip peers)
|
25
|
+
attr_reader :list
|
26
|
+
|
27
|
+
# Response attributes
|
28
|
+
attr_reader :attribute
|
29
|
+
|
30
|
+
# Creates new Response class instance. Sets instance
|
31
|
+
# variables from HTTP Response (like code). Parses body.
|
32
|
+
def initialize(http)
|
33
|
+
raise ArgumentError,
|
34
|
+
"Expected Net::HTTP::Response. Got #{http.class}" unless http.is_a?(Net::HTTPResponse)
|
35
|
+
@attribute = []
|
36
|
+
@list = []
|
37
|
+
@code = http.code.to_i
|
38
|
+
return unless httpok?
|
39
|
+
parse_body http.body
|
40
|
+
set_session_id http
|
41
|
+
end
|
42
|
+
|
43
|
+
# HTTP request status
|
44
|
+
def httpok?
|
45
|
+
@code.eql? 200
|
46
|
+
end
|
47
|
+
|
48
|
+
# Is AJAM action/command successful
|
49
|
+
def success?
|
50
|
+
@success
|
51
|
+
end
|
52
|
+
|
53
|
+
# opaque_data from command response
|
54
|
+
def data
|
55
|
+
@data || @nodes
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
# Parses HTTP response body.
|
60
|
+
# Body should by xml string. Otherwise will raise
|
61
|
+
# exception InvalidHTTPBody
|
62
|
+
def parse_body xml
|
63
|
+
set_nodes xml
|
64
|
+
verify_response
|
65
|
+
set_eventlist
|
66
|
+
end
|
67
|
+
|
68
|
+
# parse xml body and set result to internal variable for farther processing
|
69
|
+
def set_nodes xml
|
70
|
+
raise InvalidHTTPBody,
|
71
|
+
"Empty response body" if xml.to_s.empty?
|
72
|
+
src = LibXML::XML::Parser.string(xml).parse
|
73
|
+
@nodes = src.root.find('response/generic').to_a
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# Check if AJAM response is successfull and set internal variable
|
78
|
+
# @success
|
79
|
+
def verify_response
|
80
|
+
node = @nodes.shift
|
81
|
+
@success = ['success', 'follows'].include? node[:response].to_s.downcase
|
82
|
+
@attribute = node.attributes.to_h
|
83
|
+
|
84
|
+
set_command_data
|
85
|
+
end
|
86
|
+
|
87
|
+
# extract mansession_id from cookies
|
88
|
+
def set_session_id(http)
|
89
|
+
if /mansession_id=(['"])([^\1]+)\1/ =~ http['Set-Cookie']
|
90
|
+
@session_id = $2
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# for reponses that contain eventlist of values set it to
|
95
|
+
# internal attributes
|
96
|
+
def set_eventlist
|
97
|
+
return unless @attribute['eventlist'].to_s.downcase.eql? 'start'
|
98
|
+
@nodes.each do |node|
|
99
|
+
next if node['eventlist'].eql? 'Complete'
|
100
|
+
@list << node.attributes.to_h
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def set_command_data
|
105
|
+
if @attribute.has_key?('opaque_data')
|
106
|
+
@data = @attribute['opaque_data']
|
107
|
+
@attribute.delete 'opaque_data'
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'net/https' # this will also include net/http and uri
|
2
|
+
#
|
3
|
+
# = asterisk/ajam/session.rb
|
4
|
+
#
|
5
|
+
module Asterisk
|
6
|
+
module AJAM
|
7
|
+
#
|
8
|
+
# == Session class for Asterisk AJAM
|
9
|
+
# Used to controll connection to Astersik AJAM via HTTP protocol
|
10
|
+
#
|
11
|
+
|
12
|
+
# Session errors
|
13
|
+
# This class extends StandardError and raises when problem with
|
14
|
+
# AJAM URL found, for ex: missing scheme or hostname
|
15
|
+
# TODO: should be also used for HTTPS connections
|
16
|
+
class InvalidURI < StandardError; end #:nodoc:
|
17
|
+
|
18
|
+
# This class extends StandardError and raises when problems
|
19
|
+
# with loggin AJAM server found or when missing importent
|
20
|
+
# parameters like username or password.
|
21
|
+
class InvalidAMILogin < StandardError; end #:nodoc:
|
22
|
+
|
23
|
+
# Class extends StandardError and is raised when trying to
|
24
|
+
# send command to not AJAM server where not logged in
|
25
|
+
class NotLoggedIn < StandardError; end #:nodoc:
|
26
|
+
|
27
|
+
# This class establish connection to AJAM server using TCP connection
|
28
|
+
# and HTTP protocol.
|
29
|
+
class Session
|
30
|
+
|
31
|
+
# Asterisk AJAM server URI
|
32
|
+
# URI class instance
|
33
|
+
attr_accessor :uri
|
34
|
+
|
35
|
+
# AMI user name
|
36
|
+
attr_accessor :ami_user
|
37
|
+
|
38
|
+
# AMI password
|
39
|
+
attr_accessor :ami_password
|
40
|
+
|
41
|
+
# http access user
|
42
|
+
attr_accessor :proxy_user
|
43
|
+
|
44
|
+
# http access password
|
45
|
+
attr_accessor :proxy_pass
|
46
|
+
|
47
|
+
# Create new Asterisk AJAM session without initializing
|
48
|
+
# TCP network connection
|
49
|
+
def initialize(options={})
|
50
|
+
self.uri = options[:uri]
|
51
|
+
@ami_user = options[:ami_user]
|
52
|
+
@ami_password = options[:ami_password]
|
53
|
+
@proxy_pass = options[:proxy_pass]
|
54
|
+
@proxy_user = options[:proxy_user]
|
55
|
+
@use_ssl = options[:use_ssl]
|
56
|
+
end
|
57
|
+
|
58
|
+
# login action. Also stores session identificator for
|
59
|
+
# sending many actions within same session
|
60
|
+
def login
|
61
|
+
ami_user_valid?
|
62
|
+
ami_pass_valid?
|
63
|
+
send_action :login, username: @ami_user, secret: @ami_password
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
# send AMI command
|
68
|
+
def command(command)
|
69
|
+
action_command command: command
|
70
|
+
end
|
71
|
+
|
72
|
+
# Verify if session esteblished connection and set session id
|
73
|
+
def connected?
|
74
|
+
/^[0-9a-z]{8}$/.match(@response.session_id).is_a? MatchData
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
# handling action_ methods
|
79
|
+
def method_missing(method, *args)
|
80
|
+
method = method.id2name
|
81
|
+
raise NoMethodError,
|
82
|
+
"Undefined method #{method}" unless /^action_\w+$/.match(method)
|
83
|
+
raise NotLoggedIn, "Not logged in" unless connected?
|
84
|
+
send_action method.sub(/^action_/,'').to_sym, *args
|
85
|
+
end
|
86
|
+
|
87
|
+
# send action to Asterisk AJAM server
|
88
|
+
def send_action(action, params={})
|
89
|
+
set_params Hash[action: action].merge params
|
90
|
+
@response = http_send_action
|
91
|
+
end
|
92
|
+
|
93
|
+
# Send HTTP request to AJAM server using "#uri"
|
94
|
+
def http_send_action
|
95
|
+
http = http_inst
|
96
|
+
req = http_post
|
97
|
+
Response.new http.request req
|
98
|
+
end
|
99
|
+
|
100
|
+
# create new Net::HTTP instance
|
101
|
+
def http_inst
|
102
|
+
http = Net::HTTP.new(@uri.host, @uri.port)
|
103
|
+
if @uri.scheme.downcase.eql? 'https'
|
104
|
+
http.use_ssl = true
|
105
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
106
|
+
end
|
107
|
+
http
|
108
|
+
end
|
109
|
+
|
110
|
+
# create new Net::HTTP::Post instance
|
111
|
+
def http_post
|
112
|
+
req = Net::HTTP::Post.new @uri.request_uri, request_headers
|
113
|
+
req.set_form_data params
|
114
|
+
req.basic_auth @proxy_user, @proxy_pass if @proxy_pass && @proxy_user
|
115
|
+
req
|
116
|
+
end
|
117
|
+
|
118
|
+
# Post parameters
|
119
|
+
def params
|
120
|
+
@params
|
121
|
+
end
|
122
|
+
|
123
|
+
# set AJAM POST parameters
|
124
|
+
def set_params(params)
|
125
|
+
@params = params
|
126
|
+
end
|
127
|
+
|
128
|
+
# verifies if AMI username is set and not empty
|
129
|
+
def ami_user_valid?
|
130
|
+
raise InvalidAMILogin,
|
131
|
+
"Missing AMI username" if @ami_user.to_s.empty?
|
132
|
+
end
|
133
|
+
|
134
|
+
# verifies if AMI password (secret) is set and not empty
|
135
|
+
def ami_pass_valid?
|
136
|
+
raise InvalidAMILogin,
|
137
|
+
"Missing AMI user pass" if @ami_password.to_s.empty?
|
138
|
+
end
|
139
|
+
|
140
|
+
# setup AJAM URI
|
141
|
+
def uri=(uri)
|
142
|
+
@uri = URI.parse uri
|
143
|
+
raise InvalidURI,
|
144
|
+
"No AJAM URI given" unless @uri
|
145
|
+
raise InvalidURI,
|
146
|
+
"Unsupported uri.scheme" unless %w/http https/.include? @uri.scheme
|
147
|
+
end
|
148
|
+
|
149
|
+
# initialize request headers for Net::HTTPRequest class
|
150
|
+
def request_headers
|
151
|
+
return nil unless @response
|
152
|
+
Hash[
|
153
|
+
'Cookie' => %Q!mansession_id="#{@response.session_id}"!
|
154
|
+
]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Asterisk
|
4
|
+
module AJAM
|
5
|
+
describe Response do
|
6
|
+
# stub successful login
|
7
|
+
let(:res_login_ok) do
|
8
|
+
http = get_simple_httpok
|
9
|
+
http.stub(:body => get_body_success_login)
|
10
|
+
http
|
11
|
+
end
|
12
|
+
# stub failed login
|
13
|
+
let(:res_login_fail) do
|
14
|
+
http = get_simple_httpok
|
15
|
+
http.stub(:body => get_body_failed_login)
|
16
|
+
http
|
17
|
+
end
|
18
|
+
|
19
|
+
#new
|
20
|
+
describe "#new" do
|
21
|
+
it "raises error if parameter is not Net::HTTPResponse instance" do
|
22
|
+
expect{
|
23
|
+
Response.new nil
|
24
|
+
}.to raise_error(ArgumentError)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "raises error if empty response body" do
|
28
|
+
http = get_simple_httpok
|
29
|
+
http.stub(:body => '')
|
30
|
+
expect{
|
31
|
+
Response.new http
|
32
|
+
}.to raise_error(InvalidHTTPBody)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "raises error if invalid xml response body" do
|
36
|
+
http = get_simple_httpok
|
37
|
+
http.stub(:body => '<a>this is not xml')
|
38
|
+
expect{
|
39
|
+
Response.new http
|
40
|
+
}.to raise_error(LibXML::XML::Error)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "set session id" do
|
44
|
+
sid = "df901b5f"
|
45
|
+
res = res_login_ok
|
46
|
+
res.stub(:[]).with('Set-Cookie').and_return(%Q[mansession_id="#{sid}"; Version=1; Max-Age=60])
|
47
|
+
response = Response.new res
|
48
|
+
response.session_id.should eql(sid)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
#httpok?
|
53
|
+
describe "#httpok?" do
|
54
|
+
it "returns true if successfully received response from AJAM" do
|
55
|
+
res = Response.new res_login_ok
|
56
|
+
res.should be_httpok
|
57
|
+
end
|
58
|
+
it "returns false if response from AJAM anything but 200 OK" do
|
59
|
+
res = Response.new get_http_unauth
|
60
|
+
res.should_not be_httpok
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
#success?
|
65
|
+
describe "#success?" do
|
66
|
+
it "returns true if AJAM response is successful" do
|
67
|
+
res = Response.new res_login_ok
|
68
|
+
res.should be_success
|
69
|
+
end
|
70
|
+
it "returns false if AJAM response id error" do
|
71
|
+
res = Response.new res_login_fail
|
72
|
+
res.should_not be_success
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "actions" do
|
77
|
+
it "set recieved response list to class attributes" do
|
78
|
+
http = get_simple_httpok
|
79
|
+
http.stub(:body => get_body_sippeers)
|
80
|
+
res = Response.new http
|
81
|
+
res.list.first.should include('event' => 'PeerEntry', 'objectname' => '5555')
|
82
|
+
res.list.last.should include('objectname' => '8903')
|
83
|
+
end
|
84
|
+
|
85
|
+
it "when response has single result then it stored in :response" do
|
86
|
+
http = get_simple_httpok
|
87
|
+
http.stub(:body => get_body_corestatus)
|
88
|
+
res = Response.new http
|
89
|
+
res.attribute['corestartuptime'].should eql('17:13:00')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "#command" do
|
94
|
+
it "set data variable with response from AJAM" do
|
95
|
+
http = get_simple_httpok
|
96
|
+
http.stub(:body => cmd_body_dialplan_reload)
|
97
|
+
res = Response.new http
|
98
|
+
res.data.should match(/Dialplan reloaded/)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Asterisk
|
4
|
+
module AJAM
|
5
|
+
describe Session do
|
6
|
+
let(:uri_http){'http://ajam.asterisk.com:8088/mxml'}
|
7
|
+
let(:manses_id){"84d22b60"}
|
8
|
+
let(:http) do
|
9
|
+
http = get_simple_httpok
|
10
|
+
http.stub(:body => get_body_success_login)
|
11
|
+
http.stub(:[]).with('Set-Cookie').and_return(%Q|mansession_id="#{manses_id}"; Version=1; Max-Age=60|)
|
12
|
+
http
|
13
|
+
end
|
14
|
+
subject{Session.new uri: uri_http, ami_user: 'admin', ami_password: 'passw0rd'}
|
15
|
+
before(:each, :logginok => true) do
|
16
|
+
Net::HTTP.stub(:new).and_return(double('Net::HTTP', :request => http, :use_ssl= => true))
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#new" do
|
20
|
+
it "set AJAM uri" do
|
21
|
+
URI.stub(:parse => URI(uri_http))
|
22
|
+
URI.should_receive(:parse).with(uri_http)
|
23
|
+
Session.new uri: uri_http
|
24
|
+
end
|
25
|
+
|
26
|
+
it "raises error if URI missing " do
|
27
|
+
expect{
|
28
|
+
Session.new
|
29
|
+
}.to raise_error(URI::InvalidURIError)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "raises error if scheme is not http or https" do
|
33
|
+
uri = "ftp://127.0.0.1/path"
|
34
|
+
expect{
|
35
|
+
Session.new uri: uri
|
36
|
+
}.to raise_error(InvalidURI)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#login", :logginok => true do
|
41
|
+
it "raises InvalidAMILogin if empty AMI username" do
|
42
|
+
subject.ami_user = nil
|
43
|
+
expect{
|
44
|
+
subject.login
|
45
|
+
}.to raise_error(InvalidAMILogin)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "raises InvalidAMILogin if empty AMI password" do
|
49
|
+
subject.ami_password = nil
|
50
|
+
expect{
|
51
|
+
subject.login
|
52
|
+
}.to raise_error(InvalidAMILogin)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should return Session class instance on success" , :mock_login => true do
|
56
|
+
response = subject.login
|
57
|
+
response.should be_kind_of Session
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "#connected?", :logginok => true do
|
63
|
+
it "returns true when successfuly logged in", :mock_login => true do
|
64
|
+
subject.login
|
65
|
+
subject.should be_connected
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "action methods" do
|
70
|
+
before(:each) {subject.stub(:connected?).and_return(true)}
|
71
|
+
it "when call action without parameters then expects send_action with action name" do
|
72
|
+
subject.should_receive(:send_action).once.with(:sippeers)
|
73
|
+
subject.action_sippeers
|
74
|
+
end
|
75
|
+
|
76
|
+
it "when call action with parameters then expects send_action with hash" do
|
77
|
+
params = Hash[family: 'user', key: 'extension', val: '5555']
|
78
|
+
subject.should_receive(:send_action).with(:dbput, params)
|
79
|
+
subject.action_dbput params
|
80
|
+
end
|
81
|
+
|
82
|
+
it "uses cookies when sending action", :logginok => true do
|
83
|
+
subject.login
|
84
|
+
Net::HTTP::Post.should_receive(:new).
|
85
|
+
with(anything(),hash_including("Cookie"=>%Q!mansession_id="#{manses_id}"!)).
|
86
|
+
and_return(double('Net::HTTPResponse', :set_form_data => true))
|
87
|
+
subject.action_corestatus
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "#command" do
|
92
|
+
before(:each) {subject.stub(:connected?).and_return(true)}
|
93
|
+
it "sends command to action method" do
|
94
|
+
http = get_simple_httpok
|
95
|
+
http.stub(:body => cmd_body_sip_show_peers)
|
96
|
+
http.stub(:[]).and_return(%Q|mansession_id="#{manses_id}"; Version=1; Max-Age=60|)
|
97
|
+
http.stub(:set_form_data).with(anything())
|
98
|
+
http.stub(:use_ssl=)
|
99
|
+
http.stub(:request => http)
|
100
|
+
Net::HTTP.stub(:new => http)
|
101
|
+
Net::HTTP::Post.stub(:new).and_return(double('Net::HTTP::Post', :set_form_data => true))
|
102
|
+
res = subject.command 'sip show peers'
|
103
|
+
res.data.should match(/88888\s+\(Unspecified\)/)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
require 'coveralls'
|
3
|
+
Coveralls.wear!
|
4
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
5
|
+
SimpleCov::Formatter::HTMLFormatter,
|
6
|
+
Coveralls::SimpleCov::Formatter
|
7
|
+
]
|
8
|
+
SimpleCov.start do
|
9
|
+
add_filter "spec/"
|
10
|
+
end
|
11
|
+
require 'asterisk/ajam'
|
12
|
+
|
13
|
+
# this fixture create valid HTTPOK response
|
14
|
+
def get_simple_httpok
|
15
|
+
Net::HTTPOK.new 1.1, 200, 'OK'
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_http_unauth
|
19
|
+
Net::HTTPUnauthorized.new 1.1, 401, 'Unauthorized request'
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_body_success_login
|
23
|
+
%q{<ajax-response>
|
24
|
+
<response type='object' id='unknown'><generic response='Success' message='Authentication accepted' /></response>
|
25
|
+
</ajax-response>
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_body_failed_login
|
30
|
+
%q{
|
31
|
+
<ajax-response>
|
32
|
+
<response type='object' id='unknown'><generic response='Error' message='Authentication failed' /></response>
|
33
|
+
</ajax-response>
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_body_sippeers
|
38
|
+
%q{<ajax-response>
|
39
|
+
<response type='object' id='unknown'><generic response='Success' actionid='321321321' eventlist='start' message='Peer status list will follow' /></response>
|
40
|
+
<response type='object' id='unknown'><generic event='PeerEntry' actionid='321321321' channeltype='SIP' objectname='5555' chanobjecttype='peer' ipaddress='-none-' ipport='0' dynamic='yes' forcerport='no' videosupport='no' textsupport='no' acl='yes' status='UNKNOWN' realtimedevice='no' /></response>
|
41
|
+
<response type='object' id='unknown'><generic event='PeerEntry' actionid='321321321' channeltype='SIP' objectname='8734' chanobjecttype='peer' ipaddress='-none-' ipport='0' dynamic='yes' forcerport='yes' videosupport='no' textsupport='no' acl='yes' status='UNKNOWN' realtimedevice='no' /></response>
|
42
|
+
<response type='object' id='unknown'><generic event='PeerEntry' actionid='321321321' channeltype='SIP' objectname='8801' chanobjecttype='peer' ipaddress='-none-' ipport='0' dynamic='yes' forcerport='yes' videosupport='no' textsupport='no' acl='yes' status='UNKNOWN' realtimedevice='no' /></response>
|
43
|
+
<response type='object' id='unknown'><generic event='PeerEntry' actionid='321321321' channeltype='SIP' objectname='8803' chanobjecttype='peer' ipaddress='-none-' ipport='0' dynamic='yes' forcerport='yes' videosupport='no' textsupport='no' acl='yes' status='UNKNOWN' realtimedevice='no' /></response>
|
44
|
+
<response type='object' id='unknown'><generic event='PeerEntry' actionid='321321321' channeltype='SIP' objectname='88888' chanobjecttype='peer' ipaddress='-none-' ipport='0' dynamic='yes' forcerport='yes' videosupport='no' textsupport='no' acl='yes' status='UNKNOWN' realtimedevice='no' /></response>
|
45
|
+
<response type='object' id='unknown'><generic event='PeerEntry' actionid='321321321' channeltype='SIP' objectname='8901' chanobjecttype='peer' ipaddress='-none-' ipport='0' dynamic='yes' forcerport='yes' videosupport='no' textsupport='no' acl='yes' status='UNKNOWN' realtimedevice='no' /></response>
|
46
|
+
<response type='object' id='unknown'><generic event='PeerEntry' actionid='321321321' channeltype='SIP' objectname='8902' chanobjecttype='peer' ipaddress='-none-' ipport='0' dynamic='yes' forcerport='yes' videosupport='no' textsupport='no' acl='yes' status='UNKNOWN' realtimedevice='no' /></response>
|
47
|
+
<response type='object' id='unknown'><generic event='PeerEntry' actionid='321321321' channeltype='SIP' objectname='8903' chanobjecttype='peer' ipaddress='-none-' ipport='0' dynamic='yes' forcerport='yes' videosupport='no' textsupport='no' acl='yes' status='UNKNOWN' realtimedevice='no' /></response>
|
48
|
+
<response type='object' id='unknown'><generic event='PeerlistComplete' eventlist='Complete' listitems='8' actionid='321321321' /></response>
|
49
|
+
</ajax-response>
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_body_corestatus
|
54
|
+
%q{<ajax-response>
|
55
|
+
<response type='object' id='unknown'><generic response='Success' corestartupdate='2013-05-25' corestartuptime='17:13:00' corereloaddate='2013-05-25' corereloadtime='17:13:00' corecurrentcalls='0' /></response>
|
56
|
+
</ajax-response>
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def cmd_body_dialplan_reload
|
61
|
+
%q{<ajax-response>
|
62
|
+
<response type='object' id='unknown'><generic response='Follows' privilege='Command' opaque_data='Dialplan reloaded.
|
63
|
+
--END COMMAND--' /></response>
|
64
|
+
</ajax-response>
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def cmd_body_sip_show_peers
|
69
|
+
%q{<ajax-response>
|
70
|
+
<response type='object' id='unknown'><generic response='Follows' privilege='Command' opaque_data='Name/username Host Dyn Forcerport ACL Port Status
|
71
|
+
5555 (Unspecified) D A 0 UNKNOWN
|
72
|
+
8734 (Unspecified) D N A 0 UNKNOWN
|
73
|
+
8801 (Unspecified) D N A 0 UNKNOWN
|
74
|
+
8803 (Unspecified) D N A 0 UNKNOWN
|
75
|
+
88888 (Unspecified) D N A 0 UNKNOWN
|
76
|
+
8901 (Unspecified) D N A 0 UNKNOWN
|
77
|
+
8902 (Unspecified) D N A 0 UNKNOWN
|
78
|
+
8903 (Unspecified) D N A 0 UNKNOWN
|
79
|
+
8 sip peers [Monitored: 0 online, 8 offline Unmonitored: 0 online, 0 offline]
|
80
|
+
--END COMMAND--' /></response>
|
81
|
+
</ajax-response>
|
82
|
+
}
|
83
|
+
end
|
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: asterisk-ajam
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stas Kobzar
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-06-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rdoc
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: libxml-ruby
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Ruby module for interacting with Asterisk management interface (AMI)
|
84
|
+
via HTTP
|
85
|
+
email:
|
86
|
+
- stas@modulis.ca
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- .gitignore
|
92
|
+
- .travis.yml
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- asterisk-ajam.gemspec
|
98
|
+
- example/ajam.rb
|
99
|
+
- lib/asterisk/ajam.rb
|
100
|
+
- lib/asterisk/ajam/response.rb
|
101
|
+
- lib/asterisk/ajam/session.rb
|
102
|
+
- lib/asterisk/ajam/version.rb
|
103
|
+
- spec/asterisk/ajam/response_spec.rb
|
104
|
+
- spec/asterisk/ajam/session_spec.rb
|
105
|
+
- spec/spec_helper.rb
|
106
|
+
homepage: https://github.com/staskobzar/asterisk-ajam
|
107
|
+
licenses:
|
108
|
+
- MIT
|
109
|
+
metadata: {}
|
110
|
+
post_install_message:
|
111
|
+
rdoc_options: []
|
112
|
+
require_paths:
|
113
|
+
- lib
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ! '>='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ! '>='
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
requirements: []
|
125
|
+
rubyforge_project:
|
126
|
+
rubygems_version: 2.0.3
|
127
|
+
signing_key:
|
128
|
+
specification_version: 4
|
129
|
+
summary: AMI via HTTP
|
130
|
+
test_files:
|
131
|
+
- spec/asterisk/ajam/response_spec.rb
|
132
|
+
- spec/asterisk/ajam/session_spec.rb
|
133
|
+
- spec/spec_helper.rb
|