freeswitch-esl
freeswitch-esl is a Ruby gem for interacting with FreeSWITCH through ESL (Event Socket Library).
This version is intentionally focused on one direction only: your Ruby process connects to mod_event_socket and uses a single shared client, Freeswitch::ESL.client.
The implementation keeps the public API small and groups protocol concerns separately:
Freeswitch::ESL::ClientFreeswitch::ESL::ConnectionFreeswitch::ESL::Protocol::MessageFreeswitch::ESL::Protocol::Event
Why ESL
For FreeSWITCH, ESL is still the most complete and stable integration surface for external control. Other interfaces exist, but they are generally less complete:
mod_xml_rpc: useful for some management operations, but not a replacement for event streaming and call control- custom REST/gRPC wrappers: possible, but usually built on top of ESL rather than replacing it
If you need event subscriptions, command execution and background jobs, ESL remains the right interface.
Installation
Choose one installation mode based on your environment.
Local path (development)
Use this while developing the gem and application together:
gem 'freeswitch-esl', path: '/path/to/freeswitch-esl'
Git source (production)
Use this when you want to pin a tag or commit from your repository:
gem 'freeswitch-esl', git: 'https://gitlab.example.com/your-group/freeswitch-esl.git', tag: 'v0.1.0'
RubyGems style (for future releases)
When the gem is published on RubyGems, the classic entry is:
gem 'freeswitch-esl'
Then require it:
require 'freeswitch-esl'
Configuration
Configure the library once and then use the shared client:
require 'logger'
require 'freeswitch-esl'
Freeswitch::ESL.configure do |config|
config.freeswitch.host = 'freeswitch.example.com'
config.freeswitch.port = 8021
config.freeswitch.password = 'ClueCon'
config.freeswitch.timeout = 5
config.freeswitch.retry_delay = 1.0
config.freeswitch.max_retries = 5
config.logger = Logger.new($stdout)
end
Defaults:
config.freeswitch.host = '127.0.0.1'config.freeswitch.port = 8021config.freeswitch.password = 'ClueCon'config.freeswitch.timeout = 5config.freeswitch.retry_delay = 1.0config.freeswitch.max_retries = 5config.logger = nil
FreeSWITCH Configuration
Inbound ESL (mod_event_socket)
FreeSWITCH must expose mod_event_socket.
For local development, a minimal configuration looks like this:
<configuration name="event_socket.conf" description="Socket Client">
<settings>
<param name="listen-ip" value="0.0.0.0"/>
<param name="listen-port" value="8021"/>
<param name="password" value="ClueCon"/>
<param name="apply-inbound-acl" value="lan"/>
</settings>
</configuration>
For production, tighten the ACL further and do not expose port 8021 publicly.
In the included Docker setup, lan is intentional because it allows both loopback and Docker private-network traffic while still avoiding a fully open event socket.
Usage
Shared client
require 'freeswitch-esl'
Freeswitch::ESL.configure do |config|
config.freeswitch.host = '127.0.0.1'
config.freeswitch.port = 8021
config.freeswitch.password = 'ClueCon'
end
client = Freeswitch::ESL.client
client.subscribe('CHANNEL_CREATE', 'CHANNEL_HANGUP', 'DTMF')
client.on('CHANNEL_CREATE') do |event|
puts "new channel: #{event.uuid}"
end
client.on('DTMF') do |event|
puts "digit=#{event['DTMF-Digit']} duration=#{event['DTMF-Duration']}"
end
response = client.api('status')
puts response.body
Freeswitch::ESL.reset_client!
Background API
require 'freeswitch-esl'
client = Freeswitch::ESL.client
job_uuid = client.bgapi('originate', 'sofia/default/1000 &park') do |event|
puts "job #{event.job_uuid} finished"
puts event.body
end
puts "submitted job=#{job_uuid}"
Auto reconnect
Event handlers remain registered across reconnects. After reconnect, re-subscribe to events:
require 'freeswitch-esl'
client = Freeswitch::ESL.client
client.on('CHANNEL_HANGUP') do |event|
puts "hangup cause=#{event['Hangup-Cause']}"
end
client.on_reconnect do
client.subscribe('CHANNEL_HANGUP')
end
client.subscribe('CHANNEL_HANGUP')
Docker Compose Test Lab
This repository includes a local FreeSWITCH environment for ESL experimentation. It is intended for development and manual testing, and it can later serve as a base for realistic integration tests.
Files included:
docker-compose.ymldocker/freeswitch/autoload_configs/event_socket.conf.xmlexamples/inbound_status.rb
The compose file uses the public image safarov/freeswitch:latest and overlays only the ESL-specific configuration needed for local development.
Start FreeSWITCH
docker compose up -d
If you change image or base distro later, verify that the configuration directory is still /etc/freeswitch.
Check that FreeSWITCH is up
docker compose exec freeswitch fs_cli -x 'status'
Test inbound ESL connectivity
require 'freeswitch-esl'
Freeswitch::ESL.configure do |config|
config.freeswitch.host = '127.0.0.1'
config.freeswitch.port = 8021
config.freeswitch.password = 'ClueCon'
end
puts Freeswitch::ESL.client.api('status').body
Freeswitch::ESL.reset_client!
Or run the ready-made example:
ruby examples/inbound_status.rb
Production Notes
- keep ESL bound to a private interface whenever possible
- protect the event socket with ACLs and a non-default password
- do not use the included Docker configuration as-is in production
- prefer explicit event subscriptions instead of
ALLunless you really need global event traffic
Development
Run style checks:
bundle exec rubocop
Run tests:
bundle exec rspec
GitLab CI local
Run the full local pipeline:
gitlab-ci-local
Run only the test job:
gitlab-ci-local rspec
Run only lint:
gitlab-ci-local rubocop
Test stability note
Some client specs use reconnect threads. To avoid order-dependent failures in random runs, the test teardown always closes created clients. This makes sure reconnect-related activity is stopped before the next example starts.