Re: регистрация PSTN шлюза linksys SPA-3102 в CGP

От: Dmitry Akindinov <CGatePro_at_mx_ru>
Дата: Tue 14 Jul 2009 - 11:27:54 MSD

Здравствуйте,

Эдуард Ковалев wrote:
>> -----Original Message-----
>> From: CommuniGate Pro Russian Discussions [mailto:CGatePro@mx.ru]
>> Sent: Monday, July 13, 2009 5:13 PM
>> To: CommuniGate Pro Russian Discussions
>> Subject: Re: [CGP] регистрация PSTN шлюза linksys SPA-3102 в CGP
>>
>> Здравствуйте,
>>
>> Эдуард Ковалев wrote:
>>> Добрый день!
>>>
>>> Есть железка Linksys SPA-3102 в удаленном офисе. Имеет белый IP, но
>> он динамический.
>>
>> А регистрацию в DNS сервере каком-нибудь для этого динамического IP она
>> имеет?

> 
> Если имеется ввиду DynDNS, то железка этого не умеет...
> 

>>> В железке есть порт FXO, для подключения имеется два режима:
>>> 1. когда CGP регистрируется в ней. Использовали SIP -> Gateways или
>> PSTN настройки аккаунтов для подключения
>>
>> Регистрация сервер на железке нужна лишь для возможности отдать
>> входящий
>> из PSTN звонок серверу (CGPro)
> 
> . Но можно обойтись и без регистрации,

>> если в настройках PSTN Line этой железки прописать в dial plan
>> приблизительно такое:
>>
>> (S0< :office@10.1.0.1:5060 >)
>>
>> (пробелы после и перед угловыми скобками существенны).
>> По этому плану железка будет передавать входящие из PSTN звонки на SIP
>> URI sip:office@10.1.0.1:5060
> 
> Да, именно таким образом приходят входящие с железки
> 

>>> Но в текущей задаче это сделать не удастся так как IP динамический
>>> 2. есть еще режим, когда железка сама подключается к серверу и это
>> подключение используется для исходящих звонков.
>>> Насколько я понимаю к CGP подключаться можно только к экстеншенам,
>> однако их нельзя использовать для исходящих звонков (поправьте если не
>> прав)
>>
>> Если железка будет регистрироваться в аккаунт сервера, то все запросы к
>> железке в Request URI будут по необходимости иметь имя аккаунта, куда
>> эта железка зарегистрирована.
> 
> Мы в свое время тестировали 3CX Phone System, железку подключали по этой инструкции: 
> http://www.3cx.com/voip-gateways/linksys-3102.html 
> так вот в 3CX pstn линия имеет вид экстеншена и необходимо чтобы железка портом FXO регистрировалась на сервере, и тот использует данную регистрацию для исходящих звонков
> правда логи подключения и звонка мы не смотрели...

Я вот проямо сейчас попробовал перенаправлять исходящие звонки на аккаунт регистрации SPA - но она тогда требует аутентификации. Если ее настроить, чтобы аутентификация не требовалась, то вместо донабора номера из заголовка To она просто выдает гудок и завершает звонок при донаборе номера.

> 
>  Сможет ли она извлечь PSTN номер из,

>> скажем, заголовка Тo: - сильно сомнительно.
> 
> Согласен
> 

>> Можно, конечно, соорудить схему, когда текущий адрес железки будет
>> извлекаться из данных ее регистрации и уже потом использоваться как
>> адрес нормального PSTN шлюза. Только все это громоздко и некрасиво.
> 
> У железки есть еще один порт FXS (аналоговый телефон) и соответственно он подключается к экстеншену. Т.е. в принципе IP железки видно в подключенных (Status - Registered Devices)
> Можно ли это использовать для построения менее громоздкой схемы?

Нет, не получается. При звонках на эту линию она даже выдает ответы 180 (типа, звонит) - но в PSTN звонок при этом не идет. Он идет в fxs порт, к которому может быть подсоединен аналоговый телефон.

