adhearsion 1.2.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ 1.2.1
2
+ - Removed the restful_rpc component since it is now in a gem.
3
+ - Allow overriding the path to a component in the testing framework so as to support new style components (lib/)
4
+ - Added a GUID to the default recording filename to ensure uniqueness
5
+ - ECONNRESET exceptions are now handled as a call hangup
6
+ - Fixed escaping of TTS strings containing commas when used with Cepstral via #speak
7
+ - Made logging exceptions the responsibility of the framework rather than the app, so that this may not be disabled
8
+
1
9
  1.2.0
2
10
  - New method: #play_or_speak allows playback of an audio file with TTS fallback
3
11
  - #input now takes :speak as a hash for TTS prompt or fallback
@@ -35,12 +35,6 @@ class AhnGenerator < RubiGen::Base
35
35
  m.file *["components/disabled/stomp_gateway/stomp_gateway.yml"]*2
36
36
  m.file *["components/disabled/stomp_gateway/README.markdown"]*2
37
37
 
38
- m.file *["components/disabled/restful_rpc/restful_rpc.rb"]*2
39
- m.file *["components/disabled/restful_rpc/restful_rpc.yml"]*2
40
- m.file *["components/disabled/restful_rpc/README.markdown"]*2
41
- m.file *["components/disabled/restful_rpc/example-client.rb"]*2
42
- m.file *["components/disabled/restful_rpc/spec/restful_rpc_spec.rb"]*2
43
-
44
38
  m.file *["config/environment.rb"]*2
45
39
  m.file *["config/startup.rb"]*2
46
40
  m.file *["dialplan.rb"]*2
@@ -89,7 +83,6 @@ EOS
89
83
  components/disabled/stomp_gateway
90
84
  components/disabled/xmpp_gateway
91
85
  components/ami_remote
92
- components/disabled/restful_rpc/spec
93
86
  config
94
87
  script
95
88
  )
@@ -1,3 +1,10 @@
1
1
  source "http://rubygems.org"
2
2
 
3
- gem "adhearsion", ">= 1.0.1"
3
+ gem "adhearsion", ">= 1.2.1"
4
+
5
+ #
6
+ # Here are some example components you might like to use. Simply
7
+ # uncomment them, run `bundle install` and enable in startup.rb.
8
+ #
9
+
10
+ # gem 'ahn-restful-rpc'
@@ -30,6 +30,7 @@ Adhearsion::Configuration.configure do |config|
30
30
  # For Asterisk >= 1.6, use ","
31
31
  # The delimiter can also be specified in Asterisk's asterisk.conf.
32
32
  # This setting applies only to AGI. The AMI delimiter is auto-detected.
33
+ # NB: The AMI user should have write access in order to execute actions, and AMI connections will fail otherwise.
33
34
  config.enable_asterisk :argument_delimiter => '|'
34
35
  # config.asterisk.enable_ami :host => "127.0.0.1", :username => "admin", :password => "password", :events => true
35
36
 
@@ -31,9 +31,3 @@
31
31
  #
32
32
  # Note: events are mostly for components to register and expose to you.
33
33
  ##
34
-
35
- events.exception.each do |e|
36
- ahn_log.error "#{e.class}: #{e.message}"
37
- ahn_log.error e.backtrace.join("\n\t")
38
- end
39
-
@@ -7,9 +7,10 @@ module ComponentTester
7
7
  #
8
8
  # @return [Module] an anonymous module which includes the ComponentTester module.
9
9
  #
10
- def new(component_name, component_directory)
10
+ def new(component_name, component_directory, main_file = nil)
11
11
  component_directory = File.expand_path component_directory
12
- main_file = component_directory + "/#{component_name}/#{component_name}.rb"
12
+ main_file ||= "/#{component_name}/#{component_name}.rb"
13
+ main_file.insert 0, component_directory
13
14
 
14
15
  component_manager = Adhearsion::Components::ComponentManager.new(component_directory)
15
16
  component_module = Adhearsion::Components::ComponentManager::ComponentDefinitionContainer.load_file main_file
@@ -1,6 +1,6 @@
1
1
  # Monkey patch Object to support the #tap method.
2
2
  # This method is present in Ruby 1.8.7 and later.
3
- if !Object.respond_to?(:tap)
3
+ unless Object.respond_to?(:tap)
4
4
  class Object
5
5
  def tap
6
6
  yield self
@@ -143,6 +143,7 @@ module Adhearsion
143
143
  create_pid_file if pid_file
144
144
  bootstrap_rc
145
145
  initialize_log_file
146
+ initialize_exception_logger
146
147
  load_all_init_files
147
148
  init_datasources
148
149
  init_components_subsystem
@@ -355,6 +356,13 @@ Adhearsion will abort until you fix this. Sorry for the incovenience.
355
356
  Logging::DefaultAdhearsionLogger.redefine_outputters
356
357
  end
357
358
 
359
+ def initialize_exception_logger
360
+ Events.register_callback :exception do |e|
361
+ ahn_log.error "#{e.class}: #{e.message}"
362
+ ahn_log.debug e.backtrace.join("\n\t")
363
+ end
364
+ end
365
+
358
366
  def create_pid_file
359
367
  if pid_file
360
368
  File.open pid_file, 'w' do |file|
@@ -2,7 +2,7 @@ module Adhearsion #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 1 unless defined? MAJOR
4
4
  MINOR = 2 unless defined? MINOR
5
- TINY = 0 unless defined? TINY
5
+ TINY = 1 unless defined? TINY
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.') unless defined? STRING
8
8
  end
