Apr 13, 2019

Implementing SMS API using Azure Serverless Functions

my article published at codeproject.com

Introduction

The SMS API has three main pieces which fit together as follows:
  1. SMS Providers: There are various third party SMS providers which allow sending of SMS to any mobile device without charging anything, for this piece of Azure function we are choosing 160by2.com SMS provider which allows sending SMS to any mobile for free.
  2. Screen Scraping: The SMS providers do not allow sending SMS without actually visiting the site, Using C# code, we will be submitting login form and then programmatically submit the web form which sends SMS.
  3. Serverless function on Azure: The serverless function hosted on Azure will allow us to set up web API which will be consumable by any REST client.

Background

Before you go through this article, have a look at the following:
  1. What is serverless
  2. Introduction to serverless functions on Azure
  3. Web scraping with C#
  4. CsQuery: jQuery like DOM manipulation using .NET

Using the Code

Before we dive into writing our SMS web API using Azure function, we need to understand the sequence of actions which will be carried out:
Sequence diagram for Azure serverless function - click to enlarge image
The SmsWebClient class inherits from C# WebClient class which allow us to programmatically send HTTP POST/GET methods.
we will implement the programmatic execution of HTTP POST and GET methods to the 160by2.com SMS provider. 160by2.com is a free SMS provider, you need obtain a username and password to send SMS. The SMS provider class has Login() and SendSms() functions to handle the main job. We are using the CsQuery library to perform HTML DOM manipulations.
On the 160by2.com website we have login form containing the username and password,

when we inspect the HTML of the web form we can see that there are two input fields with keys id=username and id=password

When we click on Login button the form data is posted using HTTP POST methd,
To programatically do this we will create a C# NameValueCollection and add the values for web form keys
and then submit the form using the UploadValues method of WebClient.
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

The send sms form simply has the mobile number and message in ui.

To inspect what form values are submitted when we submit this web form we will use the google chrome console you can use Fiddler too.
Click F12 to open the chrome developer console, then go to network tab and in ui submit the send sms form by clicking send now. The submitted request appears as follows:

just like the login form, we need to programatically submit these form data keys along with values.
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      
The final class is as follows
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;
        }
    }
}
Now our main piece of cake containing the cherry...
To send sms we create instance of OneSixtybyTwo class, call the Login function and the call the SendSMS function.
OneSixtybyTwo objSender = new OneSixtybyTwo ("160BY2.COM_USERNAME", "160BY2.COM_PASSWORD");
if (objSender.Login()) { 
    var sendResult = objSender.SendSms(number, message);
}
Let's dive into the Azure serverless function having HTTP trigger:
The function can be intiated either by HTTP GET of POST method, so we read the posted mobile number and message using following code:
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;
The final function is as follows:
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);
            }
        }
    }
}

Angular Client for API

I will be using Angular 7 app as client for the web api. You can use any desired client.
Before we consume the API we need to allow requests to the api from all origins.
To do this navigate to the Azure function => Click on Platform features => Click on CORS
Delete existing entries and add new entry '*' as shown below:
cors in Azure function - click to enlarge image
Now in the angular 7 client to send the SMS, we write the following code:
//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: ""
    };
        });
  }
}

The user interface simply contains mobile number and message
sms ui

The pseudo angular7 client demo app is available at: Stackblitz & Github
Also you can download the  attached source code files.

Points of Interest

  1. CsQuery: The CsQuery library allows us to make jquery like dom manipulations
  2. SMS Providers: There are many SMS providers which allow sending SMS for free, I have implemented few of them using screen scraping, the project is available on github.
  3. Fiddler web debuggerFiddler allows inspecting submitted web foms

History

  • 5th April, 2019: Initial draft
  • 6th April, 2019: Angular client code added
  • 8th April, 2019: More details about code

No comments:

Post a Comment

Be the first to comment on this post.