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::Client
  • Freeswitch::ESL::Connection
  • Freeswitch::ESL::Protocol::Message
  • Freeswitch::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 = 8021
  • config.freeswitch.password = 'ClueCon'
  • config.freeswitch.timeout = 5
  • config.freeswitch.retry_delay = 1.0
  • config.freeswitch.max_retries = 5
  • config.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.yml
  • docker/freeswitch/autoload_configs/event_socket.conf.xml
  • examples/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 ALL unless 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.