@@ -55,36 +55,40 @@ module Adhearsion
55
55
 
56
56
  # Utility method to read from pbx. Hangup if nil.
57
57
  def read
58
- from_pbx.gets.tap do |message|
59
- # AGI has many conditions that might indicate a hangup
60
- raise Hangup if message.nil?
61
-
62
- ahn_log.agi.debug "<<< #{message}"
63
-
64
- code, rest = *message.split(' ', 2)
65
-
66
- case code.to_i
67
- when 510
68
- # This error is non-fatal for the call
69
- ahn_log.agi.warn "510: Invalid or unknown AGI command"
70
- when 511
71
- # 511 Command Not Permitted on a dead channel
72
- ahn_log.agi.debug "511: Dead channel. Raising Hangup"
73
- raise Hangup
74
- when 520
75
- # This error is non-fatal for the call
76
- ahn_log.agi.warn "520: Invalid command syntax"
77
- when (500..599)
78
- # Assume this error is non-fatal for the call and try to keep running
79
- ahn_log.agi.warn "#{code}: Unknown AGI protocol error."
80
- end
58
+ begin
59
+ from_pbx.gets.tap do |message|
60
+ # AGI has many conditions that might indicate a hangup
61
+ raise Hangup if message.nil?
62
+
63
+ ahn_log.agi.debug "<<< #{message}"
64
+
65
+ code, rest = *message.split(' ', 2)
66
+
67
+ case code.to_i
68
+ when 510
69
+ # This error is non-fatal for the call
70
+ ahn_log.agi.warn "510: Invalid or unknown AGI command"
71
+ when 511
72
+ # 511 Command Not Permitted on a dead channel
73
+ ahn_log.agi.debug "511: Dead channel. Raising Hangup"
74
+ raise Hangup
75
+ when 520
76
+ # This error is non-fatal for the call
77
+ ahn_log.agi.warn "520: Invalid command syntax"
78
+ when (500..599)
79
+ # Assume this error is non-fatal for the call and try to keep running
80
+ ahn_log.agi.warn "#{code}: Unknown AGI protocol error."
81
+ end
81
82
 
82
- # If the message starts with HANGUP it's a silly 1.6 OOB message
83
- case message
84
- when /^HANGUP/, /^HANGUP\n?$/i, /^HANGUP\s?\d{3}/i
85
- ahn_log.agi.debug "AGI HANGUP. Raising hangup"
86
- raise Hangup
83
+ # If the message starts with HANGUP it's a silly 1.6 OOB message
84
+ case message
85
+ when /^HANGUP/, /^HANGUP\n?$/i, /^HANGUP\s?\d{3}/i
86
+ ahn_log.agi.debug "AGI HANGUP. Raising hangup"
87
+ raise Hangup
88
+ end
87
89
  end
90
+ rescue Errno::ECONNRESET
91
+ raise Hangup
88
92
  end
89
93
  end
90
94
 
@@ -408,7 +412,7 @@ module Adhearsion
408
412
  #
409
413
  def base_record_to_file(*args)
410
414
  options = args.last.kind_of?(Hash) ? args.pop : {}
411
- filename = args.shift || "/tmp/recording_%d"
415
+ filename = args.shift || "/tmp/recording_#{new_guid}_%d"
412
416
 
413
417
  if filename.index("%d")
414
418
  if @call.variables.has_key?(:recording_counter)
@@ -872,7 +876,7 @@ module Adhearsion
872
876
  def cepstral(call, text, options = {})
873
877
  # We need to aggressively escape commas so app_swift does not
874
878
  # think they are arguments.
875
- text.gsub! /,/, '\\\\\\,'
879
+ text.gsub! /,/, '\\\\,'
876
880
  command = ['Swift', text]
877
881
 
878
882
  if options[:interrupt_digits]
@@ -274,6 +274,20 @@ describe 'hangup command' do
274
274
  end
275
275
  end
276
276
 
277
+ describe 'receiving a hangup' do
278
+ include DialplanCommandTestHelpers
279
+
280
+ it "should treat a ECONNRESET as a hangup" do
281
+ pbx_should_respond_with_success
282
+ def input.gets()
283
+ raise Errno::ECONNRESET
284
+ end
285
+ the_following_code {
286
+ mock_call.read()
287
+ }.should raise_error(Adhearsion::Hangup)
288
+ end
289
+ end
290
+
277
291
  describe "writing a command" do
278
292
  include DialplanCommandTestHelpers
279
293
 
@@ -729,8 +743,10 @@ describe 'the #record method' do
729
743
 
730
744
  it 'create a default filename if no file is specifed and icrement it on subsequent calls' do
731
745
  mock_call.call.variables.delete :recording_counter
732
- mock_call.should_receive(:response).once.with('RECORD FILE', '/tmp/recording_0', 'gsm', '26', -1, 0).and_return("200 result=0 (timeout) endpos=21600\n")
733
- mock_call.should_receive(:response).once.with('RECORD FILE', '/tmp/recording_1', 'gsm', '26', -1, 0).and_return("200 result=0 (timeout) endpos=21600\n")
746
+ mock_call.should_receive(:new_guid).once.and_return('2345')
747
+ mock_call.should_receive(:new_guid).once.and_return('4322')
748
+ mock_call.should_receive(:response).once.with('RECORD FILE', '/tmp/recording_2345_0', 'gsm', '26', -1, 0).and_return("200 result=0 (timeout) endpos=21600\n")
749
+ mock_call.should_receive(:response).once.with('RECORD FILE', '/tmp/recording_4322_1', 'gsm', '26', -1, 0).and_return("200 result=0 (timeout) endpos=21600\n")
734
750
  mock_call.record(:beep => nil, :escapedigits => '26').should == '/tmp/recording_0.gsm'
