PageRenderTime 56ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/sipsorcery-core/SIPSorcery.AppServer.DialPlan/DialPlanApps/GoogleVoiceCall.cs

https://github.com/thecc4re/sipsorcery-mono
C# | 403 lines | 308 code | 36 blank | 59 comment | 30 complexity | 44411083ca78219b8d07edd69c802787 MD5 | raw file
Possible License(s): CC-BY-SA-3.0
  1. //-----------------------------------------------------------------------------
  2. // Filename: GoogleVoiceCall.cs
  3. //
  4. // Description: A dial plan command that places HTTP request to initiate a call
  5. // through the Google Voice service and bridges the callback with the original caller.
  6. //
  7. // History:
  8. // 11 Aug 2009 Aaron Clauson Created.
  9. //
  10. // License:
  11. // This software is licensed under the BSD License http://www.opensource.org/licenses/bsd-license.php
  12. //
  13. // Copyright (c) 2008 Aaron Clauson (aaronc@blueface.ie), Blue Face Ltd, Dublin, Ireland (www.blueface.ie)
  14. // All rights reserved.
  15. //
  16. // Redistribution and use in source and binary forms, with or without modification, are permitted provided that
  17. // the following conditions are met:
  18. //
  19. // Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  20. // Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
  21. // disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of Blue Face Ltd.
  22. // nor the names of its contributors may be used to endorse or promote products derived from this software without specific
  23. // prior written permission.
  24. //
  25. // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
  26. // BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  27. // IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
  28. // OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
  29. // OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  30. // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  31. // POSSIBILITY OF SUCH DAMAGE.
  32. //-----------------------------------------------------------------------------
  33. using System;
  34. using System.Collections.Generic;
  35. using System.IO;
  36. using System.Linq;
  37. using System.Net;
  38. using System.Security;
  39. using System.Text;
  40. using System.Text.RegularExpressions;
  41. using System.Threading;
  42. using System.Web;
  43. using SIPSorcery.SIP;
  44. using SIPSorcery.SIP.App;
  45. using SIPSorcery.Sys;
  46. using log4net;
  47. namespace SIPSorcery.AppServer.DialPlan
  48. {
  49. public class GoogleVoiceCall
  50. {
  51. private const string PRE_LOGIN_URL = "https://www.google.com/accounts/ServiceLogin";
  52. private const string LOGIN_URL = "https://www.google.com/accounts/ServiceLoginAuth?service=grandcentral";
  53. private const string VOICE_HOME_URL = "https://www.google.com/voice";
  54. private const string VOICE_CALL_URL = "https://www.google.com/voice/call/connect";
  55. private const string CANCEL_CALL_URL = "https://www.google.com/voice/call/cancel";
  56. private const int MIN_CALLBACK_TIMEOUT = 3;
  57. private const int MAX_CALLBACK_TIMEOUT = 60;
  58. private const int WAIT_FOR_CALLBACK_TIMEOUT = 30;
  59. private const int HTTP_REQUEST_TIMEOUT = 5;
  60. private static ILog logger = AppState.logger;
  61. private SIPMonitorLogDelegate Log_External;
  62. private SIPTransport m_sipTransport;
  63. private ISIPCallManager m_callManager;
  64. private string m_username;
  65. private string m_adminMemberId;
  66. private SIPEndPoint m_outboundProxy;
  67. private string m_forwardingNumber;
  68. private string m_fromURIUserRegexMatch;
  69. private string m_destinationNumber;
  70. private ManualResetEvent m_waitForCallback = new ManualResetEvent(false);
  71. private ISIPServerUserAgent m_callbackCall;
  72. private bool m_clientCallCancelled;
  73. private bool m_hasBeenCancelled;
  74. private CookieContainer m_cookies;
  75. private string m_rnrKey;
  76. internal event CallProgressDelegate CallProgress;
  77. public GoogleVoiceCall(
  78. SIPTransport sipTransport,
  79. ISIPCallManager callManager,
  80. SIPMonitorLogDelegate logDelegate,
  81. string username,
  82. string adminMemberId,
  83. SIPEndPoint outboundProxy)
  84. {
  85. m_sipTransport = sipTransport;
  86. m_callManager = callManager;
  87. Log_External = logDelegate;
  88. m_username = username;
  89. m_adminMemberId = adminMemberId;
  90. m_outboundProxy = outboundProxy;
  91. }
  92. /// <summary>
  93. /// Initiates a Google Voice callback by sending 3 HTTP requests and then waiting for the incoming SIP call.
  94. /// </summary>
  95. /// <param name="emailAddress">The Google Voice email address to login with.</param>
  96. /// <param name="password">The Google Voice password to login with.</param>
  97. /// <param name="forwardingNumber">The number to request Google Voice to do the intial callback on.</param>
  98. /// <param name="destinationNumber">The number to request Google Voice to dial out on. This is what Google will attempt to
  99. /// call once the callback on the forwardingNumber is answered.</param>
  100. /// <param name="fromUserToMatch">The FromURI user to match to recognise the incoming call. If null it will be assumed that
  101. /// Gizmo is being used and the X-GoogleVoice header will be used.</param>
  102. /// <param name="contentType">The content type of the SIP call into sipsorcery that created the Google Voice call. It is
  103. /// what will be sent in the Ok response to the initial incoming callback.</param>
  104. /// <param name="body">The content of the SIP call into sipsorcery that created the Google Voice call. It is
  105. /// what will be sent in the Ok response to the initial incoming callback.</param>
  106. /// <returns>If successful the dialogue of the established call otherwsie null.</returns>
  107. public SIPDialogue InitiateCall(string emailAddress, string password, string forwardingNumber, string destinationNumber, string fromUserRegexMatch, int phoneType, int waitForCallbackTimeout, string contentType, string body)
  108. {
  109. try
  110. {
  111. m_forwardingNumber = forwardingNumber;
  112. m_destinationNumber = destinationNumber;
  113. m_fromURIUserRegexMatch = fromUserRegexMatch;
  114. if (CallProgress != null)
  115. {
  116. //CallProgress(SIPResponseStatusCodesEnum.Ringing, "Initiating Google Voice call", null, null, null);
  117. CallProgress(SIPResponseStatusCodesEnum.Ringing, null, null, null, null, null);
  118. }
  119. m_cookies = new CookieContainer();
  120. m_rnrKey = Login(emailAddress, password);
  121. if (!m_rnrKey.IsNullOrBlank())
  122. {
  123. Log_External(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Call key " + m_rnrKey + " successfully retrieved for " + emailAddress + ", proceeding with callback.", m_username));
  124. return SendCallRequest(forwardingNumber, destinationNumber, phoneType, waitForCallbackTimeout, contentType, body);
  125. }
  126. else
  127. {
  128. Log_External(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Call key was not retrieved for " + emailAddress + " callback cannot proceed.", m_username));
  129. return null;
  130. }
  131. }
  132. catch (Exception excp)
  133. {
  134. logger.Error("Exception GoogleVoiceCall InitiateCall. " + excp.Message);
  135. throw;
  136. }
  137. }
  138. private string Login(string emailAddress, string password)
  139. {
  140. try
  141. {
  142. Log_External(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Logging into google.com for " + emailAddress + ".", m_username));
  143. // Fetch GALX
  144. HttpWebRequest galxRequest = (HttpWebRequest)WebRequest.Create(PRE_LOGIN_URL);
  145. galxRequest.ConnectionGroupName = "prelogin";
  146. galxRequest.CookieContainer = m_cookies;
  147. HttpWebResponse galxResponse = (HttpWebResponse)galxRequest.GetResponse();
  148. if (galxResponse.StatusCode != HttpStatusCode.OK)
  149. {
  150. galxResponse.Close();
  151. throw new ApplicationException("Load of the Google Voice pre-login page failed with response " + galxResponse.StatusCode + ".");
  152. }
  153. else
  154. {
  155. Log_External(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Google Voice pre-login page loaded successfully.", m_username));
  156. }
  157. StreamReader galxReader = new StreamReader(galxResponse.GetResponseStream());
  158. string galxResponseFromServer = galxReader.ReadToEnd();
  159. galxResponse.Close();
  160. Match galxMatch = Regex.Match(galxResponseFromServer, @"name=""GALX""\s+?value=""(?<galxvalue>.*?)""");
  161. if (galxMatch.Success)
  162. {
  163. Log_External(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "GALX key " + galxMatch.Result("${galxvalue}") + " successfully retrieved.", m_username));
  164. }
  165. else
  166. {
  167. throw new ApplicationException("Could not find GALX key on your Google Voice pre-login page, callback cannot proceed.");
  168. }
  169. // Build login request.
  170. string loginData = "Email=" + Uri.EscapeDataString(emailAddress) + "&Passwd=" + Uri.EscapeDataString(password) + "&GALX=" + Uri.EscapeDataString(galxMatch.Result("${galxvalue}"));
  171. HttpWebRequest loginRequest = (HttpWebRequest)WebRequest.Create(LOGIN_URL);
  172. loginRequest.CookieContainer = m_cookies;
  173. loginRequest.ConnectionGroupName = "login";
  174. loginRequest.AllowAutoRedirect = true;
  175. loginRequest.Method = "POST";
  176. loginRequest.ContentType = "application/x-www-form-urlencoded;charset=utf-8";
  177. loginRequest.ContentLength = loginData.Length;
  178. loginRequest.GetRequestStream().Write(Encoding.UTF8.GetBytes(loginData), 0, loginData.Length);
  179. loginRequest.Timeout = HTTP_REQUEST_TIMEOUT * 1000;
  180. // Send login request and read response stream.
  181. HttpWebResponse response = (HttpWebResponse)loginRequest.GetResponse();
  182. if (response.StatusCode != HttpStatusCode.OK)
  183. {
  184. response.Close();
  185. throw new ApplicationException("Login to google.com failed for " + emailAddress + " with response " + response.StatusCode + ".");
  186. }
  187. response.Close();
  188. // We're now logged in. Need to load up the Google Voice page to get the rnr hidden input value which is needed for
  189. // the HTTP call requests.
  190. HttpWebRequest rnrRequest = (HttpWebRequest)WebRequest.Create(VOICE_HOME_URL);
  191. rnrRequest.ConnectionGroupName = "call";
  192. rnrRequest.CookieContainer = m_cookies;
  193. // Send the Google Voice account page request and read response stream.
  194. response = (HttpWebResponse)rnrRequest.GetResponse();
  195. if (response.StatusCode != HttpStatusCode.OK)
  196. {
  197. response.Close();
  198. throw new ApplicationException("Load of the Google Voice account page failed for " + emailAddress + " with response " + response.StatusCode + ".");
  199. }
  200. else
  201. {
  202. Log_External(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Google Voice home page loaded successfully.", m_username));
  203. }
  204. StreamReader reader = new StreamReader(response.GetResponseStream());
  205. string responseFromServer = reader.ReadToEnd();
  206. response.Close();
  207. // Extract the rnr field from the HTML.
  208. Match rnrMatch = Regex.Match(responseFromServer, @"name=""_rnr_se"".*?value=""(?<rnrvalue>.*?)""");
  209. if (rnrMatch.Success)
  210. {
  211. return rnrMatch.Result("${rnrvalue}");
  212. }
  213. else
  214. {
  215. throw new ApplicationException("Could not find _rnr_se key on your Google Voice account page, callback cannot proceed.");
  216. }
  217. }
  218. catch (Exception excp)
  219. {
  220. logger.Error("Exception GoogleVoiceCall Login. " + excp.Message);
  221. throw;
  222. }
  223. }
  224. private SIPDialogue SendCallRequest(string forwardingNumber, string destinationNumber, int phoneType, int waitForCallbackTimeout, string contentType, string body)
  225. {
  226. try
  227. {
  228. int callbackTimeout = (waitForCallbackTimeout < MIN_CALLBACK_TIMEOUT || waitForCallbackTimeout > MAX_CALLBACK_TIMEOUT) ? WAIT_FOR_CALLBACK_TIMEOUT : waitForCallbackTimeout;
  229. CallbackWaiter callbackWaiter = new CallbackWaiter(m_username, CallbackWaiterEnum.GoogleVoice, forwardingNumber, MatchIncomingCall);
  230. m_callManager.AddWaitingApplication(callbackWaiter);
  231. string callData = "outgoingNumber=" + Uri.EscapeDataString(destinationNumber) + "&forwardingNumber=" + Uri.EscapeDataString(forwardingNumber) +
  232. "&subscriberNumber=undefined&remember=0&_rnr_se=" + Uri.EscapeDataString(m_rnrKey) + "&phoneType=" + phoneType;
  233. //logger.Debug("call data=" + callData + ".");
  234. // Build the call request.
  235. HttpWebRequest callRequest = (HttpWebRequest)WebRequest.Create(VOICE_CALL_URL);
  236. callRequest.ConnectionGroupName = "call";
  237. callRequest.CookieContainer = m_cookies;
  238. callRequest.Method = "POST";
  239. callRequest.ContentType = "application/x-www-form-urlencoded;charset=utf-8";
  240. callRequest.ContentLength = callData.Length;
  241. callRequest.GetRequestStream().Write(Encoding.UTF8.GetBytes(callData), 0, callData.Length);
  242. callRequest.Timeout = HTTP_REQUEST_TIMEOUT * 1000;
  243. HttpWebResponse response = (HttpWebResponse)callRequest.GetResponse();
  244. HttpStatusCode responseStatus = response.StatusCode;
  245. response.Close();
  246. if (responseStatus != HttpStatusCode.OK)
  247. {
  248. throw new ApplicationException("The call request failed with a " + responseStatus + " response.");
  249. }
  250. else
  251. {
  252. Log_External(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Google Voice Call to " + destinationNumber + " initiated, callback #" + forwardingNumber + ", phone type " + phoneType + ", timeout " + callbackTimeout + "s.", m_username));
  253. }
  254. if (m_waitForCallback.WaitOne(callbackTimeout * 1000))
  255. {
  256. if (!m_hasBeenCancelled)
  257. {
  258. Log_External(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Google Voice Call callback received.", m_username));
  259. return m_callbackCall.Answer(contentType, body, null, SIPDialogueTransferModesEnum.Default);
  260. }
  261. else
  262. {
  263. return null;
  264. }
  265. }
  266. else
  267. {
  268. Log_External(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Google Voice Call timed out waiting for callback.", m_username));
  269. CancelCall();
  270. return null;
  271. }
  272. }
  273. catch (Exception excp)
  274. {
  275. logger.Error("Exception GoogleVoiceCall SendCallRequest. " + excp.Message);
  276. throw;
  277. }
  278. }
  279. private void CancelCall()
  280. {
  281. try
  282. {
  283. if (!m_hasBeenCancelled)
  284. {
  285. m_hasBeenCancelled = true;
  286. m_waitForCallback.Set();
  287. string callData = "outgoingNumber=undefined&forwardingNumber=undefined&_rnr_se=" + Uri.EscapeDataString(m_rnrKey);
  288. // Build the call request.
  289. HttpWebRequest cancelRequest = (HttpWebRequest)WebRequest.Create(CANCEL_CALL_URL);
  290. cancelRequest.ConnectionGroupName = "cancel";
  291. cancelRequest.CookieContainer = m_cookies;
  292. cancelRequest.Method = "POST";
  293. cancelRequest.ContentType = "application/x-www-form-urlencoded;charset=utf-8";
  294. cancelRequest.ContentLength = callData.Length;
  295. cancelRequest.GetRequestStream().Write(Encoding.UTF8.GetBytes(callData), 0, callData.Length);
  296. cancelRequest.Timeout = HTTP_REQUEST_TIMEOUT * 1000;
  297. HttpWebResponse response = (HttpWebResponse)cancelRequest.GetResponse();
  298. HttpStatusCode responseStatus = response.StatusCode;
  299. response.Close();
  300. if (responseStatus != HttpStatusCode.OK)
  301. {
  302. logger.Warn("The GoogleVoiceCall cancel request failed with a " + responseStatus + " response.");
  303. }
  304. else
  305. {
  306. Log_External(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Google Voice Call to " + m_destinationNumber + " was successfully cancelled.", m_username));
  307. }
  308. }
  309. }
  310. catch (Exception excp)
  311. {
  312. logger.Error("Exception GoogleVoiceCall CancelCall. " + excp.Message);
  313. throw;
  314. }
  315. }
  316. private bool MatchIncomingCall(ISIPServerUserAgent incomingCall)
  317. {
  318. try
  319. {
  320. if (incomingCall.SIPAccount.Owner != m_username)
  321. {
  322. return false;
  323. }
  324. else if (m_clientCallCancelled)
  325. {
  326. // If the call has been cancelled then don't match to avoid chance of a new incoming call matching a dead Google Voice call.
  327. return false;
  328. }
  329. SIPHeader callHeader = incomingCall.CallRequest.Header;
  330. bool matchedCall = false;
  331. if (!m_fromURIUserRegexMatch.IsNullOrBlank())
  332. {
  333. if (Regex.Match(callHeader.From.FromURI.User, m_fromURIUserRegexMatch).Success)
  334. {
  335. matchedCall = true;
  336. }
  337. }
  338. else if (callHeader.UnknownHeaders.Contains("X-GoogleVoice: true") && callHeader.To.ToURI.User == m_forwardingNumber.Substring(1))
  339. {
  340. matchedCall = true;
  341. }
  342. if (matchedCall)
  343. {
  344. m_callbackCall = incomingCall;
  345. m_callbackCall.SetOwner(m_username, m_adminMemberId);
  346. m_waitForCallback.Set();
  347. return true;
  348. }
  349. else
  350. {
  351. return false;
  352. }
  353. }
  354. catch (Exception excp)
  355. {
  356. logger.Error("Exception GoogleVoiceCall MatchIncomingCall. " + excp.Message);
  357. return false;
  358. }
  359. }
  360. public void ClientCallTerminated(CallCancelCause cancelCause)
  361. {
  362. Log_External(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "GoogleVoiceCall client call cancelled, " + cancelCause + ".", m_username));
  363. m_clientCallCancelled = true;
  364. CancelCall();
  365. }
  366. }
  367. }