使用 Azure Serverless Functions 实现 SMS API
使用 Azure 函数实现无服务器的免费 SMS Web API。
引言
SMS API 由三个主要部分组成,它们按如下方式组合在一起
- SMS 提供商:有各种第三方 SMS 提供商允许向任何手机发送 SMS,而且免费。对于 Azure Function 的这部分,我们选择 160by2.com SMS 提供商,它允许免费向任何手机发送 SMS。
- 屏幕抓取:SMS 提供商不允许在未实际访问网站的情况下发送 SMS。使用 C# 代码,我们将提交登录表单,然后以编程方式提交发送 SMS 的 Web 表单。
- Azure 上的无服务器函数:托管在 Azure 上的无服务器函数允许我们设置 Web API,任何 REST 客户端都可以使用它。
背景
在阅读本文之前,请先了解以下内容:
- 什么是无服务器
- Azure 无服务器函数简介
- 使用 C# 进行 Web 抓取
- CsQuery:使用 .NET 进行类似 jQuery 的 DOM 操作
Using the Code
在我们深入使用 Azure Function 编写 SMS Web API 之前,我们需要了解将要执行的操作顺序。
SmsWebClient 类继承自 C# 的 WebClient 类,它允许我们以编程方式发送 HTTP POST/GET 方法。
我们将对 160by2.com SMS 提供商实现 HTTP POST 和 GET 方法的编程执行。160by2.com 是一个免费的 SMS 提供商,您需要获取用户名和密码才能发送 SMS。SMS 提供商类包含 Login() 和 SendSms() 函数来处理主要工作。我们使用 CsQuery 库来执行 HTML DOM 操作。
在 160by2.com 网站上,我们有一个包含用户名和密码的登录表单。

当我们检查 Web 表单的 HTML 时,我们可以看到有两个 ID 分别为 username 和 password 的输入字段。

当我们单击登录按钮时,表单数据将使用 HTTP POST 方法提交。
为了以编程方式完成此操作,我们将创建一个 C# NameValueCollection,并添加 Web 表单键的值,然后使用 WebClient 的 UploadValues 方法提交表单。
var Client = new WebClient();
string loginPage = "http://www.160by2.com/re-login";
NameValueCollection data = new NameValueCollection();
data.Add("username", UserName);              //username input field
data.Add("password", Password);              //password input field
Client.UploadValues(loginPage, "POST", data);//submit the form
发送 SMS 表单在 UI 中仅包含手机号码和消息。

为了检查提交此 Web 表单时提交了哪些表单值,我们将使用 Google Chrome 控制台,您也可以使用 Fiddler。
按F12打开 Chrome 开发者控制台,然后转到网络选项卡,并在 UI 中,通过单击“立即发送”来提交发送 SMS 表单。提交的请求如下所示:

就像登录表单一样,我们需要以编程方式提交这些表单数据键及其值。
var base_url = "http://www.160by2.com/";
var recipient = "8888YOURNUMBER";
var message = "This is test SMS message";
string cookieVal = CookieJar.GetCookies(new Uri(base_url))["JSESSIONID"].Value.Substring
  (cookieVal.IndexOf('~') + 1);  //we need to read the session id value from cookies 
                                 //send by the server while logging in