735
751
  mock_call.record(:beep => nil, :escapedigits => '26').should == '/tmp/recording_1.gsm'
736
752
  end
@@ -826,8 +842,10 @@ describe 'the #record_to_file method' do
826
842
  end
827
843
 
828
844
  it 'create a default filename if no file is specifed and icrement it on subsequent calls' do
829
- mock_call.should_receive(:response).once.with("RECORD FILE", "/tmp/recording_0", "gsm", "26", -1, 0).and_return("200 result=0 (timeout) endpos=21600\n")
830
- mock_call.should_receive(:response).once.with("RECORD FILE", "/tmp/recording_1", "gsm", "26", -1, 0).and_return("200 result=0 (timeout) endpos=21600\n")
845
+ mock_call.should_receive(:new_guid).once.and_return('2345')
846
+ mock_call.should_receive(:new_guid).once.and_return('4322')
847
+ mock_call.should_receive(:response).once.with("RECORD FILE", "/tmp/recording_2345_0", "gsm", "26", -1, 0).and_return("200 result=0 (timeout) endpos=21600\n")
848
+ mock_call.should_receive(:response).once.with("RECORD FILE", "/tmp/recording_4322_1", "gsm", "26", -1, 0).and_return("200 result=0 (timeout) endpos=21600\n")
831
849
  mock_call.record_to_file(:beep => nil, :escapedigits => '26').should == :success_timeout
832
850
  mock_call.record_to_file(:beep => nil, :escapedigits => '26').should == :success_timeout
833
851
  end
@@ -2723,7 +2741,7 @@ describe "speak command" do
2723
2741
 
2724
2742
  it "should properly escape commas in the TTS string" do
2725
2743
  pbx_should_respond_with_value 0
2726
- mock_call.should_receive(:execute).with('Swift', 'Once\\\\, a long\\\\, long time ago\\\\, ...')
2744
+ mock_call.should_receive(:execute).with('Swift', 'Once\, a long\, long time ago\, ...')
2727
2745
  @speech_engines.cepstral(mock_call, 'Once, a long, long time ago, ...')
2728
2746
  @output.read.should == "GET VARIABLE \"SWIFT_DTMF\"\n"
2729
2747
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: adhearsion
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.2.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -12,11 +12,12 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2011-08-14 00:00:00.000000000Z
15
+ date: 2011-09-22 00:00:00.000000000 +01:00
16
+ default_executable:
16
17
  dependencies:
17
18
  - !ruby/object:Gem::Dependency
18
19
  name: bundler
19
- requirement: &2156063620 !ruby/object:Gem::Requirement
20
+ requirement: &2153625640 !ruby/object:Gem::Requirement
20
21
  none: false
21
22
  requirements:
22
23
  - - ! '>='
@@ -24,10 +25,10 @@ dependencies:
24
25
  version: 1.0.10
25
26
  type: :runtime
26
27
  prerelease: false
27
- version_requirements: *2156063620
28
+ version_requirements: *2153625640
28
29
  - !ruby/object:Gem::Dependency
29
30
  name: log4r
30
- requirement: &2156061900 !ruby/object:Gem::Requirement
31
+ requirement: &2153625040 !ruby/object:Gem::Requirement
31
32
  none: false
32
33
  requirements:
33
34
  - - ! '>='
@@ -35,10 +36,10 @@ dependencies:
35
36
  version: 1.0.5
36
37
  type: :runtime
37
38
  prerelease: false
38
- version_requirements: *2156061900
39
+ version_requirements: *2153625040
39
40
  - !ruby/object:Gem::Dependency
40
41
  name: activesupport
41
- requirement: &2156060060 !ruby/object:Gem::Requirement
42
+ requirement: &2153624560 !ruby/object:Gem::Requirement
42
43
  none: false
43
44
  requirements:
44
45
  - - ! '>='
@@ -46,10 +47,10 @@ dependencies:
46
47
  version: 2.1.0
47
48
  type: :runtime
48
49
  prerelease: false
49
- version_requirements: *2156060060
50
+ version_requirements: *2153624560
50
51
  - !ruby/object:Gem::Dependency
51
52
  name: i18n
52
- requirement: &2156059440 !ruby/object:Gem::Requirement
53
+ requirement: &2153624180 !ruby/object:Gem::Requirement
53
54
  none: false
54
55
  requirements:
55
56
  - - ! '>='
@@ -57,10 +58,10 @@ dependencies:
57
58
  version: '0'
58
59
  type: :runtime
59
60
  prerelease: false
60
- version_requirements: *2156059440
61
+ version_requirements: *2153624180
61
62
  - !ruby/object:Gem::Dependency
62
63
  name: json
63
- requirement: &2156058260 !ruby/object:Gem::Requirement
64
+ requirement: &2153623720 !ruby/object:Gem::Requirement
64
65
  none: false
65
66
  requirements:
66
67
  - - ! '>='
@@ -68,10 +69,10 @@ dependencies:
68
69
  version: '0'
69
70
  type: :runtime
70
71
  prerelease: false
71
- version_requirements: *2156058260
72
+ version_requirements: *2153623720
72
73
  - !ruby/object:Gem::Dependency
73
74
  name: rubigen
