使用 Asterisk 进行时区处理,第一部分
去年,我去了一趟亚洲。为了保持联系,我带了一部 GSM 全球电话,可以在我访问的国家接听电话。能够使用我在家使用的同一个手机号码在地球另一端接听电话,这似乎非常酷——至少在第一个电话打进来之前是这样的!手机隐藏了手机的位置,这有利有弊。一位同事决定在周五中午给我打电话,这在周六早上很早就把我吵醒了,因为手机向他“隐藏”了我遥远的位置。
回家后,我问了几个人,为什么我的电话公司不能简单地播放一条消息来警告来电者,当我的时区变化超过四五个小时时,让他们知道这个电话可能不方便。没有人能提出技术上的理由,但我们都怀疑这是因为我订阅的移动电话公司每分钟收取几美元的通话费。作为将 GSM 电话连接到网络的过程的一部分,家庭网络需要了解电话的访问地点,并且该信息可能包括时区。
当我开始使用 Asterisk 时,我又想起了我的想法,因为它为设计 PBX 托管服务提供了广泛的工具包。任何可以在计算机中编码的东西都可以成为 Asterisk 服务。在我了解了 Asterisk 的基础知识之后,我坐下来实现一个功能,该功能可以跟踪我访问地点的白天时间,并防止在不方便的时间接听电话。
我在 Asterisk 之上构建的用于处理此功能的系统有两个主要部分。该系统的关键是维护与伦敦时间的时区偏移。(我的代码仅实现整小时的偏移,尽管它可以扩展为使用半小时或四分之一小时。)当设备首次连接到 Asterisk 时,其 IP 地址用于猜测位置,从而猜测时区偏移。在将偏移编程到系统后,然后根据远程位置的时间检查来电。在允许电话响铃之前,会检查远程位置的时间,如果来电者试图在不方便的时间完成通话,则可以警告他们。
Asterisk 没有内置的方法来根据 IP 地址估算时区,但它确实有次好的方法——Asterisk 网关接口 (AGI)。AGI 编程允许 Asterisk 分机将数据传递给外部程序,在该程序中进行计算,并将结果作为 Asterisk 通道变量返回。
我通过编写一个 AGI 脚本开始了该项目,该脚本将 IP 地址作为输入并返回估算的时区。一些现有的地理位置数据库将 IP 地址映射到地理信息。我为这个项目尝试的所有免费产品或汇编都无法直接从数据库返回时区,所以我根据经度估算时区。(地球表面有 24 个时区,每个时区大约 15 度经度。)在尝试了几个数据库后,我最终选择了 MaxMind 的 GeoLite City,它是综合性的 GeoCity 商业数据库的免费非商业用途版本。
GeoCity 具有多种编程语言的 API。我使用了 Perl,因为 Perl 也有一个 AGI 库模块,这使得处理 AGI 脚本更容易。作为输入,程序接收一个 IP 地址,然后它需要返回时区。我使用了将时区作为与伦敦时间的小时偏移量返回的约定,这导致了夏令时处理方面的一些差异。
程序的开头拉入实用程序函数,包括 Asterisk::AGI 模块,该模块解码 Asterisk 传递给程序的所有参数
#!/usr/bin/perl -w # Asterisk AGI to estimate time zone from IP address use strict; use Asterisk::AGI; use Geo::IP::PurePerl; use POSIX qw(ceil floor); # California is GMT -8 my $HOME_OFFSET = -8; my $AGI = new Asterisk::AGI; my %input = $AGI->ReadParse();
(本文中程序的完整列表可在 Linux Journal FTP 站点上找到;请参阅资源。)程序的参数是 IP 地址,这是由 Asterisk 给我们的。第一个检查是确定 IP 地址是否在 Asterisk 服务器的本地子网 192.168.1.0/24 上。大多数位置数据库不包括 RFC 1918 地址空间,并且不会返回查找结果。MaxMind API 可以接受域名作为参数,但我们不希望将域名传递给它
my $addr = $ARGV[0]; my @octets = split(/\./,$addr); if (($octets[0] eq "192") && ($octets[1] eq "168") && ($octets[2] eq "1")) { # Local IP addresses get the home offset $AGI->set_variable("TZ_OFFSET",$HOME_OFFSET); exit 0; }
数据库的使用非常简单。我们创建一个新对象,告诉 API 数据库在磁盘上的位置,然后调用 MaxMind API 中的 get_city_record_as_hash 函数,该函数将关于 IP 地址的所有信息作为哈希值返回。感兴趣的项目是哈希值的经度分量。如果没有经度分量,我们将简单地返回 -8 作为加利福尼亚州的时区偏移,并让 Asterisk 处理这个问题
my $gi = Geo::IP::PurePerl-> new( "/usr/local/share/GeoIP/GeoLiteCity.dat", GEOIP_STANDARD); my $cityref = $gi->get_city_record_as_hash($addr); if (!(defined ($cityref->{"longitude"}))) { # Guess at the home time when longitude undefined $AGI->set_variable("TZ_OFFSET",-8); exit 1; } my $longitude=$cityref->{"longitude"};
需要进行一些数学运算来处理时区边界可能是 15 度,但零度位于时区中间的事实。我们可以使用两个公式,具体取决于经度是正数还是负数。在计算时区后,我们将其在 TZ_OFFSET 通道变量中传递回 Asterisk,以便在拨号计划中使用
my $numerator; my $denominator=15; if ($longitude>0) { $numerator=$longitude+7.5; $quotient=floor($numerator/$denominator); } else { $numerator=$longitude-7.5; $quotient=ceil($numerator/$denominator); } $AGI->set_variable("TZ_OFFSET",$quotient);
尽管拥有一个可靠的 IP 地址数据库映射到正确的时区会很方便,但仍然存在处理夏季时间偏移的问题。此外,估算来自一个演示数据库,该数据库不能保证准确。因此,AGI 脚本是从一个分机中调用的,该分机用于确认估算值或保存新估算值。对于确认步骤,我创建了一个号码为 *89 的分机(因为 8-9 是字母 T-Z 的数字映射)。与之前的程序一样,为了简洁起见,删除了一些调试语句,但完整版本可从 LJ FTP 站点获得(请参阅资源)。
本文展示了 Asterisk 表达式语言 (AEL) 中的拨号计划信息条目。我开始使用 AEL 是因为它具有更好的控制结构,并且编写结构化代码更容易得多。对于语音菜单,AEL 中优越的控制结构使得用户输入的验证更加容易。
进入分机的呼叫以欢迎语和对脚本(如前所示)的调用开始,以估算时区。所有时区确认问候语都存储在 Asterisk 声音库的 msg/tz 子目录中。Asterisk 的 SIPCHANINFO 可用于获取 SIP 通道信息。具体来说,SIPCHANINFO(recvip) 的值是远程设备用于注册的可路由 Internet 地址,因此即使远程设备位于网络地址转换器之后也能工作
_*89 => { Answer; Playback(msg/tz/tz-wizard-welcome); Set(PEERIP=${SIPCHANINFO(recvip)}); // Geolocate as a first stab at time zone AGI(tz-lookup.pl,${PEERIP}); NoOp(TZ offset from script is ${TZ_OFFSET});
脚本返回 SIP 对等方在 TZ_OFFSET 变量中使用的 IP 地址的时区猜测值。但是,此调用的全部目的是确认偏移量,因此我们继续进行一系列确认步骤。系统首先确认与伦敦时间的时区偏移,并使用该偏移量来说明时间。Asterisk 将内部时间保持为纪元时间(1970 年 1 月 1 日格林威治标准时间午夜后的秒数),并将其转换为给定时区的本地时间。我正在用大锤处理夏季月份对时间的任何调整,这假设我访问的大多数国家/地区的时间表与英国大致相同,并在验证步骤的后期纠正任何错误
playoffset: Playback(msg/tz/tz-you-are-at); SayDigits(${TZ_OFFSET}); Playback(msg/tz/tz-hours-to-london); playtime: // London time keeps summer time Playback(msg/tz/tz-current-time-is); SayUnixTime($[${EPOCH}+${TZ_OFFSET}*60*60], Europe/London,A \'digits/at\' IMp);
接下来,我们要求用户确认时间是否正确。Read() 应用程序从用户那里获取一位数字。AEL 的 switch 语句非常方便用于处理用户输入,因为它可以用于轻松设置语音菜单的一系列操作,而无需大量使用分支语句。在这种情况下,switch 语句提供了一个小时校正的选项(输入数字 1),任意校正的选项(输入数字 2)以及错误语句(如果按下任何其他键,则跳回时间读数开始处)。在 AEL 的 switch 语句中使用 goto 的唯一问题是,由于控制结构的内部表示,您必须使用完全限定的 goto,包括上下文、分机号码和标签。我的内部分机上下文是 from-internal,因此 goto 的前缀是from-internal|*89:
Read(INPUT,msg/tz/tz-confirm-correct,1); switch (${INPUT}) { case 1: goto from-internal|*89|expiry; case 2: goto from-internal|*89|correction; default: Playback(msg/tz/tz-1-or-2-please); goto from-internal|*89|playoffset; };
在校正标签处,还有第二个选项。我预计在实际使用中夏季时间错误会很常见,所以我添加了一个加速的一小时校正,可以轻松地增加或减少一个小时。处理校正类型和一小时校正子例程的菜单可在 LJ FTP 站点上找到。在结构上,两者都类似于前面显示的 switch 语句。
当我到达遥远的地方时,我通常很累,我的心算能力大大降低,远不如我完全正常工作时的能力。菜单不是语音菜单来选择位置,而是提示输入当地时钟时间,并计算与伦敦时间的时区偏移。基本算法是使用 Asterisk 的 STRFTIME 函数获取伦敦的 24 小时参考时间,并计算与用户输入时间的偏移量。结果偏移量可能太大或太小,因此脚本会对此进行校正
gmtskew: Read(INPUT,msg/tz/tz-24-hour-prompt,4); Set(REMOTEHR=${INPUT:0:$[${LEN(${INPUT})}-2]}); Set(REFERENCEHR=${STRFTIME(${EPOCH}, Europe/London,%H)}); Set(TZ_OFFSET=$[${REMOTEHR}-${REFERENCEHR}]); // correct for too big/too small offsets if ( ${TZ_OFFSET} > 12 ) { Set(TZ_OFFSET=$[${TZ_OFFSET}-24] ); }; if ( ${TZ_OFFSET} < -12 ) { Set(TZ_OFFSET=$[${TZ_OFFSET}+24] ); }; Return;
当用户确认时区偏移时,更改将使用 DB 函数保存在 Asterisk 数据库中。保存更改的一部分是询问用户偏移量应保存多长时间。此代码提示输入天数,尽管可以轻松扩展代码的这一部分以询问到期日期和时间。在确定偏移量到期时间后,偏移量会在远程站点时间和本地时间中读取回给呼叫者。(请注意,我的本地时间是 US/Pacific;您需要将其替换为您自己的时区。)
出于记录保存的目的,使用控制偏移量的分机的名称存储了四个变量。有实际偏移量,以及开始时间、到期时间和 SIP 对等方的 IP 地址。如果在偏移量结束之前移动设备,我们希望自动重新确认时区偏移量
expiry: Set(NOW=${EPOCH}); Set(CURRENT_OFFSET_TIME=$[ ${EPOCH} + ${TZ_OFFSET}*60*60 ]); Set(DB(tz/${PEERNAME}-TIMESKEW)=${TZ_OFFSET}); Set(DB(tz/${PEERNAME}-TIMESKEW_START)=${NOW}); Set(DB(tz/${PEERNAME}-TIMESKEW_ADDR)=${PEERIP}); Playback(msg/tz/tz-your-offset-of); SayDigits(${TZ_OFFSET}); Playback(msg/tz/tz-hours-relative-to-london); Playback(msg/tz/tz-confirm-time); SayUnixTime(${CURRENT_OFFSET_TIME}, Europe/London,A \'digits/at\' IMp); expiration-confirm: Read(TZ_DURATION,msg/tz/tz-days-active,2); expiration-readout: Set(DB(tz/${PEERNAME}-TIMESKEW_END)= $[${NOW}+24*60*60*${TZ_DURATION}]); Playback(msg/tz/tz-shift-active-for); SayDigits(${TZ_DURATION}); Playback(msg/tz/tz-days); Read(INPUT,msg/tz/1-if-right--2-if-wrong,1); switch (${INPUT}) { case 1: // Everything is OK, read out results NoOp(Go to result read-out); break; case 2: goto *89|expiration-confirm; default: Playback(msg/tz/tz-1-or-2-please); goto *89|expiration-readout; };
此时,Asterisk 拥有限制基于一天中时间的呼叫所需的所有数据。Asterisk 拨号计划可用于检查远程站点的时间;如果在早上 8 点之前或晚上 10 点之后,电话会将远程时间播放给来电者,并询问是否仍应振铃分机。
首先,Asterisk 需要拿起电话并将当前本地时间与早上 8 点到晚上 10 点的时间窗口进行比较。Asterisk 的 STRFTIME 函数将纪元时间转换为一天中的时间。通过使用偏移值调整当前纪元时间,STRFTIME 函数返回一天中的时间。太早或太晚的呼叫会跳转到使振铃器静音的代码。当呼叫的振铃被静音时,此示例代码允许来电者覆盖
300 => { Answer; Playback(msg/remote-extension-greeting); Set(TZ_OFFSET=${DB(tz/${EXTEN}-TIMESKEW)}); Set(RMT_EPOCH=$[${EPOCH}+${TZ_OFFSET}*60*60]); Set(REMOTE_CLOCK=${STRFTIME(${RMT_EEPOCH}, Europe/London,%H:%M)}); if("${REMOTE_CLOCK}" < "08:00") { goto 300|too-early; }; if("${REMOTE_CLOCK}" >= "22:00") { goto 300|too-late; }; goto 300|normal-ring;
确认代码首先使振铃静音,但允许用户通过按 1 来启用振铃。我允许任何来电者灵活地覆盖我的时间,但这可以想象可以通过仅允许选定的来电者能够覆盖来处理。“标准”设置铃声的方式是设置 ALERT_INFO 通道变量。我的 ATA 是 Sipura,它允许定义八个铃声节奏。我定义了一个名为 silence 的节奏,它是一个从不使用铃铛的铃声
too-early: NoOp(Too early to ring); too-late: NoOp(Too late to ring); Set(__ALERT_INFO=silence); override-silence: Playback(msg/my-remote-time-is); SayUnixTime(${RMT_EPOCH},Europe/London, A \'digits/at\' IMp); Read(CONFIRM,msg/press-1-to-confirm-call,1); if (${CONFIRM}=1) { Set(__ALERT_INFO=Bellcore-r1); };
使电话振铃很简单,因为它只需要使用 Dial 应用程序,并且铃声节奏已在其他地方设置。或者,可以将呼叫直接发送到语音邮件
normal-ring: Dial(SIP/300,20); vm-only: VoiceMail(umatthew); Hangup; };
为了增强代码重用,可以将一天中的时间检查合并到宏中,该宏作为每个分机的一部分调用。
第一步,从 MaxMind 安装 GeoLiteCity 数据库,并将时区查找脚本安装到 /var/lib/asterisk/agi-bin 中。要调用该脚本,请将时区配置分机 *89 添加到您的拨号计划中。每个需要特定于一天中时间处理的分机都需要修改其拨号计划,代码类似于本文步骤 3 中所示的代码。
然后,作为 PBX 的用户,每次注册 SIP 分机时,您都需要调用 *89 来设置时区。这种手动启动配置过程的需求有点烦人。Asterisk 确实提供了一个接口,可以用于自动设置对用户的呼叫,我将在后续文章中对此进行描述。
在支持多用户的 PBX 上,几个项目将受益于集中存储。在本例中,一天中的时间比较被编码到目标分机的拨号计划中。通过将此数据存储在 Asterisk 数据库中,用户可以更改他们的一天中的时间计划,而无需管理员干预。
最后,我还为我的 PBX 配置了一个“朋友和家人”覆盖功能,该功能允许选定的来电者完成呼叫,即使该呼叫通常会被阻止。特权列表上的来电者可以请求我的位置的一天中的时间,并且即使呼叫通常会被阻止,也允许呼叫振铃。
资源
所有文件的完整源代码列表: ftp.linuxjournal.com/pub/lj/issue155/9190.tgz(时区确认菜单的拨号计划代码为 9190l1.txt,时区感知分机的拨号计划代码为 9190l2.txt,时区地理位置查找的 AGI 脚本为 9190l3.txt。)
MaxMind GeoLite City 数据库(下载和 API): www.maxmind.com/app/geolitecity
Asterisk: www.asterisk.org
Asterisk AEL: www.voip-info.org/wiki/view/Asterisk+AEL
Matthew Gast 是无线 LAN 方面领先的技术书籍 802.11 无线网络:权威指南(O'Reilly Media 出版社)的作者。您可以通过 matthew.gast@gmail.com 与他联系,但仅当他靠近海平面时。