> Вообще железка умеет следующее:
> 
> Overview
> The SPA3102 has the following ports: 
> *FXS port (Phone)-Connected to a standard analog telephone or fax machine, configured using the Line tab 
> *FXO port (Line)-.Connected to a standard telephone wall jack for connectivity to the PSTN, configured using the PSTN Line tab
> The FXO port lets the SPA3102 act as a SIP-PSTN gateway, which bridges PSTN and VoIP service.
> Line 1 does not provide a gateway because it provides only VoIP service. The VoIP-To-PSTN calling function is referred to as a PSTN gateway, and PSTN-To-VoIP calling function as a VoIP gateway. Note the following definitions: 
> *VoIP caller-One who calls the SPA3102 via VoIP to obtain PSTN service 
> *VoIP user-VoIP caller that has a user account (user-id and password) on the SPA3102 
> *PSTN caller-One who calls the SPA3102 from the PSTN to obtain VoIP service
> Line 1 can be configured with a regular VoIP account and can be used in the same way as the Line 1 of any Linksys ATA.
> A second VoIP account can be configured in the SPA3102 to support PSTN gateway calls exclusively. A different SIP port should be assigned to Line 1 and the PSTN Line. The same VoIP account may be used for both Line 1 and the PSTN Line if a different SIP port is assigned to each.
> VoIP callers can be authenticated by one of the following methods: 
> *No Authentication-All callers are accepted for service 

Вот это я попробовал - не работает.

> *PIN-Caller is prompted to enter a PIN right after the call is answered > *HTTP digest-SIP INVITE must contain a valid authorization header

То есть, звонящий сначала авторизуется на сервере, а потом железка просит еще раз авторизоваться, без B2BUA такая схема работать не будет.

> PSTN callers can be authenticated by one of the following methods: 
> *No authentication-All callers are accepted for service 
> *PIN-Caller is prompted to enter a PIN right after the call is answered
> 
> How VoIP-To-PSTN Calls Work
> To obtain PSTN services through the SPA3102, the VoIP caller establishes a connection with the PSTN Line by way of a standard SIP INVITE request addressed to the PSTN Line. The PSTN Line can be configured to support one-stage and two-stage dialing as described in the following sections.
> 
> One-Stage Dialing
> The Request-URI of the INVITE to the PSTN Line should have the form <Dialed-Number>@<SPA-Address>,

То есть, просто форкать на регистрацию железки не получится (в этом случае в Request URI будет Contact из регистрации, не телефонный номер.)

> where <Dialed-Number> is the number dialed by the VoIP caller, and <SPA-Address> is a valid address of the SPA3102, such as 10.0.0.100:5061.
> If the FXO port is currently in use (off-hook) or the PSTN line is being used by another extension, the SPA3102 replies to the INVITE with a 503 response. Otherwise, it compares the <Dialed-Number> with the <User ID> of the PSTN Line. If they are the same, the SPA3102 interprets this as a request for two-stage dialing (see the "Two-Stage Dialing" section on page 4-3). If they are different, the SPA3102 processes the <Dialed-Number> using the corresponding <Dial Plan>.
> If dial plan processing fails, the SPA3102 replies with a 403 response. Otherwise, it replies with a 200 and at the same time takes the FXO port off hook and dials the target number returned after processing the dial plan.
> ----
> Note: If <User ID> on the PSTN Line is blank, <Registration> should be disabled for the PSTN Line.
> ---
> If HTTP Digest Authentication is enabled, the SPA3102 challenges the INVITE with a 401 response if it does not have a valid Authorization header. The Authorization header should include a <User ID n> parameter, where n refers to one of eight VoIP user accounts that can be configured on the SPA3102. The credentials are computed based on the corresponding password using Message Digest 5 (MD5). The <User ID n> must match one of the VoIP accounts stored on the SPA3102. Each VoIP user account contains the information listed in Table 4-1.
> 
> Two-Stage Dialing

Это совсем неудобно.