74
- requirement: &2156057120 !ruby/object:Gem::Requirement
75
+ requirement: &2153621640 !ruby/object:Gem::Requirement
75
76
  none: false
76
77
  requirements:
77
78
  - - ! '>='
@@ -79,10 +80,10 @@ dependencies:
79
80
  version: 1.5.6
80
81
  type: :runtime
81
82
  prerelease: false
82
- version_requirements: *2156057120
83
+ version_requirements: *2153621640
83
84
  - !ruby/object:Gem::Dependency
84
85
  name: rake
85
- requirement: &2156056020 !ruby/object:Gem::Requirement
86
+ requirement: &2153621020 !ruby/object:Gem::Requirement
86
87
  none: false
87
88
  requirements:
88
89
  - - ! '>='
@@ -90,10 +91,10 @@ dependencies:
90
91
  version: '0'
91
92
  type: :runtime
92
93
  prerelease: false
93
- version_requirements: *2156056020
94
+ version_requirements: *2153621020
94
95
  - !ruby/object:Gem::Dependency
95
96
  name: pry
96
- requirement: &2156054660 !ruby/object:Gem::Requirement
97
+ requirement: &2153620340 !ruby/object:Gem::Requirement
97
98
  none: false
98
99
  requirements:
99
100
  - - ! '>='
@@ -101,10 +102,10 @@ dependencies:
101
102
  version: '0'
102
103
  type: :runtime
103
104
  prerelease: false
104
- version_requirements: *2156054660
105
+ version_requirements: *2153620340
105
106
  - !ruby/object:Gem::Dependency
106
107
  name: rubigen
107
- requirement: &2156053720 !ruby/object:Gem::Requirement
108
+ requirement: &2153619580 !ruby/object:Gem::Requirement
108
109
  none: false
109
110
  requirements:
110
111
  - - ! '>='
@@ -112,10 +113,10 @@ dependencies:
112
113
  version: 1.5.6
113
114
  type: :development
114
115
  prerelease: false
115
- version_requirements: *2156053720
116
+ version_requirements: *2153619580
116
117
  - !ruby/object:Gem::Dependency
117
118
  name: rspec
118
- requirement: &2156052900 !ruby/object:Gem::Requirement
119
+ requirement: &2153618960 !ruby/object:Gem::Requirement
119
120
  none: false
120
121
  requirements:
121
122
  - - ! '>='
@@ -123,10 +124,10 @@ dependencies:
123
124
  version: 2.4.0
124
125
  type: :development
125
126
  prerelease: false
126
- version_requirements: *2156052900
127
+ version_requirements: *2153618960
127
128
  - !ruby/object:Gem::Dependency
128
129
  name: flexmock
129
- requirement: &2156052360 !ruby/object:Gem::Requirement
130
+ requirement: &2153618560 !ruby/object:Gem::Requirement
130
131
  none: false
131
132
  requirements:
132
133
  - - ! '>='
@@ -134,10 +135,10 @@ dependencies:
134
135
  version: '0'
135
136
  type: :development
136
137
  prerelease: false
137
- version_requirements: *2156052360
138
+ version_requirements: *2153618560
138
139
  - !ruby/object:Gem::Dependency
139
140
  name: activerecord
140
- requirement: &2156051460 !ruby/object:Gem::Requirement
141
+ requirement: &2153617980 !ruby/object:Gem::Requirement
141
142
  none: false
142
143
  requirements:
143
144
  - - ! '>='
@@ -145,10 +146,10 @@ dependencies:
145
146
  version: 2.1.0
146
147
  type: :development
147
148
  prerelease: false
148
- version_requirements: *2156051460
149
+ version_requirements: *2153617980
149
150
  - !ruby/object:Gem::Dependency
150
151
  name: rake
151
- requirement: &2156050780 !ruby/object:Gem::Requirement
152
+ requirement: &2153617540 !ruby/object:Gem::Requirement
152
153
  none: false
153
154
  requirements:
154
155
  - - ! '>='
@@ -156,10 +157,10 @@ dependencies:
156
157
  version: '0'
157
158
  type: :development
158
159
  prerelease: false
159
- version_requirements: *2156050780
160
+ version_requirements: *2153617540
160
161
  - !ruby/object:Gem::Dependency
161
162
  name: simplecov
162
- requirement: &2156050020 !ruby/object:Gem::Requirement
163
+ requirement: &2153617080 !ruby/object:Gem::Requirement
163
164
  none: false
164
165
  requirements:
165
166
  - - ! '>='
@@ -167,10 +168,10 @@ dependencies:
167
168
  version: '0'
168
169
  type: :development
169
170
  prerelease: false
170
- version_requirements: *2156050020
171
+ version_requirements: *2153617080
171
172
  - !ruby/object:Gem::Dependency
172
173
  name: simplecov-rcov
173
- requirement: &2156049100 !ruby/object:Gem::Requirement
174
+ requirement: &2153616640 !ruby/object:Gem::Requirement
174
175
  none: false
175
176
  requirements:
176
177
  - - ! '>='
@@ -178,10 +179,10 @@ dependencies:
178
179
  version: '0'
179
180
  type: :development
180
181
  prerelease: false
181
- version_requirements: *2156049100
182
+ version_requirements: *2153616640
182
183
  - !ruby/object:Gem::Dependency
183
184
  name: ci_reporter
184
- requirement: &2156048440 !ruby/object:Gem::Requirement
185
+ requirement: &2153616220 !ruby/object:Gem::Requirement
185
186
  none: false
