URI: 
       First push of the new code base - warvox - VoIP based wardialing tool, forked from rapid7/warvox.
   DIR Log
   DIR Files
   DIR Refs
   DIR README
       ---
   DIR commit 1d11bb69a4d816c751af34dc79b60a4245afd3dc
   DIR parent b99f2cf35da070c8d767310bf30a24808bf788e4
  HTML Author: HD Moore <hd_moore@rapid7.com>
       Date:   Sun,  1 Mar 2009 21:31:55 +0000
       
       First push of the new code base
       
       
       Diffstat:
         A Makefile                            |      18 ++++++++++++++++++
         A README                              |      21 +++++++++++++++++++++
         M bin/automatch.rb                    |       3 +++
         A bin/warvox.rb                       |      46 +++++++++++++++++++++++++++++++
         A etc/warvox.conf                     |      28 ++++++++++++++++++++++++++++
         M lib/warvox.rb                       |      15 ++++++++++++---
         A lib/warvox/config.rb                |      68 +++++++++++++++++++++++++++++++
         M lib/warvox/db.rb                    |       2 +-
         A lib/warvox/jobs.rb                  |      60 +++++++++++++++++++++++++++++++
         A lib/warvox/jobs/analysis.rb         |     178 +++++++++++++++++++++++++++++++
         A lib/warvox/jobs/base.rb             |      24 ++++++++++++++++++++++++
         A lib/warvox/jobs/dialer.rb           |     206 +++++++++++++++++++++++++++++++
         A lib/warvox/phone.rb                 |      32 +++++++++++++++++++++++++++++++
         M src/iaxrecord/iaxrecord.c           |      10 +++++++++-
       
       14 files changed, 706 insertions(+), 5 deletions(-)
       ---
   DIR diff --git a/Makefile b/Makefile
       @@ -0,0 +1,18 @@
       +all: install
       +
       +install: iaxrecord ruby-kissfft
       +        cp -a src/iaxrecord/iaxrecord bin/
       +        cp -a src/ruby-kissfft/kissfft.so lib/
       +
       +iaxrecord:
       +        make -C src/iaxrecord/
       +
       +ruby-kissfft:
       +        ( cd src/ruby-kissfft/; ruby extconf.rb )
       +        make -C src/ruby-kissfft/
       +
       +clean:
       +        ( cd src/ruby-kissfft/; ruby extconf.rb )
       +        make -C src/ruby-kissfft/ clean
       +        make -C src/iaxrecord/ clean
       +        rm -f bin/iaxrecord lib/kissfft.so
   DIR diff --git a/README b/README
       @@ -0,0 +1,21 @@
       +WarVOX Quick Start Guide
       +========================
       +
       +1. Install all pre-requisites
       +        Ubuntu:
       +                $ sudo apt-get install libiaxclient-dev sox lame ruby gnuplot
       +
       +2. Build the WarVOX tools and modules
       +        $ make install
       +
       +3. Change the admin user/pass in etc/warvox.conf
       +
       +4. Start the WarVOX interface
       +        $ bin/warvox.rb
       +
       +5. Access the interface in a web browser
       +        $ firefox http://127.0.0.1:7777/
       +
       +6. Configure an IAX-capable VoIP provider
       +
       +7. Create a a new job
   DIR diff --git a/bin/automatch.rb b/bin/automatch.rb
       @@ -42,6 +42,9 @@ oset = wdb.keys.sort
        iset = oset.dup
        
        
       +$stdout.puts car.keys.map{|x| "#{x}-100" }.join(" ") 
       +$stdout.flush
       +        
        while(not oset.empty?)
        
                s = Time.now
   DIR diff --git a/bin/warvox.rb b/bin/warvox.rb
       @@ -0,0 +1,46 @@
       +#!/usr/bin/env ruby
       +###################
       +
       +#
       +# Load the library path
       +# 
       +base = __FILE__
       +while File.symlink?(base)
       +        base = File.expand_path(File.readlink(base), File.dirname(base))
       +end
       +
       +voxroot = File.join(File.dirname(base), '..', 'web')
       +Dir.chdir(voxroot)
       +
       +voxserv = File.join('script', 'server')
       +
       +opts    = 
       +{
       +        'ServerPort' => 7777,
       +        'ServerHost' => '127.0.0.1',
       +        'Background' => false,
       +}
       +
       +
       +# Clear ARGV
       +while(ARGV.length > 0)
       +        ARGV.shift
       +end
       +
       +# Rebuild ARGV
       +[
       +        '-p', opts['ServerPort'].to_s, 
       +        '-b', opts['ServerHost'],
       +        '-e', 'production',
       +        (opts['Background'] ? '-d' : '')
       +].each do |arg|
       +        ARGV.push arg
       +end
       +
       +$browser_url   = "http://#{opts['ServerHost']}:#{opts['ServerPort']}/"
       +
       +$stderr.puts ""
       +$stderr.puts "[*] Starting WarVOX on #{$browser_url}"
       +$stderr.puts ""
       +
       +load(voxserv)
   DIR diff --git a/etc/warvox.conf b/etc/warvox.conf
       @@ -0,0 +1,28 @@
       +#
       +# WarVOX Configuration
       +#
       +
       +
       +#
       +# Configure the username and password for the WarVOX
       +# web interface. This password is sent in clear text
       +#
       +authentication:
       +  user: admin
       +  pass: warvox
       +
       +#
       +# Configure the path to all saved data files
       +# This requires ~500M of space per prefix
       +#
       +data_path: %BASE%/data/
       +
       +#
       +# Configure filesystem paths to each required tool
       +#
       +tools:
       +  gnuplot: gnuplot
       +  sox: sox
       +  lame: lame
       +  iaxrecord: %BASE%/bin/iaxrecord
       +
   DIR diff --git a/lib/warvox.rb b/lib/warvox.rb
       @@ -2,8 +2,17 @@
        #  top level include file for warvox libaries
        ##
        
       -module WarVOX
       -end
       -
       +# Load components
       +require 'warvox/config'
       +require 'warvox/jobs'
       +require 'warvox/phone'
        require 'warvox/audio'
        require 'warvox/db'
       +
       +# Global configuration
       +module WarVOX
       +        VERSION = '1.0.0'
       +        Base = File.expand_path(File.join(File.dirname(__FILE__), '..'))
       +        Conf = File.expand_path(File.join(Base, 'etc', 'warvox.conf'))
       +        JobManager = WarVOX::JobQueue.new
       +end
   DIR diff --git a/lib/warvox/config.rb b/lib/warvox/config.rb
       @@ -0,0 +1,68 @@
       +module WarVOX
       +module Config
       +        require 'yaml'
       +        
       +        def self.authentication_creds
       +                user = nil
       +                pass = nil
       +                info = YAML.load_file(WarVOX::Conf)
       +                if( info and
       +                        info['authentication'] and
       +                        info['authentication']['user'] and
       +                        info['authentication']['pass']
       +                  )
       +                        user = info['authentication']['user']
       +                        pass = info['authentication']['pass']
       +                end                
       +                [user,pass]
       +        end
       +        
       +        def self.authenticate(user,pass)
       +                wuser,wpass = authentication_creds
       +                (wuser == user and wpass == pass) ? true : false        
       +        end
       +        
       +        def self.tool_path(name)
       +                info = YAML.load_file(WarVOX::Conf)
       +                return nil if not info
       +                return nil if not info['tools']
       +                return nil if not info['tools'][name]
       +                find_full_path(
       +                        info['tools'][name].gsub('%BASE%', WarVOX::Base)
       +                )
       +        end
       +        
       +        def self.data_path
       +                info = YAML.load_file(WarVOX::Conf)
       +                return nil if not info
       +                return nil if not info['data_path']
       +                File.expand_path(info['data_path'].gsub('%BASE%', WarVOX::Base))
       +        end
       +        
       +        # This method searches the PATH environment variable for
       +        # a fully qualified path to the supplied file name.
       +        # Stolen from Rex
       +        def self.find_full_path(file_name)
       +                
       +                # Return absolute paths unmodified
       +                if(file_name[0,1] == ::File::SEPARATOR)
       +                        return file_name
       +                end
       +                
       +                path = ENV['PATH']
       +                if (path)
       +                        path.split(::File::PATH_SEPARATOR).each { |base|
       +                                begin
       +                                        path = base + ::File::SEPARATOR + file_name
       +                                        if (::File::Stat.new(path))
       +                                                return path
       +                                        end
       +                                rescue
       +                                end
       +                        }
       +                end
       +                return nil
       +        end        
       +
       +end
       +end
   DIR diff --git a/lib/warvox/db.rb b/lib/warvox/db.rb
       @@ -137,7 +137,7 @@ class DB < ::Hash
                                tone << rec
                        end
                        
       -                (tone.empty? or tone.length == 1) ? false : tone
       +                (tone.empty? or (data.length > 5 and tone.length == 1)) ? false : tone
                end
        
                def find_carriers
   DIR diff --git a/lib/warvox/jobs.rb b/lib/warvox/jobs.rb
       @@ -0,0 +1,60 @@
       +module WarVOX
       +class JobQueue
       +        attr_accessor :active_job, :active_thread, :queue, :queue_thread
       +
       +        def initialize
       +                @queue = []
       +                @queue_thread = Thread.new{ manage_queue }
       +                super
       +        end
       +        
       +        # XXX synchronize
       +        def deschedule(job_id)
       +
       +                if(@active_job and @active_job.name == job_id)
       +                        @active_thread.kill
       +                        @active_job = @active_thread = nil
       +                end
       +                
       +                res = []
       +                @queue.each do |j|
       +                        res << j if j.name == job_id
       +                end
       +                
       +                if(res.length > 0)
       +                        res.each {|j| @queue.delete(j) }
       +                end
       +        end
       +        
       +        def schedule(job)
       +                @queue.push(job)
       +        end
       +        
       +        def manage_queue
       +                begin
       +                while(true)
       +                        if(@active_job and @active_job.status == 'completed')
       +                                @active_job    = nil
       +                                @active_thread = nil
       +                        end
       +                        
       +                        if(not @active_job and @queue.length > 0)
       +                                @active_job    = @queue.shift
       +                                @active_thread = Thread.new { @active_job.start }
       +                        end
       +
       +                        Kernel.select(nil, nil, nil, 1)
       +                end
       +                rescue ::Exception
       +                        $stderr.puts "QUEUE MANAGER:#{$!.class} #{$!}"
       +                        $stderr.flush
       +                end
       +        end        
       +
       +end
       +end
       +
       +
       +require 'warvox/jobs/base'
       +require 'warvox/jobs/dialer'
       +require 'warvox/jobs/analysis'
   DIR diff --git a/lib/warvox/jobs/analysis.rb b/lib/warvox/jobs/analysis.rb
       @@ -0,0 +1,178 @@
       +module WarVOX
       +module Jobs
       +class Analysis < Base 
       +
       +        require 'fileutils'
       +        require 'kissfft'
       +        
       +        def type
       +                'analysis'
       +        end
       +        
       +        def initialize(job_id)
       +                @name = job_id
       +        end
       +        
       +        def get_job
       +                ::DialJob.find(@name)
       +        end
       +        
       +        def start
       +                @status = 'active'
       +                
       +                begin
       +                start_processing()
       +                
       +                model = get_job
       +                model.processed = true
       +                model.save
       +                
       +                stop()
       +                
       +                rescue ::Exception => e
       +                        $stderr.puts "Exception in the job queue: #{e.class} #{e} #{e.backtrace}"
       +                end
       +        end
       +        
       +        def stop
       +                @status = 'completed'
       +        end
       +        
       +        def start_processing
       +                todo = ::DialResult.find_all_by_dial_job_id(@name)
       +                todo.each do |r|
       +                        next if r.processed
       +                        next if not r.completed
       +                        next if r.busy
       +                        next if not File.exist?(r.rawfile)
       +
       +                        bname = r.rawfile.gsub(/\..*/, '')
       +                        num   = r.number
       +                        
       +                        #
       +                        # Create the signature database
       +                        # 
       +                        raw = WarVOX::Audio::Raw.from_file(r.rawfile)
       +                        fd = File.new("#{bname}.sig", "wb")
       +                        fd.write raw.to_flow
       +                        fd.close
       +                        
       +                        #
       +                        # Create a raw decompressed file
       +                        #
       +                        
       +                        # Decompress the audio file
       +                        rawfile = Tempfile.new("rawfile")
       +                        datfile = Tempfile.new("datfile")                        
       +
       +                        # Data files for audio processing and signal graph
       +                        cnt = 0
       +                        rawfile.write(raw.samples.pack('v*'))
       +                        datfile.write(raw.samples.map{|val| cnt +=1; "#{cnt/8000.0} #{val}"}.join("\n"))
       +                        rawfile.flush
       +                        datfile.flush
       +                        
       +                        # Data files for spectrum plotting
       +                        frefile = Tempfile.new("frefile")
       +                        
       +                        # Perform a DFT on the samples
       +                        res = KissFFT.fftr(8192, 8000, 1, raw.samples)
       +                        
       +                        # Calculate the peak frequencies for the sample
       +                        maxf = 0
       +                        maxp = 0
       +                        tones = {}
       +                        res.each do |x|
       +                                rank = x.sort{|a,b| a[1].to_i <=> b[1].to_i }.reverse
       +                                rank[0..10].each do |t|
       +                                        f = t[0].round
       +                                        p = t[1].round
       +                                        next if f == 0
       +                                        next if p < 1
       +                                        tones[ f ] ||= []
       +                                        tones[ f ] << t
       +                                        if(t[1] > maxp)
       +                                                maxf = t[0]
       +                                                maxp = t[1]
       +                                        end
       +                                end
       +                        end
       +                        
       +                        # Calculate average frequency and peaks over time
       +                        avg = {}
       +                        pks = []
       +                        res.each do |slot|
       +                                pks << slot.sort{|a,b| a[1] <=> b[1] }.reverse[0]
       +                                slot.each do |freq|
       +                                        avg[ freq[0] ] ||= 0
       +                                        avg[ freq[0] ] +=  freq[1]
       +                                end
       +                        end
       +                        avg.keys.sort.each do |k|
       +                                avg[k] = avg[k] / res.length
       +                                frefile.write("#{k} #{avg[k]}\n")
       +                        end
       +                        frefile.flush
       +
       +                        # XXX: Store all frequency information
       +                        # maxf   == peak frequency
       +                        # avg    == averages over whole sample
       +                        # pks    == peaks over time
       +                        # tones  == significant frequencies
       +
       +                        # Plot samples to a graph
       +                        plotter = Tempfile.new("gnuplot")
       +
       +                        plotter.puts("set ylabel \"Signal\"")
       +                        plotter.puts("set xlabel \"Time\"")
       +
       +                        plotter.puts("set terminal png medium size 640,480 transparent")
       +                        plotter.puts("set output \"#{bname}_big.png\"")
       +                        plotter.puts("plot \"#{datfile.path}\" using 1:2 title \"#{num}\" with lines")
       +                        plotter.puts("set output \"#{bname}_big_dots.png\"")
       +                        plotter.puts("plot \"#{datfile.path}\" using 1:2 title \"#{num}\" with dots")
       +
       +                        plotter.puts("set terminal png medium size 640,480 transparent")
       +                        plotter.puts("set ylabel \"Power\"")
       +                        plotter.puts("set xlabel \"Frequency\"")
       +                        plotter.puts("set output \"#{bname}_freq_big.png\"")
       +                        plotter.puts("plot \"#{frefile.path}\" using 1:2 title \"#{num} - Peak #{maxf.round}hz\" with lines")
       +
       +                        plotter.puts("set terminal png small size 160,120 transparent")
       +                        plotter.puts("set format x ''")
       +                        plotter.puts("set format y ''")        
       +                        plotter.puts("set output \"#{bname}.png\"")
       +                        plotter.puts("plot \"#{datfile.path}\" using 1:2 notitle with lines")
       +                                        
       +                        plotter.puts("set terminal png small size 160,120 transparent")
       +                        plotter.puts("set format x ''")
       +                        plotter.puts("set format y ''")        
       +                        plotter.puts("set output \"#{bname}_freq.png\"")
       +                        plotter.puts("plot \"#{frefile.path}\" using 1:2 notitle with lines")                                                
       +                        plotter.flush
       +
       +                        system("gnuplot #{plotter.path}")
       +                        File.unlink(plotter.path)
       +                        File.unlink(datfile.path)
       +                        plotter.close
       +                        datfile.close
       +
       +                        # Generate a MP3 audio file
       +                        system("sox -s -w -r 8000 -t raw -c 1 #{rawfile.path} #{bname}.wav")
       +                        system("lame #{bname}.wav #{bname}.mp3 >/dev/null 2>&1")
       +                        File.unlink("#{bname}.wav")
       +                        File.unlink(rawfile.path)
       +                        rawfile.close
       +                        
       +                        # XXX: Dump the frequencies
       +                        
       +                        # Save the changes
       +                        r.processed = true
       +                        r.processed_at = Time.now
       +                        r.save
       +                end
       +        end
       +
       +end
       +end
       +end
   DIR diff --git a/lib/warvox/jobs/base.rb b/lib/warvox/jobs/base.rb
       @@ -0,0 +1,24 @@
       +module WarVOX
       +module Jobs
       +class Base
       +        attr_accessor :name, :status
       +        
       +        def type
       +                'base'
       +        end
       +        
       +        def name
       +                'noname'
       +        end
       +        
       +        def stop
       +                @status = 'active'
       +        end
       +        
       +        def start
       +                @status = 'completed'
       +        end
       +end
       +end
       +end
       +
   DIR diff --git a/lib/warvox/jobs/dialer.rb b/lib/warvox/jobs/dialer.rb
       @@ -0,0 +1,206 @@
       +module WarVOX
       +module Jobs
       +class Dialer < Base 
       +
       +        require 'fileutils'
       +        
       +        def type
       +                'dialer'
       +        end
       +        
       +        def initialize(job_id)
       +                @name    = job_id
       +                model    = get_job
       +                @range   = model.range
       +                @seconds = model.seconds
       +                @lines   = model.lines                
       +                @nums    = shuffle_a(WarVOX::Phone.crack_mask(@range))
       +                @cid     = '8005551212' # XXX: Read from job
       +        end
       +
       +        #
       +        # Performs a Fisher-Yates shuffle on an array
       +        #
       +        def shuffle_a(arr)
       +                len = arr.length
       +                max = len - 1
       +                cyc = [* (0..max) ]
       +                for d in cyc
       +                        e = rand(d+1)
       +                        next if e == d
       +                        f = arr[d];
       +                        g = arr[e];
       +                        arr[d] = g;
       +                        arr[e] = f;
       +                end
       +                return arr
       +        end
       +
       +        def get_providers
       +                res = []
       +
       +                ::Provider.find(:all).each do |prov|
       +                        info = {
       +                                :name  => prov.name,
       +                                :id    => prov.id,
       +                                :port  => prov.port,
       +                                :host  => prov.host,
       +                                :user  => prov.user,
       +                                :pass  => prov.pass,
       +                                :lines => prov.lines
       +                        }
       +                        1.upto(prov.lines) {|i| res.push(info) }
       +                end
       +                
       +                shuffle_a(res)
       +        end
       +        
       +        def get_job
       +                ::DialJob.find(@name)
       +        end
       +        
       +        def start
       +                begin
       +                
       +                model = get_job
       +                model.status = 'active'
       +                model.started_at = Time.now
       +                model.save
       +                
       +                start_dialing()
       +                
       +                stop()
       +                
       +                rescue ::Exception => e
       +                        $stderr.puts "Exception in the job queue: #{$e.class} #{e} #{e.backtrace}"
       +                end
       +        end
       +        
       +        def stop
       +                @status = 'completed'
       +                model = get_job
       +                model.status = 'completed'
       +                model.completed_at = Time.now
       +                model.save
       +        end
       +        
       +        def start_dialing
       +                dest = File.join(WarVOX::Config.data_path, "#{@name}-#{@range}")
       +                FileUtils.mkdir_p(dest)
       +        
       +                @nums_total = @nums.length
       +                while(@nums.length > 0)
       +                        @calls    = []
       +                        @provs    = get_providers
       +                        tasks     = []
       +                        max_tasks = [@provs.length, @lines].min
       +                        
       +                        1.upto(max_tasks) do
       +                                tasks << Thread.new do
       +                                        
       +                                        Thread.current.kill if @nums.length == 0
       +                                        Thread.current.kill if @provs.length == 0
       +                
       +                                        num  = @nums.shift
       +                                        prov = @provs.shift
       +                                        
       +                                        Thread.current.kill if not num
       +                                        Thread.current.kill if not prov
       +                                        
       +                                        out = File.join(dest, num+".raw")
       +
       +                                        begin
       +                                        # Execute and read the output
       +                                        busy = 0
       +                                        ring = 0
       +                                        fail = 1
       +                                        byte = 0
       +                                        path = ''
       +                                        
       +                                        IO.popen(
       +                                                [
       +                                                        WarVOX::Config.tool_path('iaxrecord'),
       +                                                        prov[:host],
       +                                                        prov[:user],
       +                                                        prov[:pass],
       +                                                        @cid,
       +                                                        out,
       +                                                        num,
       +                                                        @seconds
       +                                                ].map{|i| 
       +                                                        "'" + i.to_s.gsub("'",'') +"'" 
       +                                        }.join(" ")).each_line do |line|
       +                                                $stderr.puts "DEBUG: #{line.strip}"
       +                                                if(line =~ /^COMPLETED/)
       +                                                        line.split(/\s+/).map{|b| b.split('=', 2) }.each do |info|
       +                                                                busy = info[1].to_i if info[0] == 'BUSY'
       +                                                                fail = info[1].to_i if info[0] == 'FAIL'
       +                                                                ring = info[1].to_i if info[0] == 'RINGTIME'
       +                                                                byte = info[1].to_i if info[0] == 'BYTES'        
       +                                                                path = info[1]      if info[0] == 'FILE'
       +                                                        end
       +                                                end
       +                                        end
       +                                        
       +                                        res = ::DialResult.new
       +                                        res.number = num
       +                                        res.dial_job_id = @name
       +                                        res.provider_id = prov[:id]
       +                                        res.completed = (fail == 0) ? true : false
       +                                        res.busy = (busy == 1) ? true : false
       +                                        res.seconds = (byte / 16000)  # 8khz @ 16-bit
       +                                        res.ringtime = ring
       +                                        res.processed = false
       +                                        res.created_at = Time.now
       +                                        res.updated_at = Time.now
       +                                        
       +                                        if(File.exists?(out))
       +                                                system("gzip -9 #{out}")
       +                                                res.rawfile = out + ".gz"
       +                                        end
       +                                        
       +                                        @calls << res
       +                                        
       +                                        rescue ::Exception => e
       +                                                $stderr.puts "ERROR: #{e.class} #{e} #{e.backtrace} #{num} #{prov.inspect}"
       +                                        end
       +                                end
       +        
       +                                # END NEW THREAD
       +                        end
       +                        # END SPAWN THREADS
       +                        tasks.map{|t| t.join if t}
       +                        
       +                        # Save data to the database
       +                        begin
       +                        
       +                                # Iterate through the results
       +                                @calls.each do |r|
       +                                        tries = 0
       +                                        begin
       +                                                r.save
       +                                        rescue ::Exception => e
       +                                                $stderr.puts "ERROR: #{r.inspect} #{e.class} #{e}"
       +                                                tries += 1
       +                                                Kernel.select(nil, nil, nil, 0.25 * (rand(8)+1))
       +                                                retry if tries < 5
       +                                        end
       +                                end
       +                                
       +                                # Update the progress bar
       +                                model = get_job
       +                                model.progress = ((@nums_total - @nums.length) / @nums_total.to_f) * 100
       +                                model.save
       +
       +                        rescue ::SQLite3::BusyException => e
       +                                $stderr.puts "ERROR: Database lock hit trying to save, retrying"
       +                                retry
       +                        end
       +                end
       +                
       +                # ALL DONE
       +        end
       +
       +end
       +end
       +end
   DIR diff --git a/lib/warvox/phone.rb b/lib/warvox/phone.rb
       @@ -0,0 +1,32 @@
       +module WarVOX
       +class Phone
       +
       +        # Convert 123456XXXX to an array of expanded numbers
       +        def self.crack_mask(mask)
       +                res = {}
       +
       +                incdigits = 0
       +                mask.each_char do |c|
       +                        incdigits += 1 if c =~ /^[X#]$/i
       +                end
       +
       +                max = (10**incdigits)-1
       +
       +                (0..max).each do |num|
       +                        number = mask.dup # copy the mask
       +                        numstr = sprintf("%0#{incdigits}d", num) # stringify our incrementing number
       +                        j = 0 # index for numstr
       +                        for i in 0..number.length-1 do # step through the number (mask)
       +                                if number[i].chr =~ /^[X#]$/i
       +                                        number[i] = numstr[j] # replaced masked indexes with digits from incrementing number
       +                                        j += 1
       +                                end
       +                        end
       +                        res[number] = {}
       +                end
       +
       +                return res.keys.sort
       +        end
       +
       +end
       +end
   DIR diff --git a/src/iaxrecord/iaxrecord.c b/src/iaxrecord/iaxrecord.c
       @@ -26,6 +26,7 @@
        int initialized = 0;
        int debug       = 0;
        int busy        = 0;
       +int fail        = 1;
        
        float silence_threshold = 0.0f;
        int call_state  = 0;
       @@ -61,7 +62,9 @@ void usage(char **argv) {
         
        int state_event_callback(struct iaxc_ev_call_state call) {
                if(call.state & IAXC_CALL_STATE_BUSY) busy = 1;
       +        if(call.state & IAXC_CALL_STATE_COMPLETE) fail = 0;
                call_state = call.state;
       +
        /*
                fprintf(stdout, "STATE: ");
                if(call.state & IAXC_CALL_STATE_FREE)
       @@ -109,6 +112,7 @@ int audio_event_callback( struct iaxc_ev_audio audio) {
        int iaxc_callback(iaxc_event e) {
            switch(e.type) {
                        case IAXC_EVENT_TEXT:
       +                        // fprintf(stdout, "TEXT: %s\n", e.ev.text.message);
                                return ( debug ? 0 : 1 );
                                break;
                case IAXC_EVENT_STATE:
       @@ -200,13 +204,17 @@ int main(int argc, char **argv) {
                                if(iaxc_first_free_call() == call_id) break;
                                iaxc_millisleep(250);
                        }
       +        } else {
       +                fail = 1;
                }
       +        
                if(! etime) time(&etime);
                
       -        fprintf(stdout, "COMPLETED %s BYTES=%d FILE=%s BUSY=%d RINGTIME=%d\n", 
       +        fprintf(stdout, "COMPLETED %s BYTES=%d FILE=%s FAIL=%d BUSY=%d RINGTIME=%d\n", 
                        iax_num, 
                        call_bytes,
                        iax_out,
       +                fail,
                        busy,
                        (unsigned int)(etime) - (unsigned int)(stime)
                );