> In two-stage dialing, the SPA3102 takes the FXO port off-hook but does not automatically dial any digits after accepting the call. To invoke two-stage dialing, the VoIP caller should INVITE the PSTN Line without the user-id in the Request-URI or with a user-id that matches exactly the <User ID n> of the PSTN Line. A different user-id in the Request-URI is treated as a request for one-stage dialing if one-stage dialing is enabled, or dropped by the SPA3102 (as if no user-id is given) if one-stage dialing is disabled.
> ---
> Note: If Authentication is disabled, a default dial plan is assigned to all VoIP callers.
> ---
> HTTP Digest Authentication can be also used for two-stage dialing, as in one-stage dialing. If using HTTP Digest Authentication or Authentication is disabled, the VoIP caller should hear the PSTN dial tone right after the call is answered (by a SIP 200 response).
> If PIN Authentication is enabled, the VoIP caller is prompted to enter a PIN number after the SPA3102 answers the call. The PIN number must end with a # key. The inter-PIN-digit timeout is 10 seconds (not configurable). Up to eight VoIP caller PIN numbers can be configured on the SPA3102. A dial plan can be selected for each PIN number. If the caller enters a wrong PIN or the SPA3102 times out waiting for more PIN digits, the SPA3102 tears down the call immediately with a BYE request.
> ---
> Note: When the source address of the INVITE is 127.0.0.1, authentication is automatically disabled because this is a call by the local user. This applies to both one-stage and two-stage dialing.
> ---

>> Лучше бы иметь способ обращаться к железке без всяких регистраций, хотя
>> бы по имени в DNS, которое железкой будет проставляться. Хотя, я не
>> уверен, что SPA-3102 умеет регистрироваться в DNS.
>
> Скорее всего не умеет...

Да, не умеет.

Остается извлекать адрес из регистрации. Небольшое добавление в стандартный скрипт gatewaycaller позволяети отрабатывать ситуацию, когда параметр SipGatewayDomain задан в виде имени аккаунта, на который регистрируется шлюз:

    if FindSubstring(sipDomain, "@") >= 0 then

      result = ExecuteCLI("SIPContacts " + sipDomain);
      if result then
        syslog("CLI failed: " + result);
        rejectCall("501-" + result); stop;
      end if;
      registration = vars().executeCLIResult[0].("");
      syslog("Reg: " + ObjecttoString(vars().executeCLIResult));
      syslog("URI: " + ObjecttoString(registration));
      offset = FindSubstring(registration, "@");
      if offset >= 0 then
        sipDomain = SubString(registration, offset+1, 1000);
      else
        rejectCall("501-registration not found"); stop;
      end if;

    end if;

Этот кусок достает первую регистрацию для этого аккаунта (а надо бы отсортировать по Expires) и использует ее для посылки запроса. Для простых случаев работает.

-- 
Best regards,
Dmitry Akindinov -- Stalker Labs.

--cut here--
// ================================================== //
//              Gateway Caller Application            //
//                                                    //
// Version 1.7                                        //
// Copyright (c) 2006-2007, Stalker Software, Inc.    //
// ================================================== //
//
// All PSTN* settings can be specified as
//   dictionaries: {gw1=value; gw2=value;}
// Settings used:
//   PSTNGatewayDomain   - the gateway domain -- or account name the 
gateway is registered to
//   PSTNGatewayVia      - [optional] gateway address
//   PSTNFromName        - [optional] From: name to use
//   PSTNGatewayAuthName - auth name to use with gateway
//   PSTNGatewayPassword - password to use with gateway
//   PSTNBillingPlan     - parameters for billing
// Preferences used:
//   CallOutPrivacy      - if YES, then hide the From: header
//
//
// Router records can specify the gw to use as
//  the second parameter:
// S:<+1(10d)@telnum> = gatewaycaller{1*,gw1}#postmaster
// If gwN to use is not specified, and a setting
// is a dictionary, the first dictionary item is used.
//
// If the number is specified as nnnn-clip or nnnn-clir
// the prefix overrides the "CallOutPrivacy" preference.
//
function callerLeg(parameters,callPending) external;
function bridgedLoopHash(peerLeg,finishTime) external;
function getKeyedSetting(settings, settingName, gwKey) forward;
procedure 
bookCall(accountName,phoneNumber,callerIP,startTime,resultCode,priceInCents) 
forward;

function isPhoneNumber(theAddr) is
   firstSymbol = Substring(theAddr,0,1);
   return IsDigit(firstSymbol) or else firstSymbol == "+";
end function;

