From 44caaf09df64aaef5684dc467c8b7c30dbf13fed Mon Sep 17 00:00:00 2001 From: Piotr Dobrowolski Date: Sat, 24 Mar 2018 13:18:37 +0100 Subject: [PATCH] Initial commit --- .gitignore | 4 + Dockerfile | 12 ++ agi/googletts.agi | 372 ++++++++++++++++++++++++++++++++++++++++++ agi/phonetab.agi | 75 +++++++++ conf/extensions.conf | 6 + conf/logger.conf | 5 + conf/modules.conf | 2 + conf/musiconhold.conf | 6 + conf/sip.conf.tmpl | 25 +++ conf/sip.conf.toml | 6 + config.env.dist | 2 + docker-compose.yml | 13 ++ phonetab.dist | 5 + 13 files changed, 533 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100755 agi/googletts.agi create mode 100755 agi/phonetab.agi create mode 100644 conf/extensions.conf create mode 100644 conf/logger.conf create mode 100644 conf/modules.conf create mode 100644 conf/musiconhold.conf create mode 100644 conf/sip.conf.tmpl create mode 100644 conf/sip.conf.toml create mode 100644 config.env.dist create mode 100644 docker-compose.yml create mode 100644 phonetab.dist diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c69194c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +config.env +conf/orig +rec +*.swp diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..316c8fd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM dougbtv/asterisk14 + +RUN yum install -y perl-Digest-MD5 perl-libwww-perl perl-LWP-Protocol-https sox mpg123 which python-pip +RUN pip install pyst2==0.5.0 +RUN curl -L https://github.com/kelseyhightower/confd/releases/download/v0.15.0/confd-0.15.0-linux-amd64 > /usr/local/bin/confd && chmod +x /usr/local/bin/confd + +COPY conf/*.conf /etc/asterisk/ +COPY conf/*.conf.tmpl /etc/confd/templates/ +COPY conf/*.conf.toml /etc/confd/conf.d/ +COPY agi/* /var/lib/asterisk/agi-bin/ + +CMD confd -onetime -backend env && asterisk -fvv diff --git a/agi/googletts.agi b/agi/googletts.agi new file mode 100755 index 0000000..d28edd5 --- /dev/null +++ b/agi/googletts.agi @@ -0,0 +1,372 @@ +#!/usr/bin/env perl + +# +# AGI script that uses Google's translate text to speech engine. +# +# Copyright (C) 2011 - 2015, Lefteris Zafiris +# +# This program is free software, distributed under the terms of +# the GNU General Public License Version 2. See the COPYING file +# at the top of the source tree. +# +# ----- +# Usage +# ----- +# agi(googletts.agi,"text",[language],[intkey],[speed]): This will invoke the Google TTS +# engine, render the text string to speech and play it back to the user. +# If 'intkey' is set the script will wait for user input. Any given interrupt keys will +# cause the playback to immediately terminate and the dialplan to proceed to the +# matching extension (this is mainly for use in IVR, see README for examples). +# If 'speed' is set the speech rate is altered by that factor (defaults to 1.2). +# +# The script contacts google's TTS service in order to get the voice data +# which then stores in a local cache (by default /tmp/) for future use. +# +# Parameters like default language, sample rate, caching and cache dir +# can be set up by altering the following variables: +# Default langeuage: $lang +# Sample rate: $samplerate +# Speed factor: $speed +# Chace: $usecache +# Chache directory: $cachedir +# SoX Version: $sox_ver +# + +use warnings; +use strict; +use utf8; +use Encode qw(decode encode); +use File::Temp qw(tempfile); +use File::Copy qw(move); +use File::Path qw(mkpath); +use Digest::MD5 qw(md5_hex); +use URI::Escape; +use LWP::UserAgent; +use LWP::ConnCache; +$| = 1; + +# ----------------------------- # +# User defined parameters: # +# ----------------------------- # +# Default language # +my $lang = "en"; + +# Output speed factor # +my $speed = 1; + +# Use of cache mechanism # +my $usecache = 1; + +# Cache directory path # +my $cachedir = "/tmp"; + +# Output audio sample rate # +# Leave blank to auto-detect # +my $samplerate = ""; + +# SoX Version # +# Leave blank to auto-detect # +my $sox_ver = ""; + +# Verbose debugging messages # +my $debug = 0; + +# ----------------------------- # + +my %AGI; +my @text; +my $fh; +my $tmpname; +my $fexten; +my $intkey = ""; +my $tmpdir = "/tmp"; +my $maxlen = 4096; +my $timeout = 10; +my $url = "https://translate.google.com"; +my $sox = `/usr/bin/which sox`; +my $mpg123 = `/usr/bin/which mpg123`; + +# Store AGI input # +($AGI{arg_1}, $AGI{arg_2}, $AGI{arg_3}, $AGI{arg_4}) = @ARGV; +while () { + chomp; + last if (!length); + $AGI{$1} = $2 if (/^agi_(\w+)\:\s+(.*)$/); +} +my $name = " -- $AGI{request}:"; + +# Abort if required programs not found. # +fatal_log("sox or mpg123 is missing. Aborting.") if (!$sox || !$mpg123); +chomp($sox, $mpg123); + +# Sanitising input # +$AGI{arg_1} = decode('utf8', $AGI{arg_1}); +for ($AGI{arg_1}) { + s/[\\|*~<>^\(\)\[\]\{\}[:cntrl:]]/ /g; + s/\s+/ /g; + s/^\s|\s$//g; + fatal_log("No text passed for synthesis.") if (!length); + # Split input to comply with google tts requirements # + @text = /.{1,150}$|.{1,150}[.,?!:;]|.{1,150}\s/g; +} +my $lines = @text; + +# Setting language, interrupt keys and speed rate # +if (length($AGI{arg_2})) { + if ($AGI{arg_2} =~ /^[a-zA-Z]{2}(-[a-zA-Z]{2,6})?$/) { + $lang = $AGI{arg_2}; + } else { + console_log("Invalid language setting. Using default."); + } +} + +if (length($AGI{arg_3})) { + $intkey = "0123456789#*" if ($AGI{arg_3} eq "any"); + $intkey = $AGI{arg_3} if ($AGI{arg_3} =~ /^[0-9*#]+$/); +} + +if (length($AGI{arg_4})) { + $speed = $AGI{arg_4} if ($AGI{arg_4} =~ /^\d+(\.\d+)?$/); +} + +# Check cache path size: dir length + md5 + file extension # +if ($usecache) { + if ((length($cachedir) + 32 + 6) < $maxlen) { + mkpath("$cachedir") unless (-d "$cachedir"); + } else { + console_log("Cache path size exceeds limit. Disabling cache."); + $usecache = 0; + } +} + +# Answer channel if not already answered # +print "CHANNEL STATUS\n"; +my @result = checkresponse(); +if ($result[0] == 4) { + print "ANSWER\n"; + @result = checkresponse(); + if ($result[0] != 0) { + fatal_log("Failed to answer channel."); + } +} + +# Setting filename extension according to sample rate. # +if (!$samplerate) { ($fexten, $samplerate) = detect_format(); } +elsif ($samplerate == 12000) { $fexten = "sln12"; } +elsif ($samplerate == 16000) { $fexten = "sln16"; } +elsif ($samplerate == 32000) { $fexten = "sln32"; } +elsif ($samplerate == 44100) { $fexten = "sln44"; } +elsif ($samplerate == 48000) { $fexten = "sln48"; } +else { ($fexten, $samplerate) = ("sln", 8000); } + +# Initialise User angent # +my $ua = LWP::UserAgent->new(ssl_opts => { verify_hostname => 1 }); +$ua->agent("Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:52.0) Gecko/20100101 Firefox/52.0"); +$ua->env_proxy; +$ua->conn_cache(LWP::ConnCache->new()); +$ua->timeout($timeout); + +for (my $i = 0; $i < $lines; $i++) { + my $filename; + my $res; + my $len = length($text[$i]); + my $line = encode('utf8', $text[$i]); + $line =~ s/^\s+|\s+$//g; + next if (length($line) == 0); + if ($debug) { + console_log("Text passed for synthesis: $line", + "Language: $lang, Interrupt keys: $intkey, Sample rate: $samplerate", + "Speed: $speed, Caching: $usecache, Cache dir: $cachedir" + ); + } + if ($usecache) { + $filename = md5_hex("$line.$lang.$speed"); + + # Stream file from cache if it exists # + if (-r "$cachedir/$filename.$fexten") { + console_log("File already in cache.") if ($debug); + $res = playback("$cachedir/$filename", $intkey); + die if ($res < 0); + last if ($res > 0); + next; + } + } + + # Hnadle interrupts # + $SIG{'INT'} = \&int_handler; + $SIG{'HUP'} = \&int_handler; + + ($fh, $tmpname) = tempfile("ggl_XXXXXXXX", DIR => $tmpdir, UNLINK => 1); + my $token = make_token($line); + $line = uri_escape($line); + my $req = "$url/translate_tts?ie=UTF-8&q=$line&tl=$lang&total=$lines&idx=$i&textlen=$len&client=tw-ob&tk=$token&prev=input"; + console_log("URL passed: $req") if ($debug); + + my $ua_request = HTTP::Request->new('GET' => $req); + $ua_request->header( + 'Accept' => '*/*', + 'Accept-Encoding' => 'identity;q=1, *;q=0', + 'Accept-Language' => 'en-US,en;q=0.5', + 'DNT' => '1', + 'Range' => 'bytes=0-', + 'Referer' => 'https://translate.google.com/', + ); + my $ua_response = $ua->request($ua_request, $tmpname); + fatal_log("Failed to fetch file: ", $ua_response->code, $ua_response->message) unless ($ua_response->is_success); + + # Convert mp3 file to 16bit 8Khz or 16kHz mono raw # + system($mpg123, "-q", "-w", "$tmpname.wav", $tmpname) == 0 + or fatal_log("$mpg123 failed: $?"); + + # Detect sox version # + if (!$sox_ver) { + $sox_ver = (system("$sox --version > /dev/null 2>&1") == 0) ? 14 : 12; + console_log("Found sox version $sox_ver in: $sox, mpg123 in: $mpg123") if ($debug); + } + my @soxargs = get_sox_args("$tmpname.wav", "$tmpname.$fexten"); + system(@soxargs) == 0 or fatal_log("$sox failed: $?"); + unlink "$tmpname.wav"; + + # Playback and save file in cache # + $res = playback($tmpname, $intkey); + die if ($res < 0); + if ($usecache) { + console_log("Saving file $filename to cache") if ($debug); + move("$tmpname.$fexten", "$cachedir/$filename.$fexten"); + } else { + unlink "$tmpname.$fexten"; + } + last if ($res > 0); +} +exit; + +sub checkresponse { + my $input = ; + my @values; + + chomp $input; + if ($input =~ /^200 result=(-?\d+)\s?(.*)$/) { + @values = ("$1", "$2"); + } else { + $input .= if ($input =~ /^520-Invalid/); + warn "$name Unexpected result: $input\n"; + @values = (-1, -1); + } + return @values; +} + +sub playback { + my ($file, $keys) = @_; + my @response; + + print "STREAM FILE $file \"$keys\"\n"; + @response = checkresponse(); + if ($response[0] >= 32 && chr($response[0]) =~ /[\w*#]/) { + console_log("Got digit chr($response[0])") if ($debug); + print "SET EXTENSION ", chr($response[0]), "\n"; + checkresponse(); + print "SET PRIORITY 1\n"; + checkresponse(); + } elsif ($response[0] == -1) { + console_log("Failed to play $file."); + } + return $response[0]; +} + +sub detect_format { + # Detect the sound format used # + my @format; + print "GET FULL VARIABLE \${CHANNEL(audionativeformat)}\n"; + my @reply = checkresponse(); + for ($reply[1]) { + if (/(silk|sln)12/) { @format = ("sln12", 12000); } + elsif (/(speex|slin|silk)16|g722|siren7/) { @format = ("sln16", 16000); } + elsif (/(speex|slin|celt)32|siren14/) { @format = ("sln32", 32000); } + elsif (/(celt|slin)44/) { @format = ("sln44", 44100); } + elsif (/(celt|slin)48/) { @format = ("sln48", 48000); } + else { @format = ("sln", 8000); } + } + return @format; +} + +sub get_sox_args { + # Set the appropiate sox cli arguments # + my ($source_file, $dest_file) = @_; + + my @soxargs = ($sox, $source_file, "-q", "-r", $samplerate, "-t", "raw", $dest_file); + if ($speed != 1) { + if ($sox_ver >= 14) { + push(@soxargs, ("tempo", "-s", $speed)); + } else { + push(@soxargs, ("stretch", 1 / $speed, "80")); + } + } + return @soxargs; +} + +# Obfuscated crap straight from Google: +# https://translate.google.com/translate/releases/twsfe_w_20151214_RC03/r/js/desktop_module_main.js +sub make_token { + my $text = shift; + my $time = int(time() / 3600); + my @chars = unpack('U*', $text); + my $stamp = $time; + + foreach (@chars) { + $stamp = make_rl($stamp + $_, '+-a^+6'); + } + $stamp = make_rl($stamp, '+-3^+b+-f'); + if ($stamp < 0) { + $stamp = ($stamp & 2147483647) + 2147483648; + } + $stamp %= 10**6; + return ($stamp . '.' . ($stamp ^ $time)); +} + +sub make_rl { + my ($num, $str) = @_; + + for (my $i = 0; $i < length($str) - 2 ; $i += 3) { + my $d = substr($str, $i+2, 1); + if (ord($d) >= ord('a')) { + $d = ord($d) - 87; + } else { + $d = int($d); + } + if (substr($str, $i+1, 1) eq '+') { + $d = $num >> $d; + } else { + $d = $num << $d; + } + if (substr($str, $i, 1) eq '+') { + $num = $num + $d & 4294967295; + } else { + $num = $num ^ $d; + } + } + return $num; +} + +sub console_log { + foreach my $message (@_) { + warn "$name $message\n"; + print "NOOP \"$name $message\"\n"; + checkresponse(); + } +} + +sub fatal_log { + console_log(@_); + die; +} + +sub int_handler { + die "$name Interrupt signal received, terminating...\n"; +} + +END { + if ($tmpname) { + warn "$name Cleaning temp files.\n" if ($debug); + unlink glob "$tmpname*"; + } +} diff --git a/agi/phonetab.agi b/agi/phonetab.agi new file mode 100755 index 0000000..3c81a8d --- /dev/null +++ b/agi/phonetab.agi @@ -0,0 +1,75 @@ +#!/usr/bin/env python + +import datetime +import sys + +from asterisk.agi import AGI + +# Use next matched number after TIMEOUT seconds +TIMEOUT = 30 + +def part_match(value, e): + """Matches single part of cron-like range expression""" + if e == '*': + return True + elif '/' in e: + everything, diff = e.split('/') + return part_match(value, everything) and value % int(diff) == 0 + elif '-' in e: + start, end = map(int, e.split('-', 1)) + return value >= start and value <= end + else: + return int(e) == value + +def cron_match(value, expr): + """Matches cron-like range expression""" + return any(part_match(value, e) for e in expr.split(',')) + +def cronline_match(dt, expr): + """Matches cron-like expression to provided datetime""" + return all([ + cron_match(t, e) + for t, e in + zip([dt.minute, dt.hour, dt.day, dt.month, dt.weekday()], expr.split()) + ]) + +def phonetab_list(fname, dt): + """Returns matches for provided datetime from phonetab file""" + with open(fname) as fd: + return [ + line.rsplit(None, 1)[1] + for line in fd + if line.strip() + and not line.startswith('#') + and cronline_match(dt, line.rsplit(None, 1)[0]) + ] + +if __name__ == "__main__": + agi = AGI() + agi.verbose('Testing shite') + numbers = phonetab_list(sys.argv[1], datetime.datetime.now()) + agi.verbose(str(numbers)) + + for num in numbers: + # Default to SIP/...@trunk-out if no type specified, multiple numbers + # can be comma-separated + # Example: + # 123456789,SIP/2137@local -> SIP/123456789@trunk-out&SIP/2137@local + numlist = [(n if '/' in n else 'SIP/%s@trunk-out' % n)for n in num.split(',')] + dialpath = '&'.join(numlist) + + agi.verbose('Dialing %s' % (dialpath,)) + res = agi.appexec('Dial', '%s,%d,m' % (dialpath, TIMEOUT)) + agi.verbose('Response: %r' % res) + + dialstatus = agi.get_variable('DIALSTATUS') + agi.verbose('Dialstatus: %r' % dialstatus) + + if dialstatus == 'ANSWER': + break + + agi.appexec("Agi", 'googletts.agi,\\"All lines are currently busy. Please try again later.\\",en') + + agi.verbose('Number timed out, next...') + else: + agi.verbose('No more numbers') diff --git a/conf/extensions.conf b/conf/extensions.conf new file mode 100644 index 0000000..85ed73d --- /dev/null +++ b/conf/extensions.conf @@ -0,0 +1,6 @@ +[trunk-in] +exten => s,1,Answer() +;same => n,SetMusicOnHold(default) +same => n,Monitor(wav,rec-${STRFTIME(${EPOCH},,%d%m%Y-%H:%M:%S)}-${CALLERID(num)}) +same => n,agi(googletts.agi,"This is BGP.WTF abuse phone number. This call will be recorded.",en) +same => n,agi(phonetab.agi,/etc/asterisk/phonetab) diff --git a/conf/logger.conf b/conf/logger.conf new file mode 100644 index 0000000..45f69b2 --- /dev/null +++ b/conf/logger.conf @@ -0,0 +1,5 @@ +[general] + +[logfiles] +console => notice,warning,error +messages => notice,warning,error diff --git a/conf/modules.conf b/conf/modules.conf new file mode 100644 index 0000000..68db67f --- /dev/null +++ b/conf/modules.conf @@ -0,0 +1,2 @@ +[modules] +autoload=yes diff --git a/conf/musiconhold.conf b/conf/musiconhold.conf new file mode 100644 index 0000000..357f46f --- /dev/null +++ b/conf/musiconhold.conf @@ -0,0 +1,6 @@ +[general] + +[default] +mode=files +directory=moh +sort=random diff --git a/conf/sip.conf.tmpl b/conf/sip.conf.tmpl new file mode 100644 index 0000000..ad093ab --- /dev/null +++ b/conf/sip.conf.tmpl @@ -0,0 +1,25 @@ +[general] +context=sip +nat=force_rport,comedia +register => {{getv "/asterisk/trunk/username"}}:{{getv "/asterisk/trunk/secret"}}@sip.2call.pl + +[trunk-out] +type=friend +disallow=all +allow=alaw +allow=ulaw +insecure=invite +canreinvite=no + +context=trunk-in +qualify=no + +fromdomain=sip.2call.pl +host=sip.2call.pl + +defaultuser={{getv "/asterisk/trunk/username"}} +fromuser={{getv "/asterisk/trunk/username"}} +username={{getv "/asterisk/trunk/username"}} +user={{getv "/asterisk/trunk/username"}} + +secret={{getv "/asterisk/trunk/secret"}} diff --git a/conf/sip.conf.toml b/conf/sip.conf.toml new file mode 100644 index 0000000..8125d1d --- /dev/null +++ b/conf/sip.conf.toml @@ -0,0 +1,6 @@ +[template] +src = "sip.conf.tmpl" +dest = "/etc/asterisk/sip.conf" +keys = [ + "/asterisk", +] diff --git a/config.env.dist b/config.env.dist new file mode 100644 index 0000000..f8acc45 --- /dev/null +++ b/config.env.dist @@ -0,0 +1,2 @@ +ASTERISK_TRUNK_USERNAME=4821372137 +ASTERISK_TRUNK_SECRET=dupadupadupa diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..82e0e16 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: '2' +services: + asterisk: + build: . + network_mode: "host" + env_file: + - config.env + # Uncomment this to store recordings in host directory + # volumes: + # - "./rec:/var/spool/asterisk/monitor" + volumes: + - "./phonetab:/etc/asterisk/phonetab" + - "./moh:/var/lib/asterisk/moh" diff --git a/phonetab.dist b/phonetab.dist new file mode 100644 index 0000000..ef50c82 --- /dev/null +++ b/phonetab.dist @@ -0,0 +1,5 @@ +# + +* 8-20 * * 0-4 0048213721372 +* 0-7,21-24 * * * 0048424242424 +* * * * * 0048213721372