Module: RubyAsterisk::AGI::Protocol

Defined in:
lib/ruby-asterisk/agi/protocol.rb

Overview

Low-level formatter and parser for the FastAGI wire protocol.

Constant Summary collapse

ENV_LINE =
/\A(agi_\w+):\s*(.*)/
RESULT_RE =

Matches a response carrying a result value: CODE result=VALUE [extra]

/\A(\d{3})\s+result=(\S+)(?:\s+(.*?))?\s*\z/
CODE_RE =

Matches any response line (fallback): CODEmessage

/\A(\d{3})(?:[-\s](.*?))?\s*\z/

Class Method Summary collapse

Class Method Details

.collect_multiline_error(first_line, first_response, io) ⇒ Hash

Complete a (possibly multi-line) 520 error response.

Returns first_response unchanged when first_line does not begin with a continuation marker (+NNN-+). Otherwise reads continuation lines from io until the NNN <text> closing line, then returns a new frozen response hash with the accumulated body as :data.

Parameters:

  • first_line (String)

    the already-read first response line (chomped)

  • first_response (Hash)

    as returned by parse_response

  • io (#gets)

Returns:

  • (Hash)


120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/ruby-asterisk/agi/protocol.rb', line 120

def self.collect_multiline_error(first_line, first_response, io)
  return first_response unless first_line.match?(/\A\d{3}-/)

  lines = [first_line.sub(/\A\d{3}-/, '')]
  while (l = io.gets)
    chopped = l.chomp
    if chopped.match?(/\A\d{3}\s/)
      lines << chopped.sub(/\A\d{3}\s*/, '')
      break
    else
      lines << chopped.sub(/\A\d{3}-/, '')
    end
  end

  body = lines.reject(&:empty?).join(' ')
  { code: first_response[:code], result: nil, data: body.freeze }.freeze
end

.error?(response) ⇒ Boolean

Returns true when the response code signals an error.

Parameters:

Returns:

  • (Boolean)

    true when the response code signals an error



69
70
71
# File 'lib/ruby-asterisk/agi/protocol.rb', line 69

def self.error?(response)
  response[:code] >= 500
end

.escape_argument(arg) ⇒ String

Escape a single AGI command argument.

Plain alphanumeric/dash/underscore/slash tokens are returned as-is. Empty strings and strings containing whitespace, quotes, or backslashes are double-quoted with internal special characters escaped.

Parameters:

  • arg (String)

Returns:

  • (String)


34
35
36
37
38
39
40
# File 'lib/ruby-asterisk/agi/protocol.rb', line 34

def self.escape_argument(arg)
  return '""' if arg.empty?
  return arg unless arg.match?(/[\s"\\]/)

  escaped = arg.gsub('\\') { '\\\\' }.gsub('"') { '\\"' }
  "\"#{escaped}\""
end

.format_command(command, *args) ⇒ String

Format an AGI command string ready for sending to Asterisk.

Parameters:

  • command (String)

    AGI command verb (e.g. "ANSWER", "EXEC")

  • args (Array<#to_s>)

    zero or more arguments

Returns:

  • (String)

    space-joined, \n-terminated line



20
21
22
23
24
# File 'lib/ruby-asterisk/agi/protocol.rb', line 20

def self.format_command(command, *args)
  parts = [command.to_s]
  args.each { |a| parts << escape_argument(a.to_s) }
  "#{parts.join(' ')}\n"
end

.parse_env_block(io) {|line| ... } ⇒ Hash{String=>String}

Read and parse the AGI environment block from an I/O source.

Reads lines via io.gets until a blank line or EOF. Each recognised agi_key: value line is stored in the returned Hash. Unrecognised lines are yielded to the optional block (e.g. for debug logging).

Parameters:

  • io (#gets)

Yield Parameters:

  • line (String)

    each unrecognised line (without newline)

Returns:

  • (Hash{String=>String})


93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/ruby-asterisk/agi/protocol.rb', line 93

def self.parse_env_block(io)
  env = {}
  while (raw = io.gets)
    chopped = raw.chomp
    break if chopped.empty?

    pair = parse_env_line(chopped)
    if pair
      env[pair[0]] = pair[1]
    elsif block_given?
      yield chopped
    end
  end
  env
end

.parse_env_line(line) ⇒ Array(String, String)?

Parse a single line from the initial AGI environment block.

Parameters:

  • line (String)

Returns:

  • (Array(String, String), nil)

    [key, value] pair or nil if not an env line



77
78
79
80
81
82
# File 'lib/ruby-asterisk/agi/protocol.rb', line 77

def self.parse_env_line(line)
  m = ENV_LINE.match(line.chomp)
  return nil unless m

  [m[1], m[2].strip]
end

.parse_response(line) ⇒ Hash

Parse a single raw AGI response line into a frozen Hash.

Parameters:

  • line (String, nil)

Returns:

  • (Hash)

    { code: Integer, result: String|nil, data: String|nil }



57
58
59
60
61
62
63
64
65
# File 'lib/ruby-asterisk/agi/protocol.rb', line 57

def self.parse_response(line)
  return { code: 0, result: nil, data: 'Connection closed' }.freeze if line.nil?

  stripped = line.chomp
  return { code: 0, result: nil, data: stripped }.freeze if stripped.empty?

  parse_result_line(stripped) || parse_code_line(stripped) ||
    { code: 0, result: nil, data: stripped }.freeze
end

.quote(arg) ⇒ String

Always wrap arg in double quotes, escaping any internal backslashes and double-quotes. Use for AGI arguments that Asterisk requires to be quoted regardless of content (filenames, variable values, messages).

Parameters:

  • arg (String, #to_s)

Returns:

  • (String)


48
49
50
51
# File 'lib/ruby-asterisk/agi/protocol.rb', line 48

def self.quote(arg)
  escaped = arg.to_s.gsub('\\') { '\\\\' }.gsub('"') { '\\"' }
  "\"#{escaped}\""
end