186
187
  requirements:
187
188
  - - ! '>='
@@ -189,7 +190,7 @@ dependencies:
189
190
  version: '0'
190
191
  type: :development
191
192
  prerelease: false
192
- version_requirements: *2156048440
193
+ version_requirements: *2153616220
193
194
  description: Adhearsion is an open-source telephony development framework
194
195
  email: dev&Adhearsion.com
195
196
  executables:
@@ -216,11 +217,6 @@ files:
216
217
  - app_generators/ahn/templates/Rakefile
217
218
  - app_generators/ahn/templates/components/ami_remote/ami_remote.rb
218
219
  - app_generators/ahn/templates/components/disabled/HOW_TO_ENABLE
219
- - app_generators/ahn/templates/components/disabled/restful_rpc/README.markdown
220
- - app_generators/ahn/templates/components/disabled/restful_rpc/example-client.rb
221
- - app_generators/ahn/templates/components/disabled/restful_rpc/restful_rpc.rb
222
- - app_generators/ahn/templates/components/disabled/restful_rpc/restful_rpc.yml
223
- - app_generators/ahn/templates/components/disabled/restful_rpc/spec/restful_rpc_spec.rb
224
220
  - app_generators/ahn/templates/components/disabled/stomp_gateway/README.markdown
225
221
  - app_generators/ahn/templates/components/disabled/stomp_gateway/stomp_gateway.rb
226
222
  - app_generators/ahn/templates/components/disabled/stomp_gateway/stomp_gateway.yml
@@ -368,6 +364,7 @@ files:
368
364
  - spec/theatre/namespace_spec.rb
369
365
  - spec/theatre/spec_helper_spec.rb
370
366
  - spec/theatre/theatre_class_spec.rb
367
+ has_rdoc: true
371
368
  homepage: http://adhearsion.com
372
369
  licenses: []
373
370
  post_install_message:
@@ -382,7 +379,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
382
379
  version: '0'
383
380
  segments:
384
381
  - 0
385
- hash: 4384860604276313482
382
+ hash: 3254044399957334280
386
383
  required_rubygems_version: !ruby/object:Gem::Requirement
387
384
  none: false
388
385
  requirements:
@@ -391,10 +388,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
391
388
  version: '0'
392
389
  segments:
393
390
  - 0
394
- hash: 4384860604276313482
391
+ hash: 3254044399957334280
395
392
  requirements: []
396
393
  rubyforge_project:
397
- rubygems_version: 1.8.6
394
+ rubygems_version: 1.6.2
398
395
  signing_key:
399
396
  specification_version: 3
400
397
  summary: Adhearsion, open-source telephony development framework
