adhearsion 1.2.0 → 1.2.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.
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