//load the send sms web form
CQ sendSmsPage = Client.DownloadString(base_url + "SendSMS?id=" + cookieVal);
NameValueCollection data = new NameValueCollection();
//find keys for all inputs in the form
CQ form = sendSmsPage.Find("form[id=frm_sendsms]");
CQ inputs = form.Find("input[type=hidden]");
foreach (var input in inputs)
{
    CQ inp = input.Cq();
    data.Add(inp.Attr("name"), inp.Attr("value"));
}
//mobile number input
CQ mobileNumberBox = form.Find("input[placeholder='Enter Mobile Number or Name']")[0].Cq();
data.Add(mobileNumberBox.Attr("name"), recipient);
//textarea for message input
data.Add("sendSMSMsg", message);
string sendSmsPost = base_url + data["fkapps"];
data["hid_exists"] = "no";
data["maxwellapps"] = cookieVal;
//additional vals
data.Add("messid_0", "");
data.Add("messid_1", "");
data.Add("messid_2", "");
data.Add("messid_3", "");
data.Add("messid_4", "");
data.Add("newsExtnUrl", "");
data.Add("reminderDate", DateTime.Now.ToString("dd-MM-yyyy"));
data.Add("sel_hour", "");
data.Add("sel_minute", "");
data.Add("ulCategories", "29");
Client.UploadValues(sendSmsPost, data);//submit the send sms form  
最终类如下所示:
using CsQuery;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Net;
using System.Text;
using System.Linq;
namespace azuresmsapp
{
    public class OneSixtybyTwo
    {
        public string UserName { get; set; }
        public string Password { get; set; }
        private CookieContainer CookieJar { get; set; }
        private SmsWebClient Client { get; set; }
        private string base_url = "http://www.160by2.com/";
        private bool IsLoggedIn = false;
        public OneSixtybyTwo(string username, string password)
        {
            UserName = username;
            Password = password;
            CookieJar = new CookieContainer();
            Client = new SmsWebClient(CookieJar, false);
        }
        public bool Login()
        {
            string loginPage = base_url + "re-login";
            NameValueCollection data = new NameValueCollection();
            data.Add("rssData", "");
            data.Add("username", UserName);
            data.Add("password", Password);
            byte[] loginResponseBytes = Client.UploadValues(loginPage, "POST", data);
            CQ loginResponse = System.Text.Encoding.UTF8.GetString(loginResponseBytes);
            IsLoggedIn = loginResponse.Find("[type=password]").Count() == 0;
            return IsLoggedIn;
        }
        public bool SendSms(string recipient, string message)
        {
            if (IsLoggedIn == false)
                throw new Exception("Not logged in");
            string cookieVal = CookieJar.GetCookies(new Uri(base_url))["JSESSIONID"].Value;
            cookieVal = cookieVal.Substring(cookieVal.IndexOf('~') + 1);
            CQ sendSmsPage = Client.DownloadString(base_url + "SendSMS?id=" + cookieVal);
            NameValueCollection data = new NameValueCollection();
            //all inputs
            CQ form = sendSmsPage.Find("form[id=frm_sendsms]");
            CQ inputs = form.Find("input[type=hidden]");
            foreach (var input in inputs)
            {
                CQ inp = input.Cq();
                data.Add(inp.Attr("name"), inp.Attr("value"));
            }
            //sms input
            CQ mobileNumberBox = 
               form.Find("input[placeholder='Enter Mobile Number or Name']")[0].Cq();
            data.Add(mobileNumberBox.Attr("name"), recipient);
            //textarea
            data.Add("sendSMSMsg", message);
            string sendSmsPost = base_url + data["fkapps"];
            data["hid_exists"] = "no";
            data["maxwellapps"] = cookieVal;
            //additional vsls
            data.Add("messid_0", "");
            data.Add("messid_1", "");
            data.Add("messid_2", "");
            data.Add("messid_3", "");
            data.Add("messid_4", "");
            data.Add("newsExtnUrl", "");
            data.Add("reminderDate", DateTime.Now.ToString("dd-MM-yyyy"));
            data.Add("sel_hour", "");
            data.Add("sel_minute", "");
            data.Add("ulCategories", "29");
            Client.UploadValues(sendSmsPost, data);
            return true;
        }
    }
}
现在,我们的主菜完成了,加上了樱桃……
要发送 SMS,我们创建一个 OneSixtybyTwo 类的实例,调用 Login 函数,然后调用 SendSMS 函数。
OneSixtybyTwo objSender = new OneSixtybyTwo ("160BY2.COM_USERNAME", "160BY2.COM_PASSWORD");
if (objSender.Login()) { 
    var sendResult = objSender.SendSms(number, message);
}
让我们深入了解具有 HTTP 触发器的 Azure 无服务器函数。
该函数可以通过 HTTP GET 或 POST 方法启动,因此我们使用以下代码读取发布的手机号码和消息:
string number = req.Query["number"]; 
string message = req.Query["message"]; 
string requestBody = new StreamReader(req.Body).ReadToEnd(); 
dynamic data = JsonConvert.DeserializeObject(requestBody); 
number = number ?? data?.number; 
message = message ?? data?.message;
最终函数如下所示:
using System.IO;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs.Host;
using Newtonsoft.Json;
using System;
namespace azuresmsapp
{
    public static class SendSMS
    {
        [FunctionName("SendSMS")]
        public static IActionResult Run([HttpTrigger(AuthorizationLevel.Function, 
                          "get", "post", Route = null)]HttpRequest req, TraceWriter log)
        {
            try
            {
                log.Info("C# HTTP trigger function processed a request.");
                string number = req.Query["number"];
                string message = req.Query["message"];
                string requestBody = new StreamReader(req.Body).ReadToEnd();
                dynamic data = JsonConvert.DeserializeObject(requestBody);
                number = number ?? data?.number;
                message = message ?? data?.message;
                OneSixtybyTwo objSender = new OneSixtybyTwo
                            ("160BY2.COM_USERNAME", "160BY2.COM_PASSWORD");
                if (objSender.Login())
                {
                    var sendResult = objSender.SendSms(number, message);
                    if (sendResult)
                    {
                        return (ActionResult)new OkObjectResult($"Message sent");
                    }
                    else
                    {
                        throw new Exception($"Sending failed");
                    }
                }
                else
                {
                    throw new Exception("Login failed");
                }
            }
            catch (System.Exception ex)
            {
                return new BadRequestObjectResult("Unexpected error, " + ex.Message);
            }
        }
    }
}
API 的 Angular 客户端
我将使用 Angular 7 应用程序作为 Web API 的客户端。您可以使用任何您想要的客户端。
在我们使用 API 之前,我们需要允许来自所有来源的请求访问 API。
为此,请导航到 Azure Function => 单击平台功能 => 单击 CORS。
删除现有条目并添加新条目 '*',如下所示:
现在,在 Angular 7 客户端中发送 SMS,我们编写以下代码:
//pseudo code
import { Component } from '@angular/core';
import { Message } from './dtos/message';
import { HttpClient } from '@angular/common/http';
@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  public message:Message;
  private baseUrl = "https://YOUR_FUNCTION_URL_HERE";
  constructor(private httpClient: HttpClient){
    this.message = {
      message: "",
      number: ""
    };
  }
  Send(){
    alert("Sending sms...");
    this.httpClient.get(this.baseUrl + '&number=' + this.message.number + 
            '&message=' + this.message.message).subscribe((x)=>{}, (y)=>{},()=>{
            alert("Message sent successfully!");
            this.message = {
      message: "",
      number: ""
    };
        });
  }
}
用户界面仅包含手机号码和消息。

伪 Angular7 客户端演示应用程序可在以下网址找到:Stackblitz & Github
您也可以下载附带的源代码文件。
关注点
- CsQuery:CsQuery库允许我们进行类似 jQuery 的 DOM 操作。
- SMS 提供商:有许多 SMS 提供商允许免费发送 SMS。我使用屏幕抓取实现了一些,该项目可在 github 上找到。
- Fiddler Web 调试器:Fiddler 允许检查提交的 Web 表单。
历史
- 2019 年 4 月 5 日:初稿
- 2019 年 4 月 6 日:添加了 Angular 客户端代码
- 2019 年 4 月 8 日:关于代码的更多详细信息