entry Main is
   //
   // All calls to gateways must be authenticated
   //   Request AUTH if there is no AUTH, or reject if AUTH is wrong
   //
   callerEmail = RemoteRedirector();
   if callerEmail == null then callerEmail = RemoteAuthentication(); end if;
   if callerEmail == null then rejectCall(401); stop; end if;

   // Read caller's Account Settings. If failed -> reject
   callerSettings = GetAccountSettings(null,callerEmail);
   if callerSettings == null then rejectCall("500-Failed to read 
settings"); stop; end if;

   if IsString(Vars().startParameter) then
     phoneNumber = Vars().startParameter;
   else
     phoneNumber = Vars().startParameter[0];
     gwKey       = Vars().startParameter[1];
   end if;
   SysLog("calling '" + phoneNumber + "'...");

   callParams      = NewDictionary();

   // if the PSTNCallPlan setting is set, consult the plan database
   if callerSettings.PSTNBillingPlan != null and 
callerSettings.PSTNBillingPlan != "" then
   end if;

   if SubString(phoneNumber,-1,5) == "-clir" then
     callParams.Privacy = "id"; phoneNumber = 
SubString(phoneNumber,0,Length(phoneNumber)-5);
   elif SubString(phoneNumber,-1,5) == "-clip" then
     phoneNumber = SubString(phoneNumber,0,Length(phoneNumber)-5);
   elif GetAccountPreferences("~" + callerEmail + "/CallOutPrivacy") == 
"YES" then
     callParams.Privacy = "id";
   end if;


   callParams.activeSide  = true;
   callParams.useMixer    = false;
   callParams.callBridged = true;
   callParams.("Call-ID")      = PendingRequestData("Call-ID") + ".gwout";
   callParams.("Max-Forwards") = PendingRequestData("Max-Forwards")-1;
   callParams.impersonate = callerEmail;

   fromAddress = getKeyedSetting(callerSettings, "PSTNFromName", gwKey);
   if fromAddress == "*" then
     fromAddress = ReadTelnums(callerEmail)[0];
   end if;
   if fromAddress == null or else fromAddress == "" then
     fromAddress = SIPURIToEmail(RemoteURI());
   end if;

   sipDomain   = getKeyedSetting(callerSettings, "PSTNGatewayDomain", 
gwKey);
   if SubString(sipDomain, 0, 1) == "*" then      // *domainName -> use 
media Proxy
     sipDomain = SubString(sipDomain, 1, 1000);
     callParams.mediaRelay = true;
   elif SubString(sipDomain, 0, 1) == "#" then    // #domainName -> use 
media Mixer
     sipDomain = SubString(sipDomain, 1, 1000);
     callParams.useMixer    = true;
     callParams.callBridged = false;
   end if;

   if SubString(sipDomain, 0, 1) == "$" then      // $domainName -> 
impersonate as From:
     sipDomain = SubString(sipDomain, 1, 1000);
     if FindSubString(fromAddress,"@") >= 0 then
       callParams.impersonate = fromAddress;
     else
       callParams.impersonate = fromAddress + "@" +
            (isPhoneNumber(fromAddress) ? "telnum" : sipDomain);
       fromAddress = fromAddress + "@" + sipDomain;
     end if;
   end if;

   if SubString(sipDomain, 0, 1) == "&" then      // &domainName -> 
custom-impersonate as From:
     sipDomain = SubString(sipDomain, 1, 1000);
     if FindSubString(fromAddress,"@") < 0 then
       fromAddress = fromAddress + "@" + sipDomain;
     end if;
     callParams.customIdentity = fromAddress;
   end if;

   // if the PSTNGatewayDomain setting is empty, PSTN calls are prohibited
   if sipDomain == null or else sipDomain == "" then
     SysLog("'" + callerEmail + "' has an empty PSTNGatewayDomain setting");
     rejectCall("403-PSTN gateway is not specified"); stop;
   end if;

   callParams.From = EmailToSIPURI(fromAddress);

   callPrefix = getKeyedSetting(callerSettings, "PSTNPrefix", gwKey);
   if IsString(callPrefix) then
     phoneNumber = callPrefix + phoneNumber;
   end if;

   if FindSubstring(sipDomain, "@") >= 0 then
     result = ExecuteCLI("SIPContacts " + sipDomain);
     if result then
       syslog("CLI failed: " + result);
       rejectCall("501-" + result); stop;
     end if;
     registration = vars().executeCLIResult[0].("");
     syslog("Reg: " + ObjecttoString(vars().executeCLIResult));
     syslog("URI: " + ObjecttoString(registration));
     offset = FindSubstring(registration, "@");
     if offset >= 0 then
       sipDomain = SubString(registration, offset+1, 1000);
     else
       rejectCall("501-registration not found"); stop;
     end if;
   end if;

   callParams.("") = "sip:" + phoneNumber + "@" + sipDomain;
   callParams.Via  = getKeyedSetting(callerSettings, "PSTNGatewayVia", 
gwKey);
   if callParams.Via == "" then callParams.Via = null; end if;

   callParams.authUsername = getKeyedSetting(callerSettings, 
"PSTNGatewayAuthName", gwKey);
   if callParams.authUsername == "test-*domain*" then
     callParams.authUsername = "test-" + EMailDomainPart(callerEmail) + 
"@" + sipDomain;
   end if;
   callParams.authPassword = getKeyedSetting(callerSettings, 
"PSTNGatewayPassword", gwKey);

   SetReferMode("peer");
   SetBridgeBreakMode("disconnect");
   callerIP    = RemoteIPAddress();

   refreshParams = newDictionary();
   refreshParams.customIdentity = "";  // we do not want to send our PAI 
to SIP clients
   SetCallParameters(refreshParams);


   peerLeg     = callerLeg(callParams,true);
   callStarted = GMTTime();
   if not IsTask(peerLeg) then
     bookCall(callerEmail,phoneNumber,callerIP,callStarted,peerLeg,0);
     rejectCall(peerLeg); stop;
   end if;

   while IsConnected() loop
     input = bridgedLoopHash(peerLeg,null);
   exitif input != "#";
   end loop;
   bookCall(callerEmail,phoneNumber,callerIP,callStarted,null,0);
end entry;

//
// This function takes the "settingName" setting from settings
//   if the setting text is "{...}, it converts it into a dictionary
//     if gwKey is specified, it uses it as dictionary key,
//     otherwise, the first dictionary keyed value is used.
//
function getKeyedSetting(settings, settingName, gwKey) is
   setting = settings.(settingName);
   if SubString(setting,0,1) == "{" and then SubString(setting,-1,1) == 
"}" then
     setting = TextToObject(setting);
   end if;

   if IsDictionary(setting) then
     setting = setting.(gwKey != null ? gwKey : setting[0]);
   end if;

   result = null;
   if IsString(setting) then
     if setting != "" then result = setting; end if;
   end if;
   return result;
end function;

function durationTimeString(duration) is
   result = String(duration % 60);
   if duration >= 60 then
     if Length(result) < 2 then result = "0" + result;  end if;
     result = String(duration / 60 % 60) + ":" + result;
     if duration >= 3600 then
       if Length(result) < 5 then result = "0" + result; end if;
       result = String(duration / 3600) + ":" + result;
     end if;
   end if;
   return result;
end function;

function timeOfDayString(timeInSeconds) is
   return SubString("0"+String(timeInSeconds / 3600),-1,2)    + ":" +
          SubString("0"+String(timeInSeconds / 60 % 60),-1,2) + ":" +
          SubString("0"+String(timeInSeconds % 60),-1,2);
end function;

function moneyString(cents) is
   result = String(cents % 100);
   if length(result) < 2 then result = "0" + result; end if;
   result = String(cents / 100) + "." + result;
   return(result);
end function;

procedure 
bookCall(accountName,phoneNumber,callerIP,startTime,resultCode,priceInCents) 
is
   duration   = GMTTime() - startTime;
   localStart = GMTToLocal(startTime);
   logFileName= "~" + accountName + "/private/logs/PSTNOut-" +
          Month(localStart) + "-" + String(Year(localStart)) + ".txt";
   writeError = AppendSiteFile(logFileName, String(MonthDay(localStart)) 
+ "\t" +
             timeOfDayString(TimeOfDay(localStart)) + "\t" +
             phoneNumber + "\t" +
             durationTimeString(duration) + "\t" +
             moneyString(priceInCents)    + "\t" +
             String(callerIP)    + "\t" +
             (resultCode == null ? "OK" : String(resultCode)) + "\e");

   if writeError != null then SysLog("failed to write to " + logFileName 
+ ": " + writeError); end if;
end procedure;
Получено Tue Jul 14 07:28:01 2009

Этот архив был сгенерирован hypermail 2.1.8 : Tue 14 Jul 2009 - 12:16:04 MSD