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)
);