@@ -1,11 +0,0 @@
1
- Adhearsion RESTful RPC Component
2
- ================================
3
-
4
- This is a component for people want to integrate their telephony systems with non-Ruby systems. When enabled, this component
5
- will start up a HTTP server within the Adhearsion process and accept POST requests to invoke Ruby methods shared in the
6
- `methods_for(:rpc)` context.
7
-
8
- Protocol Notes
9
- --------------
10
-
11
- When POSTing your data to.
@@ -1,48 +0,0 @@
1
- require 'rubygems'
2
- require 'rest_client'
3
- require 'json'
4
-
5
- # You must have the "rest-client" and "json" gems installed for this file to work.
6
-
7
- class RESTfulAdhearsion
8
-
9
- DEFAULT_OPTIONS = {
10
- # Note: :user and :password are non-existent by default
11
- :host => "localhost",
12
- :port => "5000",
13
- :path_nesting => "/"
14
- }
15
-
16
- def initialize(options={})
17
- @options = DEFAULT_OPTIONS.merge options
18
-
19
- @path_nesting = @options.delete :path_nesting
20
- @host = @options.delete :host
21
- @port = @options.delete :port
22
-
23
- @url_beginning = "http://#{@host}:#{@port}#{@path_nesting}"
24
- end
25
-
26
- def method_missing(method_name, *args)
27
- JSON.parse RestClient::Resource.new(@url_beginning + method_name.to_s, @options).post(args.to_json)
28
- end
29
-
30
- end
31
-
32
- Adhearsion = RESTfulAdhearsion.new :host => "localhost", :port => 5000, :user => "jicksta", :password => "roflcopterz"
33
-
34
- # ### Sample component code. Try doing "ahn create component testing123" and pasting this code in.
35
- #
36
- # methods_for :rpc do
37
- # def i_like_hashes(options={})
38
- # options.has_key?(:foo)
39
- # end
40
- # def i_like_arrays(*args)
41
- # args.reverse
42
- # end
43
- # end
44
-
45
- # Note: everything returned will be wrapped in an Array
46
-
47
- p Adhearsion.i_like_hashes(:foo => "bar")
48
- p Adhearsion.i_like_arrays(1,2,3,4,5)
@@ -1,91 +0,0 @@
1
- begin
2
- require 'rack'
3
- require 'json'
4
- rescue LoadError
5
- abort "ERROR: restful_rpc requires the 'rack' and 'json' gems"
6
- end
7
-
8
- # Don't you love regular expressions? Matches only 0-255 octets. Recognizes "*" as an octet wildcard.
9
- VALID_IP_ADDRESS = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|\*)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|\*)$/
10
-
11
- def ip_allowed?(ip)
12
- raise ArgumentError, "#{ip.inspect} is not a valid IP address!" unless ip.kind_of?(String) && ip =~ VALID_IP_ADDRESS
13
-
14
- octets = ip.split "."
15
-
16
- case COMPONENTS.restful_rpc["access"]
17
- when "everyone"
18
- true
19
- when "whitelist"
20
- whitelist = COMPONENTS.restful_rpc["whitelist"]
21
- !! whitelist.find do |pattern|
22
- pattern_octets = pattern.split "."
23
- # Traverse both arrays in parallel
24
- octets.zip(pattern_octets).map do |octet, octet_pattern|
25
- octet_pattern == "*" ? true : (octet == octet_pattern)
26
- end == [true, true, true, true]
27
- end
28
- when "blacklist"
29
- blacklist = COMPONENTS.restful_rpc["blacklist"]
30
- ! blacklist.find do |pattern|
31
- pattern_octets = pattern.split "."
32
- # Traverse both arrays in parallel
33
- octets.zip(pattern_octets).map do |octet, octet_pattern|
34
- octet_pattern == "*" ? true : (octet == octet_pattern)
35
- end == [true, true, true, true]
36
- end
37
- else
38
- raise Adhearsion::Components::ConfigurationError, 'Unrecognized "access" configuration value!'
39
- end
40
- end
41
-
42
- RESTFUL_API_HANDLER = lambda do |env|
43
- json = env["rack.input"].read
44
-
45
- # Return "Bad Request" HTTP error if the client forgot
46
- return [400, {}, "You must POST a valid JSON object!"] if json.blank?
47
-
48
- json = JSON.parse json
49
-
50
- nesting = COMPONENTS.restful_rpc["path_nesting"]
51
- path = env["PATH_INFO"]
52
-
53
- return [404, {}, "This resource does not respond to #{path.inspect}"] unless path[0...nesting.size] == nesting
54
-
55
- path = path[nesting.size..-1]
56
-
57
- return [404, {"Content-Type" => "application/json"}, "You cannot nest method names!"] if path.include?("/")
58
-
59
- rpc_object = Adhearsion::Components.component_manager.extend_object_with(Object.new, :rpc)
60
-
61
- # TODO: set the content-type and other HTTP headers
62
- response_object = rpc_object.send(path, *json)
63
- [200, {"Content-Type" => "application/json"}, response_object.to_json]
64
-
65
- end
66
-
67
- initialization do
68
- config = COMPONENTS.restful_rpc
69
-
70
- api = RESTFUL_API_HANDLER
71
-
72
- port = config["port"] || 5000
73
- authentication = config["authentication"]
74
- show_exceptions = config["show_exceptions"]
75
- handler = Rack::Handler.const_get(config["handler"] || "Mongrel")
76
-
77
- if authentication
78
- api = Rack::Auth::Basic.new(api) do |username, password|
79
- authentication[username] == password
80
- end
81
- api.realm = "Adhearsion API"
82
- end
83
-
84
- if show_exceptions
85
- api = Rack::ShowStatus.new(Rack::ShowExceptions.new(api))
86
- end
87
-
88
- Thread.new do
89
- handler.run api, :Port => port
90
- end
91
- end
@@ -1,34 +0,0 @@
1
- # Use path_nesting to specify an arbitrarily nested. Could be used for additional security or in HTTP reverse proxy server.
2
- path_nesting: /
3
-
4
- port: 5000
5
-
6
- # The "handler" option here can be any valid Rack::Handler constant name.
7
- # Other options: WEBrick, EventedMongrel.
8
- # If you don't know the differences between these, "Mongrel" is definitely a good choice.
9
- handler: Mongrel
10
-
11
- # In a production system, you should make this "false" since
12
- show_exceptions: true
13
-
14
- # The "authentication" config option can either be "false" or key/value pairs representing allowed usernames and passwords.
15
-
16
- #authentication: false
17
- authentication:
18
- jicksta: roflcopterz
19
- foo: bar6213671682
20
-
21
- access: everyone # When allowing "everyone" access, no IPs are blocked.
22
- #access: whitelist # When using a whitelist, the "whitelist" data below will be used.
23
- #access: blacklist # When using a blacklist, the "blacklist" data below will be used.
24
-
25
- # This is a list of IPs which are exclusively allowed to call this web service.
26
- # Note: whitelists are far more secure than blacklists.
27
- whitelist:
28
- - 127.0.0.1
29
- - 192.168.*.*
30
-
31
- # This is a list of the IPs which are explicitly NOT allowed to call this web service. This will only be used if "access" is
32
- # set to "blacklist" above.
33
- blacklist:
34
- - 100.200.100.200
@@ -1,251 +0,0 @@
1
- require 'rubygems'
2
- require 'bundler'
3
- Bundler.setup
4
- Bundler.require
5
-
6
- require 'adhearsion/component_manager/spec_framework'
7
-
8
- RESTFUL_RPC = ComponentTester.new("restful_rpc", File.dirname(__FILE__) + "/../..")
9
-
10
- ##### This is here for a reference
11
- #{"CONTENT_LENGTH" => "12",
12
- # "CONTENT_TYPE" => "application/x-www-form-urlencoded",
13
- # "GATEWAY_INTERFACE" => "CGI/1.1",
14
- # "HTTP_ACCEPT" => "application/xml",
15
- # "HTTP_ACCEPT_ENCODING" => "gzip, deflate",
16
- # "HTTP_AUTHORIZATION" => "Basic amlja3N0YTpyb2ZsY29wdGVyeg==",
17
- # "HTTP_HOST" => "localhost:5000",
18
- # "HTTP_VERSION" => "HTTP/1.1",
19
- # "PATH_INFO" => "/rofl",
20
- # "QUERY_STRING" => "",
21
- # "rack.errors" => StringIO.new(""),
22
- # "rack.input" => StringIO.new('["o","hai!"]'),
23
- # "rack.multiprocess" => false,
24
- # "rack.multithread" => true,
25
- # "rack.run_once" => false,
26
- # "rack.url_scheme" => "http",
27
- # "rack.version" => [0, 1],
28
- # "REMOTE_ADDR" => "::1",
29
- # "REMOTE_HOST" => "localhost",
30
- # "REMOTE_USER" => "jicksta",
31
- # "REQUEST_METHOD" => "POST"
32
- # "REQUEST_PATH" => "/",
33
- # "REQUEST_URI" => "http://localhost:5000/rofl",
34
- # "SCRIPT_NAME" => "",
35
- # "SERVER_NAME" => "localhost",
36
- # "SERVER_PORT" => "5000",
37
- # "SERVER_PROTOCOL" => "HTTP/1.1",
38
- # "SERVER_SOFTWARE" => "WEBrick/1.3.1 (Ruby/1.8.6/2008-03-03)"}
39
-
40
- describe "The VALID_IP_ADDRESS regular expression" do
41
-
42
- it "should match only valid IP addresses" do
43
- valid_ip_addresses = ["192.168.1.98", "10.0.1.200", "255.255.255.0", "123.*.4.*"]
44
- invalid_ip_addresses = ["10.0.1.1 foo", "bar 255.255.255.0", "0*0*0*0", "1234"]
45
-
46
- valid_ip_addresses. each { |ip| RESTFUL_RPC::VALID_IP_ADDRESS.should =~ ip }
47
- invalid_ip_addresses.each { |ip| RESTFUL_RPC::VALID_IP_ADDRESS.should_not =~ ip }
48
- end
49
- end
50
-
51
- describe "The initialization block" do
52
-
53
- it "should create a new Thread" do
54
- mock_component_config_with :restful_rpc => {}
55
- mock(Thread).new { nil }
56
- RESTFUL_RPC.initialize!
57
- end
58
-
59
- it "should run the Rack adapter specified in the configuration" do
60
- mock(Thread).new.yields
61
- mock_component_config_with :restful_rpc => {"adapter" => "Mongrel"}
62
- mock(Rack::Handler::Mongrel).run is_a(Proc), :Port => 5000
63
- RESTFUL_RPC.initialize!
64
- end
65
-
66
- it "should wrap the RESTFUL_API_HANDLER in an Rack::Auth::Basic object if authentication is enabled" do
67
- mock(Thread).new.yields
68
- mock_component_config_with :restful_rpc => {"authentication" => {"foo" => "bar"}}
69
-
70
- proper_authenticator = lambda do |obj|
71
- request = OpenStruct.new :credentials => ["foo", "bar"]
72
- obj.is_a?(Rack::Auth::Basic) && obj.send(:valid?, request)
73
- end
74
-
75
- mock(Rack::Handler::Mongrel).run(satisfy(&proper_authenticator), :Port => 5000)
76
- RESTFUL_RPC.initialize!
77
- end
78
-
79
- it 'should wrap the RESTFUL_API_HANDLER in ShowStatus and ShowExceptions objects when show_exceptions is enabled' do
80
- mock(Thread).new.yields
81
- mock_component_config_with :restful_rpc => {"show_exceptions" => true}
82
-
83
- mock.proxy(Rack::ShowExceptions).new(is_a(Proc))
84
- mock.proxy(Rack::ShowStatus).new is_a(Rack::ShowExceptions)
85
-
86
- mock(Rack::Handler::Mongrel).run is_a(Rack::ShowStatus), :Port => 5000
87
- RESTFUL_RPC.initialize!
88
- end
89
-
90
- end
91
-
92
- describe 'Private helper methods' do
93
-
94
- describe "the RESTFUL_API_HANDLER lambda" do
95
-
96
- it "should return a 200 for requests which execute a method that has been defined in the methods_for(:rpc) context" do
97
- component_manager = Adhearsion::Components::ComponentManager.new('/path/shouldnt/matter')
98
-
99
- mock(Adhearsion::Components).component_manager { component_manager }
100
- component_manager.load_code <<-RUBY
101
- methods_for(:rpc) do
102
- def testing_123456(one,two)
103
- [two.reverse, one.reverse]
104
- end
105
- end
106
- RUBY
107
-
108
- input = StringIO.new %w[jay phillips].to_json
109
-
110
- mock_component_config_with :restful_rpc => {"path_nesting" => "/"}
111
-
112
- env = {"PATH_INFO" => "/testing_123456", "rack.input" => input}
113
-
114
- response = RESTFUL_RPC::RESTFUL_API_HANDLER.call(env)
115
- response.should be_kind_of(Array)
116
- response.should have(3).items
117
- response.first.should equal(200)
118
- JSON.parse(response.last).should eql(%w[jay phillips].map(&:reverse).reverse)
119
- end
120
-
121
- it "should return a 400 when no data is POSTed" do
122
- env = {"rack.input" => StringIO.new(""), "REQUEST_URI" => "/foobar"}
123
- RESTFUL_RPC::RESTFUL_API_HANDLER.call(env).first.should equal(400)
124
- end
125
-
126
- it "should work with a high level test of a successful method invocation" do
127
-
128
- component_manager = Adhearsion::Components::ComponentManager.new('/path/shouldnt/matter')
129
-
130
- mock(Adhearsion::Components).component_manager { component_manager }
131
-
132
- component_manager.load_code '
133
- methods_for(:rpc) do
134
- def rofl(one,two)
135
- "Hai! #{one} #{two}"
136
- end
137
- end'
138
-
139
- env = {
140
- "CONTENT_LENGTH" => "12",
141
- "CONTENT_TYPE" => "application/x-www-form-urlencoded",
142
- "GATEWAY_INTERFACE" => "CGI/1.1",
143
- "HTTP_ACCEPT" => "application/xml",
144
- "HTTP_ACCEPT_ENCODING" => "gzip, deflate",
145
- "HTTP_AUTHORIZATION" => "Basic amlja3N0YTpyb2ZsY29wdGVyeg==",
146
- "HTTP_HOST" => "localhost:5000",
147
- "HTTP_VERSION" => "HTTP/1.1",
148
- "PATH_INFO" => "/rofl",
149
- "QUERY_STRING" => "",
150
- "rack.errors" => StringIO.new(""),
151
- "rack.input" => StringIO.new('["o","hai!"]'),
152
- "rack.multiprocess" => false,
153
- "rack.multithread" => true,
154
- "rack.run_once" => false,
155
- "rack.url_scheme" => "http",
156
- "rack.version" => [0, 1],
157
- "REMOTE_ADDR" => "::1",
158
- "REMOTE_HOST" => "localhost",
159
- "REMOTE_USER" => "jicksta",
160
- "REQUEST_METHOD" => "POST",
161
- "REQUEST_PATH" => "/",
162
- "REQUEST_URI" => "http://localhost:5000/rofl",
163
- "SCRIPT_NAME" => "",
164
- "SERVER_NAME" => "localhost",
165
- "SERVER_PORT" => "5000",
166
- "SERVER_PROTOCOL" => "HTTP/1.1",
167
- "SERVER_SOFTWARE" => "WEBrick/1.3.1 (Ruby/1.8.6/2008-03-03)" }
168
-
169
- response = RESTFUL_RPC::RESTFUL_API_HANDLER.call(env)
170
- JSON.parse(response.last).should == ["Hai! o hai!"]
171
-
172
- end
173
-
174
- it "should contain backtrace information when show_errors is enabled and an exception occurs" do
175
- mock_component_config_with :restful_api => {"show_errors" => true}
176
- pending
177
- end
178
-
179
- end
180
-
181
- describe 'the ip_allowed?() method' do
182
-
183
- before :each do
184
- @method = RESTFUL_RPC.helper_method :ip_allowed?
185
- end
186
-
187
- it 'should raise a ConfigurationError if "access" is not one of "everyone", "whitelist" or "blacklist"' do
188
- good_access_values = %w[everyone whitelist blacklist]
189
- bad_access_values = %w[foo bar qaz qwerty everone blaclist whitlist]
190
-
191
- good_access_values.each do |access_value|
192
- mock_component_config_with :restful_rpc => {"access" => access_value, "whitelist" => [], "blacklist" => []}
193
- lambda { @method.call("10.0.0.1") }.should_not raise_error
194
- end
195
-
196
- bad_access_values.each do |access_value|
197
- mock_component_config_with :restful_rpc => {"access" => access_value, "authentication" => false}
198
- lambda { @method.call("10.0.0.1") }.should raise_error(Adhearsion::Components::ConfigurationError)
199
- end
200
- end
201
-
202
- describe 'whitelists' do
203
-
204
- it "should parse *'s as wildcards" do
205
- mock_component_config_with :restful_rpc => {"access" => "whitelist", "whitelist" => ["10.*.*.*"]}
206
- @method.call("10.1.2.3").should be_true
207
- end
208
-
209
- it "should allow IPs which are explictly specified" do
210
- mock_component_config_with :restful_rpc => {"access" => "whitelist", "whitelist" => ["4.3.2.1"]}
211
- @method.call("4.3.2.1").should be_true
212
- end
213
-
214
- it "should not allow IPs which are not explicitly specified" do
215
- mock_component_config_with :restful_rpc => {"access" => "whitelist", "whitelist" => %w[ 1.2.3.4 4.3.2.1]}
216
- @method.call("2.2.2.2").should be_false
217
- end
218
-
219
- end
220
-
221
- describe 'blacklists' do
222
-
223
- it "should parse *'s as wildcards" do
224
- mock_component_config_with :restful_rpc => {"access" => "blacklist", "blacklist" => ["10.*.*.*"]}
225
- @method.call("10.1.2.3").should be_false
226
- end
227
-
228
- it "should not allow IPs which are explicitly specified" do
229
- mock_component_config_with :restful_rpc => {"access" => "blacklist", "blacklist" => ["9.8.7.6", "9.8.7.5"]}
230
- @method.call("9.8.7.5").should be_false
231
- end
232
-
233
- it "should allow IPs which are not explicitly specified" do
234
- mock_component_config_with :restful_rpc => {"access" => "blacklist", "blacklist" => ["10.20.30.40"]}
235
- @method.call("1.1.1.1").should be_true
236
- end
237
-
238
- end
239
-
240
- describe '"everyone" access' do
241
- it "should return true for any IP given, irrespective of the configuration" do
242
- ip_addresses = %w[100.200.100.200 0.0.0.0 *.0.0.*]
243
- ip_addresses.each do |address|
244
- RESTFUL_RPC.helper_method(:ip_allowed?).call(address).should equal(true)
245
- end
246
- end
247
- end
248
-
249
- end
250
